Lesson 04-Svelte Component-Based Development

Component Creation and Usage

One of Svelte’s core strengths is its component system, which enables the creation of reusable UI blocks, each with its own state and behavior. Components can be nested to form complex UI structures while maintaining clear and modular code.

Basic Component Structure

A basic Svelte component typically includes the following parts:

  • <script> Tag: Defines the component’s logic, including variables, functions, and imports of other modules.
  • <style> Tag: Specifies the component’s styles, which can be local or global.
  • Template: Defines the component’s HTML structure.
<!-- Basic component structure -->
<script>
  export let message = 'Hello, World!';
</script>

<style>
  .container {
    color: blue;
  }
</style>

<div class="container">
  <p>{message}</p>
</div>

Prop Passing

Components can accept externally passed data, known as props, declared using export let. Props can be any data type, including strings, numbers, objects, and arrays.

<!-- Component with props -->
<script>
  export let name = 'World';
</script>

<p>Hello, {name}!</p>

To use a child component in a parent component, pass props like this:

<!-- Parent component using child component -->
<script>
  import Greeting from './Greeting.svelte';
</script>

<Greeting name="Alice" />

Event Handling

Components can bind event handlers using the on:eventName syntax. When the event is triggered, the handler function is called.

<!-- Component with event handling -->
<script>
  export let name = 'World';

  function handleClick() {
    console.log(`Clicked by ${name}`);
  }
</script>

<button on:click={handleClick}>Click me</button>

State Management

Components can manage local state using let or shared state using Svelte’s stores.

<!-- Component with state management -->
<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>Increment</button>
<p>Count: {count}</p>

Component Organization and Reuse

To maintain project maintainability, organize components by functionality or page structure. For example, create a top-level component for each page, then use smaller child components to build the UI.

<!-- Organizing components -->
<!-- Layout.svelte -->
<script>
  import Header from './Header.svelte';
  import Footer from './Footer.svelte';
</script>

<Header />
<main>
  <!-- Page content here -->
</main>
<Footer />

<!-- Header.svelte -->
<script>
  export let title = 'My App';
</script>

<header>
  <h1>{title}</h1>
</header>

<!-- Footer.svelte -->
<footer>
  <p>© 2023 My App</p>
</footer>

Code Analysis

Let’s analyze the creation and usage of Svelte components through a comprehensive example:

<!-- A comprehensive example of creating and using components -->
<!-- App.svelte -->
<script>
  import Greeting from './Greeting.svelte';
  import Counter from './Counter.svelte';

  let name = 'Alice';
</script>

<main>
  <Greeting name={name} />
  <Counter />
</main>

<!-- Greeting.svelte -->
<script>
  export let name = 'World';

  function handleClick() {
    console.log(`Clicked by ${name}`);
  }
</script>

<button on:click={handleClick}>Click me</button>
<p>Hello, {name}!</p>

<!-- Counter.svelte -->
<script>
  let count = 0;

  function increment() {
    count += 1;
  }
</script>

<button on:click={increment}>Increment</button>
<p>Count: {count}</p>

In this example:

  • App.svelte is the top-level component, containing two child components: Greeting and Counter.
  • Greeting accepts a name prop and includes a click event handler.
  • Counter maintains a local count state with an increment button.

Slots and Scoped Slots

Svelte’s slot mechanism is a key tool for component communication and content distribution, allowing parent components to inject dynamic content into child components. Scoped slots further enhance this by enabling child components to pass data to the parent, allowing content to be dynamically generated based on the child’s state.

Slots

Slots allow parent components to pass content to child components. By default, a Svelte component can include an unnamed slot to receive content from the parent.

<!-- Parent component -->
<script>
  import Child from './Child.svelte';
</script>

<Child>
  This is some content.
</Child>

<!-- Child component -->
<slot></slot>

Here, the <slot> tag in the Child component displays the content passed by the parent.

Named Slots

In addition to unnamed slots, Svelte supports named slots, allowing the parent to inject content into specific locations in the child component.

<!-- Parent component -->
<script>
  import Child from './Child.svelte';
</script>

<Child>
  <div slot="header">Header Content</div>
  <div slot="footer">Footer Content</div>
</Child>

<!-- Child component -->
<div class="header"><slot name="header"></slot></div>
<div class="content">Some content here</div>
<div class="footer"><slot name="footer"></slot></div>

Here, the parent injects content into the header and footer slots of the child component.

Scoped Slots

Scoped slots allow the child component to pass data to the parent, enabling the parent to generate content dynamically based on the child’s state.

<!-- Parent component -->
<script>
  import Child from './Child.svelte';
</script>

<Child let:item>
  <p>{item.name}</p>
  <p>{item.description}</p>
</Child>

<!-- Child component -->
<script>
  let item = { name: 'Apple', description: 'A fruit' };
</script>

<slot {item}></slot>

In this example, the Child component passes the item object to the parent via let:item, allowing the parent to use it to generate content.

Code Case Study

Let’s analyze Svelte’s slots and scoped slots through a comprehensive example:

<!-- A comprehensive example of slots and scoped slots -->
<!-- App.svelte -->
<script>
  import ProductCard from './ProductCard.svelte';
</script>

<ProductCard>
  <div slot="header">Featured Products</div>
  <div slot="footer">End of products</div>
</ProductCard>

<!-- ProductCard.svelte -->
<script>
  let product = { name: 'Smartphone', price: '$999' };
</script>

<div class="product-card">
  <div class="header"><slot name="header"></slot></div>
  <div class="product-info">
    <h2>{product.name}</h2>
    <p>{product.price}</p>
  </div>
  <div class="footer"><slot name="footer"></slot></div>
</div>

<!-- Usage of scoped slot -->
<ProductCard>
  <div slot="header">Featured Products</div>
  <div slot="footer">End of products</div>
  <div slot="product" let:product>
    <h2>{product.name}</h2>
    <p>{product.price}</p>
  </div>
</ProductCard>

<!-- ProductCard.svelte -->
<script>
  let products = [
    { name: 'Smartphone', price: '$999' },
    { name: 'Laptop', price: '$1499' }
  ];
</script>

<div class="product-card">
  <div class="header"><slot name="header"></slot></div>
  {#each products as product}
    <slot name="product" {product}></slot>
  {/each}
  <div class="footer"><slot name="footer"></slot></div>
</div>

In this example:

  • The ProductCard component includes three slots: header, footer, and product.
  • The parent App.svelte injects static content into the header and footer slots.
  • Using the product scoped slot, the parent accesses the child’s product data to dynamically generate product information.

Component Communication

In Svelte applications, component communication is essential for building complex user interfaces. Svelte provides several mechanisms to facilitate this, including props, events, stores, and context.

Communication via Props

Props are data passed from a parent component to a child component, representing the most direct and common communication method.

<!-- ParentComponent.svelte -->
<script>
  import ChildComponent from './ChildComponent.svelte';
</script>

<ChildComponent parentData="Hello from parent!" />

<!-- ChildComponent.svelte -->
<script>
  export let parentData;
</script>

<p>{parentData}</p>

Communication via Events

Events are messages sent from a child component to a parent. The child dispatches events, and the parent listens and responds to them.

<!-- ParentComponent.svelte -->
<script>
  import ChildComponent from './ChildComponent.svelte';

  function handleChildEvent(event) {
    console.log('Received event from child:', event.detail);
  }
</script>

<ChildComponent on:childEvent={handleChildEvent} />

<!-- ChildComponent.svelte -->
<script>
  import { createEventDispatcher } from 'svelte';

  const dispatch = createEventDispatcher();

  function dispatchEvent() {
    dispatch('childEvent', { message: 'Hello from child!' });
  }
</script>

<button on:click={dispatchEvent}>Dispatch Event</button>

Communication Using Stores

Svelte’s stores provide a mechanism for sharing state across components, with three types: readable, writable, and derived.

// store.js
import { writable } from 'svelte/store';

export const sharedState = writable({ count: 0 });
<!-- ComponentA.svelte -->
<script>
  import { sharedState } from './store.js';

  function increment() {
    sharedState.update(state => ({ ...state, count: state.count + 1 }));
  }
</script>

<button on:click={increment}>Increment</button>

<!-- ComponentB.svelte -->
<script>
  import { onMount, onDestroy } from 'svelte';
  import { sharedState } from './store.js';

  let state;

  onMount(() => {
    state = sharedState.subscribe(value => {
      console.log('State changed:', value);
    });
  });

  onDestroy(() => {
    state();
  });
</script>

<p>{$sharedState.count}</p>

Context

Introduced in Svelte 3.23.0, context provides a way to pass data through the component tree without explicitly passing props at every level.

<!-- App.svelte -->
<script>
  import { setContext } from 'svelte';
  import ChildComponent from './ChildComponent.svelte';

  const myContext = 'default value';
  setContext('myContext', myContext);
</script>

<ChildComponent />

<!-- ChildComponent.svelte -->
<script>
  import { getContext } from 'svelte';

  const contextValue = getContext('myContext');
</script>

<p>{contextValue}</p>

Comprehensive Example

Let’s combine the above communication methods in a more complex example:

<!-- App.svelte -->
<script>
  import { writable } from 'svelte/store';
  import { setContext } from 'svelte';
  import ChildComponent from './ChildComponent.svelte';

  const appState = writable({ message: 'Welcome to Svelte!' });
  setContext('appState', appState);

  function handleChildEvent(event) {
    appState.set({ message: event.detail.message });
  }
</script>

<ChildComponent on:childEvent={handleChildEvent} />

<!-- ChildComponent.svelte -->
<script>
  import { getContext, createEventDispatcher } from 'svelte';

  const appState = getContext('appState');
  const dispatch = createEventDispatcher();

  function dispatchEvent() {
    dispatch('childEvent', { message: 'Hello from child!' });
  }
</script>

<p>{$appState.message}</p>
<button on:click={dispatchEvent}>Dispatch Event</button>

In this example:

  • A writable store (appState) shares state between components.
  • The child component dispatches a childEvent to the parent.
  • Context passes the appState store through the component tree.

Share your love