Svelte journey | Actions, Advanced Bindings, Classes & Styles, Slots
Tags: javascript, tutorial, svelte, frontend
Published: 2023-12-27 17:35:26

Welcome,

In the previous part, we reviewed [Svelte’s capabilities for smooth visual effects](https://dev.to/chillyhill/svelte-journey-advanced-motion-transitions-animations-32d5), and today we’re going to explore actions, some more bindings, styles and content projection.

## Actions — element-level lifecycle functions

It feels like [behavior directives](https://angular.io/guide/attribute-directives) in Angular or [useRef + DOM manipulations](https://react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref) in React.

Useful for things like:

- interfacing with third-party libraries;
- various DOM operations.

```jsx
// actions.js 
/*
 * node - target DOM Node
 * options - optional custom params. Useful for 3rd party prop. pass 
*/
export function trapFocus(node, options) {
	function handleKeydown(event) { ... }
  ...
	node.addEventListener('keydown', handleKeydown);
	return {
		destroy() { // clean up on node unmount
			node.removeEventListener('keydown', handleKeydown);
		}
	};
}

// App.svelte
...
<div class="menu" use:trapFocus={{anyParameters: true, youWantToPass: value}}>...</div>
```

## Advanced bindings

Just a reminder, it is about bi-directional value synchronization. Advanced bindings are the same bindings by nature as “basic” ones, just use-cases are not as common as the regular `value` attribute or component’s input bindings.

```jsx
// 'contenteditable' elements support 'textContent' and 'innerHTML' bindings
<div bind:innerHTML={html} contenteditable /> 

// bind:checked, bind:value works with #loop. But this mutates the array! 
{#each todos as todo}
	<li class:done={todo.done}>
		<input type="checkbox"	bind:checked={todo.done} />
		<input type="text" bind:value={todo.text}	/>
	</li>
{/each}

// <audio> and <video> (media) el-s support:
<audio
		{src}
		bind:currentTime={time}
		bind:duration
		bind:paused
		bind:buffered
		bind:seeking
		bind:ended
		bind:readyState
		bind:playbackRate
		bind:volume
		bind:muted
	/> // + <video /> has bind:videoWidth and bind:videoHeight

// Every block-level element has clientWidth, clientHeight, offsetWidth, and offsetHeight
<div bind:clientWidth={w} bind:clientHeight={h}>
	<span style="font-size: {size}px" contenteditable>{text}</span>
	<span class="size">{w} x {h}px</span>
</div>

// bind:this is like useRef - gives us a reference to the element (or component instance)
// Stays 'undefined' until mounted
<canvas	bind:this={canvasRef}	width={32} height={32} />
<CanvasWrapper bind:this={canvas} size={size} />

// Component input bindings work the same as other bindings. Not recommended as data flow becomes implicit
<Keypad	bind:value={pin} on:submit={handleSubmit} />
```

### Bindings in a loop mutate the array

`<input>` elements will mutate the array. If you prefer to work with immutable data, you should avoid these bindings and use event handlers instead. 

```jsx
{#each todos as todo}
		<input type="text" value={todo.text} on:change={(e) => updateTodo(e, todo.id)}	/>
{/each}
```

### Block-level bindings (clientWidth, clientHeight, offsetWidth, and offsetHeight) limitations

- These bindings are readonly (if you change the bounded value it makes no effect);
- `display: inline` elements cannot be measured;
- elements that can't contain other elements (such as `<canvas>`) cannot be measured.

### `bind:this`

Works both for regular DOM elements and Svelte components (binds to an instance). It feels like useImperativeHandle with forwardRef in React. To define what can be used outside, mark it with export keyword:

```jsx
// CanvasWrapper.svelte --------------------------------------------
<script>
	export const clear = () => ...;
</script>

// App.svelte --------------------------------------------
<script>
	let canvas;
	let size = 10;
</script>
...
<CanvasWrapper bind:this={canvas} size={size} />
<button on:click={() => canvas.clear()}>reset</button>
```

## Classes and styles

Work like any other attribute.

```jsx
// Class attribute
<span class="card {isReady ? 'ready foo' : 'bar baz'}" /> // you can provide a string
<span class:flipped={isItemFlipped} /> // or, shorter, class:<name>={boolean}
<span class:flipped /> or, even shorter, when the class name meets the variable name

// Style attribute
<span style="transform: {flipped ? 'rotateY(0)' : ''}; --bg-1: palegoldenrod; --bg-2: black; --bg-3: goldenrod" />
// Or with style directive
<span 
	style:transform={flipped ? 'rotateY(0)' : ''}
	style:--bg-1="palegoldenrod"
	style:--bg-2="black"
	style:--bg-3="goldenrod" />
```

### Child component style override

The first option: **:global** modifier. ❗Bad practice approach — you gain a lot of power but it violates component approach basics. Use wisely.

Better is when the component decides which part of its styling is modifiable. For that purpose in Svelte is the mechanism that is about providing custom CSS variables:

```jsx
// Box.svelte
<div class="box" />

<style>
	.box {
		background-color: var(--color, black); // --color is provided or defaults to "black"
	}
</style>

// App.svelte
<script>
	import Box from './Box.svelte';
</script>

<div class="boxes">
	<Box --color="red" /> // --color is passed to <Box />
</div>
```

## Component composition — `<slot />`

In other words, where the component’s children should settle.

```jsx
// Default (single) slot - Card.svelte
<div class="card">
	<slot />
</div>
// Single slot - App.svelte
<Card>
	<span>Patrick BATEMAN</span> // Goes instead of <slot /> inside Card
	<span>Head of HR</span> // Goes instead of <slot /> inside Card as well
</Card>
```

**Named slots**

Pretty much the same as regular ones but if the slot is named, it goes to a relative place.

```jsx

// Named slots - Card.svelte
<div class="card">
	<header>
		<slot name="company" />
	</header>
	<slot />
	<footer>
		<slot name="address" />
	</footer>
</div>
//Named slots - App.svelte
<span>Patrick BATEMAN</span>
<span>Head of HR</span>
<span slot="company">Mental health coaching</span>
<span slot="address">358 Exchange Place, New York </span>
```

**Slot fallback**

If nothing is passed to a slot, it displays content inside.

```jsx

// Slot fallback
<div class="card">
	<header>
		<slot name="company">(Company) Fallback for named slot</slot>
	</header>
	<slot>Fallback for default slot</slot>
</div>
```

**Slot props**

To pass data *back* to the slotted content.

```jsx
// FilterableList.svelte
<script>
	export let data;
  ...
</script>
<div class="list">
	{#each data.filter(matches) as item}
		<slot {item} /> // passing required prop to the slotted content
	{/each>
</div>
...

//App.svelte
<FilterableList
	data={colors}
	field="name"
	let:item={row} // <- it tells that "item" inside iterable should be passed as "row" to this slotted content
>
	<div class="row">
		<span class="color" style="background-color: {row.hex}" />
		<span class="name">{row.name}</span>
	</div>
</FilterableList>
```

For named slots, it is pretty much the same. You just need to put `let:` inside a slot: 

```jsx
<!-- FancyList.svelte -->
<ul>
	{#each items as item}
		<li class="fancy">
			<slot name="item" {item} />
		</li>
	{/each}
</ul>

<slot name="footer" />

<!-- App.svelte -->
<FancyList {items}>
	<div slot="item" let:item>{item.text}</div>
	<p slot="footer">Copyright (c) 2019 Svelte Industries</p>
</FancyList>
```

**Check slot’s presence / conditional slots**

You can check their presence via `$$slots` :

```jsx
{#if $$slots.header}
	<div class="header">
		<slot name="header"/>
	</div>
{/if}
```

See you in the next article,

take care, go Svelte!

## Resources

- [Svelte Tutorial](https://learn.svelte.dev/tutorial) | Motions, Transitions, Animations
- [Svelte docs | Slots](https://svelte.dev/docs/special-elements#slot-slot-name-name)