This is such a common pattern in UI development that Svelte includes a special directive to simplify it:
<button class:active="{current === 'foo'}" on:click="{() => current = 'foo'}" >foo</button>
The active class is added to the element whenever the value of the expression is truthy, and removed when it's falsy. So as you can see in the example below, once class is set to active, the button will change color according to what css is set to, in this case, to color orange.
<script> let current = 'foo'; </script> <style> button { display: block; } .active { background-color: #ff3e00; color: white; } </style> <button class:active="{current === 'foo'}" on:click="{() => current = 'foo'}" >foo</button> <button class:active="{current === 'bar'}" on:click="{() => current = 'bar'}" >bar</button> <button class:active="{current === 'baz'}" on:click="{() => current = 'baz'}" >baz</button>
Shorthand Class Directive
Often, the name of the class will be the same as the name of the value it depends on:
<div class:big={big}> <!-- ... --> </div>
In those cases we can use a shorthand form:
<div class:big> <!-- ... --> </div>
Here is full example when variable big is set to true as the checkbox is checked, the class of div also change to big, so the font also get bigger as it's set in the css.
<script> let big = false; </script> <style> .big { font-size: 4em; } </style> <label> <input type=checkbox bind:checked={big}> big </label> <div class:big> some {big ? 'big' : 'small'} text </div>
Indonesia
The Class Directive
Seperti atribut yang lain, kita juga bisa menentukan class dengan atribut JavaScript, seperti berikut:
Karena begitu seringnya digunakan dalam pembuatan UI, maka Svelte memasukkan direktiv spesial untuk membuat proses ini lebih mudah:
<button class:active="{current === 'foo'}" on:click="{() => current = 'foo'}" >foo</button>
Class active akan ditambahkan pada elemen kapanpun hasil dari ekspresi adalah benar, dan dihilangkan jika hasilnya salah. Jadi seperti yang bisa dilihat pada contoh di atas, begitu class sudah diset menjadi active, tombol akan berubah warna sesuai dengan yang sudah diatur di dalam css, dalam hal ini menjadi warna oranye.
<script> let current = 'foo'; </script> <style> button { display: block; } .active { background-color: #ff3e00; color: white; } </style> <button class:active="{current === 'foo'}" on:click="{() => current = 'foo'}" >foo</button> <button class:active="{current === 'bar'}" on:click="{() => current = 'bar'}" >bar</button> <button class:active="{current === 'baz'}" on:click="{() => current = 'baz'}" >baz</button>
Shorthand Class Directive
Seringkali, nama dari class akan sama dengan nama variabel yang menentukan class tersebut:
<div class:big={big}> <!-- ... --> </div>
Pada kasus seperti itu kita bisa menggunakan bentuk singkat:
<div class:big> <!-- ... --> </div>
Berikut adalah contoh ketika variabel big diset true saat checkbox dicentang, maka class dari div juga akan berubah menjadi big, jadi ukuran font juga akan menjadi lebih besar seperti seting di css.
<script> let big = false; </script> <style> .big { font-size: 4em; } </style> <label> <input type=checkbox bind:checked={big}> big </label> <div class:big> some {big ? 'big' : 'small'} text </div>
Actions are essentially element-level lifecycle functions. They're useful for things like:
interfacing with third-party libraries
lazy-loaded images
tooltips
adding custom event handlers
In this app, we want to make the orange box 'pannable'. It has event handlers for the panstart, panmove and panend events, but these aren't native DOM events. We have to dispatch them ourselves. First, import the pannable function and then use it with <div class="box"> element.
Then on the pannable.js file. Like transition functions, an action function receives a node and some optional parameters, and returns an action object. That object can have a destroy function, which is called when the element is unmounted.
We want to fire panstart event when the user mouses down on the element, panmove events (with dx and dy properties showing how far the mouse moved) when they drag it, and panend events when they mouse up. One possible implementation looks like this, you can try to move box around.
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); } }; }
Adding Parameters
Like transitions and animations, an action can take an argument, which the action function will be called with alongside the element it belongs to.
Here, we're using a longpress action that fires an event with the same name whenever the user presses and holds the button for a given duration. Here is the example of longpress.js. Important part is duration in second parameter of function declaration; pass duration value in setTimeout; and update(newDuration) in return value.
And here is App.svelte, where we can pass the duration value to the action using this <button use:longpress={duration}:
<script> import { longpress } from './longpress.js'; let pressed = false; let duration = 2000; </script> <label> <input type=range bind:value={duration} max={2000} step={100}> {duration}ms </label> <button use:longpress={duration} on:longpress="{() => pressed = true}" on:mouseenter="{() => pressed = false}" >press and hold</button> {#if pressed} <p>congratulations, you pressed and held for {duration}ms</p> {/if}
Indonesia
The Use Directive
Pada dasarnya action adalah fungsi lifecycle pada tingkat elemen. Action bermanfaat untuk hal-hal seperti:
memberikan interface untuk library pihak ketiga
melakukan lazy-loaded pada gambar
tooltips
menambahkan event handlers custom
Pada aplikasi ini, kita ingin membuat kotak oranye menjadi bisa di pan. Kotak mempunyai even handler untuk even panstart, panmove dan panend, tapi even-even ini bukan even asli bawaan DOM. Kita harus melakukan dispatch even ini sendiri. Pertama import fungsi pannable kemudian gunakan di dalam elemen <div class="box">.
Kemudian pada file pannable.js. Seperti pada fungsi transisi, sebuah fungsi action akan menerima node dan beberapa parameter optional, dan me return sebuah object action. Object tersebut bisa memiliki fungsi destroy, yang akan dipanggil ketika elemen di unmount.
Kita ingin menjalankan even panstart ketika terjadi mouse down pada elemen, even panmove (dengan properti dx dan dy menunjukkan sejauh mana mouse sudah bergerak) ketika kotaknya di drag, dan even panend ketika mouse up. Satu contoh implementasi yang mungkin adalah sebagai berikut, kita bisa coba menggerak-gerakkan kotaknya.
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); } }; }
Adding Parameters
Seperti pada transisi dan animasi, sebuah action bisa menerima argumen, yang akan digunakan untuk memanggil fungsi action pada elemen yang akan menjadi tempatnya.
Sekarang, kita akan menggunakan action longpress yang akan memicu even yang memiliki nama sama kapanpun user menekan dan menahan tombol selama waktu yang ditentukan. Di bawah ini adalah contoh dari longpress.js. Bagian penting adalah duration dalam parameter sekon pada deklarasi fungsi; pass nilai duration ke dalam setTimeout; dan update(newDuration) pada nilai return.
You'll see that if you check something and it moved to other side,remaining item will slowly going up, instead of immediately jump. If you want to change speed of movement, we can add a duration parameter:
durationcan also be ad => miliseconds function, where dis the number of pixels the element has to travel.
Note that all the transitions and animations are being applied with CSS, rather than JavaScript, meaning they won't block (or be blocked by) the main thread.
Indonesia
The Animate Directive
Pada contoh sebelumnya, kita menggunakan transisi defer untuk membuat ilusi gerakan saat satu elemen pindah dari daftar todo ke daftar satunya.
Untuk melengkapi, kita juga bisa memberikan efek gerakan untuk elemen yang tidak pindah. Untuk keperluan ini kita bisa menggunakan direktiv animate.
<label in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" animate:flip > Kamu akan bisa lihat, jika kita memilih sesuatu kemudian pindah ke daftar sebelahnya, item yang tersisa akan pelan-pelan naik ke atas, dan tidak lompat seketika. Jika ingin mengganti kecepatan gerakan, kita bisa menambahkan parameter duration: <label in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" animate:flip="{{duration: 200}}" >
durationjuga bisa berupa fungsid => miliseconds, dadalah jumlah pixel yang dilewati elemen. Perhatikan bahwa semua transisi dan animasi akan dijalankan pada CSS, dan bukan JavaScript, jadi mereka tidak akan menghalangi (atau dihalangi) oleh main thread.
We can make more appealing user interfaces by gracefully transitioning elements into and out of the DOM. Svelte makes this very easy with the transition directive.
The Transitions Directive
This is the transitions that will fade your sentence in and out as it is appear or disappear.
<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}
Adding Parameters
Transitions function can receive parameters, here is example of fly transitions:
<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 }}"> Flies in and out </p> {/if}
Note that the transition is reversible — if you toggle the checkbox while the transition is ongoing, it transitions from the current point, rather than the beginning or the end.
In and Out
Instead of the transition directive, an element can have an in or an out directive, or both together. Notice the transition directive is replaced with in and out directive.
<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, fades out </p> {/if}
Custom CSS Transitions
The svelte/transition module has a handful of built-in transitions, but it's very easy to create your own. By way of example, this is the source of the fade transition:
function fade(node, { delay = 0, duration = 400 }) { const o = +getComputedStyle(node).opacity; return { delay, duration, css: t => `opacity: ${t * o}` }; }
The function takes two arguments — the node to which the transition is applied, and any parameters that were passed in — and returns a transition object which can have the following properties:
delay — milliseconds before the transition begins
duration — length of the transition in milliseconds
easing — a p => t easing function (see the chapter on )
css — a (t, u) => css function, where u === 1 - t
tick — a (t, u) => {...} function that has some effect on the node. The t value is 0 at the beginning of an intro or the end of an outro, and 1 at the end of an intro or beginning of an outro.
Most of the time you should return the css property and not the tick property, as CSS animations run off the main thread to prevent jank where possible. Svelte 'simulates' the transition and constructs a CSS animation, then lets it run.
For example, the fade transition generates a CSS animation somewhat like this: 0% { opacity: 0 } 10% { opacity: 0.1 } 20% { opacity: 0.2 } /* ... */ 100% { opacity: 1 }
We can get a lot more creative though. Let's make something truly gratuitous: <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>
As the words is going visible (intro) it will spin and change color as we set up in spin function
Custom JS Transitions
While you should generally use CSS for transitions as much as possible, there are some effects that can't be achieved without JavaScript, such as a typewriter effect:
function typewriter(node, { speed = 50 }) { const valid = ( node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE ); 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); } }; }
Here is how we're gonna call the function
<p in:typewriter> The quick brown fox jumps over the lazy dog </p>
Transitions Events
It can be useful to know when transitions are beginning and ending. Svelte dispatches events that you can listen to like any other DOM event:
<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>
Local Transitions
Ordinarily, transitions will play on elements when any container block is added or destroyed. In the example here, toggling the visibility of the entire list also applies transitions to individual list elements.
Instead, we'd like transitions to play only when individual items are added and removed — in other words, when the user drags the slider.
We can achieve this with a local transition, which only plays when the immediate parent block is added or removed:
<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}
Defered Transitions
A particularly powerful feature of Svelte's transition engine is the ability to defer transitions, so that they can be coordinated between multiple elements.
Take this pair of todo lists, in which toggling a todo sends it to the opposite list. In the real world, objects don't behave like that — instead of disappearing and reappearing in another place, they move through a series of intermediate positions. Using motion can go a long way towards helping users understand what's happening in your app.
We can achieve this effect using the crossfade function, which creates a pair of transitions called send and receive. When an element is 'sent', it looks for a corresponding element being 'received', and generates a transition that transforms the element to its counterpart's position and fades it out. When an element is 'received', the reverse happens. If there is no counterpart, the fallback transition is used. This is how we're gonna use send and receive in <label>: <label in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" >
Full example is here, this is example of todo list so if you check something on the left list (todo) it will move to right (done). Defered transitions used to make moving object smoother, instead of jumping immediately there will be moving and fade effect, you can try to remove in and out attribute to see what it would be like without crossfade.
Kita bisa membuat user interface yang menarik dengan cara melakukan transisi elemen masuk dan keluar DOM secara elegan. Svelte memberikan kemudahan untuk itu dengan directiv transition.
The Transitions Directive
Transisi ini akan memberikan efek fade masuk dan kluar saat tulisan muncul dan menghilang.
<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}
Adding Parameters
Fungsi transisi dapat menerima parameter, berikut contoh transisi fly:
<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 }}"> Flies in and out </p> {/if} Perhatikan bawah transisi bersifat reversible, artinya jika kita toggle checkbox saat transisi sedang berjalan, maka transisi akan dilanjutkan dari titik terakhir, bukan dari awal atau akhir.
In and Out
Selain direktiv transition, suatu elemen juga bisa mempunya direktiv in dan atau out. Perhatikan direktiv transition digantikan dengan direktiv in dan out.
<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, fades out </p> {/if}
Custom CSS Transitions
Modul svelte/transition memiliki sejumlah transisi built-in yang berguna, tapi kita bisa membuat transisi sendiri. Sebagai contoh berikut adalah isi program dari transisi fade:
function fade(node, { delay = 0, duration = 400 }) { const o = +getComputedStyle(node).opacity; return { delay, duration, css: t => `opacity: ${t * o}` }; }
Fungsi ini menerima 2 argumen — node tempat transisi dijalankan, dan semua parameter yang kita masukkan — dan me return object transisi yang bisa memiliki properti sebagai berikut:
delay — milisekon sebelum transisi dimulai
duration — lama transisi dalam milisekon
easing — sebuah fungsi easing p => t (lihat tutorial sebelumnya )
css — sebuah fungsi (t, u) => css , dimana u === 1 - t
tick — sebuah fungsi (t, u) => {...} yang mempunyai efek pada node. Nilai t adalah 0 pada permulaan intro atau pada akhir outro, dan 1 pada kebalikannya.
Sebaiknya lebih diutamakan untuk mereturn properti css dan bukan properti tick, karena animasi CSS dijalankan di luar main thread untuk menghindari gangguan semaksimal mungkin. Svelte akan mensimulasikan transisi dan akan membentuk animasi CSS, kemudian baru menjalankannya.
Sebagai contoh transisi fade membentuk animasi CSS kurang lebih seperti ini: 0% { opacity: 0 } 10% { opacity: 0.1 } 20% { opacity: 0.2 } /* ... */ 100% { opacity: 1 }
Kita bisa lebih kreatif lagi, untuk membuat sesuatu yang sebetulnya tidak terlalu penting: <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>
Selagi tampilan kata mulai muncul (intro) kalimat akan berputar dan berubah warna seperti yang kita buat di dalam fungsi spin.
Custom JS Transitions
Sementara kita sebaiknya mengutamakan menggunakan transisi CSS, kadang ada efek tertentu yang tidak bisa dihasilkan kecuali menggunakan JavaScript, seperti efek mengetik:
function typewriter(node, { speed = 50 }) { const valid = ( node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE ); 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); } }; }
Kemudian tinggal memanggil fungsi typewriter seperti ini:
<p in:typewriter> The quick brown fox jumps over the lazy dog </p>
Transitions Events
Terkadang kita perlu mengakses kapan saat transisi sedang dimulai atau berakhir. Svelte akan men dispatch even yang bisa kita akses seperti even DOM pada umumnya:
<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>
Local Transitions
Biasanya, transisi akan dimainkan pada element ketika suatu blok kontainer ditambahkan atau dihancurkan. Pada contoh berikut, mengubah show list akan menjalankan juga transisi ke masing-masing elemen.
Padahal kita ingin transisi hanya berjalan saat kita menambah atau menghilangkan item, atau dengan kata lain saat kita menggeser slider.
Kita bisa mencapai keinginan kita dengan memanfaatkan local transition, yang hanya akan berjalan ketika blok parent ditambahkan atau dihilangkan:
<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}
Defered Transitions
Suatu fitur yang sangat hebat dari engine transisi Svelte adalah kemampuannya untuk defer (=menunda) transisi, jadi transisi bisa dikoordinasikan antar elemen.
Sebagai contoh di sini kita mempunyai daftar todo, yang jika kita klik akan memindah seketika item ke daftar done. Pada dunia nyata, benda tidak hilang dan muncul tiba-tiba seperti itu melainkan akan bergerak dari tempat asal ke tempat tujuan. Menggunakan gerakan seperti ini bisa membantu user untuk memahami maksud dari aplikasi yang kita buat.
Kita bisa mendapatkan efek ini dengan menggunakan fungsi crossfade,yang akan menghasilkan sepasang efek transisi yang disebut send dan receive. Ketika satu elemen dikirim, maka dia akan mencari elemen lain yang berhubungan yang sedang diterima, dan menghasilkan transisi yang akan memindahkan elemen ke posisi sebelah dan kemudian menghilang dari tempat asalnya. Ketika elemen diterima maka akan terjadi kebalikannya. Sedangkan jika tidak menemukan pasangannya, makan transisi fallback yang akan digunakan, berikut adalah cara kita akan menggunakan send dan receive pada <label>: <label in:receive="{{key: todo.id}}" out:send="{{key: todo.id}}" > Contoh lengkap bisa dilihat di sini. ini adalah contoh aplikasi yang menampilkan daftar todo, yang jika kita klik maka item akan pindah ke sebelah kanan ke daftar done. Transisi defered digunakan untuk membuat transisi menjadi lebih mulus, dan tidak pindah tempat begitu saja. Kamu bisa coba menghilangkan atribut in dan out untuk melihat hasilnya akan seperti apa tanpa efek crossfade.