Vue - Bug or design with template vslots - vue.js

I notice that some of my computed properties don't fire when they call upon the same data. As a super-simplistic example let's say I have this:
<template v-for="item in foo" v-slot:['sameSlot`]="{sameBlah}">
{{ JSON.stringify(item) }}
</template>
<template v-for="item in bar" v-slot:['sameSlot`]="{sameBlah}">
{{ JSON.stringify(item) }}
</template>
<template v-for="item in baz" v-slot:['sameSlot`]="{sameBlah}">
{{ JSON.stringify(item) }}
</template>
...
data: () => ({
someData: [
{type:'foo', value: 1},
{type:'bar', value: 2},
{type:'baz', value: 3},
],
});
computed: {
foo() {
console.log('Hello from foo');
return this.someData.filter(f => f.type === 'foo');
},
bar() {
console.log('Hello from bar');
return this.someData.filter(f => f.type === 'bar');
},
baz() {
console.log('Hello from baz');
return this.someData.filter(f => f.type === 'baz');
},
}
If I run that, I only see console output from one of the computed methods, not all. Same with seeing results in the templates. Is this by design?
BTW I'm aware I could eliminate the computed properties by creating a method where a filter key could be passed in, but that doesn't showcase this 'issue'.

The code in the post targets the same slot 3 times, so Vue completely ignores the first 2 without running any of the code in the first two templates even once.
This is a smart feature to minimize workload because it's not sensible to target the same slot more than once to begin with. If anything, maybe there should be a warning from the compiler in this situation.
You could even have code that tries to use variables which don't exist, and there will be no compiler warning. For example:
<!-- First call: completely ignored, no error -->
<template v-for="item in notDefinedOnTheInstance" v-slot:[`sameSlot`]="{sameBlah}">
{{ JSON.stringify(item) }}
</template>
<!-- Second call: used for the slot -->
<template v-for="item in baz" v-slot:[`sameSlot`]="{sameBlah}">
{{ JSON.stringify(item) }}
</template>
Despite not existing in the instance, notDefinedOnTheInstance won't throw a warning because that whole section has been ignored. So it's for this reason that you also don't see the computeds output in the console, because they never run.

Related

How to make single property in array reactive when using `ref` instead of `reactive`?

I have a component that displays rows of data which I want to toggle to show or hide details. This is how this should look:
This is done by making the mapping the data to a new array and adding a opened property. Full working code:
<script setup>
import { defineProps, reactive } from 'vue';
const props = defineProps({
data: {
type: Array,
required: true,
},
dataKey: {
type: String,
required: true,
},
});
const rows = reactive(props.data.map(value => {
return {
value,
opened: false,
};
}));
function toggleDetails(row) {
row.opened = !row.opened;
}
</script>
<template>
<div>
<template v-for="row in rows" :key="row.value[dataKey]">
<div>
<!-- Toggle Details -->
<a #click.prevent="() => toggleDetails(row)">
{{ row.value.key }}: {{ row.opened ? 'Hide' : 'Show' }} details
</a>
<!-- Details -->
<div v-if="row.opened" style="border: 1px solid #ccc">
<div>opened: <pre>{{ row.opened }}</pre></div>
<div>value: </div>
<pre>{{ row.value }}</pre>
</div>
</div>
</template>
</div>
</template>
However, I do not want to make the Array deeply reactive, so i tried working with ref to only make opened reactive:
const rows = props.data.map(value => {
return {
value,
opened: ref(false),
};
});
function toggleDetails(row) {
row.opened.value = !row.opened.value;
}
The property opened is now fully reactive, but the toggle doesn't work anymore:
How can I make this toggle work without making the entire value reactive?
The problem seems to come from Vue replacing the ref with its value.
When row.opened is a ref initialized as ref(false), a template expression like this:
{{ row.opened ? 'Hide' : 'Show' }}
seems to be interpreted as (literally)
{{ false ? 'Hide' : 'Show' }}
and not as expected as (figuratively):
{{ row.opened.value ? 'Hide' : 'Show' }}
But if I write it as above (with the .value), it works.
Same with the if, it works if I do:
<div v-if="row.opened.value">
It is interesting that the behavior occurs in v-if and ternaries, but not on direct access, i.e. {{ rows[0].opened }} is reactive but {{ rows[0].opened ? "true" : "false" }} is not. This seems to be an issue with Vue's expression parser. There is a similar problem here.

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]}`)
...
}

Using Slots or slot-scopes in v-for loops to access properties?

I'm having a difficult time understanding slots for some reason and why they should even be used. The only reason I can think of that would be nice for usuage is if we can reference specific properties within a v-for loop of an element and output different templates quicker perhaps...
So, am thinking, and possibly I could be wrong in thinking this, but if I have a variable like so:
<script>
const items: [
{
label: 'My Label',
url: '#',
headerTitle: 'My Header Title'
},
{
label: 'My Label 2',
url: 'https://www.myurl.com',
headerTitle: 'My Header Title 2'
},
{
label: 'My Label 3',
url: 'https://www.myurl3.com'
}
]
export default {
data () {
return {
items: items
}
}
}
</script>
And than in the template, possibly this:
<template>
<div v-for="(item, index) in items" :key="item.id">
<template slot-scope="headerTitle">
<h1>{{ item.headerTitle }}</h1>
</template>
<template slot-scope="label">
<div class="mylabel">
{{ item.label }}
</div>
</template>
<template slot-scope="url">
<a :href="item.url">{{ item.label }}</a>
</template>
</div>
</template>
I don't know if this makes sense or not, but basically using the property as a slot-scope and than for everytime that property is defined, it will output something. But this doesn't work properly. Is this not what slot-scopes are for within component v-for loops? Is this not how to use these properties of an array of objects?
This kinda makes sense to me. Anyways to do it like this perhaps?

Can I insert a computed property into a Vue component dynamically

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.

"Property or method is not defined on the instance but referenced during render"

I need to display the item.title outside the <v-carousel> but I get this error message:
[Vue warn]: Property or method "item" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
I checked the results from the Stackoverflow search but I really struggle to understand it. I would be grateful if somebody could explain it to me by this example.
Here is my code:
<v-carousel>
<v-carousel-item v-for="(item,i) in items" v-bind:src="item.src" :key="i">
</v-carousel-item>
</v-carousel>
<h1>TITLE: {{ item.title }}</h1>
<script>
export default {
data () {
return {
items: [
{
src: '/static/a.jpg',
title: 'A',
text: 'texta'
},
{
src: '/static/b.jpg',
title: 'B',
text: 'textb'
}
{
}
}
}
</script>
This is what I need to archive:
As soon as an image changes to the next one - the a text outside of the scope should change too. I tried to check the value of the item array outside the scope but it didn't work:
<h1 v-if="(item,i) === 1">Lion</h1> <h1 v-if="(item,i) === 2">Ape</h1>
How to access the value of the current carousel item outside of the scope?
You need to add v-model on v-carousel component:
<v-carousel v-model="carousel">
<v-carousel-item
v-for="(item,i) in items"
v-bind:src="item.src"
:key="i"
></v-carousel-item>
</v-carousel>
//then set title like this:
<h1>TITLE: {{ items[carousel].title }}</h1>
And add carousel variable to data
data () {
return {
carousel: 0, //like this
items: [
...