2020-04-24

Svelte Tutorial 10: Transitions (English & Indonesia)

English


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>

We will use spin function like this :

<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}

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.

<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: '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>





Indonesia


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>

Kita akan menggunakan fungsi spin seperti ini :

<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}

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.

<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: '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>

Svelte Tutorial 9: Motion (English & Indonesia)

English

Tweened


Tweened is svelte tools to help you build slick user interfaces that use animation to communicate changes. Below is example of progress bar, tweened is used to make transition smoother. easing is even make it more smoother.

<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>

The full set of options available to tweened:
  • delay — milliseconds before the tween starts
  • duration — either the duration of the tween in milliseconds, or a (from, to) => milisecons function allowing you to (e.g.) specify longer tweens for larger changes in value
  • easing — a p => t function
  • interpolate — a custom (from, to) => t => value function for interpolating between arbitrary values. By default, Svelte will interpolate between numbers, dates, and identically-shaped arrays and objects (as long as they only contain numbers and dates or other valid arrays and objects). If you want to interpolate (for example) colour strings or transformation matrices, supply a custom interpolator
You can also pass these options to progress.set and progress.update as a second argument, in which case they will override the defaults. The set and update methods both return a promise that resolves when the tween completes.

Spring


The spring function is an alternative to tweened that often works better for values that are frequently changing. In this example we have two stores — one representing the circle's coordinates, and one representing its size. Both springs have default stiffness and damping values, which control the spring's, well... springiness. We can specify our own initial values:

<script>
  import { spring } from 'svelte/motion';
  let coords = spring({ x: 50, y: 50 }, {
    stiffness: 0.1,
    damping: 0.25
  })
  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>

Waggle your mouse around, and try dragging the sliders to get a feel for how they affect the spring's behaviour. Notice that you can adjust the values while the spring is still in motion.






Indonesia

Tweened


tweened adalah alat yang disediakan svelte untuk membantu kita membuat user interface yang enak digunakan, yang menggunakan animasi untuk menampilkan suatu perubahan. Dibawah ini adalah contoh program untuk membuat progress bar, tweened digunakan untuk membuat transisi lebih mulus. easing membuatnya makin mulus lagi.

<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>

Berikut adalah daftar lengkap option yang tersedia untuk tweened:
  • delay — milisekon sebelum tween mulai
  • duration — antara durasi tween dalam milisekon, atau sebuah fungsi (from, to) => milisecons yang memungkinkan kita untuk (misalnya) mengatur tween yang lebih lama untuk perubahan yang lebih besar
  • easing — sebuah fungsi p => t 
  • interpolate — sebuah fungsi custom (from, to) => t => value untuk melakukan interpolasi antara nilai arbitari. Secara default, svelte akan melakukaninterpolasi antara angka, tanggal, dan array yang bentuknya identik, dan object (selama hanya mengandung angka dan tanggal atau array yang valid dan object). Jika kita ingin melakukan interpolasi (sebagai contoh) sekumpulan warna atau matriks transformasi, sediakan interpolator custom
Kita juga bisa pass option ini ke  progress.set dan progress.update sebagai argumen kedua, sehingga akan melakukan override untuk nilai default nya. Method set dan update keduanya akan me return promise yang akan resolve ketika tween selesai.

Spring


Fungsi spring adalah alternatif dari tweened yang biasanya akan bekerja lebih baik untuk nilai2 yang sering berubah. Pada contoh berikut kita akan memiliki dua store — satu menandakan koordinat lingkaran, dan satunya menyimpan ukuran lingkaran. spring mempunyai nilai default stiffness dan damping, yang akan mengontrol ke spring an nya.. Kita juga bisa memberikan nilai sendiri:

<script>
  import { spring } from 'svelte/motion';
  let coords = spring({ x: 50, y: 50 }, {
    stiffness: 0.1,
    damping: 0.25
  })
  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>

Goyang-goyangkan mouse, dan cobalah menggeser slider untuk memahami bagaimana mereka mempengaruhi kelakuan spring. Perhatikan bahwa kita juga bisa mengubah efeknya ketika spring masih sedang bergerak.

Svelte Tutorial 8: Stores (English & Indonesia)

English

Stores


Not all application state belongs inside your application's component hierarchy. Sometimes, you'll have values that need to be accessed by multiple unrelated components, or by a regular JavaScript module.

Writable Stores


In Svelte, we do this with stores. A store is simply an object with a subscribe method that allows interested parties to be notified whenever the store value changes. Below is an example where, count is a store, and we're setting count_value in the count.subscribe callback.

let count_value;

const unsubscribe = count.subscribe(value => {
  count_value = value;
});

This is the definition of count. It's a writable store, which means it has set and update methods in addition to subscribe.

import { writable } from 'svelte/store';
export const count = writable(0);

count can be updated or set like this:

function increment() { count.update(n => n + 1); }
function reset() { count.set(0); }

Full example can be seen here.


Auto-Subscriptions


The app in the previous example works, but there's a subtle bug — the unsubscribe function never gets called. If the component was instantiated and destroyed many times, this would result in a memory leak.

One way to fix it would be to use the onDestroy lifecycle hook:

<script>
  import { onDestroy } from 'svelte';
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Resetter from './Resetter.svelte';

  let count_value;
  const unsubscribe = count.subscribe(value => {
    count_value = value;
  });

  onDestroy(unsubscribe);
</script>

<h1>The count is {count_value}</h1>

It starts to get a bit boilerplatey though, especially if your component subscribes to multiple stores. Instead, Svelte has a trick up its sleeve — with autosubscribe means you can reference a store value by prefixing the store name with $ (you don't need to manually do onDestroy too, it's automatic):

<script>
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

You can see the difference in first and second example, much simpler by using $. Auto-subscription only works with store variables that are declared (or imported) at the top-level scope of a component.

You're not limited to using $count inside the markup, either — you can use it anywhere in the <script> as well, such as in event handlers or reactive declarations.

Any name beginning with $ is assumed to refer to a store value. It's effectively a reserved character — Svelte will prevent you from declaring your own variables with a $ prefix.

Readable Stores


Not all stores should be writable by whoever has a reference to them. For example, you might have a store representing the mouse position or the user's geolocation, and it doesn't make sense to be able to set those values from 'outside'. For those cases, we have readable stores.

Create a stores.js file. The first argument to readable is an initial value, which can be null or undefined if you don't have one yet. The second argument is a start function that takes a set callback and returns a  stop function. The start function is called when the store gets its first subscriber; stop is called when the last subscriber unsubscribes.

// this is stores.js
import { readable } from 'svelte/store';

export const time = readable(new Date(), function start(set) { 
  // called at subscribe
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return function stop() { // called when unsubscribe
    clearInterval(interval);
  };
});

Then we can use time in 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>


Derived Stores


You can create a store whose value is based on the value of one or more other stores with derived. Building on our previous example, we can create a store that derives the time the page has been open:

export const elapsed = derived(
  time,
  $time => Math.round(($time - start) / 1000)
);

It's possible to derive a store from multiple inputs, and to explicitly set a value instead of returning it (which is useful for deriving values asynchronously). Consult the API reference for more information. Full example can be seen here.

Custom Stores


As long as an object correctly implements the subscribe method, it's a store. Beyond that, anything goes. It's very easy, therefore, to create custom stores with domain-specific logic.

For example, the count store from our earlier example could include incrementdecrement and reset methods and avoid exposing set and update:

function createCount() {
  const { subscribe, set, update } = writable(0);
  
  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(0)
  };
}

And here is how we're gonna use it in main app:

<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>

Stores Binding


If a store is writable — i.e. it has a set method — you can bind to its value, just as you can bind to local component state. In this example we have a writable store name and a derived store greeting.

import { writable, derived } from 'svelte/store';

export const name = writable('world');
export const greeting = derived(
  name,
  $name => `Hello ${$name}!`
);

Changing the input value will now update name and all its dependents. We can also assign directly to store values inside a component. Add a <button> element:

<script> import { name, greeting } from './stores.js'; </script>

<h1>{$greeting}</h1>
<input bind:value={$name}>
<button on:click="{() => $name+='!'}">
  Add exclamation mark!
</button>

The $name+='!' assignment is equivalent to name.set($name+'!').




Indonesia

Stores


Tidak semua nilai dalam aplikasi berada di dalam hirarki komponen. Kadang, kita akan mempunyai suatu nilai yang perlu diakses oleh beberapa komponen yang tidak berhubungan, atau bisa juga oleh modul JavaScript biasa.

Writable Stores


Di svelte, kita bisa menggunakan store untuk keperluan ini. Store adalah suatu object dengan method subscribe yang memungkinkan bagian yang membutuhkan untuk mendapat notifikasi saat nilai dari store itu berubah. Di bawah ini adalah contoh store, count dan kita akan mengatur nilai dari count_value pada callback count.subscribe.

let count_value;

const unsubscribe = count.subscribe(value => {
  count_value = value;
});

Di bawah ini adalah definisi dari count. Yang merupakan writable store, berarti count memiliki method set dan update selain subscribe.

import { writable } from 'svelte/store';
export const count = writable(0);

ini contoh cara update atau set count:

function increment() { count.update(n => n + 1); }
function reset() { count.set(0); }

Contoh lengkap bisa dilihat di sini.

Auto-Subscriptions


Aplikasi di contoh sebelumnya akan berjalan, tapi ada sedikit kekurangan — fungsi unsubscribe tidak pernah dijalankan. Jika komponen itu akan di instantiate dan di destroy berulang kali, akan ada memori yang bocor.

Salah satu cara untuk memperbaikinya adalah dengan menggunakan pancingan lifecycle onDestroy :

<script>
  import { onDestroy } from 'svelte';
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Resetter from './Resetter.svelte';

  let count_value;
  const unsubscribe = count.subscribe(value => {
    count_value = value;
  });

  onDestroy(unsubscribe);
</script>

<h1>The count is {count_value}</h1>

Tapi programnya mulai terlihat sedikit ribet, terutama jika komponen itu melakukan subscribe ke banyak store. Maka, svelte sudah menyediakan satu kemudahan — dengan autosubscribe, yang berarti kita bisa langsung melakukan referensi ke suatu store dengan memberikan awalan nama store dengan $ (maka kita tidak perlu lagi melakukan unsubscribe di onDestroy):

<script>
  import { count } from './stores.js';
  import Incrementer from './Incrementer.svelte';
  import Decrementer from './Decrementer.svelte';
  import Resetter from './Resetter.svelte';
</script>

<h1>The count is {$count}</h1>

Kita bisa liat perbedaan dari contoh pertama dan kedua, akan jauh lebih mudah dengan menggunakan $. Auto subscription hanya bekerja dengan variabel store yang dideklarasikan (atau di import) pada bagian atas dari scope suatu komponen.

Kita juga tidak terbatas untuk menggunakan $count hanya di dalam markup, kita bisa gunakan di dalam  <script> juga, seperti di event handle atau deklarasi reactive.

Semua nama yang berawalan dengan $ akan dianggap mengacu pada nilai store. Maka karakter ini adalah karakter reserved  — Svelte akan mencegah kita deklarasi variabel dengan awalan  $.

Readable Stores


Tidak semua store perlu bisa writable. Sebagai contoh , kita bisa mempunyai store yang menyimpan posisi mouse, atau geolokasi, maka akan tidak masuk akal jika nilai store itu mau dirubah-rubah. Untuk kasus seperti itu ada readable store, store yang hanya readonly.

Pertama buat file stores.js . Argumen pertama untuk readable adalah inisialisasi awal, bisa berisi null atau undefined jika memang belum ada. Argumen kedua adalah fungsi start yang akan menerima callback set dan me return fungsi  stop. Fungsi start akan dijalankan ketika store mendapat subsriber pertamanya, dan stop akan dijalankan saat subscriber terakhir unsubscribe.

// this is stores.js
import { readable } from 'svelte/store';

export const time = readable(new Date(), function start(set) { 
  // called at subscribe
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  return function stop() { // called when unsubscribe
    clearInterval(interval);
  };
});

Sesudah itu kita bisa menggunakan komponen time di dalam 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>

Derived Stores


Kita bisa membuat store yang nilainya berdasarkan dari nilai satu atau lebih store dengan derived. Melanjutkan dari contoh sebelumnya, kita bisa membuat store yang akan menghitung waktu berapa lama halaman aplikasi sudah dijalankan:

export const elapsed = derived(
  time,
  $time => Math.round(($time - start) / 1000)
);

Kita bisa men derive store dari berbagai input, dan secara eksplisit memberikan nilai daripada me return nya (biasanya digunakan untuk derive sesuatu secara asinkron). Cek  API reference untuk informasi lebih lanjut. Contoh lengkap bisa dilihat di sini.

Custom Stores


Selama suatu object menjalankan method subscribe dengan benar, maka dia adalah store. Maka dari itu kita bisa dengan mudah membuat store custom dengan logika yang spesifik untuk domain tertentu.

Sebagai contoh store count dari contoh sebelumnya bisa menambakan method incrementdecrement dan reset sehingga tidak perlu mengekspos set dan update pada saat penggunaan komponen:

function createCount() {
  const { subscribe, set, update } = writable(0);
  
  return {
    subscribe,
    increment: () => update(n => n + 1),
    decrement: () => update(n => n - 1),
    reset: () => set(0)
  };
}

Kemudian bisa digunakan di aplikasi utama seperti ini:

<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>

Stores Binding


Jika suatu store writable — mempunyai method set — kita bisa melakukan bind pada nilainya, seperti kita bisa melakukan bind pada state komponen lokal. Pada contoh berikut kita memiliki store writable  name dan derived store greeting.

import { writable, derived } from 'svelte/store';

export const name = writable('world');
export const greeting = derived(
  name,
  $name => `Hello ${$name}!`
);

Maka mengubah nilai input sekarang akan meng update name dan semua dependensinya. Kita juga bisa melakukan assign nilai secara langsung pada store di dalam komponen. Tambahkan elemen <button> :

<script> import { name, greeting } from './stores.js'; </script>

<h1>{$greeting}</h1>
<input bind:value={$name}>
<button on:click="{() => $name+='!'}">
  Add exclamation mark!
</button>

Proses $name+='!' akan menghasilkan sesuatu yang sama dengan name.set($name+'!').

2020-04-23

Svelte Tutorial 7: Lifecycle (English & Indonesia)

English


Lifecycle


Every component has a lifecycle that starts when it is created, and ends when it is destroyed. There are a handful of functions that allow you to run code at key moments during that lifecycle.

onMount


The one you'll use most frequently is onMount, which runs after the component is first rendered to the DOM. We briefly encountered it  when we needed to interact with a <canvas> element after it had been rendered. We'll add an onMount handler that loads some data over the network:

<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>

It's recommended to put the fetch in onMount rather than at the top level of the <script> because of server-side rendering (SSR). With the exception of onDestroy, lifecycle functions don't run during SSR, which means we can avoid fetching data that should be loaded lazily once the component has been mounted in the DOM.

Lifecycle functions must be called while the component is initialising so that the callback is bound to the component instance — not (say) in a setTimeout.

If the onMount callback returns a function, that function will be called when the component is destroyed.

onDestroy


To run code when your component is destroyed, use onDestroy. For example, we can add a setInterval function when our component initialises, and clean it up when it's no longer relevant. Doing so prevents memory leaks.

<script>

  import { onDestroy } from 'svelte';
  let seconds = 0;
  const interval = setInterval(() => seconds += 1, 1000);
  onDestroy(() => clearInterval(interval));
</script>

While it's important to call lifecycle functions during the component's initialisation, it doesn't matter where you call them from. So if we wanted, we could abstract the interval logic into a helper function in util.js...

import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
 const interval = setInterval(callback, milliseconds);
  onDestroy(() => {
  clearInterval(interval);
 });
}

then import it into our component:

<script>
 import { onInterval } from './utils.js';
 let seconds = 0;
 onInterval(() => seconds += 1, 1000);
</script>

beforeUpdate and afterUpdate


The beforeUpdate function schedules work to happen immediately before the DOM has been updated. afterUpdate is its counterpart, used for running code once the DOM is in sync with your data. Together, they're useful for doing things imperatively that are difficult to achieve in a purely state-driven way, like updating the scroll position of an element.

let div;
let autoscroll;
beforeUpdate(() => {
  autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});
afterUpdate(() => {
  if (autoscroll) div.scrollTo(0, div.scrollHeight);
});

Note that beforeUpdate will first run before the component has mounted, so we need to check for the existence of div before reading its properties. See working example here.

Tick


The tick function is unlike other lifecycle functions in that you can call it any time, not just when the component first initialises. It returns a promise that resolves as soon as any pending state changes have been applied to the DOM (or immediately, if there are no pending state changes).

When you invalidate component state in Svelte, it doesn't update the DOM immediately. Instead, it waits until the next microtask to see if there are any other changes that need to be applied, including in other components. Doing so avoids unnecessary work and allows the browser to batch things more effectively.

You can see that behaviour in this example. Select a range of text and hit the tab key. Because the <textarea> value changes, the current selection is cleared and the cursor jumps, annoyingly, to the end. We can fix this by importing tick... ...and running it immediately before we set this.selectionStart and .selectionEnd at the end of handleKeydown:

<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();  // update textarea value first
                   // then put selection in place
    this.selectionStart = selectionStart;
    this.selectionEnd = selectionEnd;
  }
</script>

<style> textarea { width: 100%; height: 200px; } </style>

<textarea value={text} on:keydown={handleKeydown}></textarea>




Indonesia


Lifecycle


Setiap komponen mempunyai lifecycle yang dimulai ketika komponen itu dicreate, dan berakhir ketika komponen didestroy. Ada beberapa fungsi yang berguna untuk memungkinkan kita menjalankan program pada saat tertentu selama lifecycle.

onMount


onMount adalah fungsi yang paling sering digunakan, fungsi ini akan berjalan saat komponen pertama kali dirender ke dalam DOM. Kita sempat menggunakan onMount saat melakukan interaksi dengan elemen  <canvas> sesudah dirender. Kita akan menambahkan handler onMount yang akan melakukan loading data melalui jaringan:

<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>

Alangkah baiknya jika kita meletakkan fetch di onMount dan bukan di baris atas <script> hal ini dikarenakan proses server-side rendering (SSR). Dengan perkecualian untuk onDestroy, fungsi lifecycle tidak berjalan saat SSR, maka kita bisa menghindari fetch data yang bisa diload belakangan kita komponen sudah termount pada DOM.

Fungsi lifecycle harus dipanggil saat komponen dalam proses pembentukan jadi callback nya akan terikat pada komponen itu  — jangan (misalnya) dalam setTimeout.

Jika callback onMount mereturn suatu fungsi, fungsi tersebut akan dipanggil saat komponen di destroy.

onDestroy


Untuk menjalankan program saat komponen di destroy, gunakan onDestroy. Sebagai contoh, kita bisa menambakan fungsi setInterval ketika komponen dibentuk, dan membersihkan ketika interval sudah tidak lagi dibutuhkan. Hal ini untuk mencegah kehabisan memori.

<script>

  import { onDestroy } from 'svelte';
  let seconds = 0;
  const interval = setInterval(() => seconds += 1, 1000);
  onDestroy(() => clearInterval(interval));
</script>

Meskipun penting untuk memanggil fungsi lifecycle selama masa pembentukan komponen. Tapi tidak masalah kita memanggil dari mana. Jadi jika kita mau, kita bisa melakukan proses logika interval  dalam fungsi helper di dalam util.js...

import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
 const interval = setInterval(callback, milliseconds);
  onDestroy(() => {
  clearInterval(interval);
 });
}

kemudian diimporkan ke dalam komponen:

<script>
 import { onInterval } from './utils.js';
 let seconds = 0;
 onInterval(() => seconds += 1, 1000);
</script>


beforeUpdate and afterUpdate


Fungsi beforeUpdate menjadwalkan kerja program untuk terjadi segera sebelum DOM nya ter update. afterUpdate adalah kebalikannya, digunakan untuk menjalankan program, setelah DOM sudah tersinkronisasi dengan data dalam program. Bersama-sama, kedua fungsi ini berguna untuk melakukan hal-hal yang susah dilakukan dalam cara yang murni berhubungan dengan state, seperti mengupdate posisi scroll suatu elemen.

let div;
let autoscroll;
beforeUpdate(() => {
  autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});
afterUpdate(() => {
  if (autoscroll) div.scrollTo(0, div.scrollHeight);
});

Jangan lupa bahwa beforeUpdate akan dijalankan duluan sebelum komponen sudah ter mount, jadi kita harus melakukan pengecekan keberadaan div, sebelum mengecek properti nya. Contoh aplikasi yang sudah berjalan bisa dilihat di sini.

Tick


Fungsi tick tidak seperti fungsi lifecycle yang lainnya, dalam hal fungsi ini bisa dipanggil kapan saja, bukan hanya saat komponen pertama terbentuk. Fungsi ini akan mereturn promise yang akan cair saat semua perubahan state yang masih pending sudah diterapkan pada DOM (atau segera, jika tidak ada perubahan state yang pending).

Ketika terjadi invalidasi state komponen, perubahan DOM tidak dilakukan langsung. Melainkan menunggu beberapa perubahan kecil lain untuk melihat apabila ada beberapa perubahan yang bisa dilakukan bersamaan, termasuk perubahan di komponen lain. Hal ini dimaksudkan untuk menghindari pekerjaan tambahan dan juga membantui browser untuk bekerja lebih efektif.

Kita bisa melihat kelakukan itu di contoh ini. Pilih sekumpulan teks dan pencet tombol tab. Karena nilai <textarea> berubah, pilihan teks akan hilang dan kursor akan lompat ke bagian akhir, sangat mengganggu.. Kita bisa memperbaiki ini dengan menggunakan tick... dan langsung menjalankan secara langsung sebelum kita mengatur this.selectionStart dan this.selectionEnd di bagian akhir handleKeydown:

<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();  // tick akan update textarea dulu
                   // baru selection akan diset ulang
    this.selectionStart = selectionStart;
    this.selectionEnd = selectionEnd;
  }
</script>

<style> textarea { width: 100%; height: 200px; } </style>

<textarea value={text} on:keydown={handleKeydown}></textarea>