JSON and v-if troubles with Vue.js - vue.js

In the following example neither of the v-if related divs seem to get rendered before or after clicking the Add button. It seems like Vue.js isn't running any updates when the pizzas JSON object is updated.
Is there a solution to this problem without resorting to changing the pizzas variable into being an array?
<div id="app">
<div v-for="pizza in pizzas">
{{ pizza }}
</div>
<div v-if="totalPizzas === 0">
No pizza. :(
</div>
<div v-if="totalPizzas > 0">
Finally, some pizza! :D
</div>
<button #click="add">Add</button>
</div>
var app = new Vue({
el: '#app',
data: {
pizzas: {}
},
methods: {
add: function() {
this.pizzas['pepperoni'] = { size: 16, toppings: [ 'pepperoni', 'cheese' ] };
this.pizzas['meaty madness'] = { size: 14, toppings: [ 'meatballs', 'sausage', 'cajun chicken', 'pepperoni' ] };
},
totalPizzas: function() {
return Object.keys(this.pizzas).length;
}
}
});

There are several things to be improved in your code. Most of them are about syntax. For example, methods should be called, but computed properties can be queried directly: that's why it's #click="add()", but totalPizzas === 0 makes sense only if it's a computed property.
The crucial thing to understand, however, is how reactivity works in VueJS. See, while you change your object innards, adding new properties to it, this change is not detected by VueJS. Quoting the docs:
Vue does not allow dynamically adding new root-level reactive
properties to an already created instance. However, it’s possible to
add reactive properties to a nested object using the Vue.set(object, key, value) method:
Vue.set(vm.someObject, 'b', 2)
You can also use the vm.$set instance method, which is an alias to the
global Vue.set:
this.$set(this.someObject, 'b', 2)
Sometimes you may want to assign a number of properties to an existing
object, for example using Object.assign() or _.extend(). However, new
properties added to the object will not trigger changes. In such
cases, create a fresh object with properties from both the original
object and the mixin object:
// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
And this is how it might work:
var app = new Vue({
el: '#app',
data: {
pizzas: {}
},
computed: {
totalPizzas: function() {
return Object.keys(this.pizzas).length;
}
},
methods: {
add: function() {
this.pizzas = Object.assign({}, this.pizzas, {
pepperoni: { size: 16, toppings: [ 'pepperoni', 'cheese' ] },
['meaty madness']: { size: 14, toppings: [ 'meatballs', 'sausage', 'cajun chicken', 'pepperoni' ] }
});
},
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<div v-for="pizza in pizzas">
Size: {{ pizza.size }} inches
Toppings: {{ pizza.toppings.join(' and ') }}
</div>
<div v-if="totalPizzas === 0">
No pizza. :(
</div>
<div v-if="totalPizzas > 0">
Finally, some pizza! :D
</div>
<button #click="add()">Add</button>
</div>

Related

Vue sorting a frozen list of non-frozen data

I have a frozen list of non-frozen data, the intent being that the container is not reactive but the elements are, so that an update to one of the N things does not trigger dependency checks against the N things.
I have a computed property that returns a sorted version of this list. But Vue sees the reactive objects contained within the frozen list, and any change to an element results in triggering the sorted computed prop. (The goal is to only trigger it when some data about the sort changes, like direction, or major index, etc.)
The general concept is:
{
template: someTemplate,
data() {
return {
list: Object.freeze([
Vue.observable(foo),
Vue.observable(bar),
Vue.observable(baz),
Vue.observable(qux)
])
}
},
computed: {
sorted() {
return [...this.list].sort(someOrdering);
}
}
}
Is there a Vue idiom for this, or something I'm missing?
...any change to an element results in triggering the sorted computed prop
I have to disagree with that general statement. Look at the example below. If the list is sorted by name, clicking "Change age" does not trigger recompute and vice versa. So recompute is triggered only if property used during previous sort is changed
const app = new Vue({
el: "#app",
data() {
return {
list: Object.freeze([
Vue.observable({ name: "Foo", age: 22}),
Vue.observable({ name: "Bar", age: 26}),
Vue.observable({ name: "Baz", age: 32}),
Vue.observable({ name: "Qux", age: 52})
]),
sortBy: 'name',
counter: 0
}
},
computed: {
sorted() {
console.log(`Sort executed ${this.counter++} times`)
return [...this.list].sort((a,b) => {
return a[this.sortBy] < b[this.sortBy] ? -1 : (a[this.sortBy] > b[this.sortBy] ? 1 : 0)
});
}
}
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<div id="app">
<div>Sorted by: {{ sortBy }}</div>
<hr>
<button #click="list[0].name += 'o' ">Change name</button>
<button #click="list[0].age += 1 ">Change age</button>
<hr>
<button #click="sortBy = 'name'">Sort by name</button>
<button #click="sortBy = 'age'">Sort by age</button>
<hr>
<div v-for="item in sorted" :key="item.name">{{ item.name }} ({{item.age}})</div>
</div>

VueJS: How to create v-model value inside object array

I have several input fields like so:
<input type=text" v-model="InputVModel1">
<input type=text" v-model="InputVModel2">
and I want to place these v-model values inside an array of objects, like so:
array = [{id:1,value:InputVModel1},{id:2,value:InputVModel2},..]
What's the best way to achieve this?
Is it to use $set in a computed value to 'push' these into the array like:
computed: {
computed_array: function(){
object= {"id":1,"value":InputVModel1}
this.$set(this.array, object) //push them with a for loop
[..]
}
}
Or is there a more elegant way to do this?
Background: I want to use the final array for 'vuedraggable' to change the order of the objects while maintaining other important meta infos for each value.
new Vue({
el: '#app',
data: {
list: [
{ id: 1, value: 'foo' },
{ id: 2, value: 'bar' },
{ id: 3, value: 'baz' }
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div
v-for="(record, i) in list"
:key="i"
>
{{ record.id }}
<input v-model="record.value"/>
{{ record.value }} <!-- To check if value key is updating in the list -->
</div>
</div>
PS: Use list (final array) with for vue-draggable.

How is the method Total being called in this example

I see in the code below how the list item's class and state is being modified but I don't understand where or how the total() method is being triggered. The total is added to the markup in the <span>{{total() | currency}}</span> but there is no click event or anything reactive that I see in the code that is bound to it.
<template>
<!-- v-cloak hides any un-compiled data bindings until the Vue instance is ready. -->
<form id="main" v-cloak>
<h1>Services</h1>
<ul>
<!-- Loop through the services array, assign a click handler, and set or
remove the "active" css class if needed -->
<li
v-for="service in services"
v-bind:key="service.id"
v-on:click="toggleActive(service)"
v-bind:class="{ 'active': service.active}">
<!-- Display the name and price for every entry in the array .
Vue.js has a built in currency filter for formatting the price -->
{{service.name}} <span>{{service.price | currency}}</span>
</li>
</ul>
<div class="total">
<!-- Calculate the total price of all chosen services. Format it as currency. -->
Total: <span>{{total() | currency}}</span>
</div>
</form>
</template>
<script>
export default {
name: 'OrderForm',
data(){
return{
// Define the model properties. The view will loop
// through the services array and genreate a li
// element for every one of its items.
services: [
{
name: 'Web Development',
price: 300,
active:true
},{
name: 'Design',
price: 400,
active:false
},{
name: 'Integration',
price: 250,
active:false
},{
name: 'Training',
price: 220,
active:false
}
]
}
},
// Functions we will be using.
methods: {
toggleActive: function(s){
s.active = !s.active;
},
total: function(){
var total = 0;
this.services.forEach(function(s){
if (s.active){
total+= s.price;
}
});
return total;
}
},
filters: {
currency: function(value) {
return '$' + value.toFixed(2);
}
}
}
</script>
EDIT:
Working example https://tutorialzine.com/2016/03/5-practical-examples-for-learning-vue-js
So I believe the explanation for what is happening is that data's services object is reactive. Since the total method is being bound to it, when the toggleActive method is being called, it is updating services which causes the total method to also be called.
From the docs here 'How Changes Are Tracked' https://v2.vuejs.org/v2/guide/reactivity.html
Every component instance has a corresponding watcher instance, which records any properties “touched” during the component’s render as dependencies. Later on when a dependency’s setter is triggered, it notifies the watcher, which in turn causes the component to re-render.
Often I find simplifying what is going on helps me understand it. If you did a very simplified version of above it might look like this.
<div id="app">
<button #click="increment">Increment by 1</button>
<p>{{total()}}</p>
</div>
new Vue({
el: "#app",
data: {
counter: 0,
},
methods: {
increment: function(){
this.counter += 1;
},
total: function(){
return this.counter;
}
}
})
working example: https://jsfiddle.net/skribe/yq4moz2e/10/
If you simplify it even further by putting the data property counter in the template, when its value changes, you would naturally expect the value in the template to also be updated. So this should help you understand why the total method gets called.
<div id="app">
<button #click="increment">Increment by 1</button>
<p>{{counter}}</p>
</div>
new Vue({
el: "#app",
data: {
counter: 0,
},
methods: {
increment: function(){
this.counter += 1;
},
}
})
working example: https://jsfiddle.net/skribe/yq4moz2e/6/
When you update the data, the template in the components rerendered. That means that the template will trigger all methods bind to the templates. You can see it by adding dynamic date for example.
<div id="app">
<button #click="increment">Increment by 1</button>
<p>{{total()}}</p>
<p>
// Date will be updated after clicking on increment:
{{date()}}
</p>
</div>
new Vue({
el: "#app",
data: {
counter: 0,
},
methods: {
increment: function(){
this.counter += 1;
},
total: function(){
return this.counter;
},
date: function() {
return new Date();
}
}
})

Vue slot-scope with v-for, data changed but not re-rendered

I have a question while I'm studying vuejs2
I made an example with slot-scope && v-for, but it has an error which I can't understand.
Here is example code
https://jsfiddle.net/eywraw8t/6839/
app.vue
<template id="list-template">
<div>
<ul>
<slot name="row" v-for="item in list" v-bind=item />
</ul>
</div>
</template>
<div id="app">
<list-component :list="list">
<li slot="row" slot-scope="item">
{{item.name}}
</li>
</list-component>
</div>
Vue.component('list-component', {
template: '#list-template',
props: {
list: {
type: Array
}
}
});
new Vue({
el: '#app',
data () {
return {
list: [
{id: 1, name: 'Apple'},
{id: 2, name: 'Banana'},
{id: 3, name: 'Cherry'},
{id: 4, name: 'Durian'},
{id: 5, name: 'Eggplant'}
]
}
},
methods: {
h (item) {
item.name = item.name.toUpperCase()
console.log('Changed!')
console.log(item)
}
}
});
Strange thing is, the method 'h' is triggered and then, the console said 'Changed!' and data also changed but, the view is not re-rendered.
What am I missing? I think slot-scoped object data is not referencing the original object data.
What should I do to modify the original data?
Thanks for reading this question.
You are directly trying to modify the value of an item in an array. Due to some limitations vue cannot detect such array modifications.
So update your code to use vm.$set() to make chamges to the array item.
methods: {
h (item) {
let i = this.items.findIndex((it) => it.id === item.id);
this.$set(this.list, i, {...item, name: item.name.toUpperCase()});
console.log(item.id)
}
Here is the updated fiddle

How can I add operator ternary in input type text on vue.js 2?

My vue component like this :
<template>
<div>
...
<li v-for="category in categories">
...
<input type="radio" class="category-radio" :value="category.id" (category.id == categoryId) ? 'checked data-waschecked=true' : ''>
...
</li>
...
</div>
</template>
<script>
export default {
props: ['categories', 'categoryId'],
}
</script>
I want to add condition in input type text. I use operator ternary like above code
If the code executed, it does not work
There is no error. So i'm confused to solve it
Maybe my code is still not correct
How can I solve it?
The issue is you're trying to use JavaScript expression inside plain HTML. This won't work.
You can either bind each attribute manually like this:
:checked="(expression) ? true : false"
or bind to a computed property which depends on your expression and returns your calculated property. Alternatively, you can bind an object with one to many properties, and bind the whole object at once (this is possible also):
new Vue({
el: '#app',
data: {
categories: [
{ id: 1, name: 'one' },
{ id: 2, name: 'two' },
{ id: 3, name: 'three' }
],
selectedId: 2 // for simplicity
},
computed: {
attrs: function() {
return function(id) { // computed can also return a function, so you can use args
return (id === this.selectedId) ? { checked: true, 'data-waschecked': true } : {}
}
}
},
mounted() { // log your element
console.log(document.querySelector('input[data-waschecked=true]'))
}
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="app">
<ul>
<li v-for="category in categories">
<input type="checkbox" v-bind="attrs(category.id)">
</li>
</ul>
</div>