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.
Related
Very simple question. I'm learning VueJS and have created a simple component:
Vue.component('blog-post', {
props: ['title'],
template: '<h3>{{ title }}</h3>'
})
I then have parsed some data to it like this:
new Vue({
el: '#blog-post-demo',
data: {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
})
My question is how can get the title of a specefic element based on the id in my HTML? For now I can only render through the items and get them all, but I want to be able to specify which title I want to display based on the Id. Here is my HTML which gives me all the data:
<div id="blog-post-demo">
<blog-post
v-for="post in posts"
v-bind:key="post.id"
v-bind:title="post.title"
></blog-post>
</div>
You can achieve with COMPUTED property, like that
<template>
<div id="blog-post-demo">
<p v-for="post in thisOne" :key="post.id" >
{{post.title}}
</p>
</div>
</template>
<script>
export default {
el: '#blog-post-demo',
data() {
return {
posts: [
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
]
}
},computed: {
thisOne(){
return this.posts.filter(x => x.id === 3); /*choose your id*/
}
}
};
</script>
Or you can use event too to select the id of the posts to display (more dynamically)
Tip: If you start with VueJS, learn about the properties of VueJs (DATA, COMPUTED, CREATED, METHOD) and look at the uses and strengths of each one. For my part, the VueJS site is very very well done for beginners: https://v2.vuejs.org/v2/guide/
I'm not sure if I understand correctly what you want to do. But if you want to go through all posts and display title of particular post then you can try this way:
<blog-post
v-for="post in posts"
:key="post.id"
:title="setTitle(post)"
/>
(: instead of :v-bind it's a short form, also if you don't pass slots in your component you can go with self-closing tag)
Then in your methods section you can create a method:
setTitle(post) {
if(post.id === 2) return post.title
}
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();
}
}
})
What I am trying to accomplish:
I have some filters that display on a page to filter the products that display on the page. In mobile, I want to hide these filters behind a button that, once pressed, will show the filters in a slide out menu from the side.
While I can duplicate the same components on the page twice, the components are not the exact same instance, that is, clicking on a filter will trigger that function to filter the products on the page, but it sets its own data attributes, which I am using to say "if data attribute 'selected' is true, add a 'selected' class to the component. When I resize the window, the other instance of the component does not have the 'selected' data attribute marked as 'true'.
I expect this, because, from the docs:
Notice that when clicking on the buttons, each one maintains its own, separate count. That’s because each time you use a component, a new instance of it is created.
...but what would be the best way to do this?
I played around with the idea of just setting a class 'mobile' on the component, and the .mobile css would style the components differently, but I need for it to break out where it is nested.
e.g.
<body>
<header>
<!-- desktop -->
<guitar-filters>
<header>
<!-- mobile -->
<guitar-filters>
</body
Here is my Vue component 'guitar-filters' that displays several components called 'instrument-filter':
Vue.component('guitar-filters', {
data: function() {
return {
isMobile: false
}
},
mounted: function() {
var comp = this;
this.setIsMobile();
window.addEventListener('resize', function() {
comp.setIsMobile();
});
},
methods: {
setIsMobile: function() {
this.isMobile = (window.innerWidth <= 900) ? true : false;
}
},
template: `
<ul class="filters" :class="{mobile: isMobile}">
<li>
All
</il>
<li>
Series
<ul>
<instrument-filter filter-by="series" filter="All">All</instrument-filter>
<instrument-filter filter-by="series" filter="Frontier">Frontier</instrument-filter>
<instrument-filter filter-by="series" filter="Legacy">Legacy</instrument-filter>
<instrument-filter filter-by="series" filter="USA">USA</instrument-filter>
</ul>
</li>
<li>
Body Shape
<ul>
<instrument-filter filter-by="bodyType" filter="All">All</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concert">Concert</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concertina">Concertina</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concerto">Concerto</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Orchestra">Orchestra</instrument-filter>
</ul>
</li>
</ul>
`
});
The instrument-filter component:
Vue.component('instrument-filter', {
data: function() {
return {
selected: false
}
},
props : [
'filterBy',
'filter'
],
methods: {
addFilter: function() {
this.$root.$emit('addFilter',{filterBy: this.filterBy,filter: this.filter});
},
clearFilter: function() {
this.$root.$emit('clearFilter',{filterBy: this.filterBy,filter: this.filter});
}
},
template: `
<li :class="{ 'selected' : selected }" #click="selected = !selected; selected ? addFilter() : clearFilter()"><slot></slot></li>
`
});
.css:
ul.filters > li > ul > li.selected::before {
content: "✔️";
...
}
The goal is to have a filter have the 'selected' class in both instances. If I click on 'concert' body shape, and then resize the window to mobile breakpoint, the other instance of that filter component will be selected also.
EDIT: I could hack this. I could move one instance of the component with javascript, but I'm learning Vue, and want to do this the Vue way and best practices.
There's a number of different ways you can handle this. It looks like you've started down the event bus path. Another option could be to use shared app state (see Vuex).
What I've done is similar to shared state, but just using app (same would apply to a common parent component) data. The shared object is passed to both instances of the component. If an item is selected, the appropriate entry is toggled. Since the object is shared, both components stay in sync.
If there was no common parent component, you'd have to look at events or state.
Take a look and see if that helps.
Vue.component('guitar-filters', {
props: [ 'data' ],
data: function() {
return {
isMobile: false
}
},
mounted: function() {
var comp = this;
this.setIsMobile();
window.addEventListener('resize', function() {
comp.setIsMobile();
});
},
methods: {
setIsMobile: function() {
this.isMobile = (window.innerWidth <= 900) ? true : false;
}
},
template: `
<ul class="filters" :class="{mobile: isMobile}">
<li>
All
</il>
<li>
Series
<instrument-filters :list="data.seriesFilters"/>
</li>
<li>
Body Shape
<instrument-filters :list="data.bodyFilters"/>
</li>
</ul>
`
});
Vue.component('instrument-filters', {
props : [ 'list', ],
methods: {
toggle(toggleItem) {
let itemInList = this.list.find((item) => item.value === toggleItem.value);
itemInList.selected = !itemInList.selected;
},
},
template: `
<ul>
<li v-for="item in list" :class="{ 'selected' : item.selected }" #click="toggle(item)">{{ item.label }}</li>
</ul>
`
});
new Vue({
el: "#app",
data: {
filterData: {
seriesFilters: [
{ label: 'All', value: 'All', selected: false },
{ label: 'Frontier', value: 'Frontier', selected: false },
{ label: 'Legacy', value: 'Legacy', selected: false },
{ label: 'USA', value: 'USA', selected: false },
],
bodyFilters: [
{ label: 'All', value: 'All', selected: false },
{ label: 'Concert', value: 'Concert', selected: false },
{ label: 'Concertina', value: 'Concertina', selected: false },
{ label: 'Concerto', value: 'Concerto', selected: false },
{ label: 'Orchestra', value: 'Orchestra', selected: false },
],
}
},
});
ul {
margin-left:20px;
}
ul > li {
cursor: pointer;
}
ul.filters > li > ul > li.selected::before {
content: "✔️";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<header>
<!-- desktop -->
<guitar-filters :data="filterData" />
</header>
<!-- mobile -->
<guitar-filters :data="filterData" />
</div>
Fiddle: https://jsfiddle.net/nigelw/jpasfkxb
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.
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>