sveltejs / svelte

Cybernetically enhanced web apps

Home Page:https://svelte.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Proposal: dynamic elements `<svelte:element>`

arxpoetica opened this issue · comments

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?

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 svelte:component 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: #3928 (comment)

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.

commented

...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.

commented

@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 svelte:component.

@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 <svelte: component> 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: sveltejs/rfcs#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

commented

In the Vue world the Dynamic Component can also be an element:
https://v3.vuejs.org/guide/component-basics.html#dynamic-components

This is valid vue code:

<template>
    <component :is="tag"></component>
</template>

<script lang="ts">
export default defineComponent({
  props:{
    {
      type: String,
       default: 'button'
    },
  }
})
</script>

Badly need this

Example use-cases:

  • content rendering
  • content editor
  • page builder
  • dynamic components (eg: button, anchor (a), etc)
commented

In dire need of this! For swapping components in <Typography/> as well as the underlying html of a stylized ´that should be able to take the form of bothand` depending of the existence of href.

Feel free to help out with reviewing and improving the existing PR: #5481

@jonatansberg If Declarative Actions were introduced, how could they interact with <svelte:element>?

Would It work?:

<script>
  import action from "action.svelte"
</script>

<svelte:element use:action>
  <slot></slot>
</svelte:element>
<!-- action.svelte -->
<script context="action">
  export let type = 'div';
  </script>

<style>
  .abstracted-styles {
    background-color: red;
  }
</style>

<target this={type} class:abstracted-styles="{true}" />

Is it worth doing type assignment from Declarative Action?

@lukaszpolowczyk I'm not familiar with that RFC. Conceptually dynamic elements are the same as their "static" counterparts, so actions should work. Passing the tag name from the action does not make much sense to me tho, what you've outlined above could already be implemented by keeping action.svelte as a component.

Feel free to help out with reviewing and improving the existing PR: #5481

So glad to see this PR is being actively discussed! Eagerly awaiting its arrival in the codebase!

Sorry, for any misunderstanding I wasn't saying I disliked this proposal I was saying I loved it! I thought it would be obvious I was saying that based on just how unmaintainable the code really was but it seems there was a misunderstanding

In the mean time, before the PR is accepted, you can use the {@html} function with JS template strings to get the desired result.

App.svelte

<script>
    import Header from "./Header.svelte";
</script>

<main>
    <Header headerType="h1" />
</main>

Header.svelte

<script>
    export let headerType;
</script>

{@html `<${headerType}>Hello, World!</${headerType}>`}

View on the Svelte REPL here.

commented

Fails on any use case that isn’t just a plain HTML element (scoped classes, bindings, actions, etc)

any new?

Would this allow feature allow for some random data being reactively set for web components like this for example?

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

  // changes to dynamicData would (hopefully) reflect inside our web component
  // thanks to sveltes magic {dynamicData} attribute/property binding
  let dynamicData = { some: ["random", "data"] };
  
  // used to not include custom element in dom when not already defined
  // to allow svelte setting dynamicData as properties instead of attributes
  let wcImported = false;

  onMount(async () => {
    // dynamically import web component script
    // this would also define the new tag / custom element
    await import(wcDefinition.scriptLocation);

    // include custom element im dom only when already defined
    wcImported = true;
  });
</script>

{#if wcImported}
  <svelte:element this={wcDefinition.tag} {dynamicData}>
  </svelte:element>
{/if}

My use case would be to have a dynamic system of loading different types of (web) components but still having the ability to interact with them using svelte sugar.

I think currently I would have to go with document.createElement() and setting data properties per hand and on every update, which feels wrong 😅

I think setting the type of element / tag name using something like tag={something} or type={something} instead of this={something} would be better, because we can already bind:this={somethingElse} to get the element reference, but here we are setting the type of element / tag name and that's kinda confusing imo.

I think setting the type of element / tag name using something like tag={something} or type={something} instead of this={something} would be better, because we can already bind:this={somethingElse} to get the element reference, but here we are setting the type of element / tag name and that's kinda confusing imo.

I believe the main goal is to be consistent with the <svelte:component /> tag, which serves a similar purpose. I somewhat agree, though.

Btw, I've been wondering why not using a same Svelte component to accommodate both use cases? If a string is passed, create a native element, otherwise consider the input is a Svelte component and instantiate it. (btw I know at least React does that)

That could simplify learning and using that feature, by interpreting native elements and Svelte components the same way in this context, leaving the user with a single important matter in mind: dynamically switch what's output at that point. I guess from a user standpoint it shouldn't matter whether this is a component or a native element behind.

But I might me missing part of the picture, maybe a will to be able in the future to more freely customize the dynamic creation of a component or a native element, in different ways.

Also, that said, it would always be possible for the end user to create their own version of this:

{#if isString}
    <svelte:element this={component} />
{:else}
    <svelte:component this={component} />
{/if}

In JSX we can do

render() {
  const Tag = this.isInline ? 'span' : 'div';
  return (
    <Tag>
      <slot />
    </Tag>
  );
}

Is it possible to add something as simple and straightforward as this to Svelte?

My use case:

  1. I want to create customizable table where I can provide web component tag names as config to control table layout
  2. I want to be able to compile this table as web component and use it inside Angular project
    I currently choosing between Stencil and Svelte. It seem Svelte is more fun and bring some new (old) spreadsheet philosophy, but in this particular case JSX looks more spry. Probably it comes for a price?

I played a bit with this example from Stephan https://svelte.dev/repl/c5c99203078e4b1587d97b6947e2d2f2?version=3.29.0
It seem work. But I feel somewhat uncomfortable with this solution, like I am doing kind of a hack which may bring issues in the future. Would like to have kinda conventional, good practice Svelte solution for my case

In JSX we can do

render() {
  const Tag = this.isInline ? 'span' : 'div';
  return (
    <Tag>
      <slot />
    </Tag>
  );
}

Is it possible to add something as simple and straightforward as this to Svelte?

My use case:

1. I want to create customizable table where I can provide web component tag names as config to control table layout

2. I want to be able to compile this table as web component and use it inside Angular project
   I currently choosing between Stencil and Svelte. It seem Svelte is more fun and bring some new (old) spreadsheet philosophy, but in this particular case JSX looks more spry. Probably it comes for a price?

I played a bit with this example from Stephan https://svelte.dev/repl/c5c99203078e4b1587d97b6947e2d2f2?version=3.29.0 It seem work. But I feel somewhat uncomfortable with this solution, like I am doing kind of a hack which may bring issues in the future. Would like to have kinda conventional, good practice Svelte solution for my case

Linked to my question (which was funnily reacted to 😉), it could also be nice if you could do what you suggested AND also assign to Tag either a native element tag OR a component reference. I never brought it up because I guess how the community might receive such proposal... I had a use case too where I could provide components through a map, to easily swap them at runtime... That would have implied using <svelte:component> everywhere instead of classic <Button>, <Icon>, etc... Which turned out to be way too ugly to be really implemented that way.

commented

I came here to ask the same thing, what about cases where you need to dynamically set an element OR a component? Example use-case:

  • Button component that can render a button tag or a router’s Link component based on the presence of a href or explicit prop

In JSX land you’d just do something like

const Element = isButton ? ‘button’ : Link;

return <Element />

I think svelte needs an equally terse way to accomplish this

@madeleineostoja if svelte:element is a thing you can easily do

<script>
export let el;
</script>

{#if typeof el === "string"}
<svelte:element this={el} / >
{:else}
<svelte:component this={el} />
{/if}

You'd be able to extract this to a component of course

commented

True, would just be nice to have that baked into a unified API, like I don't see why the svelte:{whatever} meta element can't do that for us, is there a case where that behaviour wouldn't be desirable? If this was a userland component you'd also have to spread $$restProps onto it, which I understand has some performance implications.

Considering Svelte's API philosophy is all about reducing needless boilerplate seems like this should be a consideration?

is there a case where that behaviour wouldn't be desirable?

This would quickly turn components into readability hell, exclusive directives like bind or use or even class couldn't be applied anymore.

is there a case where that behaviour wouldn't be desirable?

This would quickly turn components into readability hell, exclusive directives like bind or use or even class couldn't be applied anymore.

Correct me if I'm wrong, about your specific examples:

  • bind is not specific to components or native elements, it can be used on both
  • class can be used on both native elements and components, it's just that for the latter it has to be manually implemented with export let class = $$props.class;
  • use is indeed specific to native elements (which is unfortunate since component-based actions could be VERY useful, but that's yet another topic...)

In any case, the actual use of such dynamic components depends on what the users want to do. It's true that if they have a use case where they allow consumers of their own component to generate either a component or a native element, and on top of that they would want to support passing custom actions (with use) in case of native element only, then I don't even know if it's feasible, and in any case I agree it's messy! But if they have simpler use cases, why not having an API with known shortcomings?

But yes, that's exactly the kind of technical aspects that I am curious about (and some other people are too). It doesn't mean it's not feasible right now, and even less that it won't be on the future. But that gives insights about why it might not be done now, or done at all. At least, it's indeed not a pure trivial decision to merge <svelte:element> and <svelte:component>.

commented

I played around with Stephan's example as well, but the additional wrappers weren't acceptable in my use-case (the css workaround still broke my layout pretty severely)

I agree it would be nice to have clean solution baked into the language, but I realized my own simple needs are met by something as banal (though inelegant-feeling) as:

NativeWrapperElement.svelte

<script>
export let classes = '', element = 'div';
</script>

{#if element === 'div'}
  <div class={classes}><slot /></div>
{/if}
{#if element === 'table'}
  <table class={classes}><slot /></table>
{/if}

<!-- ... add more to support your use-cases -->

image

Edit: ... I now see similar solutions have already been posted above by @jesseskinner and perhaps others as well.