Search code examples
vue.jsv-forvuejs-transition-group

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


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'
       }
      ]
}

Solution

  • 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:

    hoisted DOM

    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