Vue.js watch not triggering when object is modified - vue.js

In Vue.js I have three templates that should work together to trigger loading of new results. My container template is a general template that contain the filters and the results templates.
I am having some difficulties with making watch() trigger changes in my results template when an object is changed in the filters template. The flow is quite simple. Here is what I have at the moment and how the flow works:
Filters template: on click my object property is updated with a new value and emit this change to the container template.
<span v-on:click='setClient(client.ccid)'>{{client.name}}</span>
data: function() {
return {
formData: {
perPage: 15,
clientId: null
}
}
}
setClient: function(clientId){
this.formData.clientId = clientId;
this.$emit('filters-update', this.formData);
}
Container template: - this has just the role to hold the filters and results template and pass the modified object to the results template.
<template>
<div class="row">
<filters v-on:filters-update="filtersUpdate"></filters>
<results :filters='filters'></results>
</div>
</template>
<script>
export default {
data: function() {
return {
filters: {}
}
},
methods: {
filtersUpdate: function(params){
this.filters = params;
}
}
}
</script>
Results template:
export default {
props: {
filters: {
type: Object
},
}
}
watch: {
filters: function(){
console.log("loading new results..");
this.loadData();
}
}

Apparently to watch over object properties change, you need a deep watch
https://v2.vuejs.org/v2/api/#watch
watch: {
filters: {
deep: true,
handler(){
this.loadData();
}
}
}

Related

Use v-model with groupBy array, return flat array

I'm trying to set up a Vue component that takes a flat list of items in an array, groups them by a property for use in a sub-component, and emits the updated flat array.
My section component uses these grouped items in their v-model and emits the updated list. The section component is a drag-and-drop with some input fields, so items are changed under the section component and the updated list is emitted.
Here's an example of the component that takes the flat list as a prop:
<template>
<div>
<div v-for="section in template.sections" :key="section.id">
<h2>{{ section.name }}</h2>
<item-section :section="section" v-model="sectionData[section.id]"></item-section>
</div>
</div>
</template>
<script type="text/javascript">
import { groupBy } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
name: "ItemAssignment",
props: {
// All items in flat array
value: {
type: Array,
required: true,
default: () => [
/**
* {
* id: null,
* section_id: null,
* name: null
* }
*/
]
},
// Template (containing available sections)
template: {
type: Object,
default: () => {
return {
sections: [
/**
* {
* id: null,
* name: null
* }
*/
]
};
}
}
},
components: {
ItemSection
},
data() {
return {
sectionData: []
};
},
mounted() {},
computed: {
flattenedData() {
return Object.values(this.sectionData).flat();
}
},
methods: {},
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val) {
this.sectionData = groupBy(val, "section_id");
}
},
// --- Causing infinite loop ---
// flattenedData(val) {
// this.$emit("input", val);
// },
}
};
</script>
The parent of this component is basically this:
<template>
<div>
<!-- List items should be updatable here or from within the assignment component -->
<item-assignment v-model="listItems"></item-assignment>
</div>
</template>
<script type="text/javascript">
import ItemAssignment from "#/components/ItemAssignment.vue";
export default {
name: "ItemExample",
props: {
},
components: {
ItemAssignment
},
data() {
return {
listItems: []
};
},
mounted() {},
computed: {
},
methods: {
// Coming from API...
importExisting(list) {
var newList = [];
list.forEach(item => {
const newItem = {
id: null, // New record, so don't inherit ID
section_id: item.section_id,
name: item.name
};
newList.push(newItem);
});
this.listItems = newList;
}
},
watch: {
}
};
</script>
When emitting the finalized flat array, Vue goes into an infinite loop trying to re-process the list and the browser tab freezes up.
I believe the groupBy and/or Object.values(array).flat() method are stripping the reactivity out so Vue constantly thinks it's different data, thus the infinite loop.
I've tried manually looping through the items and pushing them to a temporary array, but have had the same issue.
If anyone knows a way to group and flatten these items while maintaining reactivity, I'd greatly appreciate it. Thanks!
So it makes sense why this is happening...
The groupBy function creates a new array, and since you're watching the array, the input event is triggered which causes the parent to update and pass the same value which gets triggered again in a loop.
Since you're already using lodash, you may be able to include the isEqual function that can compare the arrays
import { groupBy, isEqual } from "lodash";
import ItemSection from "#/components/Section.vue";
export default {
// ...redacted code...
watch: {
// Flat list updated
value: {
immediate: true,
deep: true,
handler(val, oldVal) {
if (!isEqual(val, oldVal))
this.sectionData = groupBy(val, "section_id");
}
},
flattenedData(val) {
this.$emit("input", val);
},
}
};
this should prevent the this.sectionData from updating if the old and new values are the same.
this could also be done in flattenedData, but would require another value to store the previous state.

VueJs Data Passed From Root to Child Component via Prop Results in only an observable object

I have an app which calls a web service in the created() function and populates a property of the root data object. The property is passed via a prop to a child component and using the Chrome dev tools I can see that the prop data is available on the child component.
The problem I have is that I try to set data properties in the child component using values passed via the prop I end up with undefined property data. If I use the Chrome inspection tools and add a breakpoint I can see that the prop is an observable object in the form of {__ob__: Observer} and as such, I cannot directly access any of the data. My suspicion is that the child object sets it's data properties before the web service call has completed in the root.
How can I overcome this?
I've created a JsFiddle for this:
https://jsfiddle.net/ProNotion/a8c6nqsg/
Vue.component("mycomponent", {
template: '#my-component-template',
props: ["customer_data"],
data() {
return {
form_data: {
customerEmail: this.customer_data.customerEmail1
}
}
}
});
new Vue({
el: "#app",
data() {
return {
customer: {}
};
},
methods: {
init() {
var self = this;
axios.get("https://0bb1313e-b089-432e-b6bc-250f6162d7f0.mock.pstmn.io/GetCustomerData")
.then(response => {
self.customer = response.data;
}).catch(response => {
console.error(response);
});
}
},
created() {
this.init();
}
});
Here is my HTML markup:
<div id="app">
<mycomponent :customer_data="customer" />
</div>
<script type="x-template" id="my-component-template">
<div>
<p>{{form_data.customerEmail1}}</p>
</div>
</script>
Check response data type and format
console.log(typeof response.data) // string
{ "customerEmail1": "me#example.com", } // Remove `,`
You must parse to JSON type
axios.get(...).then(response => {
self.customer = JSON.parse(response.data.replace(',', ''))
})
Set property to watch with `deep` option
[Deep watching](https://v2.vuejs.org/v2/api/#vm-watch) will be detect nested value changes inside Objects
```
Vue.component("mycomponent", {
template: '#my-component-template',
props: ["customer_data"],
data() {
return {
form_data: {}
}
},
watch: {
customer_data: {
handler (val) {
this.form_data = val;
},
deep: true
}
}
});
```
Demo: https://jsfiddle.net/ghlee/f4gewvqn

Vue.js - data value does not set from prop

What do I have: two components, parent and child.
Parent
<UserName :name=user.name></UserName>
...
components: {UserName},
data() {
return {
user: {
name: '',
...
}
}
},
created() {
this.fetchUser()
console.log(this.user) //<- object as it is expected
},
methods: {
fetchUser() {
let that = this
axios.get(//...)
.then(response => {
for (let key in response.data) {
that.user[key] = response.data[key]
}
})
console.log(that.user) //<- object as it is expected
}
}
Child
<h3 v-if="!editing" #click="edit">{{ usersName }}</h3>
<div v-if="editing">
<div>
<input type="text" v-model="usersName">
</div>
</div>
...
props: {
name: {
type: String,
default: ''
},
},
data() {
return {
editing: false,
usersName: this.name,
...
}
},
Problem: even when name prop is set at child, usersName data value is empty. I've inspected Vue debug extension - same problem.
What have I tried so far (nothing helped):
1) props: ['name']
2)
props: {
name: {
type: String
},
},
3) usersName: JSON.parse(JSON.stringify(this.name))
4) <UserName :name="this.user.name"></UserName>
P. S. when I pass static value from parent to child
<UserName :name="'just a string'"></UserName>
usersName is set correctly.
I've also tried to change name prop to some foobar. I guessed name might conflict with component name exactly. But it also didn't helped.
user.name is initially empty, and later gets a value from an axios call. usersName is initialized from the prop when it is created. The value it gets is the initial, empty value. When user.name changes, that doesn't affect the already-initialized data item in the child.
You might want to use the .sync modifier along with a settable computed, or you might want to put in a watch to propagate changes from the prop into the child. Which behavior you want is not clear.
Here's an example using .sync
new Vue({
el: '#app',
data: {
user: {
name: ''
}
},
methods: {
fetchUser() {
setTimeout(() => {
this.user.name = 'Slartibartfast'
}, 800);
}
},
created() {
this.fetchUser();
},
components: {
userName: {
template: '#user-name-template',
props: {
name: {
type: String,
default: ''
}
},
computed: {
usersName: {
get() { return this.name; },
set(value) { this.$emit('update:name', value); }
}
},
data() {
return {
editing: false
}
}
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
{{user.name}}
<user-name :name.sync=user.name></user-name>
</div>
<template id="user-name-template">
<div>
<input type="text" v-model="usersName">
</div>
</template>
should be passed like this...
<UserName :name="user.name"></UserName>
if data property is still not being set, in mounted hook you could set the name property.
mounted() {
this.usersName = this.name
}
if this doesn't work then your prop is not being passed correctly.
sidenote: I typically console.log within the mounted hook to test such things.

V-model with props & computed properties

I have a checkbox component that tracks whether or not an item has been saved by the user as a favorite. This information is passed in as a prop.
Because we can't/shouldn't mutate props passed in from a parent component, I am using v-model on a computed property.
<template>
<input class="favorite" type="checkbox" v-model="checked">
</template>
<script>
module.exports = {
props: ['favorite'],
computed: {
checked: {
get: function getChecked() {
return this.favorite;
},
set: function setChecked(newVal) {
this.$emit('update:favorite', newVal);
}
}
}
};
</script>
The parent component controls sending requests to the favorites api & updating the state of each entity if/when the request is successful.
<template>
<input-favorite
#update:favorite="toggleFavorite"
:favorite="entity.favorite"
></input-favorite>
</template>
<script>
module.exports = {
methods: {
toggleFavorite: function toggleFavorite(val) {
if (val) {
this.$store.dispatch('postFavorite', { id: this.entity.id, name: this.entity.name });
} else {
this.$store.dispatch('deleteFavorite', this.entity.id);
}
}
}
};
</script>
If the request fails, however, is it possible to prevent the checkbox from getting checked in the first place? Both this.favorite and this.checked stay in sync, but the state of the checkbox does not.
Because the data & props stay correct, I'm also having trouble figuring out how I could trigger a re-render of the checkbox to get it back to the correct state.
I suspect the problem is that favorite never changes, so Vue doesn't see a need to update. You should update it to true upon receiving the checked value (so state is consistent) and then update it again to false when the request fails.
Vue.component('inputFavorite', {
template: '#input-favorite',
props: ['favorite'],
computed: {
checked: {
get: function getChecked() {
return this.favorite;
},
set: function setChecked(newVal) {
this.$emit('update:favorite', newVal);
}
}
}
});
new Vue({
el: '#app',
data: {
entity: {
favorite: false
}
},
methods: {
toggleFavorite: function toggleFavorite(val) {
if (val) {
console.log("Post");
this.entity.favorite = true;
// Mock up a failure
setTimeout(() => {
console.log("Failed");
this.entity.favorite = false;
}, 250);
} else {
console.log("Delete");
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<template id="input-favorite">
<input class="favorite" type="checkbox" v-model="checked">
</template>
<div id="app">
<input-favorite #update:favorite="toggleFavorite" :favorite="entity.favorite"></input-favorite>
</div>
The way you have set this up lends itself to the recently-reintroduced .sync modifier, which would simplify your HTML a bit:
<input-favorite :favorite.sync="entity.favorite"></input-favorite>
Then you do away with toggleFavorite and instead add a watch:
watch: {
'entity.favorite': function (newValue) {
console.log("Updated", newValue);
if (newValue) {
setTimeout(() => {
console.log("Failed");
this.entity.favorite = false;
}, 250);
}
}
}

Dynamic html elements in Vue.js

How is it possible to add elements dynamically to the content? Example below:
<template>
{{{ message | hashTags }}}
</template>
<script>
export default {
...
filters: {
hashTags: function(value) {
// Replace hash tags with links
return value.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>')
}
}
}
</script>
Problem is that if I press the link no action will fire. Vue do not see new elements.
Update:
Based on this answer, you can do a similar dynamic-template component in Vue 2. You can actually set up the component spec in the computed section and bind it using :is
var v = new Vue({
el: '#vue',
data: {
message: 'hi #linky'
},
computed: {
dynamicComponent: function() {
return {
template: `<div>${this.hashTags(this.message)}</div>`,
methods: {
someAction() {
console.log("Action!");
}
}
}
}
},
methods: {
hashTags: function(value) {
// Replace hash tags with links
return value.replace(/#(\S*)/g, '<a v-on:click="someAction">#$1</a>')
}
}
});
setTimeout(() => {
v.message = 'another #thing';
}, 2000);
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="vue">
<component :is="dynamicComponent" />
</div>
Vue bindings don't happen on interpolated HTML. You need something Vue sees as a template, like a partial. However, Vue only applies bindings to a partial once; you can't go back and change the template text and have it re-bind. So each time the template text changes, you have to create a new partial.
There is a <partial> tag/element you can put in your HTML, and it accepts a variable name, so the procedure is:
the template HTML changes
register new partial name for the new template HTML
update name variable so the new partial is rendered
It's a little bit horrible to register something new every time there's a change, so it would be preferable to use a component with a more structured template if possible, but if you really need completely dynamic HTML with bindings, it works.
The example below starts out with one message, link-ified as per your filter, and after two seconds, changes message.
You can just use message as the name of the partial for registering, but you need a computed that returns that name after doing the registering, otherwise it would try to render before the name was registered.
var v = new Vue({
el: 'body',
data: {
message: 'hi #linky'
},
computed: {
partialName: function() {
Vue.partial(this.message, this.hashTags(this.message));
return this.message;
}
},
methods: {
someAction: function() {
console.log('Action!');
},
hashTags: function(value) {
// Replace hash tags with links
return value.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>')
}
}
});
setTimeout(() => {
v.$set('message', 'another #thing');
}, 2000);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<partial :name="partialName"></partial>
I just learned about $compile, and it seems to fit your need very nicely. A very simple directive using $compile avoids all the registrations.
Vue.directive('dynamic', function(newValue) {
this.el.innerHTML = newValue;
this.vm.$compile(this.el);
});
var v = new Vue({
el: 'body',
data: {
message: 'hi #linky'
},
computed: {
messageAsHtml: function() {
return this.message.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>');
}
},
methods: {
someAction: function() {
console.log('Action!');
}
}
});
setTimeout(() => {
v.$set('message', 'another #thing');
}, 2000);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div v-dynamic="messageAsHtml"></div>
In Vue.js 2 it's easier:
new Vue({
...,
computed: {
inner_html() {
return ...; // any raw html
},
},
template: `<div v-html='inner_html'></div>`,
});
The best solution I found which works fine with custom html is looks like this, it's like you kind of create new component each times the html property changes. No actually one did this, we just use computed property for creating new component.
That is how it looks:
new Vue({
el: "#root",
data: {
value: '',
name: 'root',
htmlData: '<div><input #input="onInputProxy($event)" ' +
'v-model="value" ' +
'v-for="i in 3" ' +
':ref="`customInput${i}`"></div>'
},
computed: {
// our component is computed property which returns the dict
htmlDataComponent () {
return {
template: this.htmlData, // we use htmlData as template text
data() {
return {
name: 'component',
value: ''
}
},
created () {
// value of "this" is formComponent
console.log(this.name + ' created');
},
methods: {
// proxy components method to parent method,
// actually you done have to
onInputProxy: this.onInput
}
}
}
},
methods: {
onInput ($event) {
// while $event is proxied from dynamic formComponent
// value of "this" is parent component
console.log(this.name + ' onInput');
// use refs to refer to real components value
console.log(this.$refs.htmlDataComponent.value);
console.log(this.$refs.htmlDataComponent.$refs.customInput1);
console.log(this.$refs.htmlDataComponent.$refs.customInput2);
console.log(this.$refs.htmlDataComponent.$refs.customInput3);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js">
</script>
<div id="root">
<component ref="htmlDataComponent"
v-if="htmlData"
:is="htmlDataComponent"></component>
</div>
I did not check it for memory efficiency, but it looks like works just fine.
Modified version of #RoyJ's answer, works in Vue.js v2.6.10
new Vue({
...,
computed: {
inner_html() {
return ...; // any raw html
},
},
directives: {
dynamic: {
bind(el, binding) {
el.innerHTML = binding.value;
},
update(el, binding) {
el.innerHTML = binding.value;
},
},
},
template: `<div v-dynamic='inner_html'></div>`,
});
Since partial has been removed from VueJS 2 (https://v2.vuejs.org/v2/guide/migration.html#Vue-partial-removed)
A better way may be to create a component which processes its content and create appropriate DOM elements
The above component will replace hashtags by clickable links
<process-text>Hi #hashtag !</process-text>
Vue.component('process-text', {
render: function (createElement) {
var hashtagRegex = /(^|\W)(#[a-z\d][\w-]*)/ig
var text = this.$slots.default[0].text
var list = text.split(hashtagRegex)
var children = []
for (var i = 0; i < list.length; i++) {
var element = list[i]
if (element.match(hashtagRegex)) {
children.push(createElement('a', {
attrs: {
href: 'https://www.google.fr/search?q=' + element,
target: "_blank"
},
domProps: {
innerHTML: element
}
}))
} else {
children.push(element)
}
}
}
return createElement('p', {}, children) // VueJS expects root element
})