DEV Community

Cover image for Svelte journey | Actions, Advanced Bindings, Classes & Styles, Slots
Denys Sych
Denys Sych

Posted on • Updated on

Svelte journey | Actions, Advanced Bindings, Classes & Styles, Slots

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>
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Check slot’s presence / conditional slots

You can check their presence via $$slots :

{#if $$slots.header}
    <div class="header">
        <slot name="header"/>
    </div>
{/if}
Enter fullscreen mode Exit fullscreen mode

See you in the next article,

take care, go Svelte!

Resources

Top comments (0)