javascriptsveltesvelte-3

How to set dynamic html tag according to props in Svelte


I'm creating a Heading component in svelte as a part of learning the basics of this framework. The component behavior is pretty straight-forward.

The component will have a prop named level, which will render the appropriate <h> tag accordingly.

For eg.

<Heading level={3}> would render <h3>content</h3> 
<Heading level={1}> would render <h1>content</h1>

I can achieve this currently with,

<script>
  export let level = 3;
</script>

{#if level === 1}
  <h1>
   <slot></slot>
  </h1>
{:else if level === 2}
  <h2>
   <slot></slot>
  </h2>
{:else if level === 3}
  <h3>
   <slot></slot>
  </h3>
{:else if level === 4}
  <h4>
   <slot></slot>
  </h4>
{:else if level === 5}
  <h5>
   <slot></slot>
  </h5>
{/if}

But this kind of feels like a very naive approach. Is there any better way to achieve this behaviour in svelte ?


Solution

  • I know your question is geared toward feeding a numerical prop to the component to set the heading level, but since you ended your OP with this question:

    But this kind of feels like a very naive approach. Is there any better way to achieve this behaviour in svelte ?

    ...and to benefit future readers, here's a more robust solution to the core problem of wanting a component for generating heading tags.

    You could use the Context API to generate heading tags that are completely context-aware and fully automate this, if you don't mind using a wrapper component for each section of content on the page. You would need:

    Here's an example:

    Section.svelte

    <script>
      import { setContext, getContext } from 'svelte'
    
      let level
    
      // if we find a context has already been set in this component tree, 
      // it came from a parent/ancestor instance of Section.svelte
    
      if (getContext('headingLevel')) {
        // Increment the context because this is the next nesting level
        level = getContext('headingLevel') + 1
        setContext('headingLevel', level)
      } else {
        // otherwise this instance is the first of its kind in the hierarchy
        level = 2
        setContext('headingLevel', level)
      }
    </script>
    
    <section>
      <slot />
    </section>
    

    HeadingTag.svelte

    <script>
      import { getContext } from 'svelte'
    
      // prop to insert your desired contents into the heading tag
      export let message
    
      // get the context, but make sure we can't go higher than <h6>
      let level = Math.min(getContext('headingLevel'), 6)
    
      const render = () => `
        <h${level}>
          ${message}
        </h${level}>
      `
    
    </script>
    
    {@html render()}
    

    Then, in your other components or pages, just use it like so

    MyPage.svelte

    <Section>
      <HeadingTag message={"hello"} />
      <!-- renders <h2>hello</h2> -->
      <Section>
        <HeadingTag message={"hello"} />
        <!-- renders <h3>hello</h3> -->
        <Section>
          <HeadingTag message={"hello"} />
          <!-- renders <h4>hello</h4> -->
        </Section>
      </Section>
    </Section>
    
    <Section>
      <HeadingTag message={"hello"} />
      <!-- renders <h2>hello</h2> -->
    </Section>
    

    This will also work seamlessly with however your components are nested.

    Note that my example has it set it up to start at <h2> with the assumption that every page only has a single <h1> within its <main>, and that doesn't require this kind of automation. But you can adapt it to your use case as needed, e.g. if you want it to start off with an <h1> at the top-level...

    Section.svelte

    <script>
      import { setContext, getContext } from 'svelte'
    
      let level
    
      if (getContext('headingLevel')) {
        level = getContext('headingLevel') + 1
        setContext('headingLevel', level)
      } else {
        // this and the HTML below are the only things that changed
        level = 1
        setContext('headingLevel', level)
      }
    </script>
    
    {#if level === 1}
      <main>
        <slot />
      </main>
    {:else}
      <section>
        <slot />
      <section>
    {/if}
    

    Credit where it's due and for further reading for those interested: I adopted this solution in Svelte from a React-based example in this article by Heydon Pickering:

    https://medium.com/@Heydon/managing-heading-levels-in-design-systems-18be9a746fa3