Welcome,
In the previous part, we reviewed Svelte’s capabilities for smooth visual effects, 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 in Angular or useRef + DOM manipulations in React.
Useful for things like:
- interfacing with third-party libraries;
- various DOM operations.
// 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.
// '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.
{#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:
// 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.
// 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:
// 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.
// 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.
// 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.
// 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.
// 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:
<!-- 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
:
{#if $$slots.header}
<div class="header">
<slot name="header"/>
</div>
{/if}
See you in the next article,
take care, go Svelte!
Resources
- Svelte Tutorial | Motions, Transitions, Animations
- Svelte docs | Slots
Top comments (0)