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

Inline component definitions #7972

Closed
brunnerh opened this issue Oct 24, 2022 · 2 comments
Closed

Inline component definitions #7972

brunnerh opened this issue Oct 24, 2022 · 2 comments

Comments

@brunnerh
Copy link
Member

Describe the problem

Integrating Svelte with certain vanilla JS libraries or components from other systems can be cumbersome due to how only a single component can be defined per file.

Consider e.g. a data table component that allows rendering its cells via callback functions that get passed data which is fetched asynchronously from a server. To use Svelte for rendering the cells, you can define a new component for each cell or bundle all the logic into one component and use an {#if} cascade with some flag that identifies the cell to render.

A hypothetical example:

import NameCell from './NameCell.svelte';
import DescriptionCell from './DescriptionCell.svelte';
import ActionsCell from './ActionsCell.svelte';

function tableAction(node) {
	createDataTable({
		table: node,
		dataSource: '/api/data',
		columns: [
			{ title: 'Name', render: row => renderCell(row, NameCell) },
			{ title: 'Description', render: row => renderCell(row, DescriptionCell) },
			{ title: 'Actions', render: row => renderCell(row, ActionsCell) },
		],
	});
}

function renderCell(row, component) {
	const cell = document.createElement('div');
	new component({ target: cell, props: { row } });

	return cell;
}
<!-- NameCell.svelte -->
<script>
	export let row;
</script>
{row.first} {row.last}
<!-- DescriptionCell.svelte -->
<script>
	export let row;
</script>
{row.description}
<!-- ActionsCell.svelte -->
<script>
	export let row;
</script>
<a href="/user/{row.id}">Details</a>

Describe the proposed solution

Allow defining components within a Svelte file, e.g. by reusing svelte:fragment or another special element:

<script>
	let NameCell, DescriptionCell, ActionsCell;
	
	function tableAction(node) {
		createDataTable({
			table: node,
			dataSource: '/api/data',
			columns: [
				{ title: 'Name', render: row => renderCell(row, NameCell) },
				{ title: 'Description', render: row => renderCell(row, DescriptionCell) },
				{ title: 'Actions', render: row => renderCell(row, ActionsCell) },
			],
		});
	}
	
	function renderCell(row, component) {
		const cell = document.createElement('div');
		new component({ target: cell, props: { row } });
	
		return cell;
	}
</script>

<svelte:fragment bind:this={NameCell} let:row>
	{row.first} {row.last}
</svelte:fragment>
<svelte:fragment bind:this={DescriptionCell} let:row>
	{row.description}
</svelte:fragment>
<svelte:fragment bind:this={ActionsCell} let:row>
	<a href="/user/{row.id}">Details</a>
</svelte:fragment>
  • bind:this would cause the fragment to be compiled to a component that is then assigned to the referenced variable.
  • let bindings are analogous to props (export let ... ) on regular components
  • Events forwarded with on:event could be handled on the component instance via $on and the other API instance functions should work the same as for any component if possible

Ideally it would be possible to also use this to just extract elements locally, e.g. if the hierarchy needs to be dynamic but you do not want to separate common parts into extra files, e.g.:

<script>
	export let data;

	let Content;
	let inList = /* some logic */;
</script>

<svelte:fragment bind:this={Content}>
	<h2>{data.title}</h2>

	<p>Items: {data.length}</p>
	{#each data as item}
		<div>...</div>
	{/each}
</svelte:fragment>

{#if inList}
	<ul>
		<li><svelte:component this={Content} /></li>
	</ul>
{:else}
	<svelte:component this={Content} />
{/if}

Or maybe it would be possible to assign a name so the extra binding and svelte:component becomes unnecessary:

<script>
	export let data;

	let inList = /* some logic */;
</script>

<svelte:fragment name="Content">
	<h2>{data.title}</h2>
	...
</svelte:fragment>

{#if inList}
	<ul>
		<li><Content /></li>
	</ul>
{:else}
	<Content />
{/if}

Alternatives considered

The {#if} cascade in a single component is probably the best current workaround if many separate components would have to be created otherwise.

For the above example that would be something like:

<script>
	export let row;
	export let cell;
</script>

{#if cell == 'name'}
	{row.first} {row.last}
{:else if cell == 'description'}
	{row.description}
{:else if cell == 'action'}
	<a href="/user/{row.id}">Details</a>
{/if}

Importance

would make my life easier

@Conduitry
Copy link
Member

There is existing discussion for this sort of thing over in sveltejs/rfcs#34

@brunnerh
Copy link
Member Author

brunnerh commented Dec 5, 2023

Closing this as being mostly addressed by snippets.

For programmatic rendering a simple wrapper component can be used, e.g.

<!-- Snippet.svelte -->
<script context="module">
	import { mount } from 'svelte';
	import Snippet from './Snippet.svelte';

	export function mountSnippet(target, snippet, args) {
		return mount(Snippet, { target, props: { snippet, args }});
	}
</script>

<script>
	const { snippet, args } = $props();
</script>

{@render snippet(args)}
<script>
	import { mountSnippet } from './Snippet.svelte';

	$effect(() => {
		mountSnippet(document.body, snippet, 'world');
	});
</script>

{#snippet snippet(arg)}
	Hello {arg}!
{/snippet}

@brunnerh brunnerh closed this as completed Dec 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants