# Svelte tutorial Dette er lærdommer og notater fra https://svelte.dev/tutorial/ som deles innad i #playground-svelte i Knowit. Eirik Vågeskar begynte å skrive dem, men hvem som helst er velkomne til å bidra. Ved å klikke på snakkeboblen som dukker opp til høyre for elementer kan du legge igjen kommentarer og stille spørsmål. ## Grunnleggende ### Syntaks Filer skrives som en blanding av HTML, script og style. Du bruker Javascript-variabler i HTML ved å bruke krøllparenteser, `{}`: `<h1>Hello {name}!</h1>` ```htmlmixed= <script> let src = 'tutorial/image.gif'; let name = 'Rick Astley'; </script> <style> p { color: purple; font-family: 'Comic Sans MS'; font-size: 2em; } </style> <img {src} alt="{name} dances."> <p>This is in purple comic sans</p> ``` Du importerer ting med import-utsagn: ```htmlmixed= <script> import Nested from './Nested.svelte'; </script> <style> p { color: purple; font-family: 'Comic Sans MS'; font-size: 2em; } </style> <p>This is a paragraph.</p> <Nested/> ``` Du kan inline HTML ved å bruke @html før variabelen ```htmlmixed= <script> let string = `this string contains some <strong>HTML!!!</strong>`; </script> <p>{@html string}</p> ``` ### Kompilering Svelte har sin egen kompilator. For å bruke den, må du integrere den i bygget ditt. Da kan du bruke: - Rollup / rollup-plugin-svelte - webpack / svelte-loader - Parcel / parcel-plugin-svelte ### Behandle hendelser Hendelses- og meldingsbehandlere bruker `on:`-prefiks. DOM-en skjønner selv når den må oppdateres: ```htmlmixed= <script> let count = 0; function handleClick() { count += 1; } </script> <button on:click={handleClick}> Clicked {count} {count === 1 ? 'time' : 'times'} </button> ``` ### Reaktivitet «Reaktive» verdier er verdier som avhenger av andre verdier. Disse markeres med et dollartegn ved erklæring. ```htmlmixed= <script> let count = 0; $: doubled = count * 2; function handleClick() { count += 1; } </script> <button on:click={handleClick}> Clicked {count} {count === 1 ? 'time' : 'times'} </button> <p>{count} doubled is {doubled}</p> ``` Oppdateringene fyres når man bruker tilegningsoperatoren, altså `=`. Så hvis man bruker muterende metoder på et objekt, for eksempel `.push()` må man samtidig tilegne variabelen til seg selv på nytt. ```htmlmixed= function addNumber() { numbers.push(numbers.length + 1); numbers = numbers; } ``` Men det kan vere like greit å bruke spread-operatorer og annen snasen syntaks. Legg merke til at `$` også kan brukes til utsagn som ikke har et resultat, som console.logging når sum endrer seg. ```htmlmixed= <script> let numbers = [1, 2, 3, 4]; function addNumber() { numbers = [...numbers, numbers.length + 1]; } $: sum = numbers.reduce((t, n) => t + n, 0); $: console.log(sum); </script> <p>{numbers.join(' + ')} = {sum}</p> <button on:click={addNumber}> Add a number </button> ``` ![](https://i.imgur.com/0WyzOl9.png) ## Props Props er noe du dytter nedover til komponenter du bruker, som i React. Syntaksen for dette i Svelte er litt rar, men du blir vant til det: I stedet for å tilegne en verdi til en variabel, skriver du `export` foran den. For å gi en standardverdi, setter du likhetstegn etter. ```htmlmixed= <!-- Nested.svelte --> <script> export let answer = "a mystery"; </script> <p>The answer is {answer}</p> <!-- App.svelte --> <script> import Nested from './Nested.svelte'; </script> <Nested answer={42}/> <Nested/> ``` ![](https://i.imgur.com/mI4OCXy.png) Det går an å bruke spredning til å legge props på komponenter. ```htmlmixed= <script> import Info from './Info.svelte'; const pkg = { name: 'svelte', version: 3, speed: 'blazing', website: 'https://svelte.dev' }; </script> <Info {...pkg}/> ``` Du kan også referere props som ikke ble erklært i en komponent ved å bruke `$$props`-variabelen inni komponenten, men det anbefales ikke, siden det er vanskelig for Svelte å optimalisere. ## Logikk ### Kondisjonalitet For å uttrykke logikk, bruker man forskjellige varianter av uttrykk inni krølleparenteser. `#` uttrykker åpning, `:` fortsettelse og `/` avslutning av en blokk. ```htmlmixed= <script> let user = { loggedIn: false }; function toggle() { user.loggedIn = !user.loggedIn; } </script> {#if user.loggedIn} <button on:click={toggle}> Log out </button> {/if} {#if !user.loggedIn} <button on:click={toggle}> Log in </button> {/if} ``` ```htmlmixed= {#if x > 10} <p>{x} is greater than 10</p> {:else if 5 > x} <p>{x} is less than 5</p> {:else} <p>{x} is between 5 and 10</p> {/if} ``` ### Løkker Løkker lager du med `#each`. I eksemplet under, legg også merke til: - Til løkken får man også med et andre argument: indeks - Man kan destrukturere objekter, slik at cat -> {id, name} ```htmlmixed= <script> let cats = [ { id: 'J---aiyznGQ', name: 'Keyboard Cat' }, { id: 'z_AbfPXTKms', name: 'Maru' }, { id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' } ]; </script> <h1>The Famous Cats of YouTube</h1> <ul> {#each cats as {id, name}, i} <li><a target="_blank" href="https://www.youtube.com/watch?v={id}"> {i+1}: {name} </a></li> {/each} </ul> ``` #### Nøkler i løkker Under panseret gjør Svelte noen optimaliseringer som gjør at man ikke trygt kan endre innholdet i en liste og regne med at det blir oppdatert. Så det er lurt å fortelle Svelte om hva som er en unik identifikator for objektet. Slik som `thing.id` i eksempelet under. Du kan også bruke objektet i seg selv, men tutorialen anbefaler at man bruker en streng eller et tall, fordi man ellers kan komme borti problemer med referanselikhet kontra faktisk likhet, for eksempel hvis man oppdaterer med data fra et API. **Merk:** Jeg er usikker på om denne nøkkelen er nødt til å endre seg dersom noe inni objektet endrer seg. For eksempel, hvis man henter nytt ```htmlmixed= <script> import Thing from './Thing.svelte'; let things = [ { id: 1, value: 'a' }, { id: 2, value: 'b' }, { id: 3, value: 'c' }, { id: 4, value: 'd' }, { id: 5, value: 'e' } ]; function handleClick() { things = things.slice(1); } </script> <button on:click={handleClick}> Remove first thing </button> {#each things as thing (thing.id)} <Thing value={thing.value}/> {/each} ``` #### Await-blokker En av de kuleste tingene i Svelte: Du kan vente på løfter (promises) og asynkrone funksjoner med `{#await}`-blokker. ```htmlmixed= <script> let promise = getRandomNumber(); async function getRandomNumber() { const res = await fetch(`tutorial/random-number`); const text = await res.text(); if (res.ok) { return text; } else { throw new Error(text); } } function handleClick() { promise = getRandomNumber(); } </script> <button on:click={handleClick}> generate random number </button> {#await promise} <p>...waiting</p> {:then number} <p>The number is {number}</p> {:catch error} <p style="color: red">{error.message}</p> {/await} ``` ## Hendelser (events) Du kan reagere på hendelser fra Dom-en med `on:<hendelsesnavn>`. ```htmlmixed= <script> let m = { x: 0, y: 0 }; function handleMousemove(event) { m.x = event.clientX; m.y = event.clientY; } </script> <style> div { width: 100%; height: 100%; } </style> <div on:mousemove={handleMousemove}> The mouse position is {m.x} x {m.y} </div> ``` Du kan fint definere hendelsesbehandleren (event handler) på stedet. I motsetning til i andre språk, der dette har en kostnad, koster det ingenting her. Legg merke til hermetegn rundt definisjonen. Dette er ikke nødvendig, sier tutorialen, men det er for å hjelpe med syntaksmarkering i visse editorer. ```htmlmixed= <script> let m = { x: 0, y: 0 }; </script> <style> div { width: 100%; height: 100%; } </style> <div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}"> The mouse position is {m.x} x {m.y} </div> ``` ### Modifikatorer Hendelsesbehandlere kan ha modifikatorer som gjør noe med oppførselen deres. ```htmlmixed= <button on:click|once="{() => alert('clicked')}"> Click me </button> ``` Her er alle modifikatorer, ifølge tutorialen > - `preventDefault` — calls event.preventDefault() before running the handler. Useful for client-side form handling, for example. > - `stopPropagation` — calls event.stopPropagation(), preventing the event reaching the next element > - `passive` — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so) > - `capture` — fires the handler during the capture phase instead of the bubbling phase ([Introduction to events - Learn web development | MDN](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture)) > - `once` — remove the handler after the first time it runs ### Komponent-hendelser For å sende beskjeder mellom komponenter, kan du avfyre (dispatche) egne hendelser. I utgangspunktet bobler ikke hendelsene oppover; du er altså nødt til å definere en måte å behandle hendelsen _på_ selve objektet. ```htmlmixed= <!-- Inner.svelte --> <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); function sayHello() { dispatch('message', { text: 'Hello!' }); } </script> <button on:click={sayHello}> Click to say hello </button> <!-- App.svelte --> <script> import Inner from './Inner.svelte'; function handleMessage(event) { alert(event.detail.text); } </script> <Inner on:message={handleMessage}/> ``` ### Videresending Hvis du vil at komponenter lenger oppe skal reagere, må du videresende hendelsen. Dette går an å gjøre på forskjellige måter, men den enkleste er å bare skrive `<X on:<meldingstype> />` uten å skrive en funksjon bak; da videresendes meldingene automatisk til komponenten utenfor. Se [Events / Event forwarding • Svelte Tutorial](https://svelte.dev/tutorial/event-forwarding) hvis du trenger å se et eksempel. Du kan også videresende Dom-hendelser til komponentens forelder. ```htmlmixed= <!-- FancyButton.svelte --> <style> button { font-family: 'Comic Sans MS'; font-size: 2em; padding: 0.5em 1em; color: royalblue; background: gold; border-radius: 1em; box-shadow: 2px 2px 4px rgba(0,0,0,0.5); } </style> <button on:click> Click me </button> ``` ```htmlmixed= <!-- App.svelte --> <script> import FancyButton from './FancyButton.svelte'; function handleClick() { alert('clicked'); } </script> <FancyButton on:click={handleClick} /> ``` ## Binding ### Bundne variabler Hvis jeg nå hadde sagt at du skulle sendt en verdi fra barn til forelder, ville du sannsynligvis brukt meldinger til det. Det blir litt «boilerplatey», som tutorialen kaller det. Derfor kan man bruke `bind:value={<variabelnavn>}` til det. Denne bindingen er toveis, så en endring av `variabelnavn` vil også føre til at den bundne verdien oppdaterer seg. **Kult, hæh?** ```htmlmixed= <script> const originalName = 'world'; let name = 'world'; </script> <input bind:value={name}> <h1>Hello {name}!</h1> <button on:click="{() => name = originalName}"> Tilbakestill </button> ``` ### Automatisk typing av numeriske inputs ved binding I den vanlige Dom-en er alt en streng, også numeriske inputs. Men når man binder en variabel til en type, tar Svelte seg automatisk av å konvertere typen. ```htmlmixed= <script> let a = 1; let b = 2; </script> <label> <input type=number bind:value={a} min=0 max=10> <input type=range bind:value={a} min=0 max=10> </label> <label> <input type=number bind:value={b} min=0 max=10> <input type=range bind:value={b} min=0 max=10> </label> <p>{a} + {b} = {a + b}</p> ``` ![](https://i.imgur.com/58r79yU.png) ### Sjekkbokser For sjekkbokser må du huske på å binde til «checked»-attributtet, ikke «value». Det er bare slik det er. ```htmlmixed= <script> let yes = false; </script> <label> <input type=checkbox bind:checked={yes}> Yes! Send me regular email spam </label> {#if yes} <p>Thank you. We will bombard your inbox and sell your personal details.</p> {:else} <p>You must opt in to continue. If you're not paying, you're the product.</p> {/if} <button disabled={!yes}> Subscribe </button> ``` ### Input-grupper Radioknapper og avkrysningsbokser kommer ofte i grupper. Under panseret er representasjonen av verdien til en radioknappgruppe en (ikke-nullbar) verdi, mens avkrysningsboksgrupper er en tabell med en liste av de valgte verdiene. Svelte gjør det lett å holde styr på slike grupper hvis du bruker `bind:group`. ```htmlmixed= <script> let scoops = 1; let flavours = ['Mint choc chip']; $: console.log(flavours); function join(flavours) { if (flavours.length === 1) return flavours[0]; return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`; } </script> <h2>Size</h2> <label> <input type=radio bind:group={scoops} value={1}> One scoop </label> <label> <input type=radio bind:group={scoops} value={2}> Two scoops </label> <label> <input type=radio bind:group={scoops} value={3}> Three scoops </label> <h2>Flavours</h2> <label> <input type=checkbox bind:group={flavours} value="Cookies and cream"> Cookies and cream </label> <label> <input type=checkbox bind:group={flavours} value="Mint choc chip"> Mint choc chip </label> <label> <input type=checkbox bind:group={flavours} value="Raspberry ripple"> Raspberry ripple </label> {#if flavours.length === 0} <p>Please select at least one flavour</p> {:else if flavours.length > scoops} <p>Can't order more flavours than scoops!</p> {:else} <p> You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'} of {join(flavours)} </p> {/if} ``` ### Kortform av binding Hvis noe du binder til heter det samme som variabelen du skal lagre til, kan du bruke kortform. For eksempel heter verdien til et tekstområde `value`; da kan man bruke `bind:value`. ```htmlmixed= <script> import marked from 'marked'; let value = `Some words are *italic*, some are **bold**`; </script> <style> textarea { width: 100%; height: 200px; } </style> <textarea bind:value/> {@html marked(value)} ``` ### Valglister (select) Valglister kan også få en bunden verdi. I eksempelet under er det en annen lærdom: Variabelen `selected` blir ikke bundet før select-boksen er initialisert i Dom-en. Derfor må man være forsiktig og ikke referere til `selected.id` før selected er definert (se siste linje). ```htmlmixed= <script> let questions = [ { id: 1, text: `Where did you go to school?` }, { id: 2, text: `What is your mother's name?` }, { id: 3, text: `What is another personal fact that an attacker could easily find with Google?` } ]; let selected; let answer = ''; function handleSubmit() { alert(`answered question ${selected.id} (${selected.text}) with "${answer}"`); } </script> <style> input { display: block; width: 500px; max-width: 100%; } </style> <h2>Insecurity questions</h2> <form on:submit|preventDefault={handleSubmit}> <select bind:value={selected} on:change="{() => answer = ''}"> {#each questions as question} <option value={question}> {question.text} </option> {/each} </select> <input bind:value={answer}> <button disabled={!answer} type=submit> Submit </button> </form> <p>selected question {selected ? selected.id : '[waiting...]'}</p> ``` #### Velge flere verdier Det går an å lage en select-boks med flere verdier ved å bruke attributtet `multiple`. Her er vi tilbake til iskrem-eksemplet vi startet med: ```htmlmixed= <script> let scoops = 1; let flavours = ['Mint choc chip']; let menu = [ 'Cookies and cream', 'Mint choc chip', 'Raspberry ripple' ]; function join(flavours) { if (flavours.length === 1) return flavours[0]; return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`; } </script> <h2>Size</h2> <label> <input type=radio bind:group={scoops} value={1}> One scoop </label> <label> <input type=radio bind:group={scoops} value={2}> Two scoops </label> <label> <input type=radio bind:group={scoops} value={3}> Three scoops </label> <h2>Flavours</h2> <select multiple bind:value={flavours}> {#each menu as flavour} <option value={flavour}> {flavour} </option> {/each} </select> {#if flavours.length === 0} <p>Please select at least one flavour</p> {:else if flavours.length > scoops} <p>Can't order more flavours than scoops!</p> {:else} <p> You ordered {scoops} {scoops === 1 ? 'scoop' : 'scoops'} of {join(flavours)} </p> {/if} ``` ### Redigerbart innhold (contenteditable) Du kan redigere innholdet i et element ved å sette `contenteditable="true"`. Da får du ut to verdier: `textContent` og `innerHTML`. I eksempelet under bruker vi innerHTML. ```htmlmixed= <script> let html = '<p>Write some text!</p>'; </script> <div contenteditable="true" bind:innerHTML={html}></div> <pre>{html}</pre> <style> /* Dette er en selektor for en attributt. Kult, hæh? */ [contenteditable] { padding: 0.5em; border: 1px solid #eee; border-radius: 4px; } </style> ``` ![](https://i.imgur.com/5TQVZ27.png) Hvis du har lyst til å lage en HTML-editor, kan du kanskje kjøre med litt textContent og `@html`: ```htmlmixed= <script> let textContent = '<p>Write some text!</p>'; </script> <div contenteditable="true" bind:textContent={textContent}></div> <div> {@html textContent} </div> <style> /* Dette er en selektor for en attributt. Kult, hæh? */ [contenteditable] { padding: 0.5em; border: 1px solid #eee; border-radius: 4px; } </style> ``` ![](https://i.imgur.com/FAzLBZO.png) ### Binding inni each-løkker Du kan binde data også inni each-løkker. Referansene blir like sterke som ved binding utenfor løkkene. Merk: Tutorialen nevner noe om at dette muterer det faktiske arrayet; om man ønsker å jobbe med ikke-muterbar data, må man sende oppdateringsmeldinger i stedet. ```htmlmixed= <script> let todos = [ { done: false, text: 'finish Svelte tutorial' }, { done: false, text: 'build an app' }, { done: false, text: 'world domination' } ]; function add() { todos = [... todos, { done: false, text: '' }]; } function clear() { todos = todos.filter(t => !t.done); } $: remaining = todos.filter(t => !t.done).length; </script> <style> .done { opacity: 0.4; } </style> <h1>Todos</h1> {#each todos as todo} <div class:done={todo.done}> <input type=checkbox bind:checked={todo.done} > <input placeholder="What needs to be done?" bind:value={todo.text} > </div> {/each} <p>{remaining} remaining</p> <button on:click={add}> Add new </button> <button on:click={clear}> Clear completed </button> ``` ### Binde til mediaelementer Mediaelementer har også egenskaper som du kan binde til. Du har fire egenskaper som kun kan leses: > - duration (readonly) — the total duration of the video, in seconds > - buffered (readonly) — an array of {start, end} objects > - seekable (readonly) — ditto > - played (readonly) — ditto … og fire egenskaper som du kan lage toveisbindinger til: > - currentTime — the current point in the video, in seconds > - playbackRate — how fast to play the video, where 1 is 'normal' > - paused — this one should be self-explanatory > - volume — a value between 0 and 1 Du kan gjøre en oppgave relatert til disse på https://svelte.dev/tutorial/media-elements ### Binde til elementdimensjoner Elementer har fire variabler som forteller deg om et elements dimensjoner i nettleseren: `clientWidth`, `clientHeight`, `offsetWidth` og `offsetHeight`. Forskjellen på «client» og «offset»-dataen er at offset inkluderer noe mer av det omkringliggende for elementet, for eksempel padding (men ikke margin. Du finner et nøyaktig svar her: https://stackoverflow.com/a/4106585/1858138 Du kan bytte mellom offset og client under for å se forskjellen. ```htmlmixed= <script> let w; let h; let size = 42; let text = 'edit me'; </script> <style> input { display: block; } div { display: inline-block; padding: 50px; } </style> <input type=range bind:value={size}> <input bind:value={text}> <p>size: {w}px x {h}px</p> <div bind:offsetWidth={w} bind:offsetHeight={h}> <span style="font-size: {size}px">{text}</span> </div> ``` ### this-binding Alle elementer har en `this`. Dette er en referanse til elementet i seg selv, slik det er er rendret. Du kan bruke denne referansen til å gi instrukser til et element mens det kjører. Under ser du et eksempel på hvorfor dette er nyttig: Her har vi et canvas-element som vi imperativt skal endre farge på. Vi kan ikke kjøre kode på det i HTML-delen av komponenten – vi er nødt til å ha en referanse til det i script-delen av elementet, så vi skriver `bind:this={canvas}`. I `onMount` instruerer vi om hvordan elementet skal endre seg. **Hvorfor onMount? Hvorfor ikke bare legge funksjoner på elementet med en gang?** Fordi elementet ikke ennå er initialisert når komponenten starter. Derfor må vi vente til ting har montert seg. ```htmlmixed= <script> import { onMount } from 'svelte'; let canvas; onMount(() => { const ctx = canvas.getContext('2d'); let frame; (function loop() { frame = requestAnimationFrame(loop); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let p = 0; p < imageData.data.length; p += 4) { const i = p / 4; const x = i % canvas.width; const y = i / canvas.height >>> 0; const t = window.performance.now(); const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000)); const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000)); const b = 128; imageData.data[p + 0] = r; imageData.data[p + 1] = g; imageData.data[p + 2] = b; imageData.data[p + 3] = 255; } ctx.putImageData(imageData, 0, 0); }()); return () => { cancelAnimationFrame(frame); }; }); </script> <style> canvas { width: 100%; height: 100%; background-color: #666; -webkit-mask: url(logo-mask.svg) 50% 50% no-repeat; mask: url(logo-mask.svg) 50% 50% no-repeat; } </style> <canvas bind:this={canvas} width={32} height={32} ></canvas> ``` ### Komponent-bindinger Hvis du bruker en komponent, kan du lage en toveisbinding til en av dens indre variabler ved å bruke `bind:<barnetsVariabel>:{<forelderensVariabel>}`. **Merk:** Opplæringen sier at dette er et triks man må bruke _med måte_: Hvis man ikke har én enkelt kilde til sannheten, blir det fort vanskelig å spore dataflyt rundt i applikasjonen. ```htmlmixed= <!-- App.svelte --> <script> import Keypad from './Keypad.svelte'; let pin; $: view = pin ? pin.replace(/\d(?!$)/g, '•') : 'enter your pin'; function handleSubmit() { alert(`submitted ${pin}`); } </script> <h1 style="color: {pin ? '#333' : '#ccc'}">{view}</h1> <Keypad on:submit={handleSubmit} bind:value={pin}/> <!-- Keypad.svelte --> <script> import { createEventDispatcher } from 'svelte'; export let value = ''; const dispatch = createEventDispatcher(); const select = num => () => value += num; const clear = () => value = ''; const submit = () => dispatch('submit'); </script> <style> .keypad { display: grid; grid-template-columns: repeat(3, 5em); grid-template-rows: repeat(4, 3em); grid-gap: 0.5em } button { margin: 0 } </style> <div class="keypad"> <button on:click={select(1)}>1</button> <button on:click={select(2)}>2</button> <button on:click={select(3)}>3</button> <button on:click={select(4)}>4</button> <button on:click={select(5)}>5</button> <button on:click={select(6)}>6</button> <button on:click={select(7)}>7</button> <button on:click={select(8)}>8</button> <button on:click={select(9)}>9</button> <button disabled={!value} on:click={clear}>clear</button> <button on:click={select(0)}>0</button> <button disabled={!value} on:click={submit}>submit</button> </div> ``` ## Livssyklus ### onMount Som i React, bruker man onMount til å kjøre ting når komponenten først monteres. **Merk:** Å vise loading når `photos.length == 0` er et skikkelig anti-mønster. Bruk heller en egnet datatype til å representere dataen: https://github.com/nvie/lemons.js/blob/master/src/LazyResult.js (Usikker på om nevnte datatype er veldig egnet for Svelte pga. muterbarhet, men vi får rett og slett se). ```htmlmixed= <script> import { onMount } from 'svelte'; let photos = []; onMount(async () => { const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`); photos = await res.json(); }) </script> <style> .photos { width: 100%; display: grid; grid-template-columns: repeat(5, 1fr); grid-gap: 8px; } figure, img { width: 100%; margin: 0; } </style> <h1>Photo album</h1> <div class="photos"> {#each photos as photo} <figure> <img src={photo.thumbnailUrl} alt={photo.title}> <figcaption>{photo.title}</figcaption> </figure> {:else} <!-- this block renders when photos.length === 0 --> <p>loading...</p> {/each} </div> ``` ### onDestroy `onDestroy` brukes til å rydde opp når komponenten demonteres. Eksempelvis rydder vi i eksempelet under opp etter en callback som kunne blitt kalt i all evighet. Livssyklusmetoder trenger ikke å kalles direkte i komponenten; de kan importeres fra en hjelperfil, slik som under. ```htmlmixed= <!-- App.svelte --> <script> import { onInterval } from './utils.js'; let seconds = 0; onInterval(() => {seconds += 1}, 1000) </script> <p> The page has been open for {seconds} {seconds === 1 ? 'second' : 'seconds'} </p> ``` ```javascript= // utils.js import { onDestroy } from 'svelte'; export function onInterval(callback, milliseconds) { const interval = setInterval(callback, milliseconds); onDestroy(() => clearInterval(interval)) } ``` ### beforeUpdate og afterUpdate Disse to funksjonene skjer på følgende tidspunkter i livssyklusen: Rett _før_ Dom-en oppdateres, og rett _etter_ at Dom-en oppdateres. Hva er så poenget med slikt? Jo, for eksempel hvis man skal autoscrolle, slik som med chatbotten nedenfor. ```htmlmixed= <script> import Eliza from 'elizabot'; import { beforeUpdate, afterUpdate } from 'svelte'; let div; let autoscroll; beforeUpdate(() => { autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20); }); afterUpdate(() => { if (autoscroll) div.scrollTo(0, div.scrollHeight); }); const eliza = new Eliza(); let comments = [ { author: 'eliza', text: eliza.getInitial() } ]; function handleKeydown(event) { if (event.which === 13) { const text = event.target.value; if (!text) return; comments = comments.concat({ author: 'user', text }); event.target.value = ''; const reply = eliza.transform(text); setTimeout(() => { comments = comments.concat({ author: 'eliza', text: '...', placeholder: true }); setTimeout(() => { comments = comments.filter(comment => !comment.placeholder).concat({ author: 'eliza', text: reply }); }, 500 + Math.random() * 500); }, 200 + Math.random() * 200); } } </script> <style> .chat { display: flex; flex-direction: column; height: 100%; max-width: 320px; } .scrollable { flex: 1 1 auto; border-top: 1px solid #eee; margin: 0 0 0.5em 0; overflow-y: auto; } article { margin: 0.5em 0; } .user { text-align: right; } span { padding: 0.5em 1em; display: inline-block; } .eliza span { background-color: #eee; border-radius: 1em 1em 1em 0; } .user span { background-color: #0074D9; color: white; border-radius: 1em 1em 0 1em; } </style> <div class="chat"> <h1>Eliza</h1> <div class="scrollable" bind:this={div}> {#each comments as comment} <article class={comment.author}> <span>{comment.text}</span> </article> {/each} </div> <input on:keydown={handleKeydown}> </div> ``` ### tick Tick brukes til å holde tilbake endringer frem til alle andre endringer har skjedd i Dom-en. Det er nyttig å bruke når fremtidige endringer i Dom-en hadde overskrevet endringene du gjorde, fordi komponenter ble montert på nytt. I eksempelet under bruker vi tick til å vente med å sette selectionStart og selectionEnd til alle oppdateringer er påført. Uten den hadde tekstmarkøren bare hoppet til slutten. ```htmlmixed= <script> import { tick } from 'svelte'; let text = `Select some text and hit the tab key to toggle uppercase`; async function handleKeydown(event) { if (event.which !== 9) return; event.preventDefault(); const { selectionStart, selectionEnd, value } = this; const selection = value.slice(selectionStart, selectionEnd); const replacement = /[a-z]/.test(selection) ? selection.toUpperCase() : selection.toLowerCase(); text = ( value.slice(0, selectionStart) + replacement + value.slice(selectionEnd) ); await tick(); this.selectionStart = selectionStart; this.selectionEnd = selectionEnd; } </script> <style> textarea { width: 100%; height: 200px; } </style> <textarea value={text} on:keydown={handleKeydown}></textarea> ``` ## Lagre (stores) Et lager (store) brukes til å lagre data som flere urelaterte komponenter skal ha tilgang til. Tenk innloggingsdetaljer, innholdet i en handlekurv og så videre. Det sentrale med et lager er at den har en `subscribe`-metode. Denne bruker man til å abonnere på endringer i lageret. Et lager har også en `unsubscribe`-metode, som man kaller for å slutte å abonnere på oppdateringer (og forhindre minnelekkasjer). Dette gjør man typisk i `onDestroy`-livssyklusmetoden. ### Skrivbart lager (writable) Et skrivbart lager har metodene `set` og `update`. `set` setter en verdi uavhengig av den forrige verdien, mens `update` gjør at du kan basere deg på den nåværende verdien. Et skrivbart lager initialiseres med ett argument: initialverdi. ```htmlmixed= <!-- stores.js --> import { writable } from 'svelte/store'; export const count = writable(0); ``` ### Autoabonnering med $-prefiksing For å slippe å kalle `subscribe` og `unsubscribe`, kan du prefikse en lagervariabel med `$`. Dette fungerer bare med variabler som er importert eller erklært på toppnivå i komponenten. ### Lesbart lager Et lesbart brukes i tilfeller der man ikke ønsker at abonnerende komponenter skal kunne overskrive verdien. Et lesbart lager har to argumenter når det initialiseres: 1. Initialverdi 1. En start-funksjon hvis signatur er `(set: (value) => void) => (stop: () => void)`. Dette vil si: funksjonen tar ett argument, funksjonen `set`, som kan endre lagerets verdi. Den må returnere en funksjon, kalt `stop`, som kalles for å stoppe oppdateringen idet den siste abonnenenten avslutter abonnementet. ```javascript= // stores.js import { readable } from 'svelte/store'; export const time = readable(null, function start(set) { const interval = setInterval(() => { set(new Date()); }, 1000) return function stop() { clearInterval(interval) }; }); ``` ```htmlmixed= <!-- App.svelte --> <script> import { time } from './stores.js'; const formatter = new Intl.DateTimeFormat('en', { hour12: true, hour: 'numeric', minute: '2-digit', second: '2-digit' }); </script> <h1>The time is {formatter.format($time)}</h1> ``` ### Avledet (derived) lager Et avledet lager er et lager hvis verdi avhenger av en eller flere andre verdier. Et avledet lager initialiseres med minst to argumenter og et valgfritt tredje argument ![](https://i.imgur.com/jpZTBFU.png) 1. Hvilke verdier det avhenger av, gitt som enten ett variabelnavn eller en tabell med variabelnavn. 2. En callbackfunksjon hvis første argument er verdiene som man avhenger av. Fordi man kan ønske å bremse mengden oppdateringer (f.eks. ved avhengighet av tid) har denne callback-funksjonen et valgfritt andreargument som heter set. 3. Initialverdi. ```htmlmixed= <!-- App.svelte --> <script> import { time, elapsed } from './stores.js'; const formatter = new Intl.DateTimeFormat('en', { hour12: true, hour: 'numeric', minute: '2-digit', second: '2-digit' }); </script> <h1>The time is {formatter.format($time)}</h1> <p> This page has been open for {$elapsed} {$elapsed === 1 ? 'second' : 'seconds'} </p> ``` ```javascript= // stores.js import { readable, derived } from 'svelte/store'; export const time = readable(new Date(), function start(set) { const interval = setInterval(() => { set(new Date()); }, 1000); return function stop() { clearInterval(interval); }; }); const start = new Date(); export const elapsed = derived( time, $time => Math.round(($time - start) / 1000) ); ``` ### Skreddersydde lager (custom stores) Så lenge noe implementerer en subscribe-funksjon, kan det kalles et lager. For eksempel kan det være lurere å eksponere metoder som gjør spesifikke ting heller enn å eksponere set-metoden direkte. Her er et telle-lager som kan oppdateres med `increment` og `decrement`: ```htmlmixed= <!-- App.svelte --> <script> import { count } from './stores.js'; </script> <h1>The count is {$count}</h1> <button on:click={count.increment}>+</button> <button on:click={count.decrement}>-</button> <button on:click={count.reset}>reset</button> ``` ```javascript= // stores.js import { writable } from 'svelte/store'; function createCount() { const { subscribe, set, update } = writable(0); return { subscribe, increment: () => update(n => n+1), decrement: () => update(n => n-1), reset: () => {} }; } export const count = createCount(); ``` ### Lagerbindinger Slik man kan binde til en vanlig verdi, kan man også binde til en lagerverdi (gitt at lageret har en `set`-metode). Se på hvordan vi binder verdien i input-feltet. Legg også merke til hvordan vi oppdaterer verdien ved knappetrykk. ``` The $name += '!' assignment is equivalent to name.set($name + '!'). ``` ```htmlmixed= <script> import { name, greeting } from './stores.js'; </script> <h1>{$greeting}</h1> <input bind:value={$name}> <button on:click="{() => $name += '!'}"> Legg til utropstegn </button> ``` ```javascript= import { writable, derived } from 'svelte/store'; export const name = writable('world'); export const greeting = derived( name, $name => `Hello ${$name}!` ); ``` ## Bevegelse ### Mellomtegning (tweening) Du kan bruke et `tweened`-lager til å lage mellomtegnede verdier. Du kan gjøre overgangen gradvis ved å bruke `easing`. Under er et eksempel på det. Modulen `svelte/easing` inneholder de såkalte Penner easing equations. Du kan lese mer om dem på denne siden, sammen med interaktive demonstrasjoner: http://robertpenner.com/easing/ ```htmlmixed= <!-- App.svelte --> <script> import { tweened } from 'svelte/motion'; import { cubicOut } from 'svelte/easing'; const progress = tweened(0, {duration: 400, easing: cubicOut }); </script> <style> progress { display: block; width: 100%; } </style> <progress value={$progress}></progress> <button on:click="{() => progress.set(0)}"> 0% </button> <button on:click="{() => progress.set(0.25)}"> 25% </button> <button on:click="{() => progress.set(0.5)}"> 50% </button> <button on:click="{() => progress.set(0.75)}"> 75% </button> <button on:click="{() => progress.set(1)}"> 100% </button> ``` ### Fjæring (spring) Du kan få bevegelser til å oppføre seg på samme måte som om de var forårsaket av en fjær med `spring`-typen fra `svelte/motion`. En fjær har to egenskaper: Stivhet og demping. Det er litt vanskelig å forklare hvordan disse egenskapene er, men etter min egen forklaring er vel stivhet dens tendens til å følge bevegelsen mens dempingen er tendensen dens til å stoppe bevegelsen (lav demping -> bevegelsen fortsetter). Prøv eksempelet under for å skjønne. ```htmlmixed= <script> import { spring } from 'svelte/motion'; let coords = spring({ x: 50, y: 50 }); let size = spring(10); </script> <style> svg { width: 100%; height: 100%; margin: -8px; } circle { fill: #ff3e00 } </style> <div style="position: absolute; right: 1em;"> <label> <h3>stiffness ({coords.stiffness})</h3> <input bind:value={coords.stiffness} type="range" min="0" max="1" step="0.01"> </label> <label> <h3>damping ({coords.damping})</h3> <input bind:value={coords.damping} type="range" min="0" max="1" step="0.01"> </label> </div> <svg on:mousemove="{e => coords.set({ x: e.clientX, y: e.clientY })}" on:mousedown="{() => size.set(30)}" on:mouseup="{() => size.set(10)}" > <circle cx={$coords.x} cy={$coords.y} r={$size}/> </svg> ``` ## Overganger (transitions) ### Transition-direktivet Med overganger kan du styre hvordan elementer beveger seg inn og ut av dom-en. Du gjør det ved å sette `transition:<overgang>` på elementet. ```htmlmixed= <script> import { fade } from 'svelte/transition'; let visible = true; </script> <label> <input type="checkbox" bind:checked={visible}> visible </label> {#if visible} <p transition:fade> Fades in and out </p> {/if} ``` ### Parametriserte overganger Man parametriserer overganger ved å sette `="{{parametre}}"` etter dem. Under ser du et eksempel med bruk av `fly`-overgangen. ```htmlmixed= <script> import { fly } from 'svelte/transition'; let visible = true; </script> <label> <input type="checkbox" bind:checked={visible}> visible </label> {#if visible} <p transition:fly="{{ y:200, duration: 2000}}"> Fades in and out </p> {/if} Hei ``` Legg merke til at dersom du angrer deg midtveis i overgangen, blir animasjonen reversert på en umerkbar måte; ikke noen omstart. Legg merke til at "Hei" blir værende til overgangen er fullført. ### Inn og ut Du kan spesifisere at egne overganger skal brukes ved entré og sorti (som inngang og utgang kalles i teaterspråket). Da bruker du `in:` og `out:` i stedet for `fade:`. ```htmlmixed= <script> import { fade, fly } from 'svelte/transition'; let visible = true; </script> <label> <input type="checkbox" bind:checked={visible}> visible </label> {#if visible} <p in:fly="{{ y: 200, duration: 2000 }}" out:fade> Flies in and out </p> {/if} ``` NB: Her reverseres ikke overgangene på en myk måte. Eller, tutorialen sier så, men det virker i alle fall som at den klarer en `fade` midtveis i en `fly`, men ikke omvendt. ### Skreddersydde CSS-overganger Å lage sine egne overganger er lett. Man må bare tilby en signatur med samme signatur som de medfølgende overgangene i `svelte/transition`. Funksjonen tar to argumenter: Dom-node og parametere som blir sendt inn. Den returnerer et objekt med følgende fem nøkler (direkte sitat): > `delay` — milliseconds before the transition begins > `duration` — length of the transition in milliseconds > `easing` — a `p => t` easing function (see the chapter on tweening) >`css` — a `(t, u) => css` function, where `u === 1 - t` >`tick` — a `(t, u) => {...}` function that has some effect on the node Det anbefales å bruke css-objektet og ikke tick-argumentet for å gjøre overgangene myke: CSS-animasjoner kan kjøres utenfor hovedtråden (hvis nettleseren støtter det), slik at alle blir glade. For øvrig, CSS-animasjonene pre-rendres av Svelte, så en fade-animasjon ser litt slik ut: ```css= 0% { opacity: 0 } 10% { opacity: 0.1 } 20% { opacity: 0.2 } /* ... */ 100% { opacity: 1 } ``` Bruk dette med varsomhet. Under et er grelt eksempel som viser hvor ille det kan bli. ```htmlmixed= <script> import { fade } from 'svelte/transition'; import { elasticOut } from 'svelte/easing'; let visible = true; function spin(node, { duration }) { return { duration, css: t => { const eased = elasticOut(t); return ` transform: scale(${eased}) rotate(${eased * 1080}deg); color: hsl( ${~~(t * 360)}, ${Math.min(100, 1000 - 1000 * t)}%, ${Math.min(50, 500 - 500 * t)}% );` } }; } </script> <style> .centered { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } span { position: absolute; transform: translate(-50%,-50%); font-size: 4em; } </style> <label> <input type="checkbox" bind:checked={visible}> visible </label> {#if visible} <div class="centered" in:spin="{{duration: 8000}}" out:fade> <span>transitions!</span> </div> {/if} ``` ### Skreddersydde Javascript-overganger Med Javascript kan du gjøre ting som er umulig med CSS. Kostnaden er at det går utover ytelsen. Dette gjør man ved hjelp av `tick`-nøkkelen som ble nevnt sist. ```htmlmixed= <script> let visible = false; function typewriter(node, { speed = 50 }) { const valid = ( node.childNodes.length === 1 && node.childNodes[0].nodeType === 3 ); if (!valid) { throw new Error(`This transition only works on elements with a single text node child`); } const text = node.textContent; const duration = text.length * speed; return { duration, tick: t => { const i = ~~(text.length * t); node.textContent = text.slice(0, i); } }; } </script> <label> <input type="checkbox" bind:checked={visible}> visible </label> {#if visible} <p in:typewriter> The quick brown fox jumps over the lazy dog </p> {/if} ``` ### Overgangshendelser Fire hendelser blir utstedet mens en overgang kjøres på et element `[intro/outro][start/end]`. Du kan lytte på disse og gjøre … hva du vil. ```htmlmixed= <script> import { fly } from 'svelte/transition'; let visible = true; let status = 'waiting...'; </script> <p>status: {status}</p> <label> <input type="checkbox" bind:checked={visible}> visible </label> {#if visible} <p transition:fly="{{ y: 200, duration: 2000 }}" on:introstart="{() => status = 'intro started'}" on:outrostart="{() => status = 'outro started'}" on:introend="{() => status = 'intro ended'}" on:outroend="{() => status = 'outro ended'}" > Flies in and out </p> {/if} ``` ### Lokale overganger Hvis man vil at en overgang kun skal brukes når man legger eller fjerner ett element i motsetning til en hel gruppe, kan man bruke `local`-nøkkelordet. ```htmlmixed= <script> import { slide } from 'svelte/transition'; let showItems = true; let i = 5; let items = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten']; </script> <style> div { padding: 0.5em 0; border-top: 1px solid #eee; } </style> <label> <input type="checkbox" bind:checked={showItems}> show list </label> <label> <input type="range" bind:value={i} max=10> </label> {#if showItems} {#each items.slice(0, i) as item} <div transition:slide|local> {item} </div> {/each} {/if} ``` ### Utsatte overganger (deferred transitions) Med «utsatte overganger» kan du samordne overganger mellom elementer ved hjelp av å sammenligne identifikatorer. I eksempelet under gjør bruken av `crossfade` sammen med `in:receive` og `out:send` at todo-elementene glir fra en liste til en annen. Det kommer av at de mottar `{key: todo.id}` som parameter. Hvis Svelte ikke finner et samsvarende element, bruker den `fallback`-funksjonen som man gir som andreargument. ```htmlmixed= <script> import { quintOut } from 'svelte/easing'; import { crossfade } from 'svelte/transition'; const [send, receive] = crossfade({ duration: d => Math.sqrt(d * 200), fallback(node, params) { const style = getComputedStyle(node); const transform = style.transform === 'none' ? '' : style.transform; return { duration: 600, easing: quintOut, css: t => ` transform: ${transform} scale(${t}); opacity: ${t} ` }; } }); let uid = 1; let todos = [ { id: uid++, done: false, description: 'write some docs' }, { id: uid++, done: false, description: 'start writing blog post' }, { id: uid++, done: true, description: 'buy some milk' }, { id: uid++, done: false, description: 'mow the lawn' }, { id: uid++, done: false, description: 'feed the turtle' }, { id: uid++, done: false, description: 'fix some bugs' }, ]; function add(input) { const todo = { id: uid++, done: false, description: input.value }; todos = [todo, ...todos]; input.value = ''; } function remove(todo) { todos = todos.filter(t => t !== todo); } function mark(todo, done) { todo.done = done; remove(todo); todos = todos.concat(todo); } </script> <div class='board'> <input placeholder="what needs to be done?" on:keydown={e => e.which === 13 && add(e.target)} > <div class='left'> <h2>todo</h2> {#each todos.filter(t => !t.done) as todo (todo.id)} <label in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" > <input type=checkbox on:change={() => mark(todo, true)}> {todo.description} <button on:click="{() => remove(todo)}">remove</button> </label> {/each} </div> <div class='right'> <h2>done</h2> {#each todos.filter(t => t.done) as todo (todo.id)} <label class="done" in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" > <input type=checkbox checked on:change={() => mark(todo, false)}> {todo.description} <button on:click="{() => remove(todo)}">remove</button> </label> {/each} </div> </div> <style> .board { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1em; max-width: 36em; margin: 0 auto; } .board > input { font-size: 1.4em; grid-column: 1/3; } h2 { font-size: 2em; font-weight: 200; user-select: none; margin: 0 0 0.5em 0; } label { position: relative; line-height: 1.2; padding: 0.5em 2.5em 0.5em 2em; margin: 0 0 0.5em 0; border-radius: 2px; user-select: none; border: 1px solid hsl(240, 8%, 70%); background-color:hsl(240, 8%, 93%); color: #333; } input[type="checkbox"] { position: absolute; left: 0.5em; top: 0.6em; margin: 0; } .done { border: 1px solid hsl(240, 8%, 90%); background-color:hsl(240, 8%, 98%); } button { position: absolute; top: 0; right: 0.2em; width: 2em; height: 100%; background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E"); background-size: 1.4em 1.4em; border: none; opacity: 0; transition: opacity 0.2s; text-indent: -9999px; cursor: pointer; } label:hover button { opacity: 1; } </style> ``` ## Animasjoner ### Animate-direktivet Med `animate:`-direktivet kan du animere de elementene som ikke utsettes for en utsatt overgang (som i forrige underkapittel). I eksempelet under bruker vi [`flip`-funksjonen, som er den eneste funksjonen som eksporteres fra `svelte/animate`](https://svelte.dev/docs#svelte_animate). Det står for First, Last, Invert Play. Den tar start og sluttposisjon og kalkulerer en overgang mellom dem. ```htmlmixed= <script> import { quintOut } from 'svelte/easing'; import { crossfade } from 'svelte/transition'; import { flip } from 'svelte/animate'; const [send, receive] = crossfade({ duration: d => Math.sqrt(d * 200), fallback(node, params) { const style = getComputedStyle(node); const transform = style.transform === 'none' ? '' : style.transform; return { duration: 600, easing: quintOut, css: t => ` transform: ${transform} scale(${t}); opacity: ${t} ` }; } }); let uid = 1; let todos = [ { id: uid++, done: false, description: 'write some docs' }, { id: uid++, done: false, description: 'start writing blog post' }, { id: uid++, done: true, description: 'buy some milk' }, { id: uid++, done: false, description: 'mow the lawn' }, { id: uid++, done: false, description: 'feed the turtle' }, { id: uid++, done: false, description: 'fix some bugs' }, ]; function add(input) { const todo = { id: uid++, done: false, description: input.value }; todos = [todo, ...todos]; input.value = ''; } function remove(todo) { todos = todos.filter(t => t !== todo); } function mark(todo, done) { todo.done = done; remove(todo); todos = todos.concat(todo); } </script> <div class='board'> <input placeholder="what needs to be done?" on:keydown={e => e.which === 13 && add(e.target)} > <div class='left'> <h2>todo</h2> {#each todos.filter(t => !t.done) as todo (todo.id)} <label in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" animate:flip > <input type=checkbox on:change={() => mark(todo, true)}> {todo.description} <button on:click="{() => remove(todo)}">remove</button> </label> {/each} </div> <div class='right'> <h2>done</h2> {#each todos.filter(t => t.done) as todo (todo.id)} <label class="done" in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" animate:flip > <input type=checkbox checked on:change={() => mark(todo, false)}> {todo.description} <button on:click="{() => remove(todo)}">remove</button> </label> {/each} </div> </div> <style> .board { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1em; max-width: 36em; margin: 0 auto; } .board > input { font-size: 1.4em; grid-column: 1/3; } h2 { font-size: 2em; font-weight: 200; user-select: none; margin: 0 0 0.5em 0; } label { position: relative; line-height: 1.2; padding: 0.5em 2.5em 0.5em 2em; margin: 0 0 0.5em 0; border-radius: 2px; user-select: none; border: 1px solid hsl(240, 8%, 70%); background-color:hsl(240, 8%, 93%); color: #333; } input[type="checkbox"] { position: absolute; left: 0.5em; top: 0.6em; margin: 0; } .done { border: 1px solid hsl(240, 8%, 90%); background-color:hsl(240, 8%, 98%); } button { position: absolute; top: 0; right: 0.2em; width: 2em; height: 100%; background: no-repeat 50% 50% url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23676778' d='M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M17,7H14.5L13.5,6H10.5L9.5,7H7V9H17V7M9,18H15A1,1 0 0,0 16,17V10H8V17A1,1 0 0,0 9,18Z'%3E%3C/path%3E%3C/svg%3E"); background-size: 1.4em 1.4em; border: none; opacity: 0; transition: opacity 0.2s; text-indent: -9999px; cursor: pointer; } label:hover button { opacity: 1; } </style> ``` ## Actions ### use-direktivet En action-funksjon er en funksjon som mottar DOM-noden som et objekt er montert i (slik at den kan gjøre morsomme ting med den). Den har altså signaturen `function foo(node)`. For å bruke `foo` på komponenten `Bar` skriver du `<Bar use:foo />` på komponenten. Bruksområdene for slike funksjoner er ganske avanserte, men under ser du i alle fall et eksempel, der vi gjør en boks `pannable` ved å skrive `use:pannable` på den. I filen `pannable.js` finner vi koden som styrer panning. Den benytter seg mye av [`CustomEvent`-API-et fra nettleseren](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) for å sende ut forskjellige hendelser på objektet. ```htmlmixed= <!-- App.svelte --> <script> import { spring } from 'svelte/motion'; import {pannable} from './pannable.js' const coords = spring({ x: 0, y: 0 }, { stiffness: 0.2, damping: 0.4 }); function handlePanStart() { coords.stiffness = coords.damping = 1; } function handlePanMove(event) { coords.update($coords => ({ x: $coords.x + event.detail.dx, y: $coords.y + event.detail.dy })); } function handlePanEnd(event) { coords.stiffness = 0.2; coords.damping = 0.4; coords.set({ x: 0, y: 0 }); } </script> <style> .box { --width: 100px; --height: 100px; position: absolute; width: var(--width); height: var(--height); left: calc(50% - var(--width) / 2); top: calc(50% - var(--height) / 2); border-radius: 4px; background-color: #ff3e00; cursor: move; } </style> <div class="box" use:pannable on:panstart={handlePanStart} on:panmove={handlePanMove} on:panend={handlePanEnd} style="transform: translate({$coords.x}px,{$coords.y}px) rotate({$coords.x * 0.2}deg)" ></div> ``` ```javascript= // pannable.js export function pannable(node) { let x; let y; function handleMousedown(event) { x = event.clientX; y = event.clientY; node.dispatchEvent(new CustomEvent('panstart', { detail: { x, y } })); window.addEventListener('mousemove', handleMousemove); window.addEventListener('mouseup', handleMouseup); } function handleMousemove(event) { const dx = event.clientX - x; const dy = event.clientY - y; x = event.clientX; y = event.clientY; node.dispatchEvent(new CustomEvent('panmove', { detail: { x, y, dx, dy } })); } function handleMouseup(event) { x = event.clientX; y = event.clientY; node.dispatchEvent(new CustomEvent('panend', { detail: { x, y } })); window.removeEventListener('mousemove', handleMousemove); window.removeEventListener('mouseup', handleMouseup); } node.addEventListener('mousedown', handleMousedown); return { destroy() { node.removeEventListener('mousedown', handleMousedown); } }; } ``` ### Parametriserte actions [Eksempelet om parametriserte actions](https://svelte.dev/tutorial/adding-parameters-to-actions) er ikke fullført i tutorialen, så jeg går ikke gjennom det nå. Men moralen er at du kan gi argumenter til actions ved å skrive `use:foo={<argument>}`; da blir dette sendt til actionens andre parameter: `foo(node, <argument>)` ## CSS-klasser ### Class-direktivet For å forenkle _ternary expressions_ for å sette klasse-attributtet på ting har Svelte en forenklet syntaks for dette #### Før ```htmlmixed= <button class="{current === 'foo' ? 'active' : ''}" on:click="{() => current = 'foo'}" >foo</button> ``` #### Etter ```htmlmixed= <button class:active="{current === 'foo'}" on:click="{() => current = 'foo'}" >foo</button> ``` ### Hvis klasse og boolsk variabel har likt navn … … kan du bruke en enda kortere form: #### Før ```htmlmixed= <div class:big={big}> <!-- ... --> </div> ``` #### Etter ```htmlmixed= <div class:big> <!-- ... --> </div> ``` ## Å sette sammen komponenter ### Luker (slots) Dersom du vil at et element skal kunne inneholde komponenter under seg, bruker du elementet `<slot/>` for å spesifisere hvor i elementet du vil at det skal dukke opp. ```htmlmixed= <!-- App.svelte --> <script> import Box from './Box.svelte'; </script> <Box> I am boy </Box> ``` ```htmlmixed= <!-- Box.svelte --> <style> .box { width: 300px; border: 1px solid #aaa; border-radius: 2px; box-shadow: 2px 2px 8px rgba(0,0,0,0.1); padding: 1em; margin: 0 0 1em 0; } </style> <div class="box"> <slot/> </div> ``` ### Luker med standardverdi For å gi en standardverdi, skriver du noe mellom `<slot></slot>`: ```htmlmixed= <!-- App.svelte --> <script> import Box from './Box.svelte'; </script> <Box/> ``` ```htmlmixed= <!-- Box.svelte --> <style> .box { width: 300px; border: 1px solid #aaa; border-radius: 2px; box-shadow: 2px 2px 8px rgba(0,0,0,0.1); padding: 1em; margin: 0 0 1em 0; } </style> <div class="box"> <slot/> </div> ``` ### Luker med navn En komponent kan inneholde flere enn én luke. Da må man navngi dem slik at Svelte vet hva som er hva. I komponenten oppgir man `<slot name="foo"/>`. Når man angir en verdi, må man sette `<elem slot="foo" />` for å angi at elementet skal settes inn i nevnte luke. ```htmlmixed= <!-- App.svelte --> <script> import ContactCard from './ContactCard.svelte'; </script> <ContactCard> <span slot="name">Ola Nordmann</span> <span slot="address">Drammensveien 1</span> <span slot="email">foo@example.no</span> </ContactCard> ``` ```htmlmixed= <!-- ContactCard.svelte --> <style> .contact-card { width: 300px; border: 1px solid #aaa; border-radius: 2px; box-shadow: 2px 2px 8px rgba(0,0,0,0.1); padding: 1em; } h2 { padding: 0 0 0.2em 0; margin: 0 0 1em 0; border-bottom: 1px solid #ff3e00 } .address, .email { padding: 0 0 0 1.5em; background: 0 50% no-repeat; background-size: 1em 1em; margin: 0 0 0.5em 0; line-height: 1.2; } .address { background-image: url(tutorial/icons/map-marker.svg) } .email { background-image: url(tutorial/icons/email.svg) } .missing { color: #999 } </style> <article class="contact-card"> <h2> <slot name="name"> <span class="missing">Unknown name</span> </slot> </h2> <div class="address"> <slot name="address"> <span class="missing">Unknown address</span> </slot> </div> <div class="email"> <slot name="email"> <span class="missing">Unknown email</span> </slot> </div> </article> ``` ### Luke med tilbakekobling (slot props) Av og til er det ønskelig at barna til en komponent skal kunne motta informasjon fra forelderen sin. _Min personlige mening (Eirik Vågeskar) er at dette er noe av den vanskeligste syntaksen, men bare heng med._ I avsender (komponent), setter man en prop på luken: `<slot foo={bar} />`. I mottaker, altså der komponenten blir brukt, men hvor du supplerer et barn, tar du imot verdien og tilegner den til en ny verdi. ```htmlmixed= <Component let:foo={baz}>Value of baz is {baz}</Component> ``` Du kan også gjøre dette med navngitte luker, men da må du sette `let`-utsagnet på elementet med `slot=…`-attributtet. ```htmlmixed= <!-- App.svelte --> <script> import Hoverable from './Hoverable.svelte'; </script> <style> div { padding: 1em; margin: 0 0 1em 0; background-color: #eee; } .active { background-color: #ff3e00; color: white; } </style> <Hoverable let:hovering={hovering}> <div class:active={hovering}> {#if hovering} <p>I am being hovered upon.</p> {:else} <p>Hover over me!</p> {/if} </div> </Hoverable> ``` ```htmlmixed= <!-- Hoverable.svelte --> <script> let hovering; function enter() { hovering = true; } function leave() { hovering = false; } </script> <div on:mouseenter={enter} on:mouseleave={leave}> <slot hovering={hovering}></slot> </div> ``` ## Kontekst Svelte har et kontekst-API. Dette ligner veldig på Reacts kontekst-API. En kontekst er ganske likt et lager (store), men forskjellen er at et lager kun er tilgjengelig for en komponent og dens etterkommere. Dette er et ganske avansert konsept som krever en god del kode for å forstå. Jeg anbefaler derfor at man går til seksjonen i tutorialen som dreier seg om akkurat dette: https://svelte.dev/tutorial/context-api ## Spesialelementer ### svelte:self `<svelte:self>` brukes for å referere til elementet du står i nå, for eksempel hvis du vil lage et barn av denne komponenten av samme type. Dette er for eksempel nyttig hvos man skal lage en filutforsker: En mappe kan inneholde filer og andre mapper. Når man koder visningen for en mappe, vil man også at mappen skal inneholde andre mapper. ```htmlmixed= <!-- App.svelte --> <script> import Folder from './Folder.svelte'; let root = [ { type: 'folder', name: 'Important work stuff', files: [ { type: 'file', name: 'quarterly-results.xlsx' } ] }, { type: 'folder', name: 'Animal GIFs', files: [ { type: 'folder', name: 'Dogs', files: [ { type: 'file', name: 'treadmill.gif' }, { type: 'file', name: 'rope-jumping.gif' } ] }, { type: 'folder', name: 'Goats', files: [ { type: 'file', name: 'parkour.gif' }, { type: 'file', name: 'rampage.gif' } ] }, { type: 'file', name: 'cat-roomba.gif' }, { type: 'file', name: 'duck-shuffle.gif' }, { type: 'file', name: 'monkey-on-a-pig.gif' } ] }, { type: 'file', name: 'TODO.md' } ]; </script> <Folder name="Home" files={root} expanded/> ``` ```htmlmixed= <!-- File.svelte --> <script> export let name; $: type = name.slice(name.lastIndexOf('.') + 1); </script> <style> span { padding: 0 0 0 1.5em; background: 0 0.1em no-repeat; background-size: 1em 1em; } </style> <span style="background-image: url(tutorial/icons/{type}.svg)">{name}</span> ``` ```htmlmixed= <!-- Folder.svelte --> <script> import File from './File.svelte'; export let expanded = false; export let name; export let files; function toggle() { expanded = !expanded; } </script> <style> span { padding: 0 0 0 1.5em; background: url(tutorial/icons/folder.svg) 0 0.1em no-repeat; background-size: 1em 1em; font-weight: bold; cursor: pointer; } .expanded { background-image: url(tutorial/icons/folder-open.svg); } ul { padding: 0.2em 0 0 0.5em; margin: 0 0 0 0.5em; list-style: none; border-left: 1px solid #eee; } li { padding: 0.2em 0; } </style> <span class:expanded on:click={toggle}>{name}</span> {#if expanded} <ul> {#each files as file} <li> {#if file.type === 'folder'} <svelte:self {...file} /> {:else} <File {...file}/> {/if} </li> {/each} </ul> {/if} ``` ### svelte:component `<svelte:component this={foo} />` brukes som et skall for å montere komponenter som kan endre seg. For eksempel kan det være at du et sted på en side vet at du enten skal ha en analog eller en digital visning av en klokke. Kan kan du tilegne `let foo = theWatchThatUserSelected`, og så rendrer den automatisk den visningen brukeren har valgt. ### svelte:window `<svelte:window>` referer til `window`-elementet i nettleseren. Du kan for eksempel lytte etter hendelser på det. ```htmlmixed= <script> let key; let keyCode; function handleKeydown(event) { key = event.key; keyCode = event.keyCode; } </script> <style> div { display: flex; height: 100%; align-items: center; justify-content: center; flex-direction: column; } kbd { background-color: #eee; border-radius: 4px; font-size: 6em; padding: 0.2em 0.5em; border-top: 5px solid rgba(255,255,255,0.5); border-left: 5px solid rgba(255,255,255,0.5); border-right: 5px solid rgba(0,0,0,0.2); border-bottom: 5px solid rgba(0,0,0,0.2); color: #555; } </style> <svelte:window on:keydown={handleKeydown}/> <div style="text-align: center"> {#if key} <kbd>{key === ' ' ? 'Space' : key}</kbd> <p>{keyCode}</p> {:else} <p>Focus this window and press any key</p> {/if} </div> ``` ### Binde til verdier på svelte:window Du kan binde til verdier på vinduet ved å skrive `<svelte:window bind:<variabelPaaVinduet>={minVariabel}/>`. Slik som under her. ```htmlmixed= <!-- App.svelte --> <script> const layers = [0, 1, 2, 3, 4, 5, 6, 7, 8]; let y; </script> <svelte:window bind:scrollY={y}/> <a class="parallax-container" href="https://www.firewatchgame.com"> {#each layers as layer} <img style="transform: translate(0,{-y * layer / (layers.length - 1)}px)" src="https://www.firewatchgame.com/images/parallax/parallax{layer}.png" alt="parallax layer {layer}" > {/each} </a> <div class="text"> <span style="opacity: {1 - Math.max(0, y / 40)}"> scroll down </span> <div class="foreground"> You have scrolled {y} pixels </div> </div> <style> .parallax-container { position: fixed; width: 2400px; height: 712px; left: 50%; transform: translate(-50%,0); } .parallax-container img { position: absolute; top: 0; left: 0; width: 100%; will-change: transform; } .parallax-container img:last-child::after { content: ''; position: absolute; width: 100%; height: 100%; background: rgb(45,10,13); } .text { position: relative; width: 100%; height: 300vh; color: rgb(220,113,43); text-align: center; padding: 4em 0.5em 0.5em 0.5em; box-sizing: border-box; pointer-events: none; } span { display: block; font-size: 1em; text-transform: uppercase; will-change: transform, opacity; } .foreground { position: absolute; top: 711px; left: 0; width: 100%; height: calc(100% - 712px); background-color: rgb(32,0,1); color: white; padding: 50vh 0 0 0; } :global(body) { margin: 0; padding: 0; background-color: rgb(253, 174, 51); } </style> ``` ### svelte:body Svelte body kan brukes til å lytte på hendelser som fyrer på `document.body`. For eksempel fyrer `mouseenter` og `mouseleave` på disse. ```htmlmixed= <!-- App.svelte --> <script> let hereKitty = false; const handleMouseenter = () => hereKitty = true; const handleMouseleave = () => hereKitty = false; </script> <style> img { position: absolute; left: 0; bottom: -60px; transform: translate(-80%, 0) rotate(-30deg); transform-origin: 100% 100%; transition: transform 0.4s; } .curious { transform: translate(-15%, 0) rotate(0deg); } :global(body) { overflow: hidden; } </style> <svelte:body on:mouseenter={handleMouseenter} on:mouseleave={handleMouseleave} /> <!-- creative commons BY-NC http://www.pngall.com/kitten-png/download/7247 --> <img class:curious={hereKitty} alt="Kitten wants to know what's going on" src="tutorial/kitten.png" > ``` ### svelte:head Som du kanskje har forutsett, er `svelte:head` en måte å sette inn elementer i `head`-taggen. ```htmlmixed= <svelte:head> <link rel="stylesheet" href="tutorial/dark-theme.css"> </svelte:head> <h1>Hello world!</h1> ``` ### svelte:options `svelte:options` brukes for å angi kompilator-tilvalg. For eksempel kan du si at en komponent kun skal motta immuterbar data ved å sette `immutable={true}` på den. Du finner den fullstendige listen over tilvalg under [dokumentasjonen for `svelte:options`](https://svelte.dev/docs#svelte_options) ## Modul-kontekst ### Dele kode Av og til er det interessant at kode deles på tvers av alle instanser innad i en modul. For eksempel, når du trykker på «spill av» på en av avspillerne i siden under, vil alle andre avspillere automatisk stoppe. ```htmlmixed= <!-- App.svelte --> <script> import AudioPlayer from './AudioPlayer.svelte'; </script> <!-- https://musopen.org/music/9862-the-blue-danube-op-314/ --> <AudioPlayer src="tutorial/music/strauss.mp3" title="The Blue Danube Waltz" composer="Johann Strauss" performer="European Archive" /> <!-- https://musopen.org/music/43775-the-planets-op-32/ --> <AudioPlayer src="tutorial/music/holst.mp3" title="Mars, the Bringer of War" composer="Gustav Holst" performer="USAF Heritage of America Band" /> <!-- https://musopen.org/music/8010-3-gymnopedies/ --> <AudioPlayer src="tutorial/music/satie.mp3" title="Gymnopédie no. 1" composer="Erik Satie" performer="Prodigal Procrastinator" /> <!-- https://musopen.org/music/2567-symphony-no-5-in-c-minor-op-67/ --> <AudioPlayer src="tutorial/music/beethoven.mp3" title="Symphony no. 5 in Cm, Op. 67 - I. Allegro con brio" composer="Ludwig van Beethoven" performer="European Archive" /> <!-- https://musopen.org/music/43683-requiem-in-d-minor-k-626/ --> <AudioPlayer src="tutorial/music/mozart.mp3" title="Requiem in D minor, K. 626 - III. Sequence - Lacrymosa" composer="Wolfgang Amadeus Mozart" performer="Markus Staab" /> ``` ```htmlmixed= <!-- AudioPlayer.svelte --> <script context="module"> let current; </script> <script> export let src; export let title; export let composer; export let performer; let audio; let paused = true; function stopOthers() { if (current && current !== audio) current.pause(); current = audio; } </script> <style> article { margin: 0 0 1em 0; max-width: 800px } h2, p { margin: 0 0 0.3em 0; } audio { width: 100%; margin: 0.5em 0 1em 0; } .playing { color: #ff3e00; } </style> <article class:playing={!paused}> <h2>{title}</h2> <p><strong>{composer}</strong> / performed by {performer}</p> <audio bind:this={audio} bind:paused on:play={stopOthers} controls {src} ></audio> </article> ``` ### Eksport fra moduler Du kan eksportere ting, for eksempel funksjoner, fra en modul. For eksempel kan du eksportere en funksjon `stopAll` for å stoppe alle spor på en gang fra lydavspillerne i forrige eksempel. Merk deg at man ikke kan eksportere en `default`, for komponenten i seg selv er eksportert på dette kodeordet. ```htmlmixed= <!-- App.svelte --> <script> import AudioPlayer, { stopAll } from './AudioPlayer.svelte'; </script> <button on:click={stopAll}> stop all audio </button> <!-- https://musopen.org/music/9862-the-blue-danube-op-314/ --> <AudioPlayer src="tutorial/music/strauss.mp3" title="The Blue Danube Waltz" composer="Johann Strauss" performer="European Archive" /> <!-- https://musopen.org/music/43775-the-planets-op-32/ --> <AudioPlayer src="tutorial/music/holst.mp3" title="Mars, the Bringer of War" composer="Gustav Holst" performer="USAF Heritage of America Band" /> <!-- https://musopen.org/music/8010-3-gymnopedies/ --> <AudioPlayer src="tutorial/music/satie.mp3" title="Gymnopédie no. 1" composer="Erik Satie" performer="Prodigal Procrastinator" /> <!-- https://musopen.org/music/2567-symphony-no-5-in-c-minor-op-67/ --> <AudioPlayer src="tutorial/music/beethoven.mp3" title="Symphony no. 5 in Cm, Op. 67 - I. Allegro con brio" composer="Ludwig van Beethoven" performer="European Archive" /> <!-- https://musopen.org/music/43683-requiem-in-d-minor-k-626/ --> <AudioPlayer src="tutorial/music/mozart.mp3" title="Requiem in D minor, K. 626 - III. Sequence - Lacrymosa" composer="Wolfgang Amadeus Mozart" performer="Markus Staab" /> ``` ```htmlmixed= <!-- AudioPlayer.svelte --> <script context="module"> const elements = new Set(); export function stopAll() { elements.forEach(element => { element.pause(); }); } </script> <script> import { onMount } from 'svelte'; export let src; export let title; export let composer; export let performer; let audio; let paused = true; onMount(() => { elements.add(audio); return () => elements.delete(audio); }); function stopOthers() { elements.forEach(element => { if (element !== audio) element.pause(); }); } </script> <style> article { margin: 0 0 1em 0; max-width: 800px } h2, p { margin: 0 0 0.3em 0; } audio { width: 100%; margin: 0.5em 0 1em 0; } .playing { color: #ff3e00; } </style> <article class:playing={!paused}> <h2>{title}</h2> <p><strong>{composer}</strong> / performed by {performer}</p> <audio bind:this={audio} bind:paused on:play={stopOthers} controls {src} ></audio> </article> ``` ## Debugging ### @debug-taggen Det kan være nyttig å inspisere data mens den flyter gjennom appen og endrer seg. For å gjøre det, kan du sette `{@debug <variabel>}` i appen. ```htmlmixed= <!-- App.svelte --> <script> let user = { firstname: 'Ada', lastname: 'Lovelace' }; </script> <input bind:value={user.firstname}> <input bind:value={user.lastname}> {@debug user} <h1>Hello {user.firstname}!</h1> ```