@svelte-put/preaction

GitHub Github

allow Svelte action to spread SSR-friendly attriutes

@svelte-put/preaction @svelte-put/preaction @svelte-put/preaction @svelte-put/preaction

Still on Svelte 4? See the old docs site here.

Introduction

This is a proof-of-concept Svelte preprocessor that allows usage of "preaction" - an extension to Svelte action with the ability to add static attributes on server-side.

Is this stable / production ready?

It is NOT; use with caution! See "Why would I need this" for more information.

Installation

Be aware that @svelte-put/preaction requires at least Svelte 5 at the time of this writing.

npm install --save-dev @svelte-put/preaction
pnpm add -D @svelte-put/preaction
yarn add -D @svelte-put/preaction

Demo

The example below shows a pattern using Popover API. In this scenario, preaction helps set up SSR-friendly attributes that complies to Popover specs while allowing the use of Svelte action to progressively enhance runtime experience.

Example
My simple popover

(core functionalities still work even when JS is not available)

The source code is:

<script lang="ts">
	import { make, apply } from '@svelte-put/preaction';
	import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';

	const popover = {
		control: make((id: string) => {
			return {
				action: (node: HTMLButtonElement) => {
					// regular runtime Svelte action business
					console.log('popover control', node);
				},
				attributes: {
					popovertarget: id,
					class: 'c-btn',
					popovertargetaction: 'show',
				} as HTMLButtonAttributes,
			};
		}),

		target: make((id: string) => {
			return {
				action: (node: HTMLDivElement) => {
					// regular runtime Svelte action business
					console.log('popover target', node);
				},
				attributes: {
					id,
					class: 'border-2 p-10 m-auto',
					popover: 'auto',
				} as HTMLAttributes<HTMLDivElement>,
			};
		}),
	};
</script>

<button use:apply={popover.control('my-popover')}>
	Open Popover
</button>

<div use:apply={popover.target('my-popover')}>
	My simple popover
</div>

Which is equivalent to:

<script lang="ts">
	const popover = { /** truncated, same as above */ } ;
  const control = popover.control('my-popover');
  const target = popover.target('my-popover');
</script>

<button {...(control.attributes ?? {})} use:control.action={'my-popover'}>
	Open Popover
</button>

<div {...(target.attributes ?? {})} use:target.action={'my-popover'}>
  My simple popover
</div>

Attribute spread is added before any other use directives or attributes to avoid potential conflicts with user-defined attributes.

Guides

0 set up preaction preprocessor

Start by adding preaction to your Svelte config.

import { preaction } from '@svelte-put/preaction';

/** @type {import('@sveltejs/kit').Config} */
export default {
	preprocess: [
		preaction(),
    // other preprocessors
  ],

  // ...truncated...
}

1 make a preaction

Next, prepare a "preaction" using the make helper.

import type { HTMLAttributes } from 'svelte/elements';
import { make } from '@svelte-put/preaction';

export const setMyColor = make((initialColor = 'blue') => {
  // this code runs both both on server and client, as if it is top-level script code.
  // So don't rely on browser stuff such as `document` or `window`.

  return {
    action = (node: HTMLElement, color: string) => {
      // this code is a typical Svelte action and runs only on client
      return {
        update: (newColor: string) => {
          // update the color attribute dynamically at runtime
          node.dataset.color = newColor;
        },
      }
    },

    attributes: {
      // this attributes will be statically spread onto the node
      'data-color': initialColor;
    } as HTMLAttributes<HTMLElement>,
  };
});

// For typescript users, to take full advantage of language tooling,
// like make sure to provide type parameters and attributes as seen above.

Input to make is a function that returns an object with the action and optionally attributes:

function make<Node, Param>(preaction: Preaction<Node, Param>);

// note: if your action does not use any param, simply exclude it from the declaration
type Preaction<Node, Param> = (param: Param) => {
  action: import('svelte/action').Action<Node, Param>;
  attributes?: Record<string, any>;
};

make can be used anywhere in your codebase, not just .svelte files

2 apply a preaction

Finally, call your preaction (created with make) within use:apply directive to apply the encapsulated action and attributes to an element.

<script>
  import { apply } from '@svelte-put/preaction';
  import { setMyColor } from './set-my-color.ts'; // see previous step
</script>

<!-- add data-color on server, and apply action on client -->
<div use:apply={setMyColor('red')}></div>

Why would I need this? (and alternatives)

You probably don't! The intended use case is to extract HTML attribute setup with Svelte action logic into reusable modules. These can already be solved today using either native Svelte action and manual prop spreading...

<!-- alternative: just spread prop manually -->
<button use:myaction {...myprops}></button>

...or encapsulate such logic into a component...

<script>
  // preparation logics, hidden from user of this component
  const myaction = () => { /** action logic */ };
  const myprops = { /** props */ };
</script>

<button use:myaction {...myprops}></button>

Unlike in Svelte 4, the introduction of runes in Svelte 5 as well as simplification of prop declaration and event handler now allows for easier encapsulation than ever.

That being said, I sometimes find myself in need of a solution such as one introduced here because I personally find it more elegant in terms of clear encapsulation and minimal reusability.

Some more discussion about adding SSR-compatible action to Svelte core can be found in issue#4375. Although I will be happy to see this get a first-party support, I am not really sure if it will bring enough benefit over complexity.

This package is essentially a way for me to explore in user land the feasibility of such feature without breaking existing Svelte semantics.

Shoutout

This package is greatly inspired by Melt UI.


Happy pre-acting!

Edit this page on Github