How to implement <transition-group> inside a v-for loop? - vue.js

There's one suggested answer on this, but the solution isn't working for me. I have a nested v-for and would like to animate the innermost li elements as they are removed or added by my computed statement. My current code looks like so:
<transition-group #before-enter="beforeEnter" #enter="enter" #leave="leave" tag="ul" v-if="computedProviders">
<li v-for="(letter, index) in computedProviders" :key="index">
<div>
<p>{{index.toUpperCase()}}</p>
</div>
<transition-group :letter="letter" tag="ul" class="list" #before-enter="beforeEnter" #enter="enter" #leave="leave">
<li v-for="provider in letter" :key="provider.last_name">
<div>
<a :href="provider.permalink">
{{provider.thumbnail}}
</a>
<div>
<h3>
<a :href="provider.permalink">
{{provider.last_name}}, {{provider.first_name}} <span>{{provider.suffix}}</span><br>
<p>{{provider.specialty}}</p>
</a>
</h3>
</div>
</div>
</li>
</transition-group>
</li>
</transition-group>
</div>
The outer transition-group works fine, but when I set up the inner one I get
ReferenceError: letter is not defined.
I tried adding :letter="letter" as suggested here, but it's still not working for me. Any suggestions? I'm happy to reformat the code if there's a way that makes better sense.
Edit: in response to a couple of the comments here, first of all, I'm injecting Vue into a PHP-based Wordpress template, so I'm not able to create separate components. I don't know if that's part of what's causing the issue or why some of you can run the code with no errors.
Here's a sample of the JSON this is iterating over:
{
a: [
{
first_name: 'John',
last_name: 'Apple',
suffix: 'DDS',
permalink: 'www.test.com',
thumbnail: '<img src="test.com" />',
specialty: 'Some specialty'
},
{
first_name: 'Jane',
last_name: 'Apple',
suffix: 'DDS',
permalink: 'www.test.com',
thumbnail: '<img src="test.com" />',
specialty: 'Some specialty'
}
],
d: [
{
first_name: 'John',
last_name: 'Doe',
suffix: 'DDS',
permalink: 'www.test.com',
thumbnail: '<img src="test.com" />',
specialty: 'Some specialty'
},
{
first_name: 'Jane',
last_name: 'Doe',
suffix: 'DDS',
permalink: 'www.test.com',
thumbnail: '<img src="test.com" />',
specialty: 'Some specialty'
}
]
}

One of the caveats with using in-DOM templates is that the browser parses the DOM before Vue gets to it. The browser doesn't know what <transition-group tag="ul"> is, so that just gets ignored. Instead, it sees an <li> inside another <li>. Since <li> elements normally cannot be nested, the browser hoists <li v-for="provider in letter" :key="provider.last_name"> outside its parent <li> that defines letter.
Removing Vue and inspecting the DOM will reveal the problem:
When Vue processes that template, it encounters letter, which is technically undeclared outside of the v-for, resulting in the error you're seeing.
Solution 1: <template> wrapper
If you need to use in-DOM templates, wrap the inner <transition-group> with a <template> so that the browser will ignore it:
<transition-group tag="ul">
<li v-for="(letter, index) in computedProviders" :key="index">
<template> 👈
<transition-group tag="ul">
<li v-for="provider in letter" :key="provider.last_name"></li>
</transition-group>
</template>
</li>
</transition-group>
demo 1
Solution 2: String templates
Move the template into a string (using the template option), which avoids the DOM parsing caveats:
new Vue({
template:
`<transition-group tag="ul">
<li v-for="(letter, index) in computedProviders" :key="index">
<transition-group tag="ul">
<li v-for="provider in letter" :key="provider.last_name"></li>
</transition-group>
</li>
</transition-group>`
//...
}).$mount('#providers')
demo 2

Don't really know what's causing the error, but try creating a new component that accepts letter as a prop and contains your inner <transition-group>. Then embed this new component in your root v-for.
It should give something like that:
<transition-group #before-enter="beforeEnter" #enter="enter" #leave="leave" tag="ul" v-if="computedProviders">
<li v-for="(letter, index) in computedProviders" :key="index">
<div>
<p>{{index.toUpperCase()}}</p>
</div>
<my-component :letter="letter" />
</li>
</transition-group>

Related

Has anyone came across this problem? [vue/no-multiple-template-root] The template root disallows 'v-for' directives.eslint-plugin-vue

It is actually a three problems in one:
[vue/no-multiple-template-root]
The template root disallows 'v-for' directives.eslint-plugin-vue
[vue/no-parsing-error]
Parsing error: Expected to be an alias, but got empty.eslint-plugin-vue
[vue/valid-v-for]
Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive.eslint-plugin-vue
Can anyone help me please I am so fed with searching online for it everywhere
enter code
<template>
<div class="post" v-for="post" in posts >
<div><strong>Title</strong>{{post.title}}</div>
<div><strong>Desctiption</strong>{{post.body}}</div>
</div>
</template>
<script>
export default{
data(){
return{
posts:[
{ id: 1, title: 'javascript', body: "the desctiption"},
{ id: 2, title: 'javascript2', body: "the desctiption"},
{ id: 3, title: 'javascript3', body: "the desctiption"},
]
}
}
}
Vue.js must have a single element at the root of the template. If you have av-for directive, when the DOM is populated there will be multiple <div> elements at the root, which Vue does not allow.
So you just need to add another <div> element to surround your v-for div.
Then, move the in posts within your quotes and add a :key
<template>
<div>
<div class="post" v-for="post in posts" :key="post.id">
<div><strong>Title</strong>{{post.title}}</div>
<div><strong>Desctiption</strong>{{post.body}}</div>
</div>
</div>
</template>
In vue two you should one root element, using v-for loop will render multiple elements in the template like :
<div class="post" >
...
</div>
<div class="post" >
...
</div>
to avoid this add an extra div and bind key to the post id :key="post.id":
<template>
<div class="posts">
<div class="post" v-for="post in posts" :key="post.id">
<div><strong>Title</strong>{{post.title}}</div>
<div><strong>Desctiption</strong>{{post.body}}</div>
</div>
</div>
</template>

Trouble iterating over array using :v-for

I am attempting to iterate over a simple array using the v-for directive in my Nuxt.js App. Please see below.
<template>
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
<nuxt-link class="navbar-brand" to="/">
<img class="image nav-logo" :src="logoSrc" alt="Logo" />
</nuxt-link>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav" >
<li :v-for="link in links" :key="link.label">
{{ link.label }}
</li>
</ul>
</div>
</nav>
</template>
<script>
const linkArray = [
{
label: "Home",
href: "/",
class: ""
},
{
label: "ABout",
href: "/",
class: ""
},
{
label: "Our Menu",
href: "/",
class: ""
},
{
label: "Contact Us",
href: "/",
class: "db-outline-cta"
}
]
export default {
name: "Nav",
data() {
return {
logoSrc: '/img/davidsbarlogo.png',
links: linkArray
}
}
}
</script>
As you can see, this is my component. I am going to be dynamically getting data for this component inside asyncData() later on when my cms is wired up, but I wanted to have some placeholder content.
I am repeatedly getting this error:
ERROR [Vue warn]: Error in render: "TypeError: Cannot read property 'label' of undefined" 00:00:49
I have tried with and wihtout the :key property, I know I should include one. I am fairly new to vue, if anyone has a recommendation I would be most grateful.
<ul class="navbar-nav">
<li v-for="link in links" :key="link.label">{{link.label}}</li>
</ul>
Works like a charm
https://codesandbox.io/s/recursing-kapitsa-rnr4h
Try this
<li v-for="(link, index) in links" :key="index">
{{ link.label }}
</li>
However, using index as key isn't the best practice (as explained in this post) but since there's no truly unique id in your link object, this will do.

Vuejs v-for loop with tag combination

I want to loop two list tags for every loop of v-for without looping ul. Is there any internal looping available in vuejs to escape the ul tag by getting looped or is there any other method.
<ul id="example-1" v-for="(item, index) in items" :key="item.message">
<li >
{{ item.message }}
</li>
<li>
{{ item.text}}
</li>
</ul>
var example1 = new Vue({
el: '#example-1',
data: {
items: [
{ message: 'Foo', text: "baz" },
{ message: 'Bar', text: "quz" }
]
}
})
The obvious result that i can get is :
Foo
Bar
baz
quz
The result That i need:
Foo
baz
Bar
quz
You can loop on a <template> tag.
<ul id="example-1">
<template v-for="item in items">
<li>
{{ item.message }}
</li>
<li>
{{ item.text}}
</li>
</template>
</ul>
If you are using Vue <= 2.x, then you will need to assign keys to the inner elements, since templates cannot be keyed.
If you are using Vue 3.x, then you should assign a key to the <template>. This is a breaking change from earlier releases.

How to include an input to v-for in scoped slot?

I just learnt about slot today on Vue official tutorial. As I read through, I came across this example on scoped slot: https://v2.vuejs.org/v2/guide/components.html#Scoped-Slots.
I experimented a little by adding an input field to each item.
Parent:
<my-list :items="items">
<template slot="item" scope="props">
<li class="my-fancy-item">{{ props.text }}</li>
</template>
</my-list>
Child template:
<ul>
<slot name="item" v-for="item in items" :text="item.text">
</slot>
<div> // this will appear at the end of list
<input v-model = "message">
</div>
<p>{{message}}</p>
</ul>
My first attempt was to move the input to the parent scope and called a child function by passing into it the index and input value using props to modify the original array. It worked as intended.
Another attempt is to do binding on parent scope but it won't work because parent scope can't see child property: https://v2.vuejs.org/v2/guide/components.html#Compilation-Scope
What is the best way to insert this input so that it will appear in every item and still be able to bind input to child property?
Base on our discussion, I think essentially what you want is this. Because you want the message to be independently editable, it needs to be part of the item. If you make message part of my-list, then all the messages will be the same. After that, all you need to do is pass the item to the scoped template.
Vue.component("my-list",{
props:["items"],
template: "#my-list-template",
})
new Vue({
el:"#app",
data:{
items:[
{text: "item 1", message: "message 1"},
{text: "item 2", message: "message 2"},
{text: "item 3", message: "message 3"}
]
}
})
And the templates:
<div id="app">
{{items}}
<my-list :items="items">
<template slot="item" scope="props">
<li class="my-fancy-item">{{ props.item.text }}</li>
<div>
<input v-model="props.item.message">
</div>
<p>{{props.item.message}}</p>
</template>
</my-list>
</div>
<template id="my-list-template">
<ul>
<slot name="item"
v-for="item in items"
:item="item">
</slot>
</ul>
</template>
Here is the working example.

Skip object items if the value is null

I have a nested for ... in loop in vue js. What I'm trying to to is to skip elements if the value of the element is null. Here is the html code:
<ul>
<li v-for="item in items" track-by="id">
<ol>
<li v-for="child in item.children" track-by="id"></li>
</ol>
</li>
</ul>
null elements may be present in both item and item.children objects.
For example:
var data = {
1: {
id: 1,
title: "This should be rendered",
children: {
100: {
id: 100,
subtitle: "I am a child"
},
101: null
}
},
2: null,
3: {
id: 3,
title: "Should should be rendered as well",
children: {}
}
};
With this data data[1].children[101] should not be rendered and if data[1].children[100] becomes null later it should be omitted from the list.
P.S. I know this is probably not the best way to represent data but I'm not responsible for that :)
Edit: Actually, a simple v-if might work:
<li v-for="item in items" v-if="item !== null" track-by="id">
Give it a try. If not, do this:
You can add a filter for that (in main.js before App instance):
Vue.filter('removeNullProps',function(object) {
// sorry for using lodash and ES2015 arrow functions :-P
return _.reject(object, (value) => value === null)
})
then in the template:
<li v-for="item in items | removeNullProps" track-by="id">
<ol>
<li v-for="child in item.children | removeNullProps" track-by="id"></li>
</ol>
</li>
In Vue 2, filters have been deprecated in v-fors.
Now you should use computed properties. Demo below.
new Vue({
el: '#app',
data: {
items: [
'item 1',
'item 2',
null,
'item 4',
null,
'item 6'
]
},
computed: {
nonNullItems: function() {
return this.items.filter(function(item) {
return item !== null;
});
}
}
})
<script src="https://unpkg.com/vue#2"></script>
<div id="app">
Using a computed property:
<ul>
<li v-for="item in nonNullItems">
{{ item }}
</li>
</ul>
<hr>
Using v-if:
<ul>
<li v-for="item in items" v-if="item !== null">
{{ item }}
</li>
</ul>
</div>
I would advise you against using v-if and v-for in the same element. What I found worked and didn't affect performance is this :
<li v-for="(value, key) in row.item.filter(x => x !== null)" :key="key"{{value}}</li>
You just need to run the filter function on the array you are going through. This is a common use in c# and found it was no different in JavaScript. This will basically skip the nulls when iterating.
Hope this helps (3 years later).
Just use v-if to do with it. But the first, do not use track-by="id" because of the null item and null child. You can check the demo here https://jsfiddle.net/13mtm5zo/1/.
Maybe the better way is to deal with the data first before the render.
VueJs style guide tell us to :
"Never use v-if on the same element as v-for."
How to handle v-if with v-for properly according to the vue style guide :
<ul v-if="shouldShowUsers">
<li
v-for="user in users"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
See more on how to handle v-if with v-for here : https://v2.vuejs.org/v2/style-guide/#Avoid-v-if-with-v-for-essential