Svelte and SvelteKit course ยท Module 6: Template Logic

Practical Patterns

2 min read ยท
In this lesson3

Filtering and Sorting

<script>
  let warriors = $state([
    { id: 1, name: 'Ragnar', strength: 95 },
    { id: 2, name: 'Bjorn', strength: 88 },
    { id: 3, name: 'Ivar', strength: 70 }
  ]);

  let filter = $state('');
  let sortBy = $state('name');

  let filtered = $derived(
    warriors
      .filter(w => w.name.toLowerCase().includes(filter.toLowerCase()))
      .sort((a, b) => {
        if (sortBy === 'name') return a.name.localeCompare(b.name);
        return b.strength - a.strength;
      })
  );
</script>

<input bind:value={filter} placeholder="Search..." />

<select bind:value={sortBy}>
  <option value="name">By name</option>
  <option value="strength">By strength</option>
</select>

{#each filtered as warrior (warrior.id)}
  <WarriorCard {warrior} />
{:else}
  <p>No warriors found</p>
{/each}

Nested Loops

<script>
  let ships = $state([
    {
      name: 'Northern Drakkar',
      crew: ['Ragnar', 'Bjorn', 'Floki']
    },
    {
      name: 'Sea Wolf',
      crew: ['Ivar', 'Ubbe']
    }
  ]);
</script>

{#each ships as ship}
  <div class="ship">
    <h3>{ship.name}</h3>
    <ul>
      {#each ship.crew as member}
        <li>{member}</li>
      {/each}
    </ul>
  </div>
{/each}

Conditional Attributes

<script>
  let isActive = $state(true);
  let isDisabled = $state(false);
  let isHighlighted = $state(false);
  let customClass = $state('');
</script>

<!-- Attribute added only when true -->
<button disabled={isDisabled}>Click</button>

<!-- Class added only when not empty -->
<div class={customClass || undefined}>Content</div>

<!-- Multiple conditional classes: an object (Svelte 5.16+) -->
<div class={{ active: isActive, disabled: isDisabled, highlighted: isHighlighted }}>
  Element
</div>

<!-- The same with class: directives (older style) -->
<div
  class:active={isActive}
  class:disabled={isDisabled}
  class:highlighted={isHighlighted}
>
  Element
</div>

Since Svelte 5.16 the class attribute accepts an object: the key is the class name, and the value decides whether the class stays. The class: directive still works, but in new code the object is handier.

Code for this lesson: App.svelte
<script>
  // {#key} - forcing re-render
  // {@html} - raw HTML
  // {@const} - local constants

  let count = $state(0);
  let refreshKey = $state(0);

  let items = $state([
    { id: 1, name: 'Sword', rarity: 'common', description: '<strong>Common sword</strong> of a viking' },
    { id: 2, name: 'Thor\'s Axe', rarity: 'legendary', description: '<em>Legendary</em> axe with <span style="color: #ffd700">lightning</span>' },
    { id: 3, name: 'Shield', rarity: 'rare', description: 'Reinforced with <u>runes</u>' }
  ]);

  function refresh() {
    refreshKey++;
  }

  const rarityColors = {
    common: '#aaa',
    rare: '#4a90d9',
    legendary: '#ffd700'
  };
</script>

<main class="special-blocks">
  <h1>Special blocks</h1>

  <section>
    <h2>{"{#key}"} - forcing re-render</h2>
    <p>Count: {count}</p>
    <button onclick={() => count++}>+1</button>
    <button onclick={refresh}>Refresh key: {refreshKey}</button>

    {#key refreshKey}
      <div class="animated-box">
        This animation will trigger when the key changes
      </div>
    {/key}
  </section>

  <section>
    <h2>{"{@html}"} - raw HTML</h2>
    <p class="warning">Use carefully - can be dangerous!</p>

    {#each items as item}
      {@const color = rarityColors[item.rarity]}
      <div class="item" style="border-color: {color}">
        <h3 style="color: {color}">{item.name}</h3>
        <p>{@html item.description}</p>
      </div>
    {/each}
  </section>

  <section>
    <h2>{"{@const}"} - local constants in blocks</h2>
    {#each items as item}
      {@const isLegendary = item.rarity === 'legendary'}
      {@const stars = isLegendary ? '***' : item.rarity === 'rare' ? '**' : '*'}
      <div class="item-simple">
        <span>{item.name}</span>
        <span>{stars}</span>
      </div>
    {/each}
  </section>
</main>

<style>
  .special-blocks {
    font-family: 'Georgia', serif;
    max-width: 500px;
    margin: 0 auto;
    padding: 2rem;
    background: #1a0f0a;
    color: #e8d5b7;
    min-height: 100vh;
  }

  h1 {
    color: #d4af37;
    text-align: center;
  }

  section {
    background: rgba(212, 175, 55, 0.1);
    border: 1px solid #d4af37;
    border-radius: 8px;
    padding: 1rem;
    margin: 1rem 0;
  }

  h2 {
    color: #d4af37;
    font-size: 1rem;
    margin-top: 0;
  }

  button {
    padding: 0.5rem 1rem;
    background: #3d2914;
    color: #e8d5b7;
    border: 1px solid #d4af37;
    border-radius: 4px;
    cursor: pointer;
    margin-right: 0.5rem;
  }

  .animated-box {
    margin-top: 1rem;
    padding: 1rem;
    background: rgba(212, 175, 55, 0.2);
    border-radius: 4px;
    animation: fadeIn 0.5s ease-out;
  }

  @keyframes fadeIn {
    from {
      opacity: 0;
      transform: translateY(-10px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  .warning {
    background: rgba(255, 107, 107, 0.2);
    padding: 0.5rem;
    border-radius: 4px;
    font-size: 0.9rem;
  }

  .item {
    padding: 0.75rem;
    background: rgba(0,0,0,0.3);
    border-left: 3px solid;
    margin: 0.5rem 0;
    border-radius: 0 4px 4px 0;
  }

  .item h3 {
    margin: 0 0 0.25rem 0;
    font-size: 1rem;
  }

  .item p {
    margin: 0;
    font-size: 0.9rem;
  }

  .item-simple {
    display: flex;
    justify-content: space-between;
    padding: 0.5rem;
    background: rgba(0,0,0,0.3);
    margin: 0.25rem 0;
    border-radius: 4px;
  }
</style>

Docs entries used in this lesson

Spotted a mistake in this lesson?

Useful articles