Setting props of child component in vue - vue.js

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>

Related

Vue 3: How to change specific sibling component's data?

Say you have 3 components:
<Modal>
<Navbar>
<Hero>
Your Modal component has data saying whether it's open or not, along with the appropriate methods:
data() {
return {
active: false,
}
},
methods: {
open() {this.active = true},
close() {this.active = false},
switch() {this.active ? this.close(): this.open()}
}
and you want a link in your Navbar component to be able to open it:
template:
/*html*/
`<nav class="navbar">
<router-link :to="etc.">Home</router-link>
<router-link :to="etc.">About</router-link>
<a #click="openSiblingModalSomehow">Contact</a> <!-- This one -->
</nav>`
As well as the Call to Action button on your Hero component:
template:
/*html*/
`<div class="hero">
<h1>Hello, World</h1>
<button #click="openSiblingModelSomehow">Contact Me</button>
</div>`
Assuming you DON'T want a global property to access this... For example, what if you want more than one type of modal?:
<ContactModal>
<SignUpModal>
<OtherModal>
<Navbar>
<Hero>
and knowing that the Modal also needs to be able to close itself,
How would you trigger a specific sibling element / component to open the Modal (in this case, let's say ContactModal) using Vue 3?
I thought about using a variable on the App itself, but it seems a bit hectic to change a globalProperty only for a specific component with it's own data.
I had a similar challenge at my project. My approach was to not use a Boolean property.
Step by step, first declare a empty string at the parent, that provides it for your modal boxes:
data() {
return {
active: ""
}
}
Declare a method, that handles that string:
methods: {
switchActive(string) {
if (string) {
this.active = string;
}
else {
this.active = ""
}
}
}
This would be one of your modal components:
<template>
<Dialog header="Header" footer="Footer" :visible="checkActive">
I am the modal dialog.
<button #click="this.$emit('close')">Close Me</button>
</Dialog>
</template>
<script>
export default {
name: "modal-123",
props: {
active: String
},
computed: {
checkActive() {
return this.active === this.$options.name;
}
}
}
</script>
And call this component:
<modal :active="active" #close="switchActive('')"></modal>
If you want to open one of your modal boxes, you call switchActive with the name property of your modal box.

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.

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

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?

Attach v-model to a dynamic element added with .appendChild in Vue.js

I'm working with a library that doesn't have a Vue.js wrapper.
The library appends elements in the DOM in a dynamic way.
I want to be able to bind the v-model attribute to those elements with Vue and once appended work with them in my model.
I've done this in the past with other reactive frameworks such as Knockout.js, but I can't find a way to do it with vue.js.
Any pay of this doing?
It should be something among these lines I assume:
var div = document.createElement('div');
div.setAttribute('v-model', '{{demo}}');
[VUE CALL] //tell vue.js I want to use this element in my model.
document.body.appendChild(div);
You could create a wrapper component for your library and then setup custom v-model on it to get a result on the lines of what you're looking for. Since your library is in charge of manipulating the DOM, you'd have to hook into the events provided by your library to ensure your model is kept up-to-date. You can have v-model support for your component by ensuring two things:
It accepts a value prop
It emits an input event
Here's an example of doing something similar: https://codesandbox.io/s/listjs-jquery-wrapper-sdli1 and a snipper of the wrapper component I implemented:
<template>
<div>
<div ref="listEl">
<ul ref="listUlEl" class="list"></ul>
</div>
<div class="form">
<div v-for="variable in variables" :key="variable">
{{ variable }}
<input v-model="form[variable]" placeholder="Enter a value">
</div>
<button #click="add()">Add</button>
</div>
</div>
</template>
<script>
export default {
props: ["value", "variables", "template"],
data() {
return {
form: {}
};
},
mounted() {
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
this.createFormModels();
},
methods: {
createFormModels() {
for (const variable of this.variables) {
this.$set(this.form, variable, "");
}
},
add() {
this.$emit("input", [
...this.value,
{
id: this.value.slice(-1)[0].id + 1,
...this.form
}
]);
}
},
watch: {
value: {
deep: true,
handler() {
this.$refs.listUlEl.innerHTML = "";
this.list = new List(
this.$refs.listEl,
{
valueNames: this.variables,
item: this.template
},
this.value
);
}
}
},
beforeDestroy() {
// Do cleanup, eg:
// this.list.destroy();
}
};
</script>
Key points:
Do your initialization of the custom library on mounted() in order to create the DOM. If it needs an element to work with, provide one via <template> and put a ref on it. This is also the place to setup event listeners on your library so that you can trigger model updates via $emit('value', newListOfStuff).
watch for changes to the value prop so that you can reinitialize the library or if it provides a way to update its collection, use that instead. Make sure to cleanup the previous instance if the library provides support for it as well as unbind event handlers.
Call any cleanup operations, event handler removals inside beforeDestroy.
For further reference:
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components

Vue component communication

I'm looking for a concise example of two Vue components. The first component should contain a text input or textarea. The second component displays a character counter. I would like the first component to emit change events, and the second component should listen for those events and display its computed values (character count). I'm new to Vue and trying to wrap my head around the best way to implement this functionality. It seems rather straightforward in pure JavaScript but doing it the Vue way is not as clear to me. Thanks.
Here is how I'd do it in JavaScript:
Here's the textarea:
<textarea id="pagetext" name="pagetext"
onChange="characterCount();"
onKeyup="characterCount();">Type here</textarea>
Here's the JavaScript:
function characterCount()
{
var characters=document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML=characters+"";
}
My concern with Vue is passing the entire value around... for performance reasons this seems less than ideal. I may want my text editing Vue component to self-contain the value and emit the stats, ie the value for character count which would then be observed by a text stats component.
You can create a "Model" for value of textarea and provide this model to second component by using following way https://v2.vuejs.org/v2/guide/components-props.html
I've written up a snippet with four examples: your original, a simple Vue app (no components) that does the same thing, and two apps with two components that are coordinated by the parent.
The simple Vue app is actually more concise than the pure JavaScript app, and I think it shows off the reason for having a framework: your view doesn't act as a store for your program data, from which you have to pull it out.
In the final example, the parent still owns pageText, but passes it down to the my-textarea component. I like to hide the emitting behind the abstraction of a settable computed, so that the element can use v-model. Any changes are emitted up to the parent, which changes pageText, which propagates back down to the component.
I think your performance concerns fall into the realm of premature optimization, but it is possible not to use the text content as data at all, and only be concerned with the length. The fourth example does that. emitLength could have used event.target.value.length, but I wanted to use it in the mounted to initialize the length properly, so I used a ref.
function characterCount() {
var characters = document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML = characters + "";
}
new Vue({
el: '#app',
data: {
pageText: 'Type here'
}
});
new Vue({
el: '#app2',
data: {
pageText: 'Type here'
},
components: {
myTextarea: {
props: ['value'],
template: '<textarea name="pagetext" v-model="proxyValue"></textarea>',
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
new Vue({
el: '#app3',
data: {
textLength: null
},
components: {
myTextarea: {
template: '<textarea ref="ta" name="pagetext" #input="emitLength">Type here</textarea>',
methods: {
emitLength() {
this.$emit('change', this.$refs.ta.value.length);
}
},
mounted() {
this.emitLength();
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<form name="myForm">
<textarea id="pagetext" name="pagetext" onChange="characterCount();" onKeyup="characterCount();">Type here</textarea>
</form>
<div id="charcounter"></div>
<div id="app">
<h1>Vue (simple)</h1>
<form>
<textarea name="pagetext" v-model="pageText"></textarea>
</form>
<div>{{pageText.length}}</div>
</div>
<div id="app2">
<h1>Vue (with components)</h1>
<form>
<my-textarea v-model="pageText"></my-textarea>
</form>
<text-length :value="pageText.length"></text-length>
</div>
<div id="app3">
<h1>Vue emitting stats</h1>
<form>
<my-textarea #change="(v) => textLength=v"></my-textarea>
</form>
<text-length :value="textLength"></text-length>
</div>