How to access content of slot in another child component

5.6k views Asked by At

Following problem:

I have a Vue.js component which relies on its parent's DOM. But the moment the prop gets passed, it (this.$el) is undefined, probably because it's not yet mounted then.

My component's vue template file looks like this:

<template>
  <md-card>
    <md-card-content>
      <ol>
        <li v-for="item in headings(content)">
          <a :href="`#${item.id}`">{{ item.name }}</a>
        </li>
      </ol>
    </md-card-content>
  </md-card>
</template>
<script>
  export default {
    props: ['content'],
    methods: {
      headings(content) {
        // DOM element is used
        // At this moment, `content` is undefined
      },
    },
  };
</script>

The component that uses the one above includes this piece of code:

<article-index :content="this.$el"></article-index>

I thought of waiting for the parent component to be mounted, but that way I can't seem to keep the template like above, because it would always try to access the method (or variable) instantly.

How can I solve this?

Edit:

<template>
  <div class="content">
    <div class="left"><article-index :content="this.$el"></article-index></div>
    <div class="article"><slot></slot></div>
    <div class="right"><slot name="aside"></slot></div>
  </div>
</template>

Here's the parent component's template. The only thing I actually need is the .article div, or the slot's contents.

2

There are 2 answers

7
Saurabh On BEST ANSWER

You can get it using this.$slots, in the parent component's mount function you can access this.$slots and assign it to some variable which can be passed to article-index component.

Following code prints the passed slots:

Vue.component('wrapper', {
    name: 'Wrapper',
  template: `<div><slot></slot></div>`,
  mounted () {
    this.$slots.default.forEach(vnode => { 
        console.log(vnode)
    })
  }
})

Sample fiddle here.

0
sk22 On

With the help of @saurabh I was able to find out that I can access the slot I'm passing to the child directly.

But the core problem remained: The component was not mounted at that moment.

So I changed how I'm accessing the passed slot.

Instead of the parent element, I'm now passing the default slot in the parent component.

Since the slots prop is an Array of VNode objects, I cannot use any DOM methods on them. But since a VNode's elm property contains the actual DOM element, I'm using that instead.

Again, the problem: it's not mounted yet.

That's why the v-for now points to the headings data, not the method, which removed. Instead, I added a mounted() method, which automatically gets called by Vue when the components got mounted.

When that method gets called, the slot has been mounted, so I can access their elm properties. In my case, there are multiple default slots, so the slots array has more than one items. To make it possible to call a specific querySelectorAll, I've added some functional Array magic.

Edit: Since it makes more sense to directly access querySelector on the rendered content, I'm now passing the $refs attribute instead of $slots. Even though I only need $refs.article, if I pass it directly, I'll get undefined. By passing this.$refs as a whole, the child component can access the article ref even if it doesn't exist before mounting.

So this is my new parent component:

<template>
  <div class="content">
    <div class="left">
      <article-index :refs="this.$refs"></article-index>
    </div>
    <div class="article" ref="article"><slot></slot></div>
    <div class="right"><slot name="aside"></slot></div>
  </div>
</template>

and the child:

<template>
  <md-card>
    <md-card-content>
      <ol>
        <li v-for="item in headings">
          <a @click="scroll(item.id)" :href="hash">
            {{ item.name }}
          </a>
        </li>
      </ol>
    </md-card-content>
  </md-card>
</template>
<script>
  import dashify from 'dashify';

  export default {
    props: ['refs'],
    data: () => ({
      headings: {},
      hash: location.hash,
    }),
    methods: {
      scroll(to) {
        this.refs.article.querySelector(`#${to}`).scrollIntoView();
      },
    },
    mounted() {
      const elements = Array.from(this.refs.article.querySelectorAll('h2'));
      elements.forEach(node => node.id = dashify(node.innerText));

      this.headings = elements.map(node => ({
        name: node.innerText,
        id: node.id,
      }));
    },
  };
</script>