javascriptvue.jsvuejs3

Dynamically wrap and group headings in list items with Vue render functions


I want to create a Vue component <Steps></Steps that is an ordered list, but provides some flexibility in how the slots are passed in.

Each step, or <li>, will be a heading, it could be either an h2, h3, or h4.

Inside of the <Steps> component, each time a heading tag is found, that needs to create and wrap everything up to the next heading tag of the same heading level. I am running into problems also because if a an h4 is used after an h3 this should not create an extra <li>.

It seems the best way to solve this would be with Vue's render h() function but I am really stuck on this one.

Input

<Steps>
  <h3>heading a</h3>
  <p>paragraph a<p>
  <div class="code-block"><pre>const x = 1</pre></div>
  <h4>this should not create an li</h4>
  <h3>heading b</h3>
  <p>paragraph b<p>
</Steps>

Output

<ol>
  <li>
    <h3>heading a</h3>
    <p>paragraph a<p>
    <div class="code-block"><pre>const x = 1</pre></div>
    <h4>this should not create an li</h4>
  </li>
  <li>
    <h3>heading b</h3>
    <p>paragraph b<p>
  </li>
</ol>

Another thing to note is that if there is one heading at a certain level, e.g. h3, a smaller tag should not create a new li.

Input

<Steps>
  <h3>heading a</h3>
  <p>paragraph a<p>
  <div class="code-block"><pre>const x = 1</pre></div>
  <h4>this should not create an li</h4>
  <p>paragraph b<p>
</Steps>

Output

<ol>
  <li>
    <h3>heading a</h3>
    <p>paragraph a<p>
    <div class="code-block"><pre>const x = 1</pre></div>
    <h4>this should not create an li</h4>
    <p>paragraph b<p>
  </li>
</ol>

Solution

  • You can do something like this (the first encountered header taken as a li start):

    Playground

    import { h } from 'vue';
    
    const headings = new Set(['h2', 'h3', 'h4'])
    
    export default function Steps(props, {slots}){
      const slotted = slots.default?.();
      if(!slotted) return;
      let type;
      const out = [];
      let curr;
      for(let i = 0; i<slotted.length;i++){
        const vnode = slotted[i];
        if(headings.has(vnode.type)){
          type ??= vnode.type;
          if(vnode.type === type){
            curr && out.push(curr);
            curr = [vnode];
          } else{
            (curr ??= []).push(vnode);  
          }
        }else{
          (curr ??= []).push(vnode);
        }
      }
      curr && out.push(curr);
    
      return h('ol', {}, out.map(li => h('li', li)));
    
    }