Interpolation doesn't rerender in parent component after custom event from child - vue.js

I have select displayed in v-for loop:
<div v-for="(n, key) in selectedLanguages">
<select class="input input__col"
v-model="currentLang[key]"
#change="changeLanguage(currentLang[key], key)"
id="lang_select">
<option value="pl">Polski</option>
<option value="en">Angielski</option>
<option value="es">Hiszpański</option>
</select>
</div>
To each select I'm adding changeLanguage method which is:
<script>
export default {
data() {
return {
currentLang: []
}
},
methods: {
changeLanguage(value, key) {
let data = { value, key };
this.$nuxt.$emit('change::language', data);
}
},
props: ['selectedLanguages']
}
</script>
and it is in child component. In parent I'm listening for this change::language event:
this.$nuxt.$on('change::language', res => {
console.log(res);
this.selectedLanguages[res.key] = res.value;
console.log(this.selectedLanguages);
Although it's working correctly and it's updating selectedLanguages array just fine it doesn't rerender interpolation {{ selectedLanguages }} in parent. However it's correctly rerendering interpolation {{ selectedLanguages }} in child where it's passed by props. Why?
It seems like vue doesn't "catch" that selectedLanguages array have been changed. It only see when I .push or .pop this array. Is there something like apply method in vue?
I found this link in documentation: https://v2.vuejs.org/v2/guide/list.html#Caveats and added this.$set(this.selectedLanguages, res.value, res.key); in parent below my assignment but it didn't fix.

I found solution in Vue docs:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
I was doing exacly like above.
The solution is instead
this.selectedLanguages[res.key] = res.value;
use
this.$set(this.selectedLanguages, res.key, res.value);
which is basically a bit weird but it works.

Related

Vue.js this.$refs empty due to v-if

I have a simple Vue component that displays an address, but converts into a form to edit the address if the user clicks a button. The address field is an autocomplete using Google Maps API. Because the field is hidden (actually nonexistent) half the time, I have to re-instantiate the autocomplete each time the field is shown.
<template>
<div>
<div v-if="editing">
<div><input ref="autocomplete" v-model="address"></div>
<button #click="save">Save</button>
</div>
<div v-else>
<p>{{ address }}</p>
<button #click="edit">Edit</button>
</div>
</div>
</template>
<script>
export default {
data() {
editing: false,
address: ""
},
methods: {
edit() {
this.editing = true;
this.initAutocomplete();
},
save() {
this.editing = false;
}
initAutocomplete() {
this.autocomplete = new google.maps.places.Autocomplete(this.$refs.autocomplete, {});
}
},
mounted() {
this.initAutocomplete();
}
}
I was getting errors that the autocomplete reference was not a valid HTMLInputElement, and when I did console.log(this.$refs) it only produced {} even though the input field was clearly present on screen. I then realized it was trying to reference a nonexistent field, so I then tried to confine the autocomplete init to only when the input field should be visible via v-if. Even with this, initAutocomplete() is still giving errors trying to reference a nonexistent field.
How can I ensure that the reference exists first?
Maybe a solution would be to use $nextTick which will wait for your DOM to rerender.
So your code would look like :
edit() {
this.editing = true;
this.$nextTick(() => { this.initAutocomplete(); });
},
Moreover if you try to use your this.initAutocomplete(); during mounting it cannot work since the $refs.autocomplete is not existing yet but I'm not sure you need it since your v-model is already empty.
I think it's because your "refs" is plural
<input refs="autocomplete" v-model="address">
It should be:
<input ref="autocomplete" v-model="address">

Vuejs v-model binding with a select box inside a slot

Good afternoon,
I seem to have occured an issue with v-model binding in scoped slots.
I've tried to create a universal API form that would allow me to hook any URL to it, add any amount and any type of DOM elements inside a scoped slot and use data fetched from API accordingly.
So far I've managed to do the first part - fetch data and pass it to elements inside; the issue I'm having now is thanks to one-way data flow - selecting an option inside of <select></select> doesn't seem to update selectId accordingly - and I get it, that's the pattern it follows... now, how do I work around it?
edit:
If I copy this select directly into the component (without passing via scoped slot) and replce props.* with just * (props.selectedId to selectedId) - it'll work flawlessly.
It only doesn't work because props are one-way.
<template>
<form :action="action" :method="method">
<slot :selectedId="selectedId" :results="results"></slot>
</form>
</template>
<script>
export default
{
props: ['action', 'method', 'url'],
data: () =>
({
results: [],
selectedId: 0,
}),
created()
{
setTimeout(() =>
{
axios.get(this.url).then(response => (this.results = response.data))
}, 500)
},
}
</script>
and HTML:
<api-form action="/blog" method="POST" url="/api/v1/blog">
<template slot-scope="props">
<select class="form-control mb-3" v-model="props.selectedId">
<option v-for="entry, i in props.results" :value="entry">#{{ entry.title }}</option>
</select>
<button class="btn btn-danger">Delete</button>
</template>
</api-form>
Gosh, I keep posting here and finding an answer afterwards.
4 hours of googling - nothing, then I post here and suddenly come up with a solution.
For anyone having the same issue, this is caused by the fact that all non-components, if you apply $emit to it, it'll be called from <Root>, meaning you have to edit your:
created()
{
this.$on('update:selectedId', (value) =>
{
this.selectedId = value
})
},
And change it to this:
created()
{
this.$root.$on('update:selectedId', (value) =>
{
this.selectedId = value
})
},
You can pass a callback as a slot prop that will modify the data being passed also as a slot prop, here's an answer of mine on a similar question v-model and scoped slots not working?

Modify props.value from within child component

I am new to Vue and trying to build a "dropdown" component. I want to use it from a parent component like this:
<my-dropdown v-model="selection"></my-dropdown>
where selection is stored as data on the parent, and should be updated to reflect the user's selection. To do this I believe my dropdown component needs a value prop and it needs to emit input events when the selection changes.
However, I also want to modify the value from within the child itself, because I want to be able to use the dropdown component on its own (I need to modify it because otherwise the UI will not update to reflect the newly selected value if the component is used on its own).
Is there a way I can bind with v-model as above, but also modify the value from within the child (it seems I can't, because value is a prop and the child can't modify its own props).
You need to have a computed property proxy for a local value that handles the input/value values.
props: {
value: {
required: true,
}
},
computed: {
mySelection: {
get() {
return this.value;
},
set(v) {
this.$emit('input', v)
}
}
}
Now you can set your template to use the mySelection value for managing your data inside this component and as it changes, the data is emitted correctly and is always in sync with the v-model (selected) when you use it in the parent.
Vue's philosophy is: "props down, events up". It even says this in the documentation: Composing Components.
Components are meant to be used together, most commonly in
parent-child relationships: component A may use component B in its own
template. They inevitably need to communicate to one another: the
parent may need to pass data down to the child, and the child may need
to inform the parent of something that happened in the child. However,
it is also very important to keep the parent and the child as
decoupled as possible via a clearly-defined interface. This ensures
each component’s code can be written and reasoned about in relative
isolation, thus making them more maintainable and potentially easier
to reuse.
In Vue, the parent-child component relationship can be summarized as
props down, events up. The parent passes data down to the child via
props, and the child sends messages to the parent via events. Let’s
see how they work next.
Don't try to modify the value from within the child component. Tell the parent component about something and, if the parent cares, it can do something about it. Having the child component change things gives too much responsibility to the child.
You could use the following pattern:
Accept 1 input prop
Have another variable inside your data
On creation, white the incoming prop into your data variable
Using a watcher, watch the incoming prop for changes
On a change inside your component, send the change away
Demo:
'use strict';
Vue.component('prop-test', {
template: "#prop-test-template",
props: {
value: { // value is the default prop used by v-model
required: true,
type: String,
},
},
data() {
return {
dataObject: undefined,
// For testing purposes:
receiveData: true,
sendData: true,
};
},
created() {
this.dataObject = this.value;
},
watch: {
value() {
// `If` is only here for testing purposes
if(this.receiveData)
this.dataObject = this.value;
},
dataObject() {
// `If` is only here for testing purposes
if(this.sendData)
this.$emit('input', this.dataObject);
},
},
});
var app = new Vue({
el: '#app',
data: {
test: 'c',
},
});
<script src="https://unpkg.com/vue#2.0.1/dist/vue.js"></script>
<script type="text/x-template" id="prop-test-template">
<fieldset>
<select v-model="dataObject">
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
<option value="d">d</option>
<option value="e">e</option>
<option value="f">f</option>
<option value="g">g</option>
<option value="h">h</option>
<option value="i">i</option>
<option value="j">j</option>
</select>
<!-- For testing purposed only: -->
<br>
<label>
<input type="checkbox" v-model="receiveData">
Receive updates
</label>
<br>
<label>
<input type="checkbox" v-model="sendData">
Send updates
</label>
<!--/ For testing purposed only: -->
</fieldset>
</script>
<div id="app">
<prop-test v-model="test"></prop-test>
<prop-test v-model="test"></prop-test>
<prop-test v-model="test"></prop-test>
</div>
Notice that this demo has a feature that you can turn off the propagation of the events per select box, so you can test if the values are properly updated locally, this is of course not needed for production.
If you want to modify a prop inside a component, I recommend passing a "default value" prop to your component. Here is how I would do that
<MyDropdownComponent
:default="defaultValue"
:options="options"
v-model="defaultValue"
/>
And then there are 2 options to how I would go from there -
Option 1 - custom dropdown element
As you're using custom HTML, you won't be able to set a selected attribute. So you'll need to be creative about your methods.
Inside your component, you can set the default value prop to a data attribute on component creation. You may want to do this differently, and use a watcher instead. I've added that to the example below.
export default {
...
data() {
return {
selected: '',
};
},
created() {
this.selected = this.default;
},
methods: {
// This would be fired on change of your dropdown.
// You'll have to pass the `option` as a param here,
// So that you can send that data back somehow
myChangeMethod(option) {
this.$emit('input', option);
},
},
watch: {
default() {
this.selected = this.default;
},
},
};
The $emit will pass the data back to the parent component, which won't have been modified. You won't need to do this if you're using a standard select element.
Option 2 - Standard select
<template>
<select
v-if="options.length > 1"
v-model="value"
#change="myChangeMethod"
>
<option
v-for="(option, index) of options"
:key="option.name"
:value="option"
:selected="option === default"
>{{ option.name }}</option>
</select>
</template>
<script>
export default {
...
data() {
return {
value: '',
};
},
methods: {
// This would be fired on change of your dropdown
myChangeMethod() {
this.$emit('input', this.value);
},
},
};
</script>
This method is definitely the easiest, and means you need to use the default select element.
You could use a custom form input component
Form Input Components using Custom Events
Basically your custom component should accept a value prop and emit input event when value changes

2-way binding in Vue 2.3 component

I understand the .sync modifier returned in Vue 2.3, and am using it for a simple child component which implements a 'multiple-choice' question and answer. The parent component calls the child like this:
<question
:stem="What is your favourite colour?"
:options="['Blue', 'No, wait, aaaaargh!']
:answer.sync="userChoice"
>
The parent has a string data element userChoice to store the result from the child component. The child presents the question and radio buttons for the options. The essential bits of the child look like this (I'm using Quasar, hence q-radio):
<template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<label >
<q-radio v-model="option" :val="opt.val" #input="handleInput"></q-radio>
{{opt.text}}
</label>
</div>
</div>
</template>
export default {
props: {
stem: String,
options: Array,
answer: String
},
data: () => ({
option: null
}),
methods: {
handleInput () {
this.$emit('update:answer', this.option)
}
}
}
This is all working fine, apart from the fact that if the parent then changes the value of userChoice due to something else happening in the app, the child doesn't update the radio buttons. I had to include this watch in the child:
watch: {
answer () {
this.option = this.answer
}
}
But it feels a little redundant, and I was worried that emitting the event to update the parent's data would in fact cause the child 'watch' event to also fire. In this case it would have no effect other than wasting a few cycles, but if it was logging or counting anything, that would be a false positive...
Maybe that is the correct solution for true 2-way binding (i.e. dynamic Parent → Child, as well as Child → Parent). Did I miss something about how to connect the 'in' and 'out' data on both sides?
In case you're wondering, the most common case of the parent wanting to change 'userChoice' would be in response to a 'Clear Answers' button which would set userChoice back to an empty string. That should have the effect of 'unsetting' all the radio buttons.
Your construction had some oddities that didn't work, but basically answer.sync works if you propagate it down to the q-radio component where the changing happens. Changing the answer in the parent is handled properly, but to clear values, it seems you need to set it to an object rather than null (I think this is because it needs to be assignable).
Update
Your setup of options is a notable thing that didn't work.
I use answer in the q-radio to control its checked state (v-model has special behavior in a radio, which is why I use value in conjunction with v-model). From your comment, it looks like q-radio wants to have a value it can set. You ought to be able to do that with a computed based on answer, which you would use instead of your option data item: the get returns answer, and the set does the emit. I have updated my snippet to use the val prop for q-radio plus the computed I describe. The proxyAnswer emits an update event, which is what the .sync modifier wants. I also implemented q-radio using a proxy computed, but that's just to get the behavior that should already be baked-into your q-radio.
(What I describe is effectively what you're doing with a data item and a watcher, but a computed is a nicer way to encapsulate that).
new Vue({
el: '#app',
data: {
userChoice: null,
options: ['Blue', 'No, wait, aaaaargh!'].map(v => ({
value: v,
text: v
}))
},
components: {
question: {
props: {
stem: String,
options: Array,
answer: String
},
computed: {
proxyAnswer: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('update:answer', newValue);
}
}
},
components: {
qRadio: {
props: ['value', 'val'],
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
}
}
}
},
methods: {
clearSelection() {
this.userChoice = {};
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<question stem="What is your favourite colour?" :options="options" :answer.sync="userChoice" inline-template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<div>Answer={{answer && answer.text}}, option={{opt.text}}</div>
<label>
<q-radio :val="opt" v-model="proxyAnswer" inline-template>
<input type="radio" :value="val" v-model="proxyValue">
</q-radio>
{{opt.text}}
</label>
</div>
</div>
</question>
<button #click="clearSelection">Clear</button>
</div>

Use selectize with data-binding in vuejs component with vuex

I made a component which is basically bootstrap modal. In the modal I want to use selectize (I also tried bootstrap select). The problem is that if the select has some static values it works, while if I fill it with dynamic data it fails with: Uncaught TypeError: Cannot read property 'appendChild' of null
Here is the html:
<div class="form-group">
<label for="newContactModalType">Type*</label>
<select id="newContactModalType" name="type_id">
<option v-for="type in clientTypes" value="{{ type.id }}">{{ type.name }}</option>
</select>
</div>
And the component:
export default {
vuex: {
getters: {
clientTypes: state => state.clientTypes.list
}
},
events: {
'showNewContactModal': () => {
$('#newContactModal').modal()
}
},
ready() {
$('#newContactModalType').selectize()
}
}
I'm using vuex to get the clientTypes list. As I realize this is where something's wrong, because it works if I define clientTypes in component's data property. I'm not sure where I should call .selectize() too. The best I could think of is the ready() method.
Thanks in advance!