Vue.js reactivity inner mechanisms - vue.js

Given the following Vue code, how does it know to render the v-for again when selected is changed?
It makes complete sense to me when todos is changed.
So, Vue notices that there's a method isSelected involved and then uses "reflection" to watch the selected value, since it's an instance value?
Is that what's happening under the hood?
<div id="app">
<ol>
<li v-for="todo in todos" :class="{ 'selected': isSelected(todo.text) }">
{{ todo.text }}
</li>
</ol>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
todos: [
{ text: 'foo' },
{ text: 'bar' },
{ text: 'quz' }
],
'selected': 'bar'
},
methods: {
isSelected: function(text) {
return text != this.selected;
}
}
})
app.todos.push({ text: 'test' });
app.todos[0].text = 'change';
app.selected = 'foo';
</script>

Related

vue reactivity in list with slot scoped component

Good afternoon!
I have a loop and inside each element I display a separate component that takes props and gives them through the slot. I can't figure out why when new elements are added the update hooks happen on the old ones?
Example:
have: 1 2 3
add: 4
log: created 4, updated 3, updated 2, updated 1
Why is this happening?
Vue.component('slot-component', {
inheritAttrs: false,
created() {
console.log('created', this._uid);
},
updated() {
console.log('updated', this._uid);
},
render(h) {
return this.$scopedSlots.default(this.$attrs);
}
})
new Vue({
el: '#app',
data() {
return {
list: []
}
},
methods: {
add() {
this.list.push({
title: 'some'
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
<div id="app">
<div class="list">
<div
v-for="(item, n) in list"
:key="n"
class="list-item"
>
<slot-component
:title="item.title"
v-slot="props"
>
{{ props.title }}
</slot-component>
</div>
<button #click="add">add</button>
</div>
</div>
Update
Thanks to Raphael Rollet for clarifying that all array elements are updated when the array is changed globally. But then it is not entirely clear why this behavior behaves differently with a component that uses props
Pass and use props — not call updated
Vue.component('slot-component', {
props: ['title'],
created() {
console.log('created', this._uid);
},
updated() {
console.log('updated', this._uid);
},
template: `<div>{{ title }}</div>`
})
new Vue({
el: '#app',
data() {
return {
list: []
}
},
methods: {
add() {
this.list.push({ title: 'some' });
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
<div id="app">
<div class="list">
<slot-component
v-for="(item, n) in list"
:key="n"
:title="item.title"
class="list-item"
>
</slot-component>
<button #click="add">add</button>
</div>
</div>
Pass and use props with slot — call updated
Vue.component('slot-component', {
props: ['title'],
created() {
console.log('created', this._uid);
},
updated() {
console.log('updated', this._uid);
},
template: `<div><slot>{{ title }}</slot></div>`
})
new Vue({
el: '#app',
data() {
return {
list: []
}
},
methods: {
add() {
this.list.push({ title: 'some' });
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
<div id="app">
<div class="list">
<slot-component
v-for="(item, n) in list"
:key="n"
:title="item.title"
class="list-item"
>
{{ item.title }}
</slot-component>
<button #click="add">add</button>
</div>
</div>
Use $attrs — call updated
Vue.component('slot-component', {
created() {
console.log('created', this._uid);
},
updated() {
console.log('updated', this._uid);
},
template: `<div>{{ $attrs.title }}</div>`
})
new Vue({
el: '#app',
data() {
return {
list: []
}
},
methods: {
add() {
this.list.push({ title: 'some' });
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
<div id="app">
<div class="list">
<slot-component
v-for="(item, n) in list"
:key="n"
:title="item.title"
class="list-item"
>
</slot-component>
<button #click="add">add</button>
</div>
</div>
Use slot — call updated
Vue.component('slot-component', {
created() {
console.log('created', this._uid);
},
updated() {
console.log('updated', this._uid);
},
template: `<div><slot></slot></div>`
})
new Vue({
el: '#app',
data() {
return {
list: []
}
},
methods: {
add() {
this.list.push({ title: 'some' });
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
<div id="app">
<div class="list">
<slot-component
v-for="(item, n) in list"
:key="n"
:title="item.title"
class="list-item"
>
<template #default></template>
</slot-component>
<button #click="add">add</button>
</div>
</div>
The behaviour who seems "weird" is all time because vue binding.
You'r array is update so all property inside is update, someone can have a nice explanation on that because it's too deep.
But the solution is not use a update function on your child (if you don't want update something on every child), but set a watcher on what you want and do something in that case.
Like if you update a title on a child you do that:
watch: {
title(value) {
console.log(value)
}
}
Update:
For the two last example in your update section, like i explain for me it's expected, it's seems weird yes, but it's binding with vue.
About the first, you use a props so you pass a string in your component and nothing else. He have no reason to update.
For the second you do the same thing but with a slot, the only explanation for me it's, when you initialise a slot (even empty) the slot access to the parent data, and because binding is update.
I found this:
https://vuejs.org/guide/components/slots.html#render-scope
Slot content has access to the data scope of the parent component,
because it is defined in the parent.
Be honest it's assumption, if anyone have a other explanation i want to know it !

Vuejs v-on click doesn't work inside component

I use VueJs and I create the following component with it.
var ComponentTest = {
props: ['list', 'symbole'],
data: function(){
return {
regexSymbole: new RegExp(this.symbole),
}
},
template: `
<div>
<ul>
<li v-for="item in list"
v-html="replaceSymbole(item.name)">
</li>
</ul>
</div>
`,
methods: {
replaceSymbole: function(name){
return name.replace(this.regexSymbole, '<span v-on:click="test">---</span>');
},
test: function(event){
console.log('Test ...');
console.log(this.$el);
},
}
};
var app = new Vue({
el: '#app',
components: {
'component-test': ComponentTest,
},
data: {
list: [{"id":1,"name":"# name1"},{"id":2,"name":"# name2"},{"id":3,"name":"# name3"}],
symbole: '#'
},
});
and this my html code
<div id="app">
<component-test :list="list" :symbole="symbole"></component-test>
</div>
When I click on the "span" tag inside "li" tag, nothing append.
I don't have any warnings and any errors.
How I can call my component method "test" when I click in the "span" tag.
How implement click event for this case.
You cannot use vue directives in strings that you feed to v-html. They are not interpreted, and instead end up as actual attributes. You have several options:
Prepare your data better, so you can use normal templates. You would, for example, prepare your data as an object: { linkText: '---', position: 'before', name: 'name1' }, then render it based on position. I think this is by far the nicest solution.
<template>
<div>
<ul>
<li v-for="(item, index) in preparedList" :key="index">
<template v-if="item.position === 'before'">
<span v-on:click="test">{{ item.linkText }}</span>
{{ item.name }}
</template>
<template v-else-if="item.position === 'after'">
{{ item.name }}
<span v-on:click="test">{{ item.linkText }}</span>
</template>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["list", "symbole"],
computed: {
preparedList() {
return this.list.map(item => this.replaceSymbole(item.name));
}
},
methods: {
replaceSymbole: function(question) {
if (question.indexOf("#") === 0) {
return {
linkText: "---",
position: "before",
name: question.replace("#", "").trim()
};
} else {
return {
linkText: "---",
position: "after",
name: question.replace("#", "").trim()
};
}
},
test: function(event) {
console.log("Test ...");
console.log(this.$el);
}
}
};
</script>
You can put the click handler on the surrounding li, and filter the event. The first argument to your click handler is the MouseEvent that was fired.
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id" v-on:click="clickHandler"
v-html="replaceSymbole(item.name)">
</li>
</ul>
</div>
</template>
<script>
export default {
props: ["list", "symbole"],
data() {
return {
regexSymbole: new RegExp(this.symbole)
};
},
computed: {
preparedList() {
return this.list.map(item => this.replaceSymbole(item.name));
}
},
methods: {
replaceSymbole: function(name) {
return name.replace(
this.regexSymbole,
'<span class="clickable-area">---</span>'
);
},
test: function(event) {
console.log("Test ...");
console.log(this.$el);
},
clickHandler(event) {
const classes = event.srcElement.className.split(" ");
// Not something you do not want to trigger the event on
if (classes.indexOf("clickable-area") === -1) {
return;
}
// Here we can call test
this.test(event);
}
}
};
</script>
Your last option is to manually add event handlers to your spans. I do not!!! recommend this. You must also remove these event handlers when you destroy the component or when the list changes, or you will create a memory leak.

Why does the change in this property not trigger a watch in Vuejs?

I need to figure out why a property that is changed is not triggering a watch in Vue.
I have 2 components Parent and Child.
Child has as 1 Prop (item) and item has 4 properties: id, text, isImportant, isCool.
The Parent has 2 lists that are populated using two computed properties which return arrays, one where the items's "isImportant" == TRUE and the other where "isImportant" is FALSE.
In the Child, isImportant and isCool are both bound to input[type=checkbox] elements.
I have a watch (deep) set to respond to changes in the item prop of the Child.
Changing "isCool" triggers the watch while "isImportant" does not.
Changing isImportant does update the collection and the property is updated but it does not trigger the "watch".
It seems to be related to the computed property but not sure why?
https://jsfiddle.net/dclaysmith/y54b0mrq/
Vue.component('todo', {
props: {
item: Object
},
template: `
<label>
{{ item.text }}
<input type="checkbox"
v-model="item.isImportant">
Is Important?
<input type="checkbox"
v-model="item.isTicked">
Is Cool?
</label>`,
watch: {
item: {
handler: function (a, b) {
alert('Changed!')
},
deep: true
},
'item.isImportant': function (a, b) {
alert('Changed!')
}
},
})
new Vue({
el: '#app',
template: `
<div id="app">
<h2>Important:</h2>
<ol>
<li v-for="item in important">
<todo :item="item" :key="item.id"></todo>
</li>
</ol>
<br>
<h2>Not Important:</h2>
<ol>
<li v-for="item in notImportant">
<todo :item="item" :key="item.id"></todo>
</li>
</ol>
</div>
`,
data: {
todos: [
{ id: 1, text: "Learn JavaScript", isImportant: false, isTicked: false },
{ id: 2, text: "Learn Vue", isImportant: true, isTicked: false },
{ id: 3, text: "Play around in JSFiddle", isImportant: true, isTicked: false },
{ id: 4, text: "Build something awesome", isImportant: true, isTicked: false }
]
},
computed: {
important: function () {
return this.todos.filter(function(todo) {
return (todo.isImportant == true);
});
},
notImportant: function () {
return this.todos.filter(function(todo) {
return (todo.isImportant != true);
});
}
}
})
The reason that the change to isImportant isn't caught by the component, is that when you change isImportant, the component is removed, because the todo-item is moved from one list to the other.
If you have just one list of all todo's (<li v-for="item in todos">), both listeners trigger just fine.

vuejs auto-filter out an array marked "Bar"

I am new vuejs but learning a lot. I Have an array of items that renders to a list perfectly fine. I do want to not display anything marked Bar? I have tried !Bar but it does not work. Whats the correct way to do this?
var app = new Vue({
el: "#demo",
data: {
items: [{
childMsg: 'Foo'
}, {
childMsg: 'Bar'
}]
}
});
<script src="https://unpkg.com/vue"></script>
<div id="demo">
<ul v-for="item in items">
<li>{{item.childMsg}}</li>
</ul>
</div>
As usual, there are several approaches. One most straightforward is to exclude the item directly within v-for element template, like this:
<li v-if="item.childMsg !== 'Bar'">{{item.childMsg}}</li>
An alternative would be creating a computed property: array of items that do not match the pattern. Then you can rebase your v-for onto that property. Here's how it can be done:
var app = new Vue({
el: "#demo",
data: {
exclude: '',
items: [{
childMsg: 'Foo'
}, {
childMsg: 'Bar'
}]
},
computed: {
filteredItems() {
return this.items.filter(x => x.childMsg !== this.exclude);
}
}
});
<script src="https://unpkg.com/vue"></script>
<div id="demo">
<label>Exclude word... <input type="text" v-model="exclude" /></label>
<ul v-for="item in filteredItems">
<li>{{item.childMsg}}</li>
</ul>
</div>

Deleting vue component from list always delete the last element in list

I have read the documentation for rendering the custom components in list using v-for here.
But for some reason, I am not able to get this working.It always delete the last component instead of the one I send in the index. Any idea why it is not working ?
My VUE JS version is : 2.5.16.
Using PHPStorm IDE and running on docker (linux container)
And Laravel mix (I have "laravel-mix": "0.*" entry in package.json) to use webpack and compile the JS modules.
Here is the piece of some of my code
// Parent Component JS
<template>
<ul>
<li
is="child-component"
v-for="(child, index) in componentList"
:key="index"
:myVal="Something...."
#remove="dropField(index)"
#add-custom-field="addField"
></li>
</ul>
</template>
<script>
import childComponent from './ChildComponent';
export default {
name: 'CustomList',
components: {'child-component' :childComponent},
data() {
return {
componentList: []
}
},
methods: {
addField() {
console.log('Handling add-custom-field field...');
this.componentList.push(childComponent);
},
dropField(index) {
console.log(`I am deleting the component with index = ${index} from listview in parent...`);
this.componentList.splice(index, 1);
}
}
}
// Child Component JS
<template>
<div>
<input type="text" v-model="currentValue" /><button #click.prevent="$emit('remove')" > Remove </button>
</div
</template>
<script>
export default {
props: { myVal : '' },
data() { return { currentValue: ''} },
created() {this.currentValue = this.myVal;}
}
</script>
The issue is caused by in-place patch” strategy for v-for. That means Vue will not rebuild all childs when removed one element from componentList.
Check Vue Guide on an “in-place patch” strategy for v-for:
When Vue is updating a list of elements rendered with v-for, by
default it uses an “in-place patch” strategy. If the order of the data
items has changed, instead of moving the DOM elements to match the
order of the items, Vue will patch each element in-place and make sure
it reflects what should be rendered at that particular index.
Actually you already deleted the last item, but the problem is the data property=currentValue of first&second child have been 'a', 'b', when first mounted. Later when Vue re-render (delete the last child), data property=currentValue keeps same value though prop=myVal already changed.
Look at below demo, I added one input and bind myVal, you will see the differences.
Vue.config.productionTip = false
let childComponent = Vue.component('child', {
template: `<div class="item">
<p>Index:{{parentIndex}} => <button #click.prevent="removed()" > Remove </button>
Data:<input type="text" v-model="currentValue" />Props:<input type="text" v-bind:value="myVal" />
</p>
</div>`,
props: { 'myVal':{
type: String,
default: ''
} ,
'parentIndex': {
type: Number,
default: 0
}
},
data() {
return {
currentValue: ''
}
},
mounted() {
this.currentValue = this.myVal
},
methods: {
removed: function () {
this.$emit('remove')
}
}
})
app = new Vue({
el: "#app",
data() {
return {
componentList: ['a', 'b', 'c'],
componentType:childComponent
}
},
methods: {
addField() {
console.log('Handling add-custom-field field...');
this.componentList.push(childComponent);
},
dropField(index) {
console.log(`I am deleting the component with index = ${index} from listview in parent...`);
this.componentList.splice(index, 1);
}
}
})
li:nth-child(odd) {
background-color:#d0d5dd;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="(child, index) in componentList"><div
:is="componentType"
:key="index"
:my-val="child"
:parent-index="index"
#remove="dropField(index)"
#add-custom-field="addField"
>{{child}}</div></li>
</ul>
</div>
I discover that if you have another updated :key property (not index) it will work as you want
here's my example
<template>
<div id="app">
<ul>
<li
v-for="(teacher, index) in teachers_list"
v-bind="teacher"
:key="teacher.id"
>
<p>Teacher id {{teacher.id}}</p>
<button #click="deleteTeacher(index)"></button>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
teachers_list: [
{name: 'teacher a', id: 100},
{name: 'teacher b', id: 200},
{name: 'teacher c', id: 300},
]
}
},
methods: {
deleteTeacher(index) {
console.log(index);
this.teachers_list.splice(index, 1)
}
}
}
</script>