Vue - How to pass and use dynamically multiple components as props - vue.js

I'm using AgGrid as the grid to show the data in the site.
Following this example Example: Rendering using VueJS Components, I'm able to use different components to be rendered into different columns.
Now that I need to implement this grid in multiple views, I want to make a "custom component" that implements the ag-grid-vue component, and receives just the column definitions, row data and components (if the columns are rendering another component).
Something like this:
<template>
<div>
<ag-grid-vue
:animateRows="true"
:columnDefs="columnDefs"
:frameworkComponents="frameworkComponents"
:defaultColDef="defaultColDef"
:gridOptions="gridOptions"
:pagination="true"
:paginationPageSize="paginationPageSize"
:rowData="rowData"
:suppressPaginationPanel="true"
colResizeDefault="shift"
ref="agGridTable"
rowSelection="multiple"
></ag-grid-vue>
</div>
</template>
<script>
import { AgGridVue } from "ag-grid-vue";
export default {
name: "AgGrid",
components: { AgGridVue },
props: ['columnDefs', 'components', 'rowData'],
data() {
return {
gridApi: null,
gridOptions: {},
defaultColDef: {
sortable: true,
resizable: true,
suppressMenu: true
},
frameworkComponents: null
}
},
beforeMount() {
this.frameworkComponents = this.components;
}
}
</script>
Now the parent will pass all the data needed:
<ag-grid :column-defs="columnDefs" :components="frameworkComponents" :row-data="gridList"></ag-grid>
The problem comes when I try to pass these components as props:
this.frameworkComponents = {
squareRenderer: SquareRenderer,
cubeRenderer: CubeRenderer,
paramsRenderer: ParamsRenderer,
currencyRenderer: CurrencyRenderer,
childMessageRenderer: ChildMessageRenderer,
};
Obviously, my custom component doesn't know these components because they are not imported, so the view could not find those components. Since each view will display diferent columns, and these one could or not display renderer components, I need to pass all the components needed to the grid component and import them.
Any ideas how to import and use those components?

Related

Parent component updates a child component v-for list, the new list is not rendered in the viewport (vue.js)

My app structure is as follows. The Parent app has an editable form, with a child component list placed at the side. The child component is a list of students in a table.
I'm trying to update a child component list. The child component uses a 'v-for', the list is generated through a web service call using Axios.
In my parent component, I am editing a students name, but the students new name is not reflected in the List that I have on screen.
Example:
Notice on the left the parent form has the updated name now stored in the DB. However, the list (child component) remains unchanged.
I have tried a few things such as using props, ref etc. I am starting to think that my app architecture may be incorrect.
Does anyone know how I might go about solving this issue.
Sections of the code below. You may understand that I am a novice at Vue.
Assistance much appreciated.
// Child component
<component>
..
<tr v-for="student in Students.slice().reverse()" :key="student._id">
..
</component>
export default {
env: '',
// list: this.Students,
props: {
inputData: Boolean,
},
data() {
return {
Students: [],
};
},
created() {
// AXIOS web call...
},
};
// Parent component
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
Header,
Footer,
List,
},
};
// Implementation
<List />
I think that it is better to use vuex for this case and make changes with mutations. Because when you change an object in the data array, it is not overwritten. reactivity doesn't work that way read more about it here
If your list component doesn't make a fresh API call each time the form is submitted, the data won't reflect the changes. However, making a separate request each time doesn't make much sense when the component is a child of the form component.
To utilise Vue's reactivity and prevent overhead, it would be best to use props.
As a simplified example:
// Child component
<template>
...
<tr v-for="student in [...students].reverse()" :key="student._id">
...
</template>
<script>
export default {
props: {
students: Array,
},
};
</script>
// Parent component
<template>
<div>
<form #submit.prevent="submitForm">
<input v-model="studentData.name" />
<input type="submit" value="SUBMIT" />
</form>
<List :students="students" />
</div>
</template>
<script>
import List from "./components/students/listTerms";
export default {
name: "App",
components: {
List,
},
data() {
return {
students: [],
studentData: {
name: ''
}
}
},
methods: {
submitForm() {
this.$axios.post('/endpoint', this.studentData).then(() => {
this.students.push({ ...this.studentData });
}).catch(err => {
console.error(err)
})
}
}
};
</script>
Working example.
This ensures data that isn't stored successfully won't be displayed and data that is stored successfully reflects in the child component.

Use Data Variable as Vue Template Without Parent Wrapper

I am trying to embed a Codemirror instance as a Vue component, similar to what was accomplished in this project. However, instead of returning a template of the Codemirror instance inside of a parent
<div class="vue-codemirror" :class="{ merge }">
<!-- Textarea will be replaced by Codemirror instance. -->
<textarea ref="textarea" :name="name" :placeholder="placeholder" v-else>
</textarea>
</div>
I am simplying trying to return the Codemirror instance alone. The reason why I can't simply remove the parent div and have
<!-- Textarea will be replaced by Codemirror instance. -->
<textarea ref="textarea" :name="name" :placeholder="placeholder" v-else>
</textarea>
is because the method to replace the textarea, CodeMirror.fromTextArea(), requires the textarea to have a parentNode. Otherwise, a null error will be encountered.
Luckily, there is a way to create a Codemirror instance without using a textarea at all, CodeMirror(). This instance has a function getWrapperElement() that returns the DOM node:
<div class="CodeMirror cm-s-default">
...
</div>
I want to output this specific DOM node relating to Codemirror instance using the Vue template/render function. The current way I'm creating the instance is by initializing it in the component data object.
data: function () {
// Create a CodeMirror instance.
let cm = new CodeMirror(null, {
...
})
return {
// Define a CodeMirror instance for each CodeBlockView.
cm : cm,
...
}
},
Update 1: I have found one way to do this, albeit very hacker-ish. We use the createElement function that is passed in with render and pass into the data object argument the innerHTML of DOM node we want to render. It seems to be working visually but the CodeMirror instance isn't editable.
render: function (createElement) {
return createElement('div', {
class: this.dom.classList.value,
domProps: {innerHTML: this.dom.innerHTML}
})
}
Update 2: However, this doesn't pass in the actual DOM node rather it only shallowly copies it; this presents a problem as it doesn't allow it to be editable nor have a CodeMirror instance attached.
You cannot change the template anymore after the component has been created. It's not a property of the component but a parameter that is passed to the constructor as far as I know.
Hence, you would need to create the CodeMirror instance before the component is created and inject it.
However, I fail to see the problem with a wrapping component, could you explain the why?
I test many ways and the most simple template I find was keeping the textarea, after that the component should receive options and value and emit the new value.
you can test the component here
I'm not a codeMirror expert, I import many things for keep the codemirror component more flexible, you can adjust the imports with your needs.
//component codeMirror.vue
<template>
<textarea ref="myCm"></textarea>
</template>
<script>
import CodeMirror from 'codemirror';
// language
import 'codemirror/mode/javascript/javascript';
// theme css
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/monokai.css';
// require active-line.js
import 'codemirror/addon/selection/active-line';
// styleSelectedText
import 'codemirror/addon/selection/mark-selection';
import 'codemirror/addon/search/searchcursor';
// hint
import 'codemirror/addon/hint/show-hint';
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/addon/hint/javascript-hint';
// highlightSelectionMatches
import 'codemirror/addon/scroll/annotatescrollbar';
import 'codemirror/addon/search/matchesonscrollbar';
import 'codemirror/addon/search/match-highlighter';
// keyMap
import 'codemirror/mode/clike/clike';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/comment/comment';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';
import 'codemirror/addon/search/search';
import 'codemirror/keymap/sublime';
// foldGutter
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/indent-fold';
import 'codemirror/addon/fold/markdown-fold';
import 'codemirror/addon/fold/xml-fold';
export default {
name: 'codeMirror',
props: {
value: String, // give to the component a start value
options: Object, // give the codeMirror options
},
mounted() {
const myCodemirror = new CodeMirror.fromTextArea(this.$refs.myCm, this.options);
myCodemirror.setValue(this.value);
myCodemirror.on('change', (cm) => {
if (this.$emit) {
this.$emit('input', cm.getValue());
}
});
},
};
</script>
after that is easy to use this component for render codeMirror in multiple instances, readOnly, recovery de data, multiple templates, no limits. ex:
<template>
<div>
Read Only, default theme, value multiple line
<codeMirror :value="value0" :options="noChanges"></codeMirror>
Value Dynamic, theme monokai, other extra fancy things
<codeMirror :value="value" :options="monokai" #input="onCmCodeChange"></codeMirror>
Only for debug, shows the modifications made inside codemirror
{{ value }}
</div>
</template>
<script>
// # is an alias to /src
import codeMirror from '#/components/codeMirror.vue';
export default {
name: 'pageEditor',
components: {
codeMirror,
},
data() {
return {
value0: `let choco: "bombon";
let type: 'caramel;
choco + ' ' + type;`,
value: 'let frankie= "It\'s alive"',
noChanges: {
theme: 'default',
readOnly: true,
tabSize: 2,
line: true,
lineNumbers: true,
},
monokai: {
tabSize: 2,
styleActiveLine: true,
lineNumbers: true,
line: true,
foldGutter: true,
styleSelectedText: true,
mode: 'text/javascript',
keyMap: 'sublime',
matchBrackets: true,
showCursorWhenSelecting: true,
theme: 'monokai',
extraKeys: { Ctrl: 'autocomplete' },
hintOptions: {
completeSingle: false,
},
},
};
},
methods: {
onCmCodeChange(newValue) {
this.value = newValue;
},
},
};
</script>
I hope this help

How to pass mixins as props and use them in child components using Vue.js

I have a parent component which contains two child components called add and edit, these components have some common properties and i want to use mixins, for that i add an object called mix to the parent's data object and i pass it as props to child components as follow
the parent component :
<template>
<div id="app">
<add :mixin="mix" operation="add"></add>
...
<edit :mixin="mix" operation="edit"></edit>
</div>
</template>
<script>
export default {
name: "App",
data(){
return{
/****/
mix:{
data() {
return {
user: { name: "", email: "" },
users: []
};
},
methods: {
add() {
this.users.push(this.user);
},
}
}
}
/*****/
};
},
components: {
add,edit
}
};
</script>
I could receive that object (mix) in my child component, but how could i assign it to the mixins property?
A low hanging fruit kind of way to solve this would be to just refactor your code and write the mixin in a separate file. You can then import the mixin object in both of your components and assign it to the mixins property.

Setting props of child component in vue

I'm following the example here of using a Vue template as a Kendo UI template in their components:
https://www.telerik.com/kendo-vue-ui/components/framework/vue-templates/
The example isn't very clear on how to supply properties to components that are rendered with this method (as opposed to rendering right in the template). I need to supply a single value determined in the parent to all instances of this child component, and I also need to subscribe to emitted events from the child component. My assumption is that there's an overload to Vue.component() that lets me access this functionality?
Edit:
Specifically what I am looking for is a way to have a header template for each column created from a Vue component. I need each column's template to receive data from the parent so I know how to construct it, and I also need each column's template to report an event back to the parent.
I think the key point is Step 3 in the link you attached (Kendo Vue Template Usage). (Never touch Kendo Before, if anything wrong, correct me, thanks.)
First, please open this Vue kendo Sandbox, you will find one dropdownlist then each option is one button plus one text. If you click the button, it will call one method in MyTemplate.vue and another Method at DropDownStyle.vue, then its background of each option is blue which passed from DropDownStyle.vue.
Kendo will bind this function of Step 3 to its attribute=template, then fisrt parameter (and only one) is each element of the data-source.
Then this function need to return one object including template and templateArgs, then Kendo construct it.
So my solution is add your function/callback/styles into templateArgs, then do what you need at MyTemplate.vue.
Below is the codes extended from Step 3.
methods: {
getMyTemplate: function (e) {
// parameter=e: it is the value of each element of the dropdown
e.callback = this.eventCallback
e.styles="background-color:blue"
return {
template: MyTemplate,
templateArgs: e
}
},
eventCallback: function (data) {
console.log(this.dropdowns)
}
}
Below is MyTemplate.vue.
<template>
<span :style="templateArgs.styles">
<button #click="buttonClick();templateArgs.callback()">{{templateArgs.value}}</button>
{{templateArgs.text}}
</span>
</template>
<script>
export default {
name: 'template1',
methods: {
buttonClick: function (e) {
console.log('props',this.templateArgs.styles)
}
},
data () {
return {
templateArgs: {
callback:function(){
console.log('Test')
},
styles:''
}
}
}
}
</script>
Very odd design choice in terms of passing the template in like they do. Avoiding the KendoUI and focusing on VueJS methods - could you use provide/inject? Providing the value in the parent and injecting in any of the children?
Also a plugin could be created to keep track of events or values you want available to all components in the application. In essence the plugin would be a service. A singleton object that is only instantiated once.
The documentation is indeed lacking. I agree with you on that. I took a different approach with templating for Kendo UI component and got this working: https://codesandbox.io/s/github/ariellephan/vue-kendoui-template
To start, I have this dropdown component that utilizes Kendo dropdown list component:
<template>
<div>
<p>Style with template {{template}}</p>
<kendo-dropdownlist
:template="template"
:headerTemplate="headerTemplate"
:data-source="dataSourceArray"
:data-text-field="'text'"
:data-value-field="'value'"
:filter="'contains'">
</kendo-dropdownlist>
</div>
</template>
<script>
export default {
name: "Dropdown",
props: ["dataSourceArray", "template", "headerTemplate"],
data() {
return {
value: "Click Me",
text: "I'm in Template template"
};
}
};
</script>
To render different styles/templates, I parsed in props from the parent component. In this case, DropdownStyles
<template>
<div id="DropdownStyles">
<h1>KendoUI dropdown instances with different templates</h1>
<Dropdown
v-for="dropdown in dropdowns"
v-bind:key="dropdown.id"
v-bind:title="dropdown.title"
v-bind:data-source-array="dropdown.dataSourceArray"
v-bind:template="dropdown.template"
v-bind:headerTemplate="dropdown.headerTemplate"
></Dropdown>
</div>
</template>
<script>
import Dropdown from "./Dropdown";
import DropdownTemplate from "./DropdownTemplate";
export default {
name: "DropdownStyles",
components: { Dropdown },
data() {
return {
dropdowns: [
{
id: 1,
title: "x style",
dataSourceArray: [
"Football",
"Tennis",
"Basketball",
"Baseball",
"Cricket",
"Field Hockey",
"Volleyball"
],
template: `<strong class="custom-dropdown">x #:data#</strong>`,
headerTemplate: DropdownTemplate.template
},
{
id: 2,
title: "+ style",
dataSourceArray: [
"Football",
"Tennis",
"Basketball",
"Baseball",
"Cricket",
"Field Hockey",
"Volleyball"
],
template: `<strong class="custom-dropdown">+ #:data#</strong>`,
headerTemplate: `<div><h3 style="padding-left:10px;">Sports 2</h3></div>`
}
]
};
}
};
</script>
You can move the template into its own file or function. For example, the first drop down is using DropdownTemplate for its headerTemplate:
DropdownTemplate.vue
<script>
export default {
name: "DropdownTemplate",
props: ["header"],
template: `<div>
<div><h3>Sports 1</h3></div>
</div>`,
data() {
return {};
}
};
</script>
<style scoped>
h3 {
padding-left: 10px;
}
</style>

vue-chartjs load data from parent component

I have a component for a LineChart, here is the code :
<script>
import { Line } from 'vue-chartjs'
export default {
extends: Line,
props: ['data', 'options'],
mounted () {
this.renderChart(this.data, this.options)
}
}
</script>
I want to use this component in another one as I can affect data to data and options value of the component Chart.vue.
I'm new to VueJS and can't understand an example like that in vue-chartjs doc.
Here is my component that will be the parent one, and what I've done from now :
<template>
<div class="dashboard">
<chart></chart>
</div>
</template>
<script>
import Chart from '#/components/Chart'
export default {
name: 'DashBoard',
components: {
'chart': Chart
},
mounted () {},
data () {
return {
datacollection: null
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
Where does your data come from?
Your example code is weird, as you are not passing your data as props.
So no wonder, that nothing shows up.
You have to pass your datacollection to your chart component.
<chart :data="datacollection" />
And keep in mind, that if you are using an API your data will arrive async. So the component mounts, renders the chart but your data is not there. So you need to add a v-if to be sure that your api call is finished.