Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: dynamic elements <svelte:element> #2324

Closed
arxpoetica opened this issue Mar 27, 2019 · 61 comments
Closed

Proposal: dynamic elements <svelte:element> #2324

arxpoetica opened this issue Mar 27, 2019 · 61 comments
Labels
feature request popular more than 20 upthumbs

Comments

@arxpoetica
Copy link
Member

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?

@arxpoetica
Copy link
Member Author

arxpoetica commented Mar 27, 2019

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 or https://svelte.dev/repl?version=3.0.0&gist=5d6c43a98bc9f6274faa0ce8ed3f915b

@abastardi
Copy link

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.

@TehShrike
Copy link
Member

#640 (comment)

@eliranhe
Copy link

eliranhe commented May 2, 2019

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

@mindrones
Copy link
Member

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}

@loilo
Copy link

loilo commented Jul 31, 2019

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

@Conduitry
Copy link
Member

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

@arxpoetica
Copy link
Member Author

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.

@slominskir
Copy link

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]}"/>

@Conduitry
Copy link
Member

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

@slominskir
Copy link

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

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

@jonatansberg
Copy link

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

erzr added a commit to erzr/svelte that referenced this issue Nov 15, 2019
erzr added a commit to erzr/svelte that referenced this issue Nov 15, 2019
@erzr
Copy link

erzr commented Nov 15, 2019

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.

@matthewrobb
Copy link

Linking from here over to my latest comment on the implementation PR: #3928 (comment)

@m4rrc0
Copy link

m4rrc0 commented Feb 11, 2020

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.

@m4rrc0
Copy link

m4rrc0 commented Feb 11, 2020

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

@YoungElPaso
Copy link

YoungElPaso commented Feb 20, 2020

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.

@arggh
Copy link
Contributor

arggh commented Feb 20, 2020

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

@jesseskinner
Copy link
Contributor

jesseskinner commented Feb 21, 2020

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.

@arggh
Copy link
Contributor

arggh commented Feb 21, 2020

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

@jesseskinner
Copy link
Contributor

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

@erzr
Copy link

erzr commented Feb 21, 2020

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

@jonatansberg
Copy link

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.

@Steakeye
Copy link

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!

@Zachiah
Copy link
Contributor

Zachiah commented Jun 5, 2021

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

@plastictortoise
Copy link

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.

@madeleineostoja
Copy link

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

@pngwn pngwn added popular more than 20 upthumbs feature request and removed triage: has pr labels Jun 26, 2021
@cbbfcd
Copy link

cbbfcd commented Jul 2, 2021

any new?

@SarcevicAntonio
Copy link
Member

SarcevicAntonio commented Jul 4, 2021

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 😅

@SarcevicAntonio
Copy link
Member

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.

@Tropix126
Copy link

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.

@ymeine
Copy link

ymeine commented Oct 4, 2021

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}

@VladislavAnkudinov
Copy link

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

@ymeine
Copy link

ymeine commented Nov 7, 2021

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.

@madeleineostoja
Copy link

madeleineostoja commented Nov 14, 2021

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

@Zachiah
Copy link
Contributor

Zachiah commented Nov 15, 2021

@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

@madeleineostoja
Copy link

madeleineostoja commented Nov 15, 2021

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?

@gterras
Copy link

gterras commented Nov 16, 2021

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.

@ymeine
Copy link

ymeine commented Nov 16, 2021

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

@alvin
Copy link

alvin commented Dec 6, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request popular more than 20 upthumbs
Projects
None yet
Development

No branches or pull requests