Data attribute bound to external variable not reactive to variable changes - vue.js

I'm beginning to incorporate Vue.js (v2.5.17) for a few small things on an ecommerce website, but I'm new to Vue and having some troubles. Due to some Laravel blade structure that I don't have the option of refactoring at this time, I have to split the various parts of this functionality into distinct components, and I'm having trouble with the data in one Vue instance reacting to events on the other.
Here's a CodePen with a stripped down version of the issue: https://codepen.io/jgabrielsen/pen/bxRroM/
For easy reference, the JS:
var cartStore = {
state: {
products: [PRODUCT ARRAY],
active: false
},
}
var cartHeader = new Vue({
el: '#cartHeader',
data: {
products: cartStore.state.products,
},
methods: {
setActiveTrue: function() {
cartStore.state.active = true;
console.log('Show Cart');
},
setActiveFalse:function() {
cartStore.state.active = false;
console.log('Hide Cart');
},
},
})
var cartPreview = new Vue({
el: '#cartPreview',
data: {
products: cartStore.state.products,
active: cartStore.state.active,
},
methods: {
total: function() {
var sum = 0;
for (var i = 0; i < this.products.length; i++) {
sum += this.products[i][1]
}
return sum
},
},
})
And the HTML
<div class="header">
<a id="cartHeader" #mouseover="setActiveTrue" #mouseout="setActiveFalse">CART({{ this.products.length }})</a>
</div>
<div id="cartPreview" v-show="active">
<ul>
<li v-for="product in products">
<div class="col-80">
{{ product[0] }}
</div>
<div class="col-20" style="text-align:right;">
${{ product[1] }}
</div>
</li>
</ul>
<div class="row">
<div class="col-100" style="text-align:right;">
Total Products: {{ this.products.length }}<br>
Total: ${{ this.total() }}
</div>
</div>
</div>
Long story short, I need the cartPreview instance to show up when cartHeader instance is hovered over (a basic popover effect).
Here's how I'm currently trying to do this:
I have a var cartStore that contains an array of all the cart preview data, as well as an active state.
cartPreview has a data attribute active bound to cartStore.state.active (which is false by default) and v-show="active", so it's hidden until something sets it's data attribute active to true.
cartHeader has #mouseover="setActiveTrue" and #mouseout="setActiveFalse", to toggle cartPreview's active attribute by way of the bound state in cartStore.
I can tell that the mouseover and mouseout events are firing because cartStore.state.active does change to true and false correctly and the console logs fire, but the corresponding data attribute in the cartPreview is not reactive to these changes.
I feel like I must be overlooking something super simple and/or making some major noob mistakes, but after poring over my code dozens of times and searching high and low, I'm stumped as to why it's not reactive.

I finally figured out a solution.
cartStore.state.products was reactive between cartHeader and cartPreview for adding and removing the products (methods I removed from my example because I didn't think they were relevant), but cartStore.state.active wasn't. The only difference I could see was that the cart data in products was stored in an array and active wasn't, so I made cartStore.state.active an array:
var cartStore = {
state: {
products: [PRODUCT ARRAY],
active: [ false ]
},
}
...updated it with splice:
methods: {
setActiveTrue: function() {
this.active.splice(0,1,true);
},
setActiveFalse:function() {
this.active.splice(0,1,false);
},
},
...and added v-show="active[0]" to the component, and lo and behold, it works.

Related

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();
}
}
})

Array of inputs, with last field always blank for new add

JSBin and Stackoverflow snippet are below.
I am trying to have a list of input components. If all input components are filled with a value (not blank), then there should be a "new blank field" visible at the end for the user to type into. When he types into it, it should make this field apart of the list above it, maintaining focus in it.
However the problem I'm having is, focus maintains in the new field, and never moves into the array. Here is my code:
JSBIN and stackoverflow snippet - https://jsbin.com/cudabicese/1/edit?html,js,output
const app = new Vue({
el: '#app',
data: {
inputs: [
{ id:'foo', value:'foo' },
{ id:'bar', value:'bar' }
]
},
methods: {
addRow(e) {
this.inputs.push({
id: Date.now(),
value: e.target.value
})
},
deleteRow(index) {
this.inputs.splice(index, 1)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app">
<ul>
<li v-for="(input, index) of inputs">
<input type="text" v-model="input.value">
</li>
<li v-if="inputs.filter(input => !!input.value).length">
<input type="text" #input="addRow">
</li>
</ul>
</div>
I'd recommend you put the input for the list within a computed function vs directly using the data. The examples at https://v2.vuejs.org/v2/examples/ are a good place to start.

Marking a Vue Todo List Item as done

This may have been answered before, but I have been unable to find an answer that works in this specific situation.
I'm new to Vue and trying to build a Todo list in which I can click on a list item when it is complete, changing or adding a class that will change the style of the item.
I guess I don't fully understand how the scopes work together between the main Vue and a component. The code I have right now does absolutely nothing. I have tried moving methods between the main and component, but it always gives me some error.
I guess I'm just looking for some guidance as to how this should be done.
Vue.component('todo-item', {
props: ['todo'],
template: '<li>{{ todo.id + 1 }}. {{ todo.text }}</li>'
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click="markDone"
:class="{'done': isDone}"
></todo-item>
</ul>
</div>
</div>
Thanks for the help, guys.
You were getting so close! You simply had your :class="{'done': isDone}" #click="markDone" in the wrong place!
The important thing to remember with components is that each one has to have their own data. In your case, you were binding all todo's to your root Vue instance's done variable. You want to instead bind each one to their own done variable in their own data.
The way you do this is by creating a function version of data that returns individual data for each component. It would look like this:
data () {
return {
isDone: false,
}
},
And then you move the :class="{'done': isDone} from the todo to the li internal to it:
<li :class="{'done': isDone}">{{ todo.id + 1 }}. {{ todo.text }}</li>
Now we have the 'done' class depending on an individual data piece for each individual todo element. All we need to do now is be able to mark it as complete. So we also want each todo component to have it's own method to do so. Add a methods: object to your todo component and move your markDone method there:
methods: {
markDone() {
this.isDone = true;
},
}
Now move the #click="markDone" to the li as well:
<li :class="{'done': isDone}" #click="markDone">{{ todo.id + 1 }}. {{ todo.text }}</li>
And there you go! Now you should be able to create as many todo's as you want, and mark them all complete!
Bonus:
Consider changing your function to toggleDone() { this.isDone = !this.isDone; }, that way you can toggle them back and forth between done and not done!
Full code below :)
Vue.component('todo-item', {
props: ['todo'],
template: `<li :class="{'done': isDone}" #click="toggleDone">{{ todo.id + 1 }}. {{ todo.text }}</li>`,
data () {
return {
isDone: false,
}
},
methods: {
toggleDone() {
this.isDone = !this.isDone;
},
}
})
var app = new Vue({
el: '#app',
data: {
isDone: false,
todos: [
{ id: 0, text: 'This is an item to do today' },
{ id: 1, text: 'This is an item to do today' },
{ id: 2, text: 'This is an item to do today' },
{ id: 3, text: 'This is an item to do today' },
{ id: 4, text: 'This is an item to do today' },
{ id: 5, text: 'This is an item to do today' }
]
},
methods: {
markDone: function(todo) {
console.log(todo)
this.isDone = true
}
}
})
.done {
text-decoration: line-through;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<div class="content">
<ul class="flex">
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
></todo-item>
</ul>
</div>
</div>
First of all, your current implementation will affect all items in the list when you mark one item as done because you are associating a single isDone property to all the items and when that property becomes true, it will be applied to all the items in your list.
So to fix that, you need to find a way to associate done to each item. And because your item is an object, you just assign a new property done dynamically and set the value to true which means it is marked as done. It will be very confusing to just explain it, so I included a full example using your existing code.
See this JS Fiddle: https://jsfiddle.net/eywraw8t/205021/
In your code, which are handling with the click event is the <li> element, but your are trying to handle it in the root of your component, there're a few ways to solve this
Use native modifier
<todo-item
v-for="todo in todos"
:todo="todo"
:key="todo.id"
#click.native="markDone"
:class="{'done': isDone}"
>
</todo-item>
You can find more info here
https://v2.vuejs.org/v2/guide/migration.html#Listening-for-Native-Events-on-Components-with-v-on-changed
Emit from component the click event
Vue.component('todo-item', {
props: ['todo'],
template: '<li #click="click()">{{ todo.id + 1 }}. {{ todo.text }}</li>',
methods: {
click () {
this.$emit('click')
}
}
})
By the way, in your current code once you click in one todo all the todos will be "marked as done" as you are using just one variable for all of them.

VueJS - pass data to component with existing row

Here's my fiddle
data: {
address_components: [
{ id:1, city_id:1, area_id: 100}
],
city_id:0,
area_id:0,
sort:1
},
I need to pass data to existing row, is that incorrect?
And if I add a new row, the id should be '2'
like this
what is the good way to do this? :'-(
modify your code like this:
addComponent() {
this.address_components.push({id:++this.sort})
},
How about creating a list in your data() and just push a object there.
<ul id="example-1">
<li v-for="item in items">
{{ item.message }}
</li>
</ul>
var example1 = new Vue({
el: '#example-1',
data: {
items: []
},
methods:
{
Add:function()
{
//process the inputs and push it to items list
this.items.push({ message: "Hi"})
}
}
})
Did not tested this and just created from example from Vue Documentation but you got the idea. Pushing a new item in list will automatically updates the component or the element

JSON and v-if troubles with 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>