Svelte: Proposal: dynamic elements `<svelte:element>`

Created on 27 Mar 2019  ·  39Comments  ·  Source: sveltejs/svelte

Just recording this as a requested feature, since it comes up from time to time. Not actually saying we should do it, but would be good to track the discussion.

Proposal for <svelte:element>

<script>
  export let type = 'div';
</script>

<svelte:element this={type}>
  <slot></slot>
</svelte:element>

What are the actual use cases? Are there any drawbacks?

proposal has pr

Most helpful comment

OK, for any element, even those that aren't svelte components. Got it.

So a "Svelte" way to do document.createElement.

All 39 comments

And is it all just syntactic sugar for what's already possible? https://v3.svelte.technology/repl?version=3.0.0-beta.21&gist=42290cfe8bae4dee4df232669d12dc29

That example doesn't allow for any arbitrary tag to be specified, and it would get unwieldy to support more than 2 or 3 options.

+1!
Although there’s a workaround, I feel it reduces a lot of boilerplate code

I just needed to do this, but I'd go probably a bit more extreme:

{#each section as {depth, title, lines}}
<h{depth}>{title}</h{depth}>
  {#each lines as line}
  <p>{content}</p>
  {/each}
{/each}

To differentiate between self closing tags and tags accepting content one could do:

{#each section as {tag, is_self_closing, props, content}}
{#if is_self_closing}
<{tag} {...props} />
{:else}
<{tag} {...props}>{content}</{tag}>
{/if}

+1

Use case: Wrapper components that need to render an element (e.g. because they attach event listeners). You'd probably use a <div> there by default but there may be places where this is not desirable for semantic reasons (e.g. in lists).

Copying my recent comment from chat here for posterity and so that I can link to it later:

There are a few technical issues that would need to be ironed out first, mostly related to figuring out how we'll avoid doing a bunch of stuff at runtime. If this is implemented, it would probably come with some caveats. Certain tag types have to be treated as special cases when setting certain attributes on them, and we won't be able to do these checks at compile time, but we don't want to include all that logic in the compiled component

don't want to include all that logic in the compiled component

Amen to that. I think we could even broadly caveat that by saying “do whatever element you want” but don't expect Svelte to care about following any HTML spec, etc.

Just to clarify, this enhancement is about using a String to dynamically create a component, correct?

We can already use tag to create components on-demand from a constructor. To do this from a String instead I just create an object containing a key for each component with the value being the constructor:

const components = {};
components['Label'] = Label;
components['Tree'] = Tree;
components['Menu'] = Menu;

Then you can create dynamically based on String later:

let name = 'Tree';
<svelte:component this="{components[name]}"/>

No, this is about using a string to create an element of that tag name.

OK, for any element, even those that aren't svelte components. Got it.

So a "Svelte" way to do document.createElement.

Another real world use case for this is dynamically embedding components in content from a CMS at runtime.

Here is an example using wordpress style shortcodes to embed React components in to plain html:
https://www.npmjs.com/package/@jetshop/flight-shortcodes

I'd love another set of eyes on my implementation for this component to see if it's on target here. This is something I've been wanting for a little while so I figured I'd try my hand at it.

Linking from here over to my latest comment on the implementation PR: https://github.com/sveltejs/svelte/pull/3928#issuecomment-562991915

For reference: an independent implementation -> https://github.com/timhall/svelte-elements

I'd love to see this implemented in a native way.
Like others here, my use case is related to multi-purpose components or semantic html. A simple example: I compose pages with sections but it is not always desirable to use the <section> tag. Having a concise way to implement this would be great.

I have got another use case that may or may not be related. It is more generic but could handle dynamic elements names.

I would like to be able generate (part of) a component tree from some other arbitrary markup. For example, I could have a component defined as JSON like:

{
    "component": "div",
    "children": [
      {
        "component": "span",
        "children": "dynamic components"
      }
    ]
  }

that should be rendered as

<div><span>dynamic components</span></div>

For now, I could do something like

<script>
  import { onMount } from "svelte";

  let container;

  const comp = {
    component: "div",
    children: [
      {
        component: "span",
        children: "dynamic components"
      }
    ]
  };

  onMount(() => {
    // a smarter function could handle many cases
    const child = document.createElement(comp.component);
    const subChild = document.createElement(comp.children[0].component);
    subChild.textContent = comp.children[0].children;
    child.appendChild(subChild);
    container.appendChild(child);
  });
</script>

<div bind:this={container} />

In React, this kind of stuff can be done with the createElement function and we can mix components and native html tags.

My idea is, why not provide an API for manipulating the dom at compile time? This would allow us to avoid the JS payload when the 'dynamic' tree is actually never redefined.
Maybe it should be placed in another specific script tag?
Maybe a rollup plugin is a better fit for this kind of operations?

Ideally, in my opinion, the 'api for easy dom manipulation' and the 'way to do stuff only at compile time' should be two distinct features.

These features would also allow us to use libraries like MDsveX on a per component basis instead of relying on the rollup bundling. -> use case: markdown from a CMS could be parsed with MDsveX on the fly.

Any pointer as of how to achieve this would be greatly appreciated. :)

My use case, which I think is a pretty obvious one, is lists. I've got a lists component that can look like this:

<Lists items={stuff} ordered />
or
<Lists items={otherStuff} />

And I think the intent is pretty clear: the first one should render as an <ol>ordered list, the second (the default) case, <ul> or unordered. I think this is a valid way to set up some variations of a component. From my users POV a list is a list and the ordering is an attribute of any given list.

Svelte:element as proposed here would nicely fit my use case and avoid some otherwise pretty ugly conditions I'll need to bake into Lists.svelte.

...or a <Button>-component that can also act as an anchor tag when necessary, or a <Heading>-component with support for levels h1-h6 etc... Implementing these components with Svelte feels very non-Sveltey currently.

Sometimes it's easier to do the ugly thing, tucked away inside an abstraction. You can make your own project-specific <svelte:element> equivalent that does something like:

<script>
export let tag = 'div';
</script>

{#if tag === 'div'}
    <div {...$$props}><slot/></div>
{:else if tag === 'span'}
    <span {...$$props}><slot/></span>
{:else if tag === 'h1'}
    <h1 {...$$props}><slot/></h1>
<!-- etc -->
{/if}

Someone could implement this <svelte:element> proposal by putting in every possible HTML and SVG tag and publish as an npm module, <SvelteElement tag='div'> or something. I'll do it, if anyone wants.

@jesseskinner I believe this has already been done, called svelte-elements or similar.

@arggh True, though that one uses a slightly different approach, with a Svelte component per tag that you import and use with .

@jesseskinner I took that approach as a work around, I just disliked the file every time I looked at it. Also felt like it increased your bundle size just to implement something that should possibly be built in.

I took a similar approach to port that react-library I mentioned above. I only added a subset of elements, but it still comes at a massive file size increase. It would be a huge improvement to have support for this on a framework level.

I also came to the same conclusion: I'm making a Shell.svelte component that holds a bunch of conditions. One benefit to this approach is that at least it's explicit and easier to edit/add to by my peers or anyone else. Still, ugly though. 🤷

@jesseskinner putting every valid html5 tag would not be possible, because of html5 allowing any tag name containing a - as a user-defined valid tag name.

@tndev yeah, good point. No generic workaround is going to allow custom elements.

It sounds like something that already does.

There's an example:
https://www.youtube.com/watch?v=5NnN1OsBR5o

It sounds like something that already does.

There's an example:
https://www.youtube.com/watch?v=5NnN1OsBR5o

That's only for components, this issue is about actual HTML tags/elements

@Hygor Your approach would generate invalid html and only "work" client side. There are several valid use cases for this described in the comments above. Ideally it would also be nice if we had the option to pass either a component or a tag name for greater compostability.

The TL;DR; version is that conditionals does not cut it when you don't know ahed of time what kind of element or component is appropriate.

Use case: I was just looking for this feature. I have a component, and want to let the user specify what HTML tags wrap certain text rendered by the component. So the component user can say use <b></b> or <h1></h1> etc. This can be important for SEO as well as styling and managing CSS.

When creating components which render supplied content I think this will be very useful.

In my case I have a component that works a bit like <details>, so for example:

<details>
    <summary>Details</summary>
    Something small enough to escape casual notice.
</details>

Users of my component will use the 'Details' text more like a heading, so they might want to be able to tell the component to render the heading using <h2></h2> or <h3></h3> etc, or just <b></b>.

Note: I'm only using the <details> tag to illustrate roughly what my component does, it doesn't use <details> at all.

PS I don't think svelte-elements support the above use case very well. I would either have to know in advance which HTML elements to offer, and import all of them into the component. Maybe good for some cases but not a general solution for this I think.

...or a <Button>-component that can also act as an anchor tag when necessary, or a <Heading>-component with support for levels h1-h6 etc... Implementing these components with Svelte feels very non-Sveltey currently.

This is my Button.svelte for bulma

<script>
    let _className = "";
    export {_className as class};

    export let button = null;
    export let primary = false;
    export let secondary = false;
    export let danger = false;
    export let warning = false;


    export let href = undefined;

    export let tag = href ? "a" : "button";

    $: if (tag !== "button" && tag !== "div" && tag !== "span" && tag !== "a") {
        throw new Error("Button.svelte Invalid tag '" + tag + "'");
    }

    const conClass = (value,className) => value ? className : '';

    $: className = `button ${conClass(primary,'is-primary')} ${conClass(secondary,'is-secondary')} ${conClass(danger,'is-danger')} ${conClass(warning,'is-warning')} ${_className}`.trim();
</script>

{#if tag === "button"}
    <button class={className} bind:this={button} on:click on:blur on:focus {...$$restProps}>
        <slot></slot>
    </button>
{:else if tag === "span"}
    <span class={className} bind:this={button} on:click on:blur on:focus {...$$restProps}>
        <slot></slot>
    </span>
{:else if tag === "div"}
    <div class={className} bind:this={button} on:click on:blur on:focus {...$$restProps}>
        <slot></slot>
    </div>
{:else if tag == "a"}
    <a class={className} {href}  bind:this={button} on:click on:blur on:focus {...$$restProps}>
        <slot></slot>
    </a>
{/if}

@Zachiah yes that's how you would do it currently, but that is not very flexible, because you need to create an if for every possible tag, and repeat yourself over and over again, making such a component hard to maintain and error-prone if you need to keep the attribute/props of those tags consistent if you need to do changes.

An alternative workaround could be to abuse actions to mount and dismount the desired element

https://svelte.dev/repl/c5c99203078e4b1587d97b6947e2d2f2?version=3.29.0

@stephane-vanraes Thats an interesting take.. Might work well enough if you're only doing client side rendering. The div could potentially be replaced with a template element so that you render nothing util you can ensure the correct element is used.

@stephane-vanraes it works, but when you try assign class to this DynamicElement compiler says: Unused CSS selector

https://svelte.dev/repl/6481ca7e07734f769c1c6886d9aa9d31?version=3.29.4

@Zachiah yes that's how you would do it currently, but that is not very flexible, because you need to create an if for every possible tag, and repeat yourself over and over again, making such a component hard to maintain and error-prone if you need to keep the attribute/props of those tags consistent if you need to do changes.

Yes, and also what if you want to reuse markup between different types of buttons? Eg: you have an icon, a spinner, and a dropdown arrow.

Now you'd need to repeat that for every element:

{#if tag === "button"}
    <button class={className} bind:this={button} on:click on:blur on:focus {...$$restProps}>
    {#if withIcon}<Icon/>{/if}
        <slot></slot>
    {#if withArrow}<Arrow/>{/if}
    {#if showSpinner}<Spinner/>{/if}
    </button>
{:else if tag == "a"}
    <a class={className} {href}  bind:this={button} on:click on:blur on:focus {...$$restProps}>
    {#if withIcon}<Icon/>{/if}
        <slot></slot>
    {#if withArrow}<Arrow/>{/if}
    {#if showSpinner}<Spinner/>{/if}
    </a>
{/if}

Edit:

I guess this could be solved with inner components: https://github.com/sveltejs/rfcs/pull/34

+1
I got code like this:

{#if href}
<a class="item" {href} {target} on:click={linkClickHandler}>
  <span class="content" tabindex="-1">
    {#if $$slots.prepend}<span on:click={prependClickHandler} class="icon prepend"><slot name="prepend" /></span>{/if}
    <span class="text">
      <Text col="inherit" bold fz="14" lhMob="18"><slot /></Text>
      <Text col="{colMainText}" op="0.3" fz="10" lhMob="12"><slot name="desc" /></Text>
    </span>
    {#if $$slots.append}<button on:click={appendClickHandler} class="icon append"><slot name="append" /></button>{/if}
  </span>
</a>
{:else}
<button class="item" class:active on:click={defaultClickHandler}>
  <span class="content" tabindex="-1">
    {#if $$slots.prepend}<span on:click={prependClickHandler} class="icon prepend"><slot name="prepend" /></span>{/if}
    <span class="text">
      <Text col="inherit" bold fz="14" lhMob="18"><slot /></Text>
      <Text col="{colMainText}" op="0.3" fz="10" lhMob="12"><slot name="desc" /></Text>
    </span>
    {#if $$slots.append}<button on:click={appendClickHandler} class="icon append"><slot name="append" /></button>{/if}
  </span>
</button>
<div class="links">
  <slot name="links"></slot>
</div>
{/if}

Code chunks are similar, only tag changes. I really would like to see solution for this problem

Was this page helpful?
0 / 5 - 0 ratings

Related issues

plumpNation picture plumpNation  ·  3Comments

mmjmanders picture mmjmanders  ·  3Comments

bestguy picture bestguy  ·  3Comments

juniorsd picture juniorsd  ·  3Comments

ricardobeat picture ricardobeat  ·  3Comments