I'm trying to render components depending on the state of an array in the parent (App.vue). I'm not sure at all that this is the correct approach for this use case (new to Vue and not experienced programmer) so I will gladly take advice if you think this is not the right way to think about this.
I'm trying to build a troubleshooter that consists of a bunch of questions. Each question is a component with data that look something like this:
data: function() {
return {
id: 2,
question: "Has it worked before?",
answer: undefined,
requires: [
{
id: 1,
answer: "Yes"
}
]
}
}
This question is suppose to be displayed if the answer to question 1 was yes.
My problem is I'm not sure on how to render my components conditionally. Current approach is to send an event from the component when it was answered, and to listen to that event in the parent. When the event triggers, the parent updates an array that holds the "state" of all answered questions. Now I need to check this array from each component to see if there are questions there that have been answered and if the right conditions are met, show the question.
My question is: How can I check for data in the parent and show/hide my component depending on it? And also - is this a good idea or should I do something different?
Here is some more code for reference:
App.vue
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: []
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
}
}
}
</script>
two.vue
<template>
<div v-if="show">
<h3>{{ question }}</h3>
<div class="c-troubleshooter__section">
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer">
<label for="question-2-a">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" id="question-2-b" name="question-2" value="nej" v-model="answer">
<label for="question-2-b">Nej</label>
</div>
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
id: 2,
question: "Bla bla bla?",
answer: undefined,
requires: [
{
id: 1,
answer: "Ja"
}
]
}
},
computed: {
show: function() {
// Check in parent to see if requirements are there, if so return true
return true;
}
},
watch: {
answer: function() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
}
}
}
</script>
 Rendering questions conditionally
as #Roy J suggests in comments, questions data probably belongs to the parent. It is the parent who handles all the data and who decides which questions should be rendered. However, there are plenty of strategies for this:
Display questions conditionally with v-if or v-show directly in the parent template:
Maybe the logic to display some questions is not at all generic. It can depend upon more things, user settings... I don't know. If that's the case, just render the questions conditionally directly in the parent, so you don't need to access the whole questions data in any question. Code should be something like the following:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions" v-if="displayQuestion(1)"/>
<two #changeAnswer="updateActiveQuestions" v-if="displayQuestion(2)"/>
</div>
</div>
</template>
<script>
import one from './components/one.vue'
import two from './components/two.vue'
export default {
name: 'app',
components: {
one,
two
},
data: function() {
return {
activeQuestions: [],
}
},
methods: {
updateActiveQuestions(event) {
let index = this.activeQuestions.findIndex( ({ id }) => id === event.id );
if ( index === -1 ) {
this.activeQuestions.push(event);
} else {
this.activeQuestions[index] = event;
}
},
displayQuestion(index){
// logic...
}
},
}
</script>
Pass a reference to the previous question to every question:
If any question should be visible only when the previous question has been answered or viewed or something like that, you can pass that as a prop to every question, so they know wether they must render or not:
<template>
<div id="app">
<div class="c-troubleshooter">
<one #changeAnswer="updateActiveQuestions"/>
<two #changeAnswer="updateActiveQuestions" prev="activeQuestions[0]"/>
</div>
</div>
</template>
And in two.vue:
props: ['prev'],
computed: {
show: function() {
return this.prev && this.prev.status === 'ANSWERED';
// or some logic related to this, idk
}
},
just pass the whole data to the children:
As you coded it, you can just pass the whole questions data as a prop to every question component, then use it in a computed property. This is not what I would do, but just works, and since objects are references this is not necessarily unperformant.
Using a generic component:
It seems weird to have a one.vue, two.vue for every question, and sure does not scale well.
I'm not really sure how modular I can do them since the template for each question can be a bit different. Some have images or custom elements in them for example, while others don't.
If template are really different from each question to another, this can get complicated. However, if, as I suspect, they share common HTML structure, with a defined header or a common 'ask' button at the bottom and stuff like that, then you should be able to address this using Vue slots.
Apart from template issues, I suppose that every question in your app can get an arbitrary number of 'sub-questions' (as two.vue having question-2-a and question-2-b). This will require a complex and flexible data structure for the questions data (which will get more complex when you start to add multiple choices, multiple possible answers etc. etc.). This can get very complex but you should probably work on this until you can use a single question.vue component, this will surely pay out.
tip: avoid watchers
You're using v-model to answer in the two.vue template, then using a watcher to track changes in the answer variable and emit the event. This is convoluted and difficult to read, you can use #input or #change events on the <input> element instead:
<input type="radio" id="question-2-a" name="question-2" value="ja" v-model="answer" #input="emitAnswer">
And then instead of the watcher, have a method:
emitAnswer() {
this.$emit('changeAnswer', {
id: this.id,
question: this.question,
answer: this.answer
})
This is a pretty broad question, but I'll try to give some useful guidance.
First data should be used for internal state. Very often, a component should use props for things you might think would be data it owns. That is the case here: the questions need to be coordinated by the parent, so the parent should own the data. That allows you to make a sensible function to control whether a question component displays.
Having the parent own the data also allows you to make one question component that configures itself according to its props. Or you might have a few different question component types (you can use :is to select the right one), but almost certainly some of them are reusable if you pass their question/answer/other info in.
To update answers, you will emit changes from the question components and let the parent actually change the value. I use a settable computed to allow the use of v-model in the component.
new Vue({
el: '#app',
data() {
return {
questions: [{
id: 1,
question: 'blah 1?',
answer: null
},
{
id: 2,
question: 'blah 2?',
answer: null,
// this is bound because data is a function
show: () => {
const q1 = this.questions.find((q) => q.id === 1);
return Boolean(q1.answer);
}
},
{
id: 3,
question: 'Shows anyway?',
answer: null
}
]
};
},
components: {
questionComponent: {
template: '#question-template',
props: ['props'],
computed: {
answerProxy: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('change', newValue);
}
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<div class="c-troubleshooter">
<question-component v-for="q in questions" v-if="!q.show || q.show()" :props="q" #change="(v) => q.answer = v" :key="q.id">
</question-component>
</div>
<h2>State</h2>
<div v-for="q in questions" :key="q.id">
{{q.question}} {{q.answer}}
</div>
</div>
<template id="question-template">
<div>
{{props.question}}
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-a`" :name="`question-${props.id}`" value="ja" v-model="answerProxy">
<label :for="`question-${props.id}-a`">Ja</label>
</div>
<div class="c-troubleshooter__input">
<input type="radio" :id="`question-${props.id}-b`" :name="`question-${props.id}`" value="nej" v-model="answerProxy">
<label :for="`question-${props.id}-b`">Nej</label>
</div>
</div>
</template>
Related
I am having a rather strange problem with vue (v2.6.14) in which I am creating a new array based on one receive as a prop. Here's the relevant code:
props: { employees: Array },
data() {
return {
sortedEmployees: [],
};
},
mounted() {
this.sortedEmployees = this.employees.slice(0);
},
Essencially what I want here is to create a new reference for the employees array so that I can sort it to display later without actually altering the original array. I am not worried about cloning the actual objects inside the array since I will not alter them.
The problem with this is that, when the app 'hot reloads'(due to some change in the code) it works as expected, the hook is called and the component data is set according to what is expected BUT if I actually refresh the page in the browser, even though the hook is called, the component data is not set and I end up with an empty array. I can solve this by setting up a watcher to the prop and then it would set the data there, but what I am interested here is understanding what's happening. If the hook is called when the page is refreshed why doesn't it set the data properly as it does when 'hot reloading'?
I have a minimal setup created with vue-cli, no fancy configurations whatsoever. Any clue what I might be missing?
I guess the employees are loaded async, right?
I don't know your exact application structure, but the problem is usually the following:
The mounted hook gets called, when the component mounts, of course. If the employees are loaded async in the parent component, the mount hook is called, before the async call is resolved. So it will copy an empty array at this time.
With a watcher you solve this problem, because the watcher fires as soon as the async call resolves (as it mutates the employees array).
Same happens to the hotreload. When the hotreload occurs, the mounted hook gets executed again - at this time the employees array is already prefilled with values and therefore the correct array is copied in the mount hook.
Update
If you want to avoid watchers, you could also wrap your component like this:
<your-component
v-if="employees.length > 0"
:employees="employees"
/>
Be aware, that the copied employees array IN your component is still not reactive. It just copies the array on the first time it has more than 1 value. A watcher really makes sense in this case.
If you use watchers, make sure to use the immediate: true option. This ensures, that the watcher is also called the first time on render (and also on hotreload).
Summary
If you really want to copy the array, use watchers (with the immediate: true flag).
If it's just about sorting, go for the computed property solution suggested by #Sebastian Scholl
It sounds like the component is Refreshing (reseting) with the prop change however it's not Re-mounting. This is what would cause the array to reset to it's default state ([]), whereas on hot-reload the actual page is reloading.
Try the following, and if it doesn't resolver the issue I would suggest going the route of using a Watcher.
<template>
<div>
sorted: {{ sortedEmployees }}
<br />
not sorted: {{ employees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data() {
return {
sortedEmployees: Array.from(this.employees),
};
}
};
</script>
Another approach is to just use a Computed method so and add any filtering/sorting logic inside that method. It would be something like:
<template>
<div>
<input v-model="sortKey" />
sorted: {{ sortedEmployees }}
</div>
</template>
<script>
export default {
props: {
employees: Array,
},
data () {
return {
sortKey: ''
}
},
computed: {
sortedEmployees() {
return Array.from(this.employees).sort(this.sortingFunction);
},
},
methods: {
sortingFunction(a, b) {
// sorting function using this.sortKey
}
}
};
</script>
UPDATED ANSWER
I switched up the code in the example a little and believe to have gotten it to work as I you're describing.
App.js
First off, I made it so that the employees array is updated after 3 seconds.
<template>
<div id="app">
<Dashboard :employees="employees" />
</div>
</template>
<script>
import Dashboard from './components/Dashboard.vue';
export default {
name: 'App',
components: { Dashboard },
data () {
return {
employees: [
{
employeeId: '1',
firstName: 'Leite',
}
]
};
},
methods: {
updateEmployees () {
this.employees = this.employees.concat([
{
employeeId: '2',
firstName: 'Jacinto',
},
{
employeeId: '3',
firstName: 'Capelo',
}
]);
}
},
mounted () {
setTimeout(this.updateEmployees, 3000)
},
};
</script>
Dashboard.js
The updated() lifecycle hook runs whenever data changes are detected (props and data properties). This effectively detected the change in the prop passed by the parent App.js component and re-rendered the data - whereas the mounted hook only ran once per page load.
<template>
<div id="dashbord">
<div
v-for="(employee, index) in sortedEmployees"
:key="employee.employeeId"
>
{{ employee.firstName }}
</div>
</div>
</template>
<script>
export default {
name: 'Dashboard',
props: {
employees: Array
},
data() {
return {
sortedEmployees: Array.from(this.employees)
};
},
updated() {
this.sortedEmployees = Array.from(this.employees)
}
};
</script>
When it comes to creating methods in child components I'm having a hard time figuring a particular feature out.
I have this parent route/component (League.vue):
In this league.vue I render a child component:
<router-view :league="league" />
Child component:
<template>
<div v-if="teams_present">
<div class="page-container__table">
<h3 class="page-container__table__header">Teams</h3>
</div>
</div>
</template>
<script>
export default {
name: 'LeagueTeams',
props: [
'league'
],
data () {
},
computed: {
teams_present: function () {
return this.league.teams.length > 0
}
}
}
</script>
ERROR:
"TypeError: Cannot read property 'length' of undefined"
So it appears that the computed callback is called before the prop can be set, I think? and if a change it to methods it never gets called. How do I handle this case?
As Ali suggested, you can return this.league.teams && this.league.teams.length > 0, which definitely will work.
However, as my experience, to avoid these situation, and for good practice, always declare the type of the Props. So in your props:
export default {
name: 'LeagueTeams',
props: {
league: {
type: Object, // type validation Object
default() { return {teams: [] }} // add a default empty state for team, you can add more
}
},
data () {
},
computed: {
teams_present: function () {
return this.league.teams.length > 0 // now the old code should work
}
}
}
</script>
By doing this, you don't need to care much about checking the edge case of this.league.teams every time, since you may need to call it again in methods or in the <template> html
Update: Another suggestion is if you are using vue-cli 4, you can use Optional chaining and nullish coalescing.
return this.league?.teams.length ?? false // replace with only this line will work
Hope this will help you 2 more ways to deal with in these situations, and depends on situations you can choose the most suitable one
I have the need to create a dynamic form, which builds up in a tree kind of way. The form can change at any time by the user (who creates/designs the form) and so the inputs I have change dynamically too.
Example:
root
--groeisnelheid
----niveau
------beginner (input radio)
------recreatief (input radio)
------competitie (input radio)
------tour (input radio)
----input text
----begeleiding
------another input
------and another
--another category
----speed
------input
------input
As you can see, not the easiest form... The user (admin user in this case) has the ability to edit or create new forms.
I probably have underestimated the job, since I am trying to create the input side of it, and am already struggling.
What I have done so far:
TreeComponent.vue
<template>
<div class="tree">
<ul class="tree-list">
<tree-node :node="treeData"></tree-node>
</ul>
</div>
</template>
<script>
export default {
props: {
treeData: [Object, Array]
},
data() {
return {
treeValues: []
};
},
methods: {
sendForm: function() {}
}
};
</script>
TreeNodeComponent.vue
<template>
<li v-if="node.children && node.children.length" class="node">
<span class="label">{{ node.name }}</span>
<ul>
<node v-for="child in node.children" :node="child" :key="child.id"></node>
</ul>
</li>
<div v-else class="form-check form-check-inline">
<input
type="radio"
class="form-check-input"
:name="'parent-' + node.parent_id"
:id="node.id"
/>
<label for class="form-check-label">{{ node.name }}</label>
</div>
</template>
<script>
export default {
name: "node",
props: {
node: [Object, Array]
}
};
</script>
This results in all the inputs showing up as I want. But now the real question is; how do I get the value of these inputs in my root component (TreeComponent.vue), so I can send this to the server. Either on change or when the user proceeds in the form.
I am used to working with v-model on this, but I have no clue on how to use this on recursive components, since the documentation only covers setting the data of the direct parent.
Any help would be much appreciated.
One way of doing this is to pass a prop down from TreeComponent to each node.
<template>
<div class="tree">
<ul class="tree-list">
<tree-node :node="treeData" :form="formRepo"></tree-node>
</ul>
</div>
</template>
Then each node passes the prop down to its children.
<node v-for="child in node.children" :node="child" :key="child.id" :form="form"></node>
This way each node will have a direct reference to the TreeComponent.
In each node you can watch the model and update the form prop. You need to use your child.id so you know which field is which.
Your formRepo can be a fully fledged object, but a hash could work just as well.
data() {
return {
treeValues: [],
formRepo: {
}
};
}
Note: if you want formRepo to be reactive you'll need to use Vue.set to add new keys to it.
Thank you for your answer.
I managed to solve it by posting the data to the server on every change, since this was a handy feature.
The way I did so was:
Call function from the input (same call for text inputs)
<input
type="radio"
class="form-check-input"
:name="'parent-' + node.parent_id"
:id="node.id"
#input="change($event, node.id, node.parent_id)"
/>
Have some data variables to fill (Route is required for the axios request)
data() {
return {
input: {
id: null,
parentId: null,
radio: false,
value: null
},
route: null
};
},
And then some magic. The change method. (Left the axios bit out.)
methods: {
change: function(event, id, parentId) {
this.input.parentId = parentId;
this.input.id = id;
if (event.target.value === "on" || event.target.value === "off") {
this.input.radio = true;
this.input.value = event.target.value === "on" ? true : false;
} else {
this.input.value = event.target.value;
}
if (this.input.value) {
axios.put().then().catch()
}
}
}
I know there is some room of improvement in the validation bit. If a user enters 'on' in a text field, this will probably fail. So there is work to be done, but the basic filling of a form is working.
As to if this is the best way, I have no clue, since I'm new to Vue.
I want to assign some attributes and classes to the children VNode through data object. That just works. But during my Vue.js investigation, I have not seen such pattern in use, that's why I don't think it's good idea to modify children VNode's.
But that approach sometimes comes in handy – for example I want to assign to all the buttons in default slot the aria-label attribute.
See example below, using default stateful components:
Vue.component('child', {
template: '<div>My role is {{ $attrs.role }}</div>',
})
Vue.component('parent', {
render(h) {
const {
default: defaultSlot
} = this.$slots
if (defaultSlot) {
defaultSlot.forEach((child, index) => {
if (!child.data) child.data = {}
if (!child.data.attrs) child.data.attrs = {}
const {
data
} = child
data.attrs.role = 'button'
data.class = 'bar'
data.style = `color: #` + index + index + index
})
}
return h(
'div', {
class: 'parent',
},
defaultSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent>
<child></child>
<child></child>
<child></child>
<child></child>
<child></child>
</parent>
</div>
And here is examples using stateless functional components:
Vue.component('child', {
functional: true,
render(h, {
children
}) {
return h('div', {
class: 'bar'
}, children)
},
})
Vue.component('parent', {
functional: true,
render(h, {
scopedSlots
}) {
const defaultScopedSlot = scopedSlots.default({
foo: 'bar'
})
if (defaultScopedSlot) {
defaultScopedSlot.forEach((child, index) => {
child.data = {
style: `color: #` + index + index + index
}
child.data.attrs = {
role: 'whatever'
}
})
}
return h(
'div', {
class: 'parent',
},
defaultScopedSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<parent>
<template v-slot:default="{ foo }">
<child>{{ foo }}</child>
<child>{{ foo }}</child>
<child>{{ foo }}</child>
</template>
</parent>
</div>
I am waiting for the following answers:
Yes, you can use it, there are no potential problems with this approach.
Yes, but these problem(s) can happen.
No, there are a lot of problem(s).
UPDATE:
That another good approach I have found it's to wrap child VNode into the another created VNode with appropriate data object, like this:
const wrappedChildren = children.map(child => {
return h("div", { class: "foo" }, [child]);
});
Using this approach I have no fear modifying children VNode's.
Thank you in advance.
There are potential problems with doing this. Used very sparingly it can be a useful technique and personally I would be happy to use it if no simple alternative were available. However, you're in undocumented territory and if something goes wrong you'll likely have to debug by stepping through Vue internals. It is not for the faint-hearted.
First, some of examples of something similar being used by others.
Patching key:
https://medium.com/dailyjs/patching-the-vue-js-virtual-dom-the-need-the-explanation-and-the-solution-ba18e4ae385b
An example where Vuetify patches a VNode from a mixin:
https://github.com/vuetifyjs/vuetify/blob/5329514763e7fab11994c4303aa601346e17104c/packages/vuetify/src/components/VImg/VImg.ts#L219
An example where Vuetify patches a VNode from a scoped slot: https://github.com/vuetifyjs/vuetify/blob/7f7391d76dc44f7f7d64f30ad7e0e429c85597c8/packages/vuetify/src/components/VItemGroup/VItem.ts#L58
I think only the third example is really comparable to the patching in this question. A key feature there is that it uses a scoped slot rather than a normal slot, so the VNodes are created within the same render function.
It gets more complicated with normal slots. The problem is that the VNodes for the slot are created in the parent's render function. If the child's render function runs multiple times it'll just keep getting passed the same VNodes for the slot. Modifying those VNodes won't necessarily do what you'd expect as the diffing algorithm just sees the same VNodes and doesn't perform any DOM updates.
Here's an example to illustrate:
const MyRenderComponent = {
data () {
return {
blueChildren: true
}
},
render (h) {
// Add a button before the slot children
const children = [h('button', {
on: {
click: () => {
this.blueChildren = !this.blueChildren
}
}
}, 'Blue children: ' + this.blueChildren)]
const slotContent = this.$slots.default
for (const child of slotContent) {
if (child.data && child.data.class) {
// Add/remove the CSS class 'blue'
child.data.class.blue = this.blueChildren
// Log it out to confirm this really is happening
console.log(child.data.class)
}
children.push(child)
}
return h('div', null, children)
}
}
new Vue({
el: '#app',
components: {
MyRenderComponent
},
data () {
return {
count: 0
}
}
})
.red {
border: 1px solid red;
margin: 10px;
padding: 5px;
}
.blue {
background: #009;
color: white;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-render-component>
<div :class="{red: true}">This is a slot content</div>
</my-render-component>
<button #click="count++">
Update outer: {{ count }}
</button>
</div>
There are two buttons. The first button toggles a data property called blueChildren. It's used to decide whether or not to add a CSS class to the children. Changing the value of blueChildren will successfully trigger a re-render of the child component and the VNode does get updated, but the DOM is unchanged.
The other button forces the outer component to re-render. That regenerates the VNodes in the slot. These then get passed to the child and the DOM will get updated.
Vue is making some assumptions about what can and can't cause a VNode to change and optimising accordingly. In Vue 3 this is only going to get worse (by which I mean better) because there are a lot more of these optimisations coming along. There's a very interesting presentation Evan You gave about Vue 3 that covers the kinds of optimisations that are coming and they all fall into this category of Vue assuming that certain things can't change.
There are ways to fix this example. When the component is performing an update the VNode will contain a reference to the DOM node, so it can be updated directly. It's not great, but it can be done.
My own feeling is that you're only really safe if the patching you're doing is fixed, such that updates aren't a problem. Adding some attributes or CSS classes should work, so long as you don't want to change them later.
There is another class of problems to overcome. Tweaking VNodes can be really fiddly. The examples in the question allude to it. What if data is missing? What if attrs is missing?
In the scoped slots example in the question the child component has class="bar" on its <div>. That gets blown away in the parent. Perhaps that's intentional, perhaps not, but trying to merge together all the different objects is quite tricky. For example, class could be a string, object or array. The Vuetify example uses _b, which is an alias for Vue's internal bindObjectProps, to avoid having to cover all the different cases itself.
Along with the different formats are the different node types. Nodes don't necessarily represent components or elements. There are also text nodes and comments, where comment nodes are a consequence of v-if rather than actual comments in the template.
Handling all the different edge cases correctly is pretty difficult. Then again, it may be that none of these edge cases cause any real problems for the use cases you actually have in mind.
As a final note, all of the above only applies to modifying a VNode. Wrapping VNodes from a slot or inserting other children between them in a render function is perfectly normal.
I'm using Vue v1.0.28 and vue-resource to call my API and get the resource data. So I have a parent component, called Role, which has a child InputOptions. It has a foreach that iterates over the roles.
The big picture of all this is a list of items that can be selected, so the API can return items that are selected beforehand because the user saved/selected them time ago. The point is I can't fill selectedOptions of InputOptions. How could I get that information from parent component? Is that the way to do it, right?
I pasted here a chunk of my code, to try to show better picture of my problem:
role.vue
<template>
<div class="option-blocks">
<input-options
:options="roles"
:selected-options="selected"
:label-key-name.once="'name'"
:on-update="onUpdate"
v-ref:input-options
></input-options>
</div>
</template>
<script type="text/babel">
import InputOptions from 'components/input-options/default'
import Titles from 'steps/titles'
export default {
title: Titles.role,
components: { InputOptions },
methods: {
onUpdate(newSelectedOptions, oldSelectedOptions) {
this.selected = newSelectedOptions
}
},
data() {
return {
roles: [],
selected: [],
}
},
ready() {
this.$http.get('/ajax/roles').then((response) => {
this.roles = response.body
this.selected = this.roles.filter(role => role.checked)
})
}
}
</script>
InputOptions
<template>
<ul class="option-blocks centered">
<li class="option-block" :class="{ active: isSelected(option) }" v-for="option in options" #click="toggleSelect(option)">
<label>{{ option[labelKeyName] }}</label>
</li>
</ul>
</template>
<script type="text/babel">
import Props from 'components/input-options/mixins/props'
export default {
mixins: [ Props ],
computed: {
isSingleSelection() {
return 1 === this.max
}
},
methods: {
toggleSelect(option) {
//...
},
isSelected(option) {
return this.selectedOptions.includes(option)
}
},
data() {
return {}
},
ready() {
// I can't figure out how to do it
// I guess it's here where I need to get that information,
// resolved in a promise of the parent component
this.$watch('selectedOptions', this.onUpdate)
}
}
</script>
Props
export default {
props: {
options: {
required: true
},
labelKeyName: {
required: true
},
max: {},
min: {},
onUpdate: {
required: true
},
noneOptionLabel: {},
selectedOptions: {
type: Array
default: () => []
}
}
}
EDIT
I'm now getting this warning in the console:
[Vue warn]: Data field "selectedOptions" is already defined as a prop. To provide default value for a prop, use the "default" prop option; if you want to pass prop values to an instantiation call, use the "propsData" option. (found in component: <default-input-options>)
Are you using Vue.js version 2.0.3? If so, there is no ready function as specified in http://vuejs.org/api. You can do it in created hook of the component as follows:
// InputOptions component
// ...
data: function() {
return {
selectedOptions: []
}
},
created: function() {
this.$watch('selectedOptions', this.onUpdate)
}
In your InputOptions component, you have the following code:
this.$watch('selectedOptions', this.onUpdate)
But I am unable to see a onUpdate function defined in methods. Instead, it is defined in the parent component role. Can you insert a console.log("selectedOptions updated") to check if it is getting called as per your expectation? I think Vue.js expects methods to be present in the same component.
Alternatively in the above case, I think you are allowed to do this.$parent.onUpdate inside this.$watch(...) - something I have not tried but might work for you.
EDIT: some more thoughts
You may have few more issues - you are trying to observe an array - selectedOptions which is a risky strategy. Arrays don't change - they are like containers for list of objects. But the individual objects inside will change. Therefore your $watch might not trigger for selectedOptions.
Based on my experience with Vue.js till now, I have observed that array changes are registered when you add or delete an item, but not when you change a single object - something you need to verify on your own.
To work around this behaviour, you may have separate component (input-one-option) for each of your input options, in which it is easier to observe changes.
Finally, I found the bug. I wasn't binding the prop as kebab-case