Can I insert a computed property into a Vue component dynamically - vue.js

I am generating the contents of a Vue component by iterating through a large array of objects. I'd like to use computed properties to determine whether to show certain nodes, but since the computed reference is used inside a loop, I need to be able to set the reference name dynamically.
Below is a notional example of what I'm trying to do. How can I make showItemX change based on the current item?
<template>
<ul>
<li v-for="item in myArr" v-if="showItemX">
{{ item.name }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
myArr: [{
id: 'item1',
name: 'Item 1'
}, {
id: 'item2',
name: 'Item 2'
}]
};
},
computed: {
showItem1: function() {
return this.$store.state.showItem1;
},
showItem2: function() {
return this.$store.state.showItem2;
}
}
}
</script>
2 possible solutions
These are the two routes I've considered so far, but I'm not sure which one would be more efficient or if another way would be preferred:
1. Return a single object for the computed property
In this option, the two computed properties above would be combined into a single property:
computed: {
showItem: function() {
return {
item1: this.$store.state.showItem1,
item2: this.$store.state.showItem2
}
}
}
Then the the v-if would be set to showItem[item.id]:
<li v-for="item in myArr" v-if="showItem[item.id]">
{{ item.name }}
</li>
The downside here is that it seems that the entire object gets recomputed each time one of the dependencies changes.
2. Use a method to get the corresponding computed property
Here I tried passing item.id to a method as a way to access the corresponding computed property:
computed: {
item1Show: function() {
return this.$store.state.showItem1;
},
item2Show: function() {
return this.$store.state.showItem2;
}
},
methods: {
showItem: function(id) {
return this[id + 'Show']
}
}
And in the template:
<li v-for="item in myArr" v-if="showItem(item.id)">
{{ item.name }}
</li>
Again, in this example, I'm not sure if I'm fully leveraging the computed properties.
Should one of these options be preferred over the other or is there a better way to accomplish this that I'm missing?

The nice thing about Vue and JavaScript is that you can use whichever approach suits your needs, but, fwiw, I'd probably find something like the following easiest to understand
<li v-for="item in myArr" v-if="showItem(item)">
{{ item.name }}
</li>
And then define the showItem method, e.g.
showItem(item) {
return item.id === "item1" ?
this.$store.state.showItem1 :
this.$store.state.showItem2;
}
That assumes you're not using the computed properties anywhere else not shown in the post

There's a better way.
For possible solution #1, you might as well do
<li v-for="(item, index) in myArr" v-if="$store.state['showItem' + (index + 1)]">
Possible solution #2, you completely miss out on Vue's optimizations.
The method, while not computationally intensive, will re-run for every element each render.
Below is a solution which fits the specific parameters of your example problem. However, it's not actually what I recommend here. More below.
<template>
<ul>
<!--
`:key` is crucial for performance.
otherwise, every element will re-render
whenever the filtered array updates.
-->
<li v-for="item in myFilteredArr" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script>
export default {
data: _ => ({
myArr: [
{
id: 'item1',
name: 'Item 1'
},
{
id: 'item2',
name: 'Item 2'
}
],
}),
computed: {
myFilteredArr () {
/*
abstracting state to a constant avoids
re-running the getter functions each iteration
*/
const state = this.$store.state;
return this.myArr.filter(
(item, index) => state['showItem' + (index + 1)]
);
}
}
}
</script>
My actual recommendation is that you move all this logic into a Vuex getter. You can read about them here: https://vuex.vuejs.org/guide/getters.html .
Since your filtering logic is already being processed in the store, the function which is setting all the showItem's can just be cut and pasted into a Vuex getter, returning myFilteredArr in the same way as above.
This way, there's no component<->store interdependency, and your store's state will be much cleaner.

Related

Why are checkboxes not reset by v-model?

This is what i have:
Template
<div
v-for="(filter, index) in filtersList"
:key="index"
class="option-block"
>
<label
v-for="value in filter.values"
:key="value.id"
class="option-block__container"
>
{{ value.title }}
<input
type="checkbox"
v-model="filtersValues[filter.name]"
:value="value.value"
>
<span class="option-block__checkmark"></span>
</label>
</div>
And the part of my vue code:
data() {
return {
filtersList: {},
filtersValues: {}
}
},
beforeMount() {
this.loadInitData();
this.initFilters();
},
methods: {
loadInitData() {
const data = JSON.parse(this.$el.getAttribute('data-data'));
this.filtersList = data.filters;
},
initFilters() {
for (let i in this.filtersList) {
if (!this.filtersList.hasOwnProperty(i)) {
continue;
}
this.filtersValues[this.filtersList[i].name] = [];
}
}
}
It works, but when i call initFilters() method again (for reseting) checkboxes are still selected, and i don't know why.
The way you are assigning new, empty arrays to filterValues is not reactive.
If you change your initFilters to assign an entire new value to filterValues, you don't need to worry about using Vue.set(). For example
initFilters() {
this.filtersValues = this.filtersList.reduce((vals, { name }) => ({
...vals,
[ name ]: []
}), {})
}
Demo ~ https://jsfiddle.net/cjx09zwt/
Where did filter.values come from in line 2 of template?
Anyways vue would not be able to track the changes you are making (judging from the visible code)
There are some caveats to vue 2's reactivity. Check here for more info.
TLDR; you will need to declare anything you want to be made reactive in the component's data option upfront.
HTH

VueJs/Nuxt Filter an array, error _vm.filtered.. is not a function

I am new to Vue/Nuxt and try to filter an array.
computed: mapState({
entries: state => state.archives.archives
}),
filteredArchive, function (objects, key) {
if (objects) {
return objects.filter(function(object) {
return object.tag === key
})
}
I want to get the result in a loop:
<li v-for="(entry, index) in (entries | filteredArchive('test'))">{{ entry.title }}</li>
This fails..
What is wrong in my approach..
Thanks for help.
I suggest creating a computed property for "filteredArchive" instead of a filter. In fact, I believe filters are going away in Vue 3. You can put it in a mixin if you need to share the logic across components.
ok. this is my solution for now:
<ul>
<li v-for="(entry, index) in filteredByTag(entries, 'test')">
<nuxt-link :to="'archive/' + entry.id">{{ entry.title }}</nuxt-link>
</li>
</ul>
computed: mapState({
entries: state => state.archives.archives,
}),
methods: {
filteredByTag(entries, tag){
return entries.filter((entry) => {
return entry.tag.match(tag)
})
}
},

Vue JS Using local variable without defining in data() function

I am a beginner to Vue JS. I have to use a variable inside a component whose value changes often.
So when I declare and define it under data() the following warn is coming in Chrome console
Since when there is a change in data() variables automatically Vue framework calls render function.
Is there any way to declare and use a variable other than declaring it in data() method ??
<template>
<ul>
<div v-for="(list,index) in itemlist" :key="index">
<div v-if="!isFirstCharSame(list.label)" >{{ firstChar }} </div>
<li>
<span>{{ list.label }}</span>
</li>
</div>
</ul>
</template>
<script>
export default {
data() {
return {
itemlist: [
{"label":"Alpha"},
{"label":"Beta"},
{"label":"Charlie"},
{"label":"Delta"}],
firstChar:"$"
}
},
methods : {
isFirstCharSame: function(str) {
if(str.startsWith(this.firstChar)) {
return true;
}
this.firstChar = str.charAt(0);
return false;
}
}
}
</script>
Expected output should be like this
Inside Group A It should display all the elements starting with A
Below we will render using a computed property to make sure its sorted alphabetically and then render your first char. Though You should be using grouping imo.
<template>
<ul>
<div v-for="(list, index) in sortedlist" :key="`people_${index}`">
<div v-if="!isFirstCharSame(list.label)" >{{ firstChar }} </div>
<li>
<span>{{ list.label }}</span>
</li>
</div>
</ul>
</template>
<script>
export default {
data() {
return {
itemlist: [
{"label":"Alpha"},
{"label":"Beta"},
{"label":"Charlie"},
{"label":"Delta"},
],
firstChar: '',
};
},
methods: {
isFirstCharSame(char) {
if (str.startsWith(this.firstChar)) {
return true;
}
this.firstChar = str.charAt(0);
return false;
},
},
computed: {
sortedList() {
return this.itemList.sort((a, b) => {
if (a.label > b.label) {
return 1;
}
if (b.label > a.label) {
return -1;
}
return 0;
});
},
},
};
</script>
And yes, You can update your data any time you wish and the component will do a re render to reflect it.
You can declare variables in your component within your methods or inside computed properties, etc., but they won't be reachable from the template or the rest of the code nor they would be reactive.
The only way for them to be reactive and reachable from the higher scope is adding the data property to the component in the following way:
data: function () {
return {
foo: 'bar'
}
},
or
data () {
return {
foo: 'bar'
}
},
Besides this, the reason of your error is that you are mutating the state of your variables inside the render. When this happens, Vue re-renders the template because the values have mutated and calls again to the function and voilĂ : there you have an infinite loop.
You should probably check the function you are calling and try to replace the changing variables from the data property with local variables that take their data from the actual data variables.

Conditional <router-link> in Vue.js dependant on prop value?

Hopefully this is a rather simple question / answer, but I can't find much info in the docs.
Is there a way to enable or disable the anchor generated by <router-link> dependent on whether a prop is passed in or not?
<router-link class="Card__link" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If there's no id passed to this component, I'd like to disable any link being generated.
Is there a way to do this without doubling up the content into a v-if?
Thanks!
Assuming you want to disable anchor tag as in not clickable and look disabled the option is using CSS. isActive should return true by checking prop id.
<router-link class="Card__link" v-bind:class="{ disabled: isActive }" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
<style>
.disabled {
pointer-events:none;
opacity:0.6;
}
<style>
If you want to just disable the navigation , you can use a route guard.
beforeEnter: (to, from, next) => {
next(false);
}
If you need to use it often, consider this:
Create new component
<template>
<router-link
v-if="!disabled"
v-bind="$attrs"
>
<slot/>
</router-link>
<span
v-else
v-bind="$attrs"
>
<slot/>
</span>
</template>
<script>
export default {
name: 'optional-router-link',
props: {
params: Object,
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
Optional, register globally:
Vue.component('optional-router-link', OptionalRouterLink);
Use it as follows:
<optional-router-link
:disabled="isDisabled"
:to="whatever"
>
My link
</optional-router-link>
The problem is that router-link renders as an html anchor tag, and anchor tags do not support the disabled attribute. However you can add tag="button" to router-link:
<router-link :to="myLink" tag="button" :disabled="isDisabled" >
Vue will then render your link as a button, which naturally supports the disabled attribute. Problem solved! The downside is that you have to provide additional styling to make it look like a link. However this is the best way to achieve this functionality and does not rely on any pointer-events hack.
I sometimes do stuff like this:
<component
:is="hasSubLinks ? 'button' : 'router-link'"
:to="hasSubLinks ? undefined : href"
:some-prop="computedValue"
#click="hasSubLinks ? handleClick() : navigate"
>
<!-- arbitrary markup -->
</component>
...
computed: {
computedValue() {
if (this.hasSubLinks) return 'something';
if (this.day === 'Friday') return 'tgif';
return 'its-fine';
},
},
But I basically always wrap router-link, so you can gain control over disabled state, or pre-examine any state or props before rendering the link, with something like this:
<template>
<router-link
v-slot="{ href, route, navigate, isActive, isExactActive }"
:to="to"
>
<a
:class="['nav-link-white', {
'nav-link-white-active': isActive,
'opacity-50': isDisabled,
}]"
:href="isDisabled ? undefined : href"
#click="handler => handleClick(handler, navigate)"
>
<slot></slot>
</a>
</router-link>
</template>
<script>
export default {
name: 'top-nav-link',
props: {
isDisabled: {
type: Boolean,
required: false,
default: () => false,
},
to: {
type: Object,
required: true,
},
},
data() {
return {};
},
computed: {},
methods: {
handleClick(handler, navigate) {
if (this.isDisabled) return undefined;
return navigate(handler);
},
},
};
</script>
In my app right now, I'm noticing that some combinations of #click="handler => handleClick(handler, navigate)" suffer significantly in performance.
For example this changes routes very slow:
#click="isDisabled ? undefined : handler => navigate(handler)"
But the pattern in my full example code above works and has no performance issue.
In general, ternary operator in #click can be very dicey, so if you get issues, don't give up right away, try many different ways to bifurcate on predicates or switch over <component :is="" based on state. navigate itself is an ornery one because it requires the implicit first parameter to work.
I haven't tried, but you should be able to use something like Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind().
For example, you might be able to do:
#click="handler => setupNavigationTarget(handler, navigate)"
...
setupNavigationTarget(handler, cb) {
if (this.isDisabled) return undefined;
return this.$root.$emit('some-event', cb.bind(this, handler));
},
...
// another component
mounted() {
this.$root.$on('some-event', (navigate) => {
if (['Saturday', 'Sunday'].includes(currentDayOfTheWeek)) {
// halt the navigation event
return undefined;
}
// otherwise continue (and notice how downstream logic
// no longer has to care about the bound handler object)
return navigate();
});
},
You could also use the following:
<router-link class="Card__link" :to="id ? { name: 'Property', params: { id: id }} : {}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If id is undefined the router won't redirect the page to the link.
I've tried different solutions but only one worked for me, maybe because I'm running if from Nuxt? Although theoretically nuxt-link should work exactly the same as router-link.
Anyway, here is the solution:
<template>
<router-link
v-slot="{ navigate }"
custom
:to="to"
>
<button
role="link"
#click="onNavigation(navigate, $event)"
>
<slot></slot>
</button>
</router-link>
</template>
<script>
export default {
name: 'componentName',
props: {
to: {
type: String,
required: true,
},
},
methods: {
onNavigation(navigate, event) {
if (this.to === '#other-action') {
// do something
} else {
navigate(event);
}
return false;
},
};
</script>

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.