Lesson 02-Svelte Basic Syntax and Concepts

Svelte Component Basics

Svelte’s core revolves around its component system, which serves as the foundation for building user interfaces.

Component Definition and Structure

A Svelte component is a self-contained UI unit defined in a .svelte file. A typical component file may include the following sections:

  • <script> Tag: Defines variables and functions within the component’s local scope.
  • <style> Tag: Specifies the component’s styles.
  • Template: Defines the component’s HTML structure and content.

For example, a simple component might look like this:

<!-- src/components/MyComponent.svelte -->
<style>
  /* Component styles */
  .my-component {
    color: blue;
  }
</style>

<script>
  // Component local scope
  export let name = 'World';

  function sayHello() {
    console.log(`Hello, ${name}`);
  }
</script>

<div class="my-component">
  <h1>Hello, {name}!</h1>
  <button on:click={sayHello}>Say Hello</button>
</div>

Props and State

  • Props: Components can receive data from external sources, known as props. In the example above, name is a prop that can be assigned externally.
  • State: Components can maintain internal state, typically defined within the component and modified by its functions. For example, let count = 0; defines a state variable.

Event Handling

Svelte allows you to define event handlers within components, which are triggered by specific events (e.g., clicks, inputs). Event handlers are typically defined in the <script> tag and bound to elements using the on:eventName syntax.

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

Styles and Animations

Svelte supports CSS styles and animations. Styles are defined in the <style> tag, while animations can be implemented using transition or animate directives.

<style>
  /* Animation styles */
  .fade-enter-active,
  .fade-leave-active {
    transition: opacity 0.5s ease;
  }

  .fade-enter-from,
  .fade-leave-to {
    opacity: 0;
  }
</style>

<div transition:fade>
  I will fade in and out!
</div>

Component Communication

Svelte provides several mechanisms for communication between components:

  • Context: Allows child components to access parent component props and event handlers.
  • $emit: Enables child components to send custom events to parent components.
  • Store: Svelte offers writable, readable, and derived stores for sharing state across multiple components.

Let’s analyze how a Svelte component works step-by-step with a more concrete example:

<!-- src/components/TodoList.svelte -->
<style>
  .todo-item {
    cursor: pointer;
  }

  .completed {
    text-decoration: line-through;
  }
</style>

<script>
  import { onMount } from 'svelte';

  export let items = [];

  let completedItems = [];

  function toggleComplete(item) {
    if (item.completed) {
      completedItems = completedItems.filter(i => i !== item);
    } else {
      completedItems.push(item);
    }
  }

  onMount(() => {
    // Initialize completed items on mount
    completedItems = items.filter(item => item.completed);
  });
</script>

<ul>
  {#each items as item}
    <li bind:class="{completed: completedItems.includes(item)}" on:click={() => toggleComplete(item)}>
      {item.text}
    </li>
  {/each}
</ul>

In this TodoList component:

  • Props: items is a prop received from the parent component, representing a list of tasks.
  • State: completedItems is an internal state tracking completed tasks.
  • Event Handling: The toggleComplete function toggles a task’s completion status.
  • Styles: CSS classes control task item styles, such as strikethrough for completed tasks.
  • Lifecycle Hook: The onMount hook initializes the list of completed tasks when the component mounts.

Svelte File Structure

Svelte projects typically follow a standardized directory structure to organize code, assets, and configurations, making the project easy to understand and maintain. Below is a typical Svelte project structure and the role of each part:

my-svelte-app/
├── public/
│   ├── assets/
│   │   ├── images/
│   │   └── ...
│   ├── index.html
│   └── ...
├── src/
│   ├── components/
│   │   ├── MyComponent.svelte
│   │   └── ...
│   ├── lib/
│   │   ├── utils.js
│   │   └── ...
│   ├── App.svelte
│   ├── main.js
│   └── ...
├── .gitignore
├── package.json
├── rollup.config.js
├── vite.config.js
└── ...

1. public/ Directory

  • index.html: The entry HTML file, typically including a <script> tag to load the application’s JavaScript.
  • assets/: Stores static assets like images, fonts, and non-Svelte-compiled CSS files.

2. src/ Directory

  • components/: Contains Svelte component files, each typically a .svelte file.
  • lib/: Stores shared JavaScript modules, such as utility functions or services.
  • App.svelte: The root component, serving as the application’s entry point.
  • main.js: The application’s entry file, responsible for bootstrapping the app.

3. Configuration Files

  • rollup.config.js: Rollup configuration file for compiling the Svelte application into browser-executable code.
  • vite.config.js: Vite configuration file for the development server, enabling fast hot-reloading during development.

4. Other Files

  • .gitignore: Specifies files to exclude from Git version control.
  • package.json: Contains project metadata and dependency information.

Let’s analyze the file structure and code organization of a simple Svelte project step-by-step:

1. Create a Project

Assume we create a project named my-svelte-app using the Svelte CLI.

2. Analyze the public/ Directory

  • index.html: The application’s entry file, containing basic HTML structure and a <script> tag to load the Svelte app.

3. Analyze the src/ Directory

  • components/MyComponent.svelte: A simple Svelte component with template, script, and styles.
<!-- src/components/MyComponent.svelte -->
<style>
  h1 {
    color: blue;
  }
</style>

<script>
  export let message = "Hello, world!";
</script>

<main>
  <h1>{message}</h1>
</main>
  • App.svelte: The root component, typically including references to other components and application-level layout.
<!-- src/App.svelte -->
<script>
  import MyComponent from './components/MyComponent.svelte';
</script>

<main>
  <MyComponent message="Welcome to Svelte!" />
</main>
  • main.js: The application’s entry file, responsible for starting the Svelte app.
// src/main.js
import App from './App.svelte';

const app = new App({
  target: document.body,
});

export default app;

4. Analyze Configuration Files

  • rollup.config.js: Configures the Rollup bundler to compile Svelte components into browser-executable code.
// rollup.config.js
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import css from 'rollup-plugin-css-only';

// ... configuration code

5. Run the Project

Run npm run dev to start the development server, and the browser will automatically open to display your Svelte application.

Element Directives

Svelte’s element directives are a unique and powerful feature, allowing declarative manipulation of DOM elements. Unlike React’s virtual DOM or Vue’s directives, Svelte’s directives operate directly on real DOM nodes, making Svelte applications more efficient at runtime.

1. {#if} Directive

The {#if} directive conditionally renders a code block. When the condition is true, the block is inserted into the DOM; when false, it is removed.

<!-- Example of using the {#if} directive -->
<script>
  let show = true;
</script>

{#if show}
  <p>This is shown because 'show' is true.</p>
{:else}
  <p>This is shown when 'show' is false.</p>
{/if}

2. {#each} Directive

The {#each} directive loops over arrays or other iterable objects, rendering a code block for each element.

<!-- Example of using the {#each} directive -->
<script>
  let items = ['Apple', 'Banana', 'Cherry'];
</script>

<ul>
  {#each items as item (item)}
    <li>{item}</li>
  {/each}
</ul>

3. bind Directive

The bind directive binds a DOM element’s property to a component’s local variable. When the variable changes, the DOM element’s property updates accordingly.

<!-- Example of using the bind directive -->
<script>
  let value = '';
</script>

<input type="text" bind:value={value} />
<p>You typed: {value}</p>

4. on Directive

The on directive adds event listeners to DOM elements, executing a function when the specified event is triggered.

<!-- Example of using the on directive -->
<script>
  function handleClick() {
    alert('Button clicked!');
  }
</script>

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

5. transition Directive

The transition directive adds transition effects to elements. It can be applied to {#if} or {#each} blocks, automatically triggering transitions when elements are inserted or removed.

<!-- Example of using the transition directive -->
<style>
  .fade {
    transition: opacity 0.5s ease;
  }
</style>

{#if show}
  <p transition:fade>This fades in and out.</p>
{/if}

6. animate Directive

The animate directive adds animation effects to elements, specifying keyframes and duration.

<!-- Example of using the animate directive -->
<style>
  .spin {
    animation: spin 2s linear infinite;
  }

  @keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
</style>

<div animate:spin class="spin">Spinning div</div>

7. store Directive

While not strictly an element directive, the store mechanism is closely tied to component state management. Svelte provides readable, writable, and derived stores for sharing state across components.

<!-- Example of using a writable store -->
<script>
  import { writable } from 'svelte/store';

  const counter = writable(0);

  function increment() {
    counter.update(n => n + 1);
  }
</script>

<button on:click={increment}>{$counter}</button>

Step-by-Step Code Analysis

Let’s analyze the application of Svelte element directives through a comprehensive example:

<!-- A comprehensive example combining multiple directives -->
<script>
  import { readable } from 'svelte/store';
  
  let show = true;
  let items = ['Red', 'Green', 'Blue'];
  const color = readable('Red', set => {
    let currentColor = 'Red';
    set(currentColor);
    
    return {
      update: (newColor) => {
        currentColor = newColor;
        set(currentColor);
      }
    };
  });

  function handleSelectChange(e) {
    color.update(e.target.value);
  }
</script>

{#if show}
  <select on:change={handleSelectChange}>
    {#each items as item (item)}
      <option value={item} selected={$color === item}>{item}</option>
    {/each}
  </select>
  <p>The selected color is: {$color}</p>
{:else}
  <p>Nothing to see here.</p>
{/if}

In this example:

  • The {#if} directive conditionally renders a select box and text.
  • The {#each} directive iterates over a color array to generate options.
  • The on directive listens for changes in the select box, updating the color.
  • The readable store shares the color state across components.

Component Directives

let Directive

The let directive declares local variables in the component template, useful for calculations or storing intermediate results.

<!-- Example of using the let directive -->
<script>
  let items = ['Apple', 'Banana', 'Cherry'];
</script>

{#each items as item (item)}
  <p let:uppercaseItem={item.toUpperCase()}>{uppercaseItem}</p>
{/each}

await Directive

The await directive handles asynchronous operations, such as API requests, within {#await} blocks, rendering different content based on the operation’s result.

<!-- Example of using the await directive -->
<script>
  async function fetchItems() {
    const response = await fetch('/api/items');
    return await response.json();
  }
</script>

{#await fetchItems() as { data, error }}
  <p>Loading...</p>
{:then data}
  <ul>
    {#each data as item}
      <li>{item.name}</li>
    {/each}
  </ul>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

slot Directive

The slot directive defines slots in components, allowing parent components to inject content into child components, enhancing reusability and flexibility.

<!-- Parent component -->
<script>
  let name = 'World';
</script>

<MyComponent><h1>Hello, {name}!</h1></MyComponent>

<!-- Child component -->
<script>
  export let name;
</script>

<div>
  <slot></slot>
</div>

bind Directive

The bind directive binds a component’s props or events to a parent component’s state or functions, updating bound values when changes occur.

<!-- Example of using the bind directive -->
<script>
  let value = '';
</script>

<MyInput bind:value={value} />

<p>You typed: {value}</p>

on Directive

The on directive listens for events on components, executing a function when the event is triggered, similar to DOM element event listeners.

<!-- Example of using the on directive -->
<script>
  function handleButtonClick() {
    console.log('Button clicked!');
  }
</script>

<MyButton on:click={handleButtonClick} />

store Directive

The store directive facilitates state sharing between components using readable, writable, and derived stores for read-only, read-write, and derived state, respectively.

<!-- Example of using the store directive -->
<script>
  import { writable } from 'svelte/store';
  const count = writable(0);
</script>

<button on:click={() => count.update(n => n + 1)}>Increment</button>

<p>The count is: {$count}</p>

Step-by-Step Code Analysis

Let’s analyze the application of Svelte component directives through a concrete example:

<!-- A comprehensive example combining multiple directives -->
<script>
  import { writable } from 'svelte/store';

  let searchText = '';
  const searchResults = writable([]);

  async function fetchSearchResults(query) {
    const response = await fetch(`/api/search?q=${query}`);
    return await response.json();
  }

  function handleSearch() {
    searchResults.set(fetchSearchResults(searchText));
  }
</script>

<input type="text" bind:value={searchText} placeholder="Search..." />
<button on:click={handleSearch}>Search</button>

{#await searchResults as { data, error }}
  <p>Loading...</p>
{:then data}
  <ul>
    {#each data as result}
      <li>{result.title}</li>
    {/each}
  </ul>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

In this example:

  • searchText and searchResults store the search input and results, respectively.
  • The bind:value directive binds the input value to searchText.
  • The on:click directive listens for button clicks, triggering the handleSearch function.
  • The await directive handles asynchronous search results, rendering a list or error message.
  • The writable store shares search results across components.

Logic Blocks

Svelte’s logic blocks allow conditional rendering of content or iterative rendering of lists. The primary logic blocks are {#if}, {#each}, and {#await}.

{#if} Logic Block

The {#if} block renders content based on a condition. If the expression is true, the block’s content is rendered; otherwise, an optional {:else} block is rendered.

<!-- Example of using the {#if} block -->
<script>
  let show = true;
</script>

{#if show}
  <p>This is shown because 'show' is true.</p>
{:else}
  <p>This is shown when 'show' is false.</p>
{/if}

{#each} Logic Block

The {#each} block iterates over arrays or iterable objects, creating a separate DOM node for each element.

<!-- Example of using the {#each} block -->
<script>
  let items = ['Apple', 'Banana', 'Cherry'];
</script>

<ul>
  {#each items as item (item)}
    <li>{item}</li>
  {/each}
</ul>

{#await} Logic Block

The {#await} block handles asynchronous operations, such as API calls, displaying a loading indicator during the wait and rendering content based on the result.

<!-- Example of using the {#await} block -->
<script>
  async function fetchItems() {
    const response = await fetch('/api/items');
    return await response.json();
  }
</script>

{#await fetchItems() as { data, error }}
  <p>Loading...</p>
{:then data}
  <ul>
    {#each data as item}
      <li>{item.name}</li>
    {/each}
  </ul>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

Tags and Elements

Svelte allows the use of standard HTML tags to build user interfaces. Beyond basic HTML elements, you can also use custom elements and Svelte components.

Using Standard HTML Tags

You can use all standard HTML tags as you would in regular HTML.

<!-- Using standard HTML tags -->
<h1>Welcome to Svelte!</h1>
<p>This is a paragraph.</p>
<button>Click me</button>

Using Custom Elements

Custom elements can be Svelte components or regular custom HTML tags.

<!-- Using a custom element -->
<MyCustomElement />

Using Svelte Components

Svelte components are a special type of custom element, encapsulating logic and styles for modularity and reusability.

<!-- Using a Svelte component -->
<script>
  import MyComponent from './MyComponent.svelte';
</script>

<MyComponent />

Let’s analyze the application of Svelte’s logic blocks, HTML tags, and elements through a comprehensive example:

<!-- A comprehensive example combining logic blocks, tags, and elements -->
<script>
  import MyCustomElement from './MyCustomElement.svelte';
  let items = ['Apple', 'Banana', 'Cherry'];
  let show = true;

  async function fetchItems() {
    const response = await fetch('/api/items');
    return await response.json();
  }
</script>

<h1>Welcome to Svelte!</h1>

{#if show}
  <p>This is shown because 'show' is true.</p>
{:else}
  <p>This is shown when 'show' is false.</p>
{/if}

<ul>
  {#each items as item (item)}
    <li>{item}</li>
  {/each}
</ul>

{#await fetchItems() as { data, error }}
  <p>Loading...</p>
{:then data}
  <ul>
    {#each data as item}
      <li>{item.name}</li>
    {/each}
  </ul>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

<MyCustomElement />

In this example:

  • The {#if} block conditionally renders text.
  • The {#each} block iterates over a fruit list.
  • The {#await} block handles asynchronous data fetching, rendering a list or error message.
  • Standard HTML tags (<h1>, <p>, <ul>, <li>) build the basic page structure.
  • The custom element MyCustomElement introduces additional functionality or styles.

Data Binding and Expressions

Data Binding

Svelte supports various types of data binding, including:

One-Way Data Binding

The simplest and most common type of data binding, used to bind a component’s props to JavaScript variables or expressions. When the variable or expression changes, the bound prop updates automatically.

<!-- One-way binding -->
<script>
  let message = "Hello, World!";
</script>

<p>{message}</p>

Here, {message} is a data-binding expression replaced with the message variable’s value.

Two-Way Data Binding

Svelte’s two-way data binding, known as “component binding,” uses the bind directive to synchronize a component’s prop with an external variable. This is particularly useful for form controls, where values can update from user input or programmatic changes.

<!-- Two-way binding -->
<script>
  let value = "";
</script>

<input type="text" bind:value={value} />
<p>You typed: {value}</p>

Here, bind:value synchronizes the input field’s value with the value variable.

Expressions

In Svelte, expressions can be embedded in templates to compute or display data, enclosed in curly braces {}. Svelte compiles these into JavaScript code.

<!-- Expressions -->
<script>
  let a = 1;
  let b = 2;
</script>

<p>The sum is: {a + b}</p>

Here, {a + b} is an expression computed and displayed as the result.

Complex Expressions

While simple expressions can be used directly in templates, complex logic should be defined in the <script> tag as functions or computed properties, then referenced in the template.

<!-- Complex expressions -->
<script>
  let a = 1;
  let b = 2;

  function calculateSum(a, b) {
    return a + b;
  }

  let sum = calculateSum(a, b);
</script>

<p>The sum is: {sum}</p>

Expressions in Event Handlers

Expressions can also be used in event handlers, allowing dynamic state modifications based on event context.

<!-- Expressions in event handlers -->
<script>
  let count = 0;
</script>

<button on:click={() => count++}>Increment</button>
<p>Count: {count}</p>

Here, clicking the button increments count using the count++ expression in the event handler.

Binding Between Components

Svelte supports data binding between parent and child components, allowing parent component props to be passed to child components and updated via the bind directive.

<!-- Binding between components -->
<!-- Parent component -->
<script>
  import ChildComponent from './ChildComponent.svelte';
  let parentValue = "Parent Value";
</script>

<ChildComponent bind:childValue={parentValue} />

<!-- Child component -->
<script>
  export let childValue;
</script>

<p>Child Value: {childValue}</p>

Here, bind:childValue binds the parent’s parentValue to the child’s childValue.

Conditional Rendering and List Rendering

Conditional Rendering

Conditional rendering allows you to render UI parts based on conditions. Svelte provides {#if} and {#await} logic blocks for this purpose.

{#if} Logic Block

The {#if} block renders content if an expression is true; otherwise, an optional {:else} block is rendered.

<!-- Example of using {#if} for conditional rendering -->
<script>
  let show = true;
</script>

{#if show}
  <p>This text will be shown because 'show' is true.</p>
{:else}
  <p>This text will be shown when 'show' is false.</p>
{/if}

{#await} Logic Block

The {#await} block handles asynchronous operations, such as API calls, displaying a loading indicator during the wait and rendering content based on the result.

<!-- Example of using {#await} for handling asynchronous operations -->
<script>
  async function fetchData() {
    const response = await fetch('/api/data');
    return await response.json();
  }
</script>

{#await fetchData() as {data, error}}
  <p>Loading...</p>
{:then data}
  <p>Data received: {data}</p>
{:catch error}
  <p>Error: {error.message}</p>
{/await}

List Rendering

List rendering repeats a code block for each element in an array or iterable object. Svelte uses the {#each} logic block for this.

{#each} Logic Block

The {#each} block iterates over an array or iterable object, rendering a code block for each element, with the as keyword specifying a variable for the current element.

<!-- Example of using {#each} for list rendering -->
<script>
  let items = ['Apple', 'Banana', 'Cherry'];
</script>

<ul>
  {#each items as item (item)}
    <li>{item}</li>
  {/each}
</ul>

Here, each element in the items array is rendered as a list item.

Combined Usage

Conditional and list rendering are often used together to render lists differently based on conditions, such as displaying a message when a list is empty.

<!-- Example of combining {#if} and {#each} -->
<script>
  let items = [];
</script>

{#if items.length > 0}
  <ul>
    {#each items as item (item)}
      <li>{item}</li>
    {/each}
  </ul>
{:else}
  <p>No items found.</p>
{/if}

Svelte’s {#if}, {#each}, and {#await} logic blocks provide powerful tools for dynamically rendering UI based on application state, ensuring responsive and user-friendly interfaces while keeping code clear and maintainable.

Event Handling

Svelte’s event handling mechanism makes it easy to respond to user interactions, such as clicks or keyboard inputs. Event handling is achieved by adding event listeners in the template, making it intuitive and straightforward.

Binding Events

Use the on:eventName syntax to bind event handlers, where eventName is the event type to listen for, such as click or input.

<!-- Example of binding an event -->
<script>
  function handleClick() {
    console.log('Button was clicked!');
  }
</script>

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

Here, clicking the button triggers the handleClick function.

Event Modifiers

Svelte supports event modifiers to alter event behavior, including .stop, .prevent, .capture, and .self.

  • .stop: Prevents event bubbling.
  • .prevent: Prevents the default behavior.
  • .capture: Triggers the event during the capture phase.
  • .self: Ensures the event only triggers on the element itself, not its children.
<!-- Example of using event modifiers -->
<script>
  function handleInput(e) {
    console.log('Input:', e.target.value);
  }
</script>

<input type="text" on:input.stop.prevent={handleInput} />

Here, .stop.prevent ensures the input event doesn’t bubble and prevents the browser’s default behavior.

Custom Events

Svelte allows components to emit custom events using the $emit method, enabling parent components to listen for state changes in child components.

<!-- Custom event in a child component -->
<script>
  export let message = '';

  function emitMessage() {
    $emit('messageChanged', message);
  }
</script>

<button on:click={emitMessage}>Send Message</button>
<!-- Listening to a custom event in a parent component -->
<script>
  import ChildComponent from './ChildComponent.svelte';

  function handleMessageChanged(event) {
    console.log('Received message:', event.detail);
  }
</script>

<ChildComponent on:messageChanged={handleMessageChanged} />

Here, clicking the button in the child component emits a messageChanged event, which the parent component listens for using on:messageChanged.

Event Object

Event handlers can access the event object, which contains details about the event, such as the target element, event type, and timestamp.

<!-- Accessing event object -->
<script>
  function handleKeydown(e) {
    if (e.key === 'Enter') {
      console.log('Enter key was pressed!');
    }
  }
</script>

<input type="text" on:keydown={handleKeydown} />

Here, the event object’s key property is checked to detect the Enter key.

Let’s analyze Svelte’s event handling mechanism through a comprehensive example:

<!-- A comprehensive example combining event handling techniques -->
<script>
  let message = '';
  let input = '';

  function handleChange(e) {
    input = e.target.value;
  }

  function handleButtonClick() {
    message = input;
  }

  function handleKeyPress(e) {
    if (e.key === 'Enter') {
      message = input;
    }
  }

  function emitMessage() {
    $emit('messageSent', message);
  }
</script>

<input type="text" bind:value={input} on:input={handleChange} on:keypress={handleKeyPress} />
<button on:click={handleButtonClick}>Send Message</button>

<p>Message: {message}</p>

<!-- In a parent component -->
<script>
  import ChildComponent from './ChildComponent.svelte';

  function handleMessageSent(event) {
    console.log('Message sent:', event.detail);
  }
</script>

<ChildComponent on:messageSent={handleMessageSent} />

In this example:

  • input and message store the input field’s value and the message to send, respectively.
  • bind:value synchronizes the input field with the input variable.
  • on:input listens for input events, and on:keypress listens for keypress events.
  • on:click listens for button clicks.
  • The child component emits a messageSent event via $emit, which the parent component listens for with on:messageSent.
Share your love