search

Home  >  Q&A  >  body text

Is there no way to reliably handle slot contents in Vue 3?

<p>I'm currently migrating a Vue 2 codebase to Vue 3. </p> <pre class="brush:php;toolbar:false;"><card> <template #default> <card-header> <title level="3">Header</title> </card-header> <card-body v-for="b in bodyCount" :key="'b' b"> Body {{ b }} </card-body> <card-footer v-for="f in footerCount" :key="'f' f"> <text>Footer {{ f }}</text> </card-footer> </template> </card></pre> <p>The Card component has a render function that calls <code>this.$slots.default()</code>. However, in Vue 3 this returns different content than in Vue 2. So if I do <code>console.log(this.$slots.default())</code> - I get an array with 3 elements, [v node of type "header", type It is a v node of Symbol(Fragment), a v node of type Symbol(Fragment)]</p> <p>More specifically, it doesn't recognize the card footer/card body as components, instead I see the symbol (fragment) in the type. Card-header is actually fine since it's not inside the v-for loop. </p> <p>There is a logic inside the rendering function of the card component that needs to know how many text/footer components it has. So I need to be able to tell the component type. Is this not possible in Vue 3? I can guess this has something to do with the v-for here, but I'm not sure how to actually get the final v-nodes. In this example, I want the array to contain card headers all card bodies all card footers instead of 3 elements. </p>
P粉714844743P粉714844743461 days ago414

reply all(2)I'll reply

  • P粉865900994

    P粉8659009942023-08-31 00:54:26

    The design of your card component doesn't look meaningful to me. I suggest you reconsider.

    This is a simple playground rendering slot

    const { createApp, h } = Vue;
    
    const Card = {
      setup(props, { attrs, slots, emit, expose }) {
          return () => [           
              h('label', 'Card'),
              h('div', 'Slots:'),
              h('div', {}, slots.default()),
              h('div', {}, slots.header()),
              h('div', {}, slots.body()),
              h('div', {}, slots.footer())
          ]
      }
    }
    
    const App = { 
      components: { Card },
      data() {
        return {
        }  
      }
    }
    const app = createApp(App)
    app.mount('#app')
    #app { line-height: 2; }
    [v-cloak] { display: none; }
    label { font-weight: bold; }
    <div id="app">
    <card>
        <template #default>
           Default
            <card-header>
                <title level="3">Header</title>
            </card-header>
        </template>
        <template #header>
           Header
        </template>
        <template #body>
           Body
        </template>
        <template #footer>
           Footer
        </template>
    </card>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

    Please heed the warning. Vue won't render card-header here because it can't parse it.

    [Vue warn]: Failed to resolve component: card-header
    If this is a native custom elemen

    If you have access to the data, why are you passing the slots action card-body/card-footer items directly through bodyCount?

    If it doesn't look reasonable to me.

    const { createApp, h } = Vue;
    
    const CardHeader = {
        template: '<div class="card-header">card-header: <slot></slot></div>'
    }
    const CardBody = {
        template: '<div class="card-body">card-body: <slot></slot></div>'
    }
    const CardFooter = {
        template: '<div class="card-footer">card-footer: <slot></slot></div>'
    }
    
    const Card = {
      components: {
        CardHeader, CardBody, CardFooter
      },
      setup(props, { attrs, slots, emit, expose }) {
          //console.log(slots.default())
          return () => [           
              h('label', 'Card'),
              h('div', 'Slots:'),
              h('div', {}, slots.default()),
          ]
      }
    }
    
    const App = { 
      components: { Card, CardHeader, CardBody, CardFooter },
      data() {
        return {
          bodyCount: ['a','b'],
          footerCount: ['a','b']
        }  
      }
    }
    const app = createApp(App)
    app.mount('#app')
    #app { line-height: 2; }
    [v-cloak] { display: none; }
    label { font-weight: bold; }
    .card-header, .card-body, .card-footer {
      border: 1px solid #F0F0F0;
    }
    <div id="app">
    <card>
        <template #default>
            <card-header>
                <div level="3">Header</div>
            </card-header>
            bodyCount: {{bodyCount.length}}
            <card-body
                v-for="b in bodyCount"
                :key="'b' + b">
                Body {{ b }}
            </card-body>
            footerCount: {{footerCount.length}}
            <card-footer
                v-for="f in footerCount"
                :key="'f' + f">
                <text>Footer {{ f }}</text>
            </card-footer>
        </template>
    </card>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

    reply
    0
  • P粉440453689

    P粉4404536892023-08-31 00:37:50

    It turns out - fragments can be expanded via the Children property on each element of the array. So in this case, v-for produces a v node, which is a fragment, but we can infer more by looking at the Children property.

    reply
    0
  • Cancelreply