Vuejs :class not working as expected in template - vue.js

The class binding is not working as expected using a template, as the image below shows:
I have a array with many categories and when the user click it has to filter, this i already did, for whose category he wants. My problem is that once i click in other category, the ones before stills on.
The template receives a array such as:
categories: ["todos", "beer", "eco-bag", "paper-bag", "suplementos", "chas", "doces", "chocolates", "dieteticos"]
Here is the template:
<template id="category-box">
<span :class="{active: currentFilter == category}" #click="setFilter(category)">
{{category}}
</span>
</template>
The call inside the #app element:
<div id="category">
<category v-for="category in categories" :category="category"></category>
</div>
The code that handles it:
const Category = Vue.component("category", {
template: "#category-box",
props: {
"category": String,
},
data: function() {
return {
currentFilter: "todos"
}
},
methods: {
setFilter: function(filter) {
this.currentFilter = filter;
this.$parent.$emit('filteredCategory', filter);
}
}
});

It looks like currentFilter is scoped to the individual Category component. Each time you set it you are setting it for that component. Move it to the parent scope so that there is only one currentFilter for all of your categories.

Related

Vue warn]: You may have an infinite update loop in a component render function

Getting an infinite loop error when looping through <a> attributes in vue.js.
I have a method that loops through and adds attributes dynamically but when I use the method by binding it to an attribute in the <a> I get the error from the title. The attributes are a nested object within the original products array of objects.
Vue Code
<template>
<div>
<p>
<a
v-for="product in products"
:href="product.product_url"
type="submit"
v-bind:additionalAttrs="addAttributes()"
>
Click Me
</a>
</p>
</div>
</template>
<script>
export default {
data () {
return {
addedAttributes: [],
};
},
props: {
products: Array,
},
methods: {
addAttributes() {
this.products.forEach(product => {
for (const [key, value] of Object.entries(product.attributes)) {
this.addedAttributes.push(`${key}: ${value}`);
}
});
}
}
}
</script>
You use a method call to pass its result to additionalAttrs prop but it's not reactive and potentially can be called as many times as you have elements in products array
You just need one computed prop instead of an array and a method because they simply depend on products prop:
<a
v-for="product in products"
:href="product.product_url"
type="submit"
v-bind:additionalAttrs="addedAttributes"
>
Click Me
</a>
computed: {
addedAttributes() {
const addedAttributes = []
this.products.forEach(product => {
for (const [key, value] of Object.entries(product.attributes)) {
addedAttributes.push(`${key}: ${value}`);
}
});
return addedAttributes
}
}

How to search within nested objects

I have done my research trying to figure out how to achieve what I am describing below, however I had no luck.
In my Algolia index, some records have nested objects.
For example, title and subtitle attributes are of the following format:
title:
{
"en": "English title",
"gr": "Greek title"
}
I would like to execute queries only for a specific subset (in our example "en" or "gr") of these attributes, withoute "exposing" any facet in the UI — language selection would ideally be done “automatically” based on a variable (lang) passed to the Vue component with props. I am using Laravel Scout package with default Vue implementation, as described in documentation here.
My InstantSearch implementation is pretty simple, I am not defining anything specific regarding queries and searchable attributes, I am currently using all the default functionality of Algolia.
<template>
<ais-instant-search
:search-client="searchClient"
index-name="posts_index"
>
<div class="search-box">
<ais-search-box placeholder="Search posts..."></ais-search-box>
</div>
<ais-hits>
<template
slot="item"
slot-scope="{ item }"
>
<div class="list-image">
<img :src="'/images/' + item.image" />
</div>
<div class="list-text">
<h2">
{{ item.title }}
</h2>
<h3>
{{ item.subtitle }}
</h3>
</div>
</template>
</ais-hits>
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite';
export default {
data() {
return {
searchClient: algoliasearch(
process.env.ALGOLIA_APP_ID,
process.env.ALGOLIA_SEARCH
),
route: route,
};
},
props: ['lang'],
computed: {
computedItem() {
// computed_item = this.item;
}
}
};
</script>
I would like to somehow pass an option to query “title.en” and “subtitle.en” when variable lang is set to “en”. All this, without the user having to select “title.en” or “subtitle.en” in the UI.
Update
Maybe computed properties is the path to go, however I cannot find how to reference search results/hits attributes (eg item.title) within computed property. It is the code I have commented out.
I think, you can use computed property. Just transform current item according to the current language variable.
new Vue({
template: "<div>{{ computedItem.title }}</div>",
data: {
langFromCookie: "en",
item: {
title: {
en: "Hello",
ru: "Привет"
}
}
},
computed: {
computedItem() {
const item = JSON.parse(JSON.stringify(this.item));
for (value in item) {
if (typeof item[value] === "object" && Object.keys(item[value]).includes(this.langFromCookie))
item[value] = item[value][this.langFromCookie];
}
return item;
}
}
}).$mount("#app")
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
If lang variable is available via props, you can check that inside list-text class and return {{title.en}} or {{title.gr}} accordingly by passing a dynamic lang value title[lang] like below
...
<div class="list-text">
<h2>
{{ item.title[lang] }}
</h2>
<h3>
{{ item.subtitle[lang] }}
</h3>
</div>
If you want to make a request according to lang prop when component mounts ,then you can make a request inside mounted() method then query like below
mounted() {
axios.get(`/getSomethingWithLang/:${this.item.title[this.lang]}`)
...
}

Conditional link behavior in VueJS

Couldn't find a proper name for the title, will be glad if someone figures out a better name.
I have a component which represents a product card. The whole component is wrapped in <router-link> which leads to product page.
However I have another case, when I do not need the component to lead to a product page, but instead I need to do some other action.
The only solution I found is to pass a callback function as a prop, and based on this, do something like:
<router-link v-if="!onClickCallback">
... here goes the whole component template ...
</router-link>
<div v-if="onClickCallback" #click="onClickCallback">
... here again goes the whole component template ...
</div>
How can I do this without copy-pasting the whole component? I tried to do this (real code sample):
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
However I got this: Invalid handler for event "click": got null
Plus not sure if it's possible to pass prevent modificator for click and this just looks weird, there should be a better architectural solution
Commenting on the error, you could use an empty function instead of null, in the real code snippet
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
This should works (replace a for "router-link" then insert right properties)
Further infos :
https://fr.vuejs.org/v2/guide/components-dynamic-async.html
v-bind is simply an Object where each keys is a props for your component, so here, I programmatically defined an object of properties depending on the wrapper (router link or a simple div). However we cannot do this for events (of course we could create our own event listener but it's a little bit tricky) so I simply but an handle method.
new Vue({
el: "#app",
data: {
products : [{onClickCallback : () => { alert("callback"); return true;}}, {}, {}]
},
methods : {
handleClick(product, event) {
if (!product.onClickCallback) return false
product.onClickCallback()
return true
},
getMyComponentName(product) {
if (product.onClickCallback) return "div"
return "a"
},
getMyComponentProperties(product) {
if (product.onClickCallback) return {is : "div"}
return {
is : "a",
href: "!#"
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<component
v-for="(product, index) in products"
:key="index"
v-bind="getMyComponentProperties(product)"
#click="handleClick(product, $event)"
>
<div class="product-card">
<div class="product-card-content">
<span v-show="product.onClickCallback">I'm a callback</span>
<span v-show="!product.onClickCallback">I'm a router link</span>
</div>
</div>
</component>
</div>
Do you have to use a <router-link>? If it can safely be a <div>, you could use something like
<div #click="handleClick" ...>
<!-- component template -->
</div>
and
methods: {
handleClick (event) {
if (this.onClickCallback) {
this.onClickCallback(event)
} else {
this.$router.push({ name: 'clothingItem', ... })
}
}
}
See https://router.vuejs.org/guide/essentials/navigation.html

VueJs reactivity with parent component property object

I'm having difficulty to get parent component's property object, with dynamically populated properties to make the values available inside of the same component.
A bit hard to explain, so please have a look at the example below:
Parent Component
<script>
export default {
data() {
return {
fields: {},
}
}
}
</script>
Child Component
<template>
<select
#change="update()"
v-model="field"
>
<option
v-for="option in options"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</template>
<script>
export default {
props: {
initialOptions: {
type: Array,
required: true
}
},
data() {
return {
field: '',
options: this.initialOptions
}
},
mounted() {
if (
(this.field === undefined || this.field === '') &&
this.options.length > 0
) {
this.field = this.options[0].value;
}
this.update();
},
methods: {
update() {
this.$emit('input', this.field);
}
}
}
</script>
DOM
<parent-component inline-template>
<div>
<child-component>
:initial-options="[{..}, {..}]"
v-model="fields.type_id"
></child-component>
</div>
<div :class="{ dn : fields.type_id == 2 }">
// ...
</div>
</parent-component>
Using Vue console I can see that fields object gets all of the child component models with their associated values as they emit input when they are mounted, however for some strange reason the :class="{ dn : fields.type_id == 2 }" does not append the class dn when the selection changes to 2. Dom doesn't seem to reflect the changes that are synced between parent and child components.
Any help on how to make it work?
Here is what I was trying to get at in comments. Vue cannot detect changes to properties that are added dynamically to an object unless you add them using $set. Your fields object does not have a type_id property, but it gets added because you are using v-model="fields.type_id". As such, Vue does not know when it changes.
Here, I have added it and the color of the text changes as you would expect.
console.clear()
Vue.component("child-component", {
template: `
<select
#change="update()"
v-model="field"
>
<option
v-for="option in options"
:value="option.value"
>
{{ option.name }}
</option>
</select>
`,
props: {
initialOptions: {
type: Array,
required: true
}
},
data() {
return {
field: '',
options: this.initialOptions
}
},
mounted() {
if (
(this.field === undefined || this.field === '') &&
this.options.length > 0
) {
this.field = this.options[0].value;
}
this.update();
},
methods: {
update() {
this.$emit('input', this.field);
}
}
})
new Vue({
el: "#app",
data: {
fields: {
type_id: null
}
}
})
.dn {
color: red;
}
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<div>
<child-component :initial-options="[{name: 'test', value: 1}, {name: 'test2', value: 2}]" v-model="fields.type_id"></child-component>
</div>
<div :class="{ dn : fields.type_id == 2 }">
Stuff
</div>
</div>
It looks like you are trying to make a re-usable component.
I would ask myself what the value of a re-usable component is when the parent component has to handle more than half of the effort. The component might be better named...
<DifficultToUseSelect/>.
Essentially, you are creating a component that provides, all by itself, all of the following HTML...
<select></select>
Everything else is managed by the parent component.
It would probably be more useful to do any of the following...
Encapsulate often needed options in a specific select component, as in
StateAbbrevsSelect v-model="state"
Pass the name of a data model to a select component. The component would then load and manage its own data via the model.
Pass the URL of a web service to the component, which it then calls to load its options.
Again, the main point I am trying to convey here is that making a re-usable component where more than half of the effort is handled by the parent component is really not very re-usable.

How to amend data in a different component?

I am new to Vue js and trying to build a simple CRUD example for myself.
Following the documentation on non parent child communication I would like to amend the heading value in the data of one component but from another component.
I set up a fiddle to showcase the relative functionality as I currently understand it and we have here the HTML:
<div id="app" v-cloak>
<person-add></person-add>
<person-list :list="people"></person-list>
</div>
<template id="person-add-template">
<div>
<h2>
<span>{{ heading }}</span>
Person
</h2>
<form #submit.prevent="handleFormSubmit">
<input type="text" placeholder="Enter persons name" v-model="name" />
<button type="submit" v-show="name">
Add Person
</button>
</form>
</div>
</template>
<template id="person-list-template">
<div>
<h2>People</h2>
<table border="1">
<tr>
<th>Person</th>
<th>Edit</th>
</tr>
<tr v-for="(person, key) in list">
<td>{{ person.name }}</td>
<td><button type="button" #click="editPerson(key)">Edit</button></td>
</tr>
</table>
</div>
</template>
And the JS:
// https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
var bus = new Vue();
// Add
Vue.component('person-add', {
template: '#person-add-template',
props: ['list'],
data: function () {
return {
heading: 'Add',
name: ''
}
},
created: function () {
bus.$on('toggle-heading', function (newHeading) {
console.log(newHeading);
this.heading = newHeading;
});
}
});
// List
Vue.component('person-list', {
template: '#person-list-template',
props: ['list'],
methods: {
editPerson: function (key) {
console.log('fired');
bus.$emit('toggle-heading', 'Edit');
}
}
});
// Vue
new Vue({
el: '#app',
data: {
people: [
{ name: 'Bob' },
{ name: 'Frank' },
{ name: 'Mary' }
]
}
});
As you can see, it presents a simple form that starts with "Add Person" and lists some people along with an edit button for each:
What I would like to happen is that when I click on edit next to a persons name then it will change the heading in the other component to say "Edit Person" as opposed to the default "Add Person".
In the method in component A I have:
editPerson: function (key) {
console.log('fired');
bus.$emit('toggle-heading', 'Edit');
}
And in the created hook within component B I have:
created: function () {
bus.$on('toggle-heading', function (newHeading) {
console.log(newHeading);
this.heading = newHeading;
});
}
When I click edit, in the console I see the logs fired and then Edit so the event seems to follow through to the person-add component but where I have tried to assign the new heading this.heading = newHeading;, the heading does not change and I am battling to understand why.
If anyone could suggest why this is happening, where I have gone wrong with this or how things should be done if this is not the right way then it would be greatly appreciated.
Many thanks in advance!
Your problem is actually to do with scope, not a lack of understanding of Vue. Your code is correct, except you are trying to access this from inside a function creating it's own this context.
Whenever you create a new function this way, it creates it's own this so when you do:
bus.$on('toggle-heading', function(newHeading) {
console.log(newHeading);
// this refers to this anonymous function only
this.heading = newHeading;
});
this only refers to the function itself, not the the Vue instance.
The way to get around this is to use an arrow function, which do not create their own this:
bus.$on('toggle-heading', (newHeading) => {
console.log(newHeading);
// No new 'this' context is created in an arrow function
this.heading = newHeading;
});
Or if you are not using ECMAScript 2015 you will need to set a reference to this outside the function:
var self = this; // set a reference to "this"
bus.$on('toggle-heading', function(newHeading) {
console.log(newHeading);
// Now self refers to the view models `this`
self.heading = newHeading;
});
I've updated your fiddle to show you the two methods:
Arrow Function: https://jsfiddle.net/abtgmx47/3/
Using var self=this reference: https://jsfiddle.net/abtgmx47/4/