Die Reaktive Offenbarung:
Eine Einführung in Svelte

Nils Röhrig | Loql

Agenda

  1. Was ist Svelte?
  2. Die Anatomie einer Svelte-Komponente
  3. Komposition von Komponenten in Svelte
  4. Ausblick auf Svelte 5

Was ist Svelte?

Svelte

/ˈsvɛlt/

schlank

=

Svelte

/ˈsvɛlt/

Svelte

/ˈsvɛlt/

schlank

=

anmutig

Svelte is a way of writing user interface components [...] that users see and interact with in their browsers.

Svelte ist ein Framework

Svelte ist ein Framework

für die Erstellung von deklarativen und wiederverwendbaren Komponenten, die den Zustand und das Verhalten von UI-Elementen umfassen

Logo
Register
Home
E-Mail
Submit
Logo
Register
Home
E-Mail
Submit

<Header>

<Navbar>

<Form>

<App>

Logo
Register
Home

<Header>

<Navbar>

<Form>

<App>

<NavItem>

<NavItem>

<Logo>

E-Mail
Submit

<Label>

<Input>

<Button>

Svelte ist ein Compiler

<h1>Simple Component</h1>

Component.svelte

<h1>Simple Component</h1>
<h1>Simple Component</h1>
<h1>Simple Component</h1>
import { SvelteComponent, detach, element, init,
         insert, noop, safe_not_equal } from "svelte/internal";

function create_fragment(ctx) {
  let h1;

  return {
    c() {
      h1 = element("h1");
      h1.textContent = "Simple Component";
    },
    m(target, anchor) {
      insert(target, h1, anchor);
    },
    p: noop,
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(h1);
    },
  };
}

class SimpleComponent extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, safe_not_equal, {});
  }
}

export default SimpleComponent;
<h1>Simple Component</h1>
import { [...] } from "svelte/internal";
function create_fragment(ctx) {
  let h1;  
  return {
    c() {
      h1 = element("h1");
      h1.textContent = "Simple Component";
    },
    m(target, anchor) {
      insert(target, h1, anchor);
    },
    p: noop, i: noop, o: noop,
    d(detaching) { if (detaching) detach(h1); },
  };
}
class Component extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, 
		safe_not_equal, {});
  }
}
export default Component;

Component.js

<h1>Simple Component</h1>
import Component from "./Component.js";

const component = new Component({
  target: document.body,
});

export default component;

main.js

Was bietet Svelte an?

  • Nah an der Webplattform – HTML, CSS und JS sind schon die halbe Miete
     
  • Vollständig ausgestattet – Lösungen für Zustandsmanagement, Transitionen und mehr
     
  • Großartige Leistung out-of-the-box – keine Abstraktionen wie ein virtuelles DOM erforderlich
     
  • Kleinstmögliche Bundle-Größe – es gibt keine Laufzeitbibliothek im üblichen Sinne
     
  • Ein ausgeklügeltes Reaktivitätsmodell – (fast) alles in Svelte ist reaktiv

Was bietet Svelte an?

Die Anatomie einer Svelte-Komponente

<h1>Simple Component</h1>

Component.svelte

<h1>Simple Component</h1>
<script>
  let text = "Simple Component";
</script>

<h1>{text}</h1>

Component.svelte

<h1>Simple Component</h1>
<script>
  let text = "Simple Component";
</script>

<h1>{text}</h1>
<script>
  let text = "Simple Component";
</script>

<h1>{text}</h1>

<style>
  h1 { color: blue; }
</style>

Component.svelte

<script>
  // State & Behavior
</script>

<!-- Declarative Markup -->

<style>
  /* Scoped Component Styles */
</style>

ComponentStructure.svelte

State & Reaktivität

<script>
  let counter = 0;
</script>

<output>{counter}</output>

<button>Decrease</button>
<button>Increase</button>

Counter.svelte

<script>
  let counter = 0;
</script>

<output>{counter}</output>

<button>Decrease</button>
<button>Increase</button>
<script>
  let counter = 0;
  
  const inc = () => counter++;
  const dec = () => counter--;
</script>

<output>{counter}</output>

<button on:click={dec}>Decrease</button>
<button on:click={inc}>Increase</button>

Counter.svelte

<script>
  let counter = 0;
</script>

<output>{counter}</output>

<button>Decrease</button>
<button>Increase</button>
<script>
  let counter = 0;
  
  const inc = () => counter++;
  const dec = () => counter--;
</script>

<output>{counter}</output>

<button on:click={dec}>Decrease</button>
<button on:click={inc}>Increase</button>
<script>
  let counter = 0;

  $: doubled = counter * 2;
  $: quadrupled = doubled * 2;

  const inc = () => counter++;
  const dec = () => counter--;
</script>

<b>x1</b>: {counter}<br>
<b>x2</b>: <output>{counter} * 2 = {doubled}</output><br>
<b>x4</b>: <output>{doubled} * 2 = {quadrupled}</output><br>

<button on:click={dec}>Decrease</button>
<button on:click={inc}>Increase</button>

Counter.svelte

Logikblöcke

<script>
  let notes = [];
</script>

<form>
  <input placeholder="New note text..." />
  <button>Add</button>

  <ul>
    <li>Note 1 <button>&cross;</button></li>
    <li>Note 2 <button>&cross;</button></li>
    <li>Note 3 <button>&cross;</button></li>
  </ul>
</form>

Notes.svelte

<script>
  let notes = [];
</script>

<form>
  <input placeholder="New note text..." />
  <button>Add</button>

  {#if notes.length > 0}
    <ul>
      {#each notes as note}
        <li>
          {note}
          <button>&cross;</button>
        </li>
      {/each}
    </ul>
  {:else}
    <p>No notes yet.</p>
  {/if}
</form>
<script>
  let notes = [];
</script>

<form>
  <input placeholder="New note text..." />
  <button>Add</button>

  <ul>
    <li>Note 1 <button>&cross;</button></li>
    <li>Note 2 <button>&cross;</button></li>
    <li>Note 3 <button>&cross;</button></li>
  </ul>
</form>

Notes.svelte

<script>
  let notes = []
  let newNote = "";

  function addNote() {
    notes = [...notes, newNote];
    newNote = "";
  }
  function removeNote(note) {
    notes = notes.filter((n) => n != note);
  }
</script>
<form>
  <input placeholder="New note text..." bind:value={newNote} />
  <button on:click={addNote}>Add</button>
  {#if notes.length > 0}
    <ul>
      {#each notes as note}
        <li>
          {note}
          <button on:click={() => removeNote(note)}>&cross;</button>
        </li>
      {/each}
    </ul>
  {:else}
    <p>No notes yet.</p>
  {/if}
</form>
<script>
  let notes = [];
</script>

<form>
  <input placeholder="New note text..." />
  <button>Add</button>

  {#if notes.length > 0}
    <ul>
      {#each notes as note}
        <li>
          {note}
          <button>&cross;</button>
        </li>
      {/each}
    </ul>
  {:else}
    <p>No notes yet.</p>
  {/if}
</form>
<script>
  let notes = [];
</script>

<form>
  <input placeholder="New note text..." />
  <button>Add</button>

  <ul>
    <li>Note 1 <button>&cross;</button></li>
    <li>Note 2 <button>&cross;</button></li>
    <li>Note 3 <button>&cross;</button></li>
  </ul>
</form>

Notes.svelte

Kurze Zusammenfassung

Lokaler State

State-Variablen halten den Kom­po­nen­ten­zu­stand und sind reaktiv. Diese Reaktivität wird durch Zuweisung, nicht duch Mutation ausgelöst.

Event Listeners

Event Listener werden über die Direktive on:eventname angehängt und reagieren auf DOM- und benutzerdefinierte Ereignisse.

Reactive Statements

Reactive Statements werden über das  $:-Label definiert und reagieren auf Veränderungen des lokalen States.

Logik-Blöcke

Logik-Blöcke steuern den Programmfluss innerhalb des Svelte-Markups. Sie kommen in verschiedenen Varianten vor, wie z.B. {#if} oder {#each}.

Komposition von Komponenten in Svelte

App Shell

Login

Username

Password

Ausgeloggter Zustand

Hello, Karl! You're logged in!

Logout

Eingeloggter Zustand

App Shell

Login

Username

Password

Ausgeloggter Zustand

Hello, Karl! You're logged in!

Logout

Eingeloggter Zustand

App Shell

Login

Username

Password

Ausgeloggter Zustand

Hello, Karl! You're logged in!

Logout

Eingeloggter Zustand

Login

Username

Password

Ausgeloggter Zustand

Hello, Karl! You're logged in!

Logout

Eingeloggter Zustand

App Shell

User Prop

Login Event

Logout Event

<script>
  import Dashboard from "./Dashboard.svelte";
  import Login from "./Login.svelte";

  let user = null;

  function extractUser(e) {
    user = e.detail.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard user={user} on:logout={logoutUser} />
  {:else}
    <Login on:login={extractUser} />
  {/if}
</div>

App.svelte

<script>
  import Dashboard from "./Dashboard.svelte";
  import Login from "./Login.svelte";

  let user = null;

  function extractUser(e) {
    user = e.detail.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard {user} on:logout={logoutUser} />
  {:else}
    <Login on:login={extractUser} />
  {/if}
</div>
<script>
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function login(e) {
    const formData = new FormData(e.target);
    dispatch("login", {
      user: {
        name: formData.get("username"),
      },
    });
  }
</script>

<form on:submit|preventDefault={login}>
  <label for="username">Username:</label>
  <input type="username" id="username" name="username" />
  <label for="password">Password:</label>
  <input type="password" id="password" name="password" />
  <button type="submit">Login</button>
</form>

Login.svelte

<script>
  import {createEventDispatcher} from 'svelte';
  
  const dispatch = createEventDispatcher()

  export let user;

  function logout() {
    dispatch("logout");
  }
</script>

<h1>Hello, {user.name}! You're logged in!</h1>
<button on:click={logout}>Logout</button>
<script>
  import Dashboard from "./Dashboard.svelte";
  import Login from "./Login.svelte";

  let user = null;

  function extractUser(e) {
    user = e.detail.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard {user} on:logout={logoutUser} />
  {:else}
    <Login on:login={extractUser} />
  {/if}
</div>
<script>
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function login(e) {
    const formData = new FormData(e.target);
    dispatch("login", {
      user: {
        name: formData.get("username"),
      },
    });
  }
</script>

<form on:submit|preventDefault={login}>
  <label for="username">Username:</label>
  <input type="username" id="username" name="username" />
  <label for="password">Password:</label>
  <input type="password" id="password" name="password" />
  <button type="submit">Login</button>
</form>

Dashboard.svelte

<script>
  import Dashboard from "./Dashboard.svelte";
  import Login from "./Login.svelte";

  let user = null;

  function extractUser(e) {
    user = e.detail.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard {user} on:logout={logoutUser} />
  {:else}
    <Login on:login={extractUser} />
  {/if}
</div>
<script>
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function login(e) {
    const formData = new FormData(e.target);
    dispatch("login", {
      user: {
        name: formData.get("username"),
      },
    });
  }
</script>

<form on:submit|preventDefault={login}>
  <label for="username">Username:</label>
  <input type="username" id="username" name="username" />
  <label for="password">Password:</label>
  <input type="password" id="password" name="password" />
  <button type="submit">Login</button>
</form>
<script>
  import {createEventDispatcher} from 'svelte';
  
  const dispatch = createEventDispatcher()

  export let user;

  function logout() {
    dispatch("logout");
  }
</script>

<h1>Hello, {user.name}! You're logged in!</h1>
<button on:click={logout}>Logout</button>

App.svelte

Component Slots

Möchten Sie diese Operation wirklich ausführen? Eingegebene Daten gehen unwiderruflich verloren.

Ja

Nein

Bestätigungsdialog

Dialogtext

Möchten Sie diese Operation wirklich ausführen? Eingegebene Daten gehen unwiderruflich verloren.

Ja

Nein

<script>
  import { afterUpdate, createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  export let open = false;
  let dialog;

  afterUpdate(() => (open ? dialog?.showModal() : dialog?.close()));

  function close(e) {
    open = false;
    dispatch('close', { answer: e.target.returnValue });
    dialog.returnValue = '';
  }
</script>
<dialog bind:this={dialog} on:close={close}>
  <form method="dialog">
    <div class="body">
      <slot />
    </div>
    <div class="footer">
      <button type="submit" class="no" value="no">
        <slot name="noButtonContent">No</slot>
      </button>
      <button type="submit" class="yes" value="yes">
        <slot name="yesButtonContent">Yes</slot>
      </button>
    </div>
  </form>
</dialog>

ConfirmationDialog.svelte

<script>
  import { afterUpdate, createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  export let open = false;
  let dialog;

  afterUpdate(() => (open ? dialog?.showModal() : dialog?.close()));

  function close(e) {
    open = false;
    dispatch('close', { answer: e.target.returnValue });
    dialog.returnValue = '';
  }
</script>
<dialog bind:this={dialog} on:close={close}>
  <form method="dialog">
    <div class="body">
      <slot />
    </div>
    <div class="footer">
      <button type="submit" class="no" value="no">
        <slot name="noButtonContent">No</slot>
      </button>
      <button type="submit" class="yes" value="yes">
        <slot name="yesButtonContent">Yes</slot>
      </button>
    </div>
  </form>
</dialog>
<script>
  import ConfirmationDialog from './ConfirmationDialog.svelte';

  let dialogOpen, dialogAnswer, dialog2Answer;
  let dialog2Open = (dialogOpen = false);
</script>

<button on:click={() => (dialogOpen = !dialogOpen)}>Toggle Dialog 1</button>
<output>Answer: {dialogAnswer}</output><br />
<button on:click={() => (dialog2Open = !dialog2Open)}>Toggle Dialog 2</button>
<output>Answer: {dialog2Answer}</output>

<ConfirmationDialog
  bind:open={dialogOpen}
  on:close={({ detail }) => (dialogAnswer = detail.answer)}
>
  A dialog text.
</ConfirmationDialog>

<ConfirmationDialog
  bind:open={dialog2Open}
  on:close={({ detail }) => (dialog2Answer = detail.answer)}
>
  Another dialog text.

  <svelte:fragment slot="noButtonContent">Definitely not</svelte:fragment>
  <svelte:fragment slot="yesButtonContent">Absolutly yes</svelte:fragment>
</ConfirmationDialog>

App.svelte

Svelte Stores

“A store is simply an object with a subscribe method that allows interested parties to be notified whenever the store value changes.”

<script>
  import { onDestroy } from "svelte";
  import { writable } from "svelte/store";

  let counterValue = 0;
  let counter = writable(counterValue);

  const unsubscribe = counter.subscribe((value) => (counterValue = value));

  onDestroy(unsubscribe);
</script>

<output>{counterValue}</output>

<button on:click={() => counter.update((v) => v - 1)}>decrement</button>
<button on:click={() => counter.update((v) => v + 1)}>increment</button>

StoreCounter.svelte

<script>
  import { writable } from "svelte/store";
  let counter = writable(0);
</script>

<output>{$counter}</output>

<button on:click={() => $counter--}>decrement</button>
<button on:click={() => $counter++}>increment</button>

StoreCounter.svelte

Kurze Zusammenfassung

Komponenten-Props

Props werden verwendet, um Daten von einer Eltern- an eine Kindkomponente zu übergeben. Sie können jeden gültigen JavaScript-Datentyp halten.

Komponenten-Events

Events werden verwendet, um Daten von einer Kind- an eine Eltern-Komponente zu übergeben. Sie werden über die Direktive on:name verwendet.

Komponenten-Slots

Slots werden verwendet, um Inhalte von einer Eltern- an eine Kind-Komponente zu übergeben. Sie werden mit dem <slot>-Tag deklariert.

Stores

Stores sind Objekte, die einen Wert halten und abonniert werden können. Sie sind überall in der App verwendbar und erfahren besondere Unterstützung in Svelte.

Ausblick auf Svelte 5

Svelte 5 is a ground-up rewrite of the framework, designed to make your apps faster, smaller, and more robust.”

Runes

Runes are function-like symbols that provide instructions to the Svelte compiler. [...] they're part of the language.”

<script>
  let counter = $state(0);
	
  let doubled = $derived(counter * 2);
  let quadrupled = $derived(doubled * 2);

  const inc = () => counter++;
  const dec = () => counter--;
</script>

<b>x1</b>: {counter}<br>
<b>x2</b>: <output>{counter} * 2 = {doubled}</output><br>
<b>x4</b>: <output>{doubled} * 2 = {quadrupled}</output><br>

<button onclick={dec}>Decrease</button>
<button onclick={inc}>Increase</button>

RunesCounter.svelte

Runes-Unterstützung

  • *.svelte
  • *.svelte.js
  • *.svelte.ts

Keine Runes-Unterstützung

  • *.js
  • *.ts
  • etc.

Event Handlers

Event handlers have been given a facelift [...], in Svelte 5 they are properties like any other”

<script>
  import Dashboard from './Dashboard.svelte';
  import Login from './Login.svelte';

  let user = $state(null);

  function extractUser(loginData) {
    user = loginData.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard {user} onlogout={logoutUser} />
  {:else}
    <Login onlogin={extractUser} />
  {/if}
</div>

App.svelte

<script>
  import Dashboard from './Dashboard.svelte';
  import Login from './Login.svelte';

  let user = $state(null);

  function extractUser(loginData) {
    user = loginData.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard {user} onlogout={logoutUser} />
  {:else}
    <Login onlogin={extractUser} />
  {/if}
</div>
<script>
  let { onlogin } = $props();

  function login(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    onlogin({
      user: {
        name: formData.get('username')
      }
    });
  }
</script>

<form onsubmit={login}>
  <label for="username">Username:</label>
  <input type="username" id="username" name="username" />
  <label for="password">Password:</label>
  <input type="password" id="password" name="password" />
  <button type="submit">Login</button>
</form>

Login.svelte

<script>
  let { user, onlogout } = $props();
</script>

<h1>Hello, {user.name}! You're logged in!</h1>
<button onclick={onlogout}>Logout</button>
<script>
  import Dashboard from "./Dashboard.svelte";
  import Login from "./Login.svelte";

  let user = null;

  function extractUser(e) {
    user = e.detail.user;
  }

  function logoutUser() {
    user = null;
  }
</script>

<div>
  {#if user}
    <Dashboard {user} on:logout={logoutUser} />
  {:else}
    <Login on:login={extractUser} />
  {/if}
</div>
<script>
  let { onlogin } = $props();

  function login(e) {
    e.preventDefault();
    const formData = new FormData(e.target);
    onlogin({
      user: {
        name: formData.get('username')
      }
    });
  }
</script>

<form onsubmit={login}>
  <label for="username">Username:</label>
  <input type="username" id="username" name="username" />
  <label for="password">Password:</label>
  <input type="password" id="password" name="password" />
  <button type="submit">Login</button>
</form>

Dashboard.svelte

Snippets and Render Tags

Snippets, and render tags, are a way to create reusable chunks of markup inside your components.”

<script>
  import ConfirmationDialog from './ConfirmationDialog.svelte';

  let dialogOpen = $state(false);
  let dialogAnswer = $state();
  let dialog2Open = $state(false);
  let dialog2Answer = $state();
</script>

<button onclick={() => (dialogOpen = !dialogOpen)}>Toggle Dialog 1</button>
<output>Answer: {dialogAnswer}</output><br />
<button onclick={() => (dialog2Open = !dialog2Open)}>Toggle Dialog 2</button>
<output>Answer: {dialog2Answer}</output>

<ConfirmationDialog bind:open={dialogOpen} onclose={({ answer }) => (dialogAnswer = answer)}>
  A dialog text.
</ConfirmationDialog>

{#snippet yesButtonContent()}Absolutely yes{/snippet}

<ConfirmationDialog
  bind:open={dialog2Open}
  onclose={({ answer }) => (dialog2Answer = answer)}
  {yesButtonContent}
>
  Another dialog text.

  {#snippet noButtonContent()}Definitely not{/snippet}
</ConfirmationDialog>

App.svelte

<script>
  import ConfirmationDialog from './ConfirmationDialog.svelte';

  let dialogOpen = $state(false);
  let dialogAnswer = $state();
  let dialog2Open = $state(false);
  let dialog2Answer = $state();
</script>

<button onclick={() => (dialogOpen = !dialogOpen)}>Toggle Dialog 1</button>
<output>Answer: {dialogAnswer}</output><br />
<button onclick={() => (dialog2Open = !dialog2Open)}>Toggle Dialog 2</button>
<output>Answer: {dialog2Answer}</output>

<ConfirmationDialog bind:open={dialogOpen} onclose={({ answer }) => (dialogAnswer = answer)}>
  A dialog text.
</ConfirmationDialog>

{#snippet yesButtonContent()}Absolutely yes{/snippet}

<ConfirmationDialog
  bind:open={dialog2Open}
  onclose={({ answer }) => (dialog2Answer = answer)}
  {yesButtonContent}
>
  Another dialog text.

  {#snippet noButtonContent()}Definitely not{/snippet}
</ConfirmationDialog>
<script>
  let { noButtonContent, yesButtonContent, children, onclose, open } = $props();

  let dialog = $state();

  $effect(() => open ? dialog.showModal(): dialog.close())

  function close(e) {
    open = false;
    onclose({ answer: e.target.returnValue });
    dialog.returnValue = '';
  }
</script>

<dialog bind:this={dialog} onclose={close}>
  <form method="dialog">
    <div class="body">
      {@render children()}
    </div>

    <div class="footer">
      <button type="submit" class="no" value="no">
        {#if noButtonContent}{@render noButtonContent()}{:else}No{/if}
      </button>
      <button type="submit" class="yes" value="yes">
        {#if yesButtonContent}{@render yesButtonContent()}{:else}Yes{/if}
      </button>
    </div>
  </form>
</dialog>

ConfirmationDialog.svelte

LinkedIn:

Xing:

Twitter:

Vielen Dank!

Die Reaktive Offenbarung: Eine Einführung in Svelte

By Nils Röhrig

Die Reaktive Offenbarung: Eine Einführung in Svelte

  • 307