Vue.js: Including same instance of component multiple times in page - vue.js

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

Related

Checked item form Array 1, and pass checked item to Array 2, then hide checked item from Array 2 rendered list

First, please check this pen I found, the concept is similar to my question, ReactJS - Baby Name Inspiration. I hope to make it via Vue.js but sorry I don't know React.
The question I want to ask if the user click the list item from Array 1, I named Array 1 as animals, the structure will show below. Then, it will pass the clicked item to Array 2, Array 2 as wished_pets_list. If for example {displayName: "Kitty", value: "cat"} clicked from animals list, animals & wished_pets_list also stored this object. When the same object in two arrays, the render of animals element will hide the object's output in HTML; it also renders to wished_pets_list as button. If click wished_pets_list's item button, it will remove the object data from wished_pets_list, and can access back on animals HTML list. And it can loop again.
The setting of data, default:
data: () => ({
animals: [
{displayName: "Kitty", value: "cat"},
{displayName: "Puppy", value: "dog"},
{displayName: "Chick", value: "bird"},
{displayName: "Fawn", value: "Deer"},
{displayName: "Joey", value: "Kangaroo"},
{displayName: "Piglet", value: "pig"},
{displayName: "Fry", value: "fish"},
{displayName: "Polliwog", value: "frog"}
],
wished_pets_list: [],
wished_pets_list_formatted: []
}),
Something I try on make it as HTML:
<div v-for="item in wished_pets_list">
<span #click="removeSelected(item.value)">{{item.displayName}}</span>
</div>
<div class="dropdown-list-container">
<div class="dropdown-list" v-for="(item, index) in animals">
<label :for="'givenID' + item.index" #click="pushSelect(item.value)">{{index}}{{item.displayName}}</label>
<input type="checkbox" v-model="wished_pets_list" :value="{'displayName': item.displayName, 'value': item.value}" :id="givenID' + item.index">
</div>
</div>
<!-- a hidden text field to submit the formatted as value only -->
<input type="text" v-model="wished_pets_list_formatted" name="anyName" v-show>
Two methods I think it should use:
methods: {
removeSelected(value){
this.wished_pets_list_formatted.push(value);
},
pushSelect(value){
this.wished_pets_list_formatted.splice(value);
}
},
Thanks, if you can, please make a similar codepen or jsfiddle.
Below is a implementation of the example codepen in Vue(didn't included the search part because I think it's irelevant in this case).
The template:
<div id="app">
<div data-reactroot="">
<main>
<div class="favourites">
<h4>Your Shortlist</h4>
<ul>
<li class="girl" v-for="(animal, index) in wished_pets_list" #click="removeFromList(index)">{{animal.displayName}}</li>
</ul>
<hr>
</div>
<ul>
<li v-for="(animal, index) in animals" :key="animal.value" class="boy" #click="addToList(index)">{{animal.displayName}}</li>
</ul>
</main>
</div>
</div>
The javascript part:
var vm = new Vue({
el: "#app",
data () {
return {
animals: [
{displayName: "Kitty", value: "cat"},
{displayName: "Puppy", value: "dog"},
{displayName: "Chick", value: "bird"},
{displayName: "Fawn", value: "Deer"},
{displayName: "Joey", value: "Kangaroo"},
{displayName: "Piglet", value: "pig"},
{displayName: "Fry", value: "fish"},
{displayName: "Polliwog", value: "frog"}
],
wished_pets_list: [],
wished_pets_list_formatted: []
}
},
methods: {
addToList(index) {
this.wished_pets_list.push(this.animals[index])
this.animals.splice(index, 1)
},
removeFromList(index) {
this.animals.push(this.wished_pets_list[index])
this.wished_pets_list.splice(index, 1)
}
}
});
For the CSS you can use the one from the codepen example.
Codepen fork
Base on #Allkin's answer, and my addition requirement, I tried to make such like Allkin's answer with an ordered list.
The template:
<div id="app">
<div>
<div class="favourites">
<h4>Your Shortlist</h4>
<ul>
<li class="girl" v-for="(animal, index) in wished_pets_list" #click="removeFromList(index, animal.value, animal.id)">{{animal.displayName}}</li>
</ul>
<hr>
</div>
<ul>
<li v-for="(animal, index) in animals" :key="animal.value" class="boy" #click="addToList(index, animal.value, animal.id)" v-show="!animal.checked">{{animal.displayName}}</li>
</ul>
<span>wished_pets_list_formatted:</span>
<div>{{wished_pets_list_formatted}}</div><br><br>
<span>animals:</span>
<div>{{animals}}</div>
</div>
</div>
js:
var vm = new Vue({
el: "#app",
data() {
return {
org_animal_list: [
{ displayName: "Kitty", value: "cat" },
{ displayName: "Puppy", value: "dog" },
{ displayName: "Chick", value: "bird" },
{ displayName: "Fawn", value: "Deer" },
{ displayName: "Joey", value: "Kangaroo" },
{ displayName: "Piglet", value: "pig" },
{ displayName: "Fry", value: "fish" },
{ displayName: "Polliwog", value: "frog" }
],
animals: null,
wished_pets_list: [],
wished_pets_list_formatted: []
};
},
methods: {
addToList(index, value, id) {
console.log("added: " + value);
this.wished_pets_list.push(this.animals[index]);
this.wished_pets_list_formatted.push(value);
this.animals[index].checked = !this.animals[index].checked;
},
removeFromList(index, value, id) {
var self = this;
this.wished_pets_list.splice(index, 1);
this.wished_pets_list_formatted.forEach(function(item, index) {
if (item == value) {
self.wished_pets_list_formatted.splice(index, 1);
}
});
for (var i = 0; i < this.animals.length; i++) {
if (self.animals[i].id == id) {
self.animals[i].checked = !self.animals[i].checked;
}
}
}
},
beforeMount: function() {
this.animals = this.org_animal_list;
for (var i = 0; i < this.animals.length; i++) {
this.$set(this.animals[i], "checked", false);
this.$set(this.animals[i], "id", i);
}
}
});
I added an original list first, then it will clone before Vue mount. This action allows the developer can use back the original data for other use.
For the full example, please check on codepen

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.

toggle class conditionally in Vue.js

Im trying to toggle a class of an element (the parent of the button being clicked) in Vue.js (which I've only started learning recently).
The idea is that the disabled class would be applied when included is not true.
I've created a toggleClass function, but even that is not being called it seems.
https://codepen.io/dagford/pen/NzQrJM
HTML
<div id="app">
<div class="item" v-for="item in items" :id="item.id" :key="item.id">
<span>{{item.amt}}</span>
<button v-on:click="item.included = !item.included" v-on:click="toggleClass()">exclude me</buton>
</div>
<br>
<div id="total">total: {{ itemTotal }}</div>
</div>
VUE
var app = new Vue({
el: "#app",
data: {
items: [
{
id: 'item1',
included: 'true',
amt: 10,
className: 'disabled'
},
{
id: 'item2',
included: 'true',
amt: 20,
className: 'disabled'
},
{
id: 'item3',
included: 'true',
amt: 30,
className: 'disabled'
}
]
},
methods: {
toggleClass: function () {
if(this.included) {
console.log('test');
// code to toggle the 'disabled' class here?
}
}
},
computed: {
itemTotal() {
return this.items.reduce(function (sum, item) {
if (item.included) {
return item.amt + sum;
}
return sum;
}, 0)
}
}
});
Managed to get it working thanks to Timothy's suggestion, that was almost there. It was just missing the .item portion.
So it should be
<div v-bind:class="{ disabled:!item.included }"></div>
<div v-bind:class="{ disabled:!included }"></div>
Maybe try using class binding :
doc

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.

Vue js toogle the items in the list

I wanted to create a side menu bar. I have added a class show when the li should be shown.
But in my case what happen is when one item is shown(1st) and next(2nd) item is clicked, the 1st get collapsed(which is fine) but the 2nd do not show up immediately.
new Vue({
el: '#app',
methods: {
setActiveItemId(itemIndex) {
this.activeItemId = itemIndex
this.isActive = !this.isActive
}
},
data: {
message: 'Hello Vue.js!',
isActive: false,
activeItemId: '',
sideBar: [{
name: "Dashboard",
url: "/dashboard",
icon: "ti-world",
children: [{
name: "Buttons",
url: "/components/buttons",
icon: "fa-book",
},
{
name: "Social Buttons",
url: "/components/social-buttons",
icon: "icon-puzzle",
}
]
},
{
name: "Components",
url: "/components",
icon: "ti-pencil-alt",
children: [{
name: "Buttons",
url: "/components/buttons",
icon: "fa-book",
},
{
name: "Social Buttons",
url: "/components/social-buttons",
icon: "icon-puzzle",
}
]
}
]
}
})
.collapse.show {
display: block;
}
.collapse {
display: none;
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.collapse.list-unstyled {
padding-left: 15px;
}
<script src="https://unpkg.com/vue"></script>
<div id="app">
<ul class="list-unstyled">
<li>
<a>
<i class="ti-home"></i>Home</a>
</li>
<li v-for="(x, itemIndex) in sideBar" :key="itemIndex">
<a #click="setActiveItemId(itemIndex)">
<i class="fa" :class="x.icon"></i>{{x.name}}
</a>
<ul :id="x.id" class="collapse list-unstyled" :class="{'show':activeItemId === itemIndex && isActive}">
<li v-for="y in x.children" :key="y.id">
<a>{{y.name}}</a>
</li>
</ul>
</li>
</ul>
</div>
So, How can i collapse the 1st and display the 2nd immediately, when 2nd item is clicked?
Fiddle
The problem is that inside your setActiveItemId method you are always toggling the isActive state, regardless of which item is being activated. That means that it toggles the same item, but when jumping to another you'll have to click twice. I'd take a different approach, where isActive is a computed property instead of residing in the data.
// ...
methods: {
setActiveItemId(itemIndex) {
// If item is currently selected, toggle
if (itemIndex === this.activeItemId) {
this.activeItemId = ''
return
}
this.activeItemId = itemIndex
}
},
computed: {
isActive () {
return this.activeItemId !== ''
}
}
Here's the updated fiddle:
https://jsfiddle.net/2ytuL46c/3/
Unrelated but worth noting: remember that your data must be a function that returns the data object, and not an object itself.