Dynamic prop binding from parent to child - vue.js

So I'm having a headache since yesterday on this. I'd like to have some kind of two way binding of data. I want for my array data(): exploredFields be able to update the values of the children components. But the update is called from the child.
Here is my parent component.
<div>
<button #click="resetMinefield(rows, mineCount)">RESET</button>
</div>
<div class="minefield">
<ul class="column" v-for="col in columns" :key="col">
<li class="row" v-for="row in rows" :key="row">
<Field
:field="mineFields[(row + (col - 1) * rows) - 1].toString()"
:id="(row + (col - 1) * rows) - 1"
:e="exploredFields[(row + (col - 1) * rows) - 1]" // This doesn't update the child !!!
#update="exploreField"
/>
</li>
</ul>
</div>
...
<script>
import Field from "#/components/Field";
export default {
name: "Minesweeper",
components: {
Field,
},
created() {
this.resetMinefield(this.rows, this.mineCount);
},
data() {
return {
rows: 7,
columns: 7,
mineCount: 5,
mineFields: [],
exploredFields: [],
}
},
methods: {
exploreField(index) {
this.exploredFields[index] = true;
},
resetMinefield(fieldSize, mineCount) {
// Updates this.mineFields
// Passes data properly to the child
}
},
}
</script>
And here is a child component. On #click it updates self data: explored and parents data: exploredFields. But the dynamic binding for props: e does not work.
<template>
<div :class="classObject" #click="changeState">
{{ field }}
</div>
</template>
<script>
export default {
name: "Field",
data() {
return {
explored: false,
}
},
props: {
id: {
type: Number,
default: -1,
},
field: {
type: String,
default: 'X',
},
e: {
type: Boolean,
default: false,
}
},
methods: {
changeState() {
this.$emit("update", this.id);
this.explored = true;
}
},
computed: {
classObject: function() {
// Stuff here
}
},
}
</script>
I also tried to do dynamic binding for data :explored instead of props :e, but no effect there too. It seams that it doesn't want to update because I'm calling the update from the child. And even I can see the data changing, it is not passed back to child dynamically

Looks like a common Vue change detection caveat.
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
This...
this.exploredFields[index] = true
won't be reactive. Try this instead
this.$set(this.exploredFields, index, true)

What you probably need is a watcher inside your child component, on the prop "e"
watch: {
e(newValue){
this.explored = newValue
}
}
Everything else looks fine to me, once you click on the field you are emitting an event and youre listening to that in the parent.

Related

How Can I capture parent component event in an event emitted by a child event

I'm trying to create a dynamic table in Vue in two levels: child component contain the cells with 'idx' as :key, and the parent has the child components in table with 'line' as :key.
My question is: when I capture the customized event #update-cellValue, I have the idx, oldValue and newValue brought to scope, but I don't have access to the details of the parent component where the event was emitted (which line from the v-for). I mean, my intention is to have both (line, idx) and work like a matrix (2D array).
I have a workaround doing it all in one level, just using v-for directly in "child's component" , but I would like to have it separately in two different components
Child (Nested) component -> Table line containing cells
<template>
<div >
<tr class='center'>
<!-- add title to the tr as prop -->
<td
id='cell'
#click="selectCellEv"
v-for="(cell, idx) in cells" :key="idx">
<input type=number #input="updateCellValueEv"
v-model.lazy.trim="cells[idx]"/>
</td>
</tr>
</div>
</template>
<script>
// var editable = document.getElementById('cell');
// editable.addEventListener('input', function() {
// console.log('Hey, somebody changed something in my text!');
// });
export default {
name: 'CaRow',
props: {
cellValArr: {
type: Array,
required: false,
default: () => [],
},
title: {
type: String,
default: `noNameRow`
}
},
data(){
return {
cells: []
}
},
methods: {
updateCellValueEv(event){
const idx = event.target.parentElement.cellIndex;
let val = event.target.value;
if (val === '') val = 0;
const oldNumber = parseFloat(this.cells[idx]);
const newNumber = parseFloat(val);
if(!isNaN(newNumber) && !isNaN(oldNumber)){
// console.log(`new ${newNumber}, old ${oldNumber}`)
this.$emit('update-cellValue', idx, newNumber, oldNumber);
this.cells[idx] = newNumber;
}
},
},
created() {
this.cellValArr.map((val,idx) => {
this.cells[idx] = val;
});
},
}
</script>
Parent component (will be used directly in the app)-> Table containing the child components as lines
<table class="center">
<CaRow
v-for="(line, idx) in table"
:key="idx"
:cellValArr="table[idx]"
#update-cellValue="updateCellVal"
>{{line}} </CaRow>
</table>
</template>
<script>
import CaRow from './components/CaRow.vue'
export default {
name: 'App',
components: {
CaRow
},
data(){
return{
table: [[1,2,3],
[4,5,6],
[7,8,9]],
selectedCell: null,
}
},
methods: {
updateCellVal(idx, newValue, oldValue) {
console.log(`cell[${idx}: New Value= ${newValue}, Old Value= ${oldValue}]`)
// this.table[line][idx] = newValue;
//HERE IS THE PROBLEM!!!
//LINE IS NOT IN THIS SCOPE
}
},
}
</script>
In CaRow.vue, wrap the event data into a single object:
//this.$emit('update-cellValue', idx, newNumber, oldNumber);
this.$emit('update-cellValue', { idx, newValue: newNumber, oldValue: oldNumber });
Then in the parent template, update the event handler binding to pass the line index (which is idx in this context) and $event (a special variable that stores the emitted event data):
<CaRow #update-cellValue="updateCellVal(idx, $event)">
And update the handler to receive the line index and $event:
export default {
methods: {
updateCellVal(line, { idx, newValue, oldValue }) {
console.log({ line, idx, newValue, oldValue });
}
}
}
Vue 2 cannot detect the change when you directly set an item with the index, so you have to use this.$set():
//this.table[line][idx] = newValue;
this.$set(this.table[line], idx, newValue);
demo

why vue v-for doesn't rerender child component when data change?

parent
export default {
name: "App",
components: {ChildComponent},
data: function() {
return {
itemList: []
};
},
methods: {
fetchData() {
callApi().then(res => { this.itemList= res; })
}
}
};
<template>
<ol>
<template v-for="(item) in itemList">
<li :key="item.id">
<child-component :item="item"></card>
</li>
</template>
</ol>
</template>
child
export default {
name: "ChildComponent",
props: {
item: { type: Object }
},
data: function() {
const {
name,
address,
.......
} = this.item;
return {
name,
address,
......
};
},
};
</script>
child get the item props which is an object.
I'm confused why the itemList point to another array, but the child doesn't update?
is it because key doesnt change? (but other data changed..)
thanks
It is because of this part of your code:
const {
name,
address,
} = this.item;
return {
name,
address,
};
What it does it copies name and address from item and return them.
It happens only once in your code while component is created.
If after that your prop item change, your data doesn't copy it and still returns the first values.
Solution:
If you don't change name or address in a child component, just use a prop
this.item.name in a script or {{ item.name }} in a template
It is already reactive so your component will update when prop changes.
Not sure what you do in your child component but this is enough to do and should react to your parent component changes. Basically the idea is to use the prop. I made some tweaks also.
export default {
name: "App",
components: {ChildComponent},
data: function() {
return {
itemList: []
};
},
methods: {
fetchData() {
callApi().then(res => { this.itemList= res; })
}
}
};
<template>
<ol>
<li v-for="(item) of itemList" :key="item.id">
<card :item="item"></card>
</li>
</ol>
</template>
export default {
name: "ChildComponent",
props: {
item: {
type: Object,
required: true
}
}
};
</script>
<template>
<div>
{{ item.name }} {{ item.address }}
</di>
</template>
It is because the data function is called only once on the creation of the component.
The recommended way to get data that depend on other data is to use computed.
export default {
name: "ChildComponent",
props: {
item: { type: Object }
},
computed: {
name() {
return this.item.name;
},
address() {
return this.item.address;
}
}
};
https://v2.vuejs.org/v2/guide/computed.html#Basic-Example

how to make el-select and v-model work together when extracting a custom component

I'm using el-select to build a select component. Something like this:
<template>
//omitted code
<el-select v-model="filterForm.client"
filterable
remote
placeholder="Please enter a keyword"
:remote-method="filterClients"
:loading="loading">
<el-option
v-for="item in clientCandidates"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<scripts>
export default {
data() {
filterForm: {
client: ''
},
clientCandidates: [],
loading: false
},
methods: {
filterClients(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.clientCandidates = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
}, 200);
} else {
this.clientCandidates = [];
}
}
}
}
</scripts>
So far so good, but since the component will appear in different pages, so I want to extract a custom component to avoid duplication.
According to the guideline,
v-model="fullName"
is equivalent to
v-bind:value="fullName"
v-on:input="$emit('input', $event)"
So I extracted the select component like this:
<template>
<el-select
v-bind:value="clientId"
v-on:input="$emit('input', $event)"
placeholder="Filter by short name"
filterable="true"
remote="true"
:remote-method="filter"
:loading="loading">
<el-option
v-for="item in clients"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<scripts>
export default {
props: {
clientId: {
type: String,
required: true
}
},
data() {
return {
clients: [],
loading: false,
}
},
methods: {
filter(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}];
}, 200);
} else {
this.clients = [];
}
}
}
}
</scripts>
And the parent component looks like this:
<select-client v-model="filterForm.clientId"></select-client>
The select drop down works fine, but unfortunately, the select does not reveal the option I selected, it remains empty after I choose an option. I suspect that maybe I should switch the v-on:input to 'v-on:change', but it does not work either.
UPDATE
I created a simple example, you can clone it here, please checkout the el-select-as-component branch. Run
npm install
npm run dev
You will see a simple page with 3 kinds of select:
The left one is a custom component written in raw select, it works fine.
The middle one is a custom component written in el-select, the dropdown remains empty but you can see the filterForm.elClientId in the console once you click Filter button. This is why I raise this question.
The right one is a plain el-select, it works fine.
The guideline says v-model is equivalent to v-bind:value and v-on:input but if you look closer, in the listener function, the variable binded is set with the event property. What you do in your exemple isn't the same, in your listener you emit another event. Unless you catch this new event, your value will never be set.
Another thing is you can't modify a props, you should consider it like a read-only variable.
If you want to listen from the parent to the emitted event into the child component, you have to do something like this
<template>
<el-select
:value="selected"
#input="dispatch"
placeholder="Filter by short name"
:filterable="true"
:remote="true"
:remote-method="filter"
:loading="loading">
<el-option
v-for="item in clients"
:key="item._id"
:label="item.name"
:value="item._id">
</el-option>
</el-select>
</template>
<script>
export default {
name: 'SelectClient',
data() {
return {
selected: '',
clients: [],
loading: false,
}
},
methods: {
filter(query) {
if (query !== '') {
this.loading = true;
setTimeout(() => {
this.loading = false
this.clients = [{_id: '1', name: 'foo'}, {_id: '2', name: 'bar'}]
}, 200)
} else {
this.clients = []
}
},
dispatch (e) {
this.$emit('input', e)
this.selected = e
}
}
}
</script>
NB: a v-model + watch pattern will work too. The important thing is to $emit the input event, so the v-model in the parent will be updated.
And in your parent you can use this component like this: <select-client v-model="clientId"/>.
Tips: if you want to modify the same data in different place, you should have a single source of truth and prefer something like vuex. Then your component will be like this
<template lang="html">
<select
v-model="clientId">
<option
disabled
value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</template>
<script>
export default {
data () {
return {
clientId: ''
}
},
watch: {
clientId (newValue) {
// Do something else here if you want then commit it
// Of course, listen for the 'setClientId' mutation in your store
this.$store.commit('setClientId', newValue)
}
}
}
</script>
Then in your other components, you can listen to $store.state.clientId value.

Passing data from a component to the parent viewmodel in Vue.js 2.x

I'm just starting with Vue.js and I'm puzzled as of how complicated it is to pass data from a component to the parent viewmodel/component.
My usecase is as follows:
I have a custom rangeslider component (https://www.npmjs.com/package/vue-range-slider). I use 3 of these in a wrapping component Typetest.vue:
<template>
<div class="typetest">
<div :class="'slider1-val-' + slider1RoundedValue">
<range-slider
class="slider"
min="1"
max="3"
step="0.01"
v-model="sliderValue1">
</range-slider>
</div>
<div :class="'slider2-val-' + slider2RoundedValue">
<range-slider
class="slider"
min="1"
max="3"
step="0.01"
v-model="sliderValue2">
</range-slider>
</div>
<div :class="'slider3-val-' + slider3RoundedValue">
<range-slider
class="slider"
min="1"
max="3"
step="0.01"
v-model="sliderValue3">
</range-slider>
<p>{{ slider3Texts[slider3RoundedValue] }}</p>
</div>
<p>Your Choice: {{ sliderSum }}</p>
<!-- this value needs to be passed to the parent viewmodel whenever it changes -->
</div>
</template>
<script>
import RangeSlider from 'vue-range-slider'
import 'vue-range-slider/dist/vue-range-slider.css'
export default {
name: 'typetest',
data () {
return {
sliderValue1: 2,
sliderValue2: 2,
sliderValue3: 2,
}
},
computed: {
slider1RoundedValue: function() {
return this.calcSliderValue(this.sliderValue1).toString();
},
slider2RoundedValue: function() {
return (this.calcSliderValue(this.sliderValue2)*10).toString();
},
slider3RoundedValue: function() {
return (this.calcSliderValue(this.sliderValue3)*100).toString();
},
sliderSum: function() {
return this.slider1RoundedValue*1 + this.slider2RoundedValue*1 + this.slider3RoundedValue*1
},
},
methods: {
hello: function() { console.log(this.sliderSum); },
calcSliderValue: function(val) {
switch (true) {
case val < 1.67:
return 1
break
case val < 2.34:
return 2
break
default:
return 3
}
}
},
components: {
RangeSlider
}
}
</script>
Now, whenever the computed property sliderSum mutates, I need to pass the result to the parent viewmodel. Several other components then need to update according to the parent viewmodel's sliderSum property, which at all times needs to be in sync with the component's computed property. It reflects the state of the three range sliders in one value. Depending on this sum I can decide in the parent which texts and images to show.
How would I achieve this?
Okay, I solved it doing the following:
1) I added a data property sliderSum on the parent viewmodel,
initially putting an empty string.
2) I added a method to the parent viewmodel that would update the sliderSum property when called, with the argument passed.
data() {
return {
sliderSum: ""
}
},
methods: {
updateSliderValue: function(newVal) {
this.sliderSum = newVal;
}
}
3) In my parent viewmodel's template, I pass the updateSliderValue method to be called whenever an event sliderValueHasMutated is emitted:
<typetest #sliderValueHasMutated="updateSliderValue"></typetest>
4) Inside the Typetest.vue component, I changed the (indepent, though identically named) computed property sliderSum as follows:
sliderSum: function () {
var sum = this.slider1RoundedValue * 1 + this.slider2RoundedValue * 1 + this.slider3RoundedValue * 1
this.$emit('sliderValueHasMutated', sum)
return sum
},
So now, whenever the computed property changes, it emits a sliderValueHasMutated event that triggers the parent's updateValue() method.
Done!

VueJs: Textarea input binding

I'm trying to figure out how to detect change of the value on the textarea from within the component.
For input we can simply use
<input
:value="value"
#input="update($event.target.value)"
>
However on textarea this won't work.
What I'm working with is the CKEditor component, which should update wysiwyg's content when model value of the parent component (attached to this child component) is updated.
My Editor component currently looks like this:
<template>
<div class="editor" :class="groupCss">
<textarea :id="id" v-model="input"></textarea>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: ''
},
id: {
type: String,
required: false,
default: 'editor'
}
},
data() {
return {
input: this.$slots.default ? this.$slots.default[0].text : '',
config: {
...
}
}
},
watch: {
input(value) {
this.update(value);
}
},
methods: {
update(value) {
CKEDITOR.instances[this.id].setData(value);
},
fire(value) {
this.$emit('input', value);
}
},
mounted () {
CKEDITOR.replace(this.id, this.config);
CKEDITOR.instances[this.id].setData(this.input);
this.fire(this.input);
CKEDITOR.instances[this.id].on('change', () => {
this.fire(CKEDITOR.instances[this.id].getData());
});
},
destroyed () {
if (CKEDITOR.instances[this.id]) {
CKEDITOR.instances[this.id].destroy()
}
}
}
</script>
and I include it within the parent component
<html-editor
v-model="fields.body"
id="body"
></html-editor>
however, whenever parent component's model value changes - it does not trigger the watcher - effectively not updating the editor's window.
I only need update() method to be called when parent component's model fields.body is updated.
Any pointer as to how could I approach it?
That's a fair bit of code to decipher, but what I would do is break down the text area and the WYSIWYG HTML window into two distinct components and then have the parent sync the values, so:
TextArea Component:
<template id="editor">
<textarea :value="value" #input="$emit('input', $event.target.value)" rows="10" cols="50"></textarea>
</template>
/**
* Editor TextArea
*/
Vue.component('editor', {
template: '#editor',
props: {
value: {
default: '',
type: String
}
}
});
All I'm doing here is emitting the input back to the parent when it changes, I'm using input as the event name and value as the prop so I can use v-model on the editor. Now I just need a wysiwyg window to show the code:
WYSIWYG Window:
/**
* WYSIWYG window
*/
Vue.component('wysiwyg', {
template: `<div v-html="html"></div>`,
props: {
html: {
default: '',
type: String
}
}
});
Nothing much going on there, it simply renders the HTML that is passed as a prop.
Finally I just need to sync the values between the components:
<div id="app">
<wysiwyg :html="value"></wysiwyg>
<editor v-model="value"></editor>
</div>
new Vue({
el: '#app',
data: {
value: '<b>Hello World</b>'
}
})
Now, when the editor changes it emits the event back to the parent, which updates value and in turn fires that change in the wysiwyg window. Here's the entire thing in action: https://jsfiddle.net/Lnpmbpcr/