How to set dynamic html tag according to props in Svelte
Asked Answered
I

3

25

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 ?

Intuitionism answered 19/4, 2020 at 11:2 Comment(0)
O
6

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:

  • a wrapper component to set the context
  • a component to generate heading tags
    • the heading tag component accepts a prop so you can feed it your desired contents

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

Obumbrate answered 27/6, 2021 at 22:29 Comment(1)
I find that pretty elegant. Thanks so much for sharing this pattern.Samson
T
29

Svelte has native support for this starting with 3.47.0, using the svelte:element tag. Examples:

<svelte:element this="h1">this will be rendered as a top-level heading</svelte:element>

<svelte:element this={tag}>will render the element named in 'tag'</svelte:element>

<svelte:element this={null}>will not render for falsey values</svelte:element>

See the docs or the tutorial for more details.

Toxicogenic answered 15/5, 2022 at 22:2 Comment(0)
O
6

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:

  • a wrapper component to set the context
  • a component to generate heading tags
    • the heading tag component accepts a prop so you can feed it your desired contents

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

Obumbrate answered 27/6, 2021 at 22:29 Comment(1)
I find that pretty elegant. Thanks so much for sharing this pattern.Samson
F
5

Try this:

<script>
export let level = 3;
let displayText = "<h" + level + ">" +
                    "Sample header text" +
                  "</h" + level + ">";
</script>

<main>
    <div>
        {@html displayText}
    </div>
</main>

You can build your html out as a string by concatenating the value of level into the tag, then display it using the "@html" variable annotation, which interprets your string as html, rather than plain text.

Farmhouse answered 19/4, 2020 at 19:38 Comment(1)
This very simple solution was exactly what I was looking for to generate dynamic HTML in general. Thanks!Jerold

© 2022 - 2024 — McMap. All rights reserved.