Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

svelte:component with DOM elements

Goal

I'm creating a button component in Svelte that will either render as <button> or <a> element, depending on whether it's a link or not. Is it possible to use svelte:component?

Something like this…

<script lang='ts'>
  export let href: string = ''

  $: component = href ? a : button // where "a" and "button" are the HTML DOM elements
</script>

<svelte:component this={component}>
 <slot></slot>
</svelte:component>

So far, I've only seen examples of svelte:component rendering custom Svelte components, not DOM elements

https://svelte.dev/tutorial/svelte-component

How to dynamically render components in Svelte?

Motivation

It is possible to use if/else to get the desired results…

<script lang='ts'>
  export let href: string = ''
</script>

{# if href}
  <a {href}>
    <slot></slot>
  </a>
{:else}
  <button>
    <slot></slot>
  </button>
{/if}

…but this is not maintainable.

  • The entire contents is duplicated. In the simple example above, the contents is just the children. In reality, there are multiple slots for (e.g, prefixes/suffixes), leading to lots of duplicated logic.
  • I can see uses for this design pattern in many other components with more than 2 variants (e.g., a Container component that can be a div, section, article, aside, etc.). More variants results in an even messier code structure.

React equivalent

Here's an example React component with the desired functionality.

const Button = (props) => {
  const Tag = props.href ? 'a' : 'button'

  return <Tag href={props.href}>{contents}</Tag>
}

Solutions I wish to avoid

1. The if/else pattern

Same pattern as above.

2. Creating a "children" component

Instead of duplicating the children multiple times, you could move it into its own component and just import the child into the if/else chain. At least then there won't be duplicated logic, but the props/slots will need to be duplicated.

<script lang='ts'>
  import Children from './children.svelte'

  export let href: string = ''
</script>

{# if href}
  <a {href}>
    <Children><slot></slot></Children>
  </a>
{:else}
  <button>
    <Children><slot></slot></Children>
  </button>
{/if}

3. Create wrapper components

For every wrapper DOM element, just create a new Svelte component. Then, import those as actual Svelte components and use svelte:component

<!-- dom-a.svelte -->

<a {...$$props}><slot></slot></a>
<!-- dom-button.svelte -->

<button {...$$props}><slot></slot></button>
<!-- button.svelte -->
<script lang='ts'>
  import A from './dom-a.svelte'
  import Button from './dom-button.svelte'

  export let href: string = ''

  $: component = href ? A : Button
</script>

<svelte:component this={component}>
 <slot></slot>
</svelte:component>

While this is the nicest to use as a developer, there is a performance penalty for having unknown props. Therefore, it's not idea.

I suppose you could specify every single possible prop in the dom-button.svelte and dom-a.svelte components, but that seems like overkill.

like image 805
Nick Avatar asked Mar 02 '23 15:03

Nick


1 Answers

It is not possible to use <svelte:component> for this. There is a proposal to add a new <svelte:element> tag for this functionality, but it has not been implemented yet. There is an open PR to add this tag to Svelte.

EDIT: this is now possible in Svelte 3.47.0 with svelte:element. You can now do the following:

<script>
    export let href = '';
    
    let tag = href ? 'a' : 'button';
</script>

<svelte:element this={tag} {href}>
    <slot></slot>
</svelte:element>
like image 153
Geoff Rich Avatar answered Mar 12 '23 06:03

Geoff Rich