Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


In this article, we’re going to visit Svelte, one of Javascript Frameworks for creating web user interfaces (frontend).

Overview

There are so many Javascript Frameworks for building web user interfaces, especially for SPA (single page application) purpose, 2 notable popular frameworks are React and Vue. Svelte on the other hand has a different aspect which makes it quite unique: instead of packaging the webapp with the runtime library, Svelte compiles the code into vanilla Javascript codes. In our perspective as developers, there will be no DOM diffing, in users perspective, our web will load faster, they don’t need the runtime library to download, because in the end, it is the vanilla Javascript codes that will arrive in users’ browsers.

Here’s an overview of what how Svelte works:

  • Svelte has a concept of single reusable component inside .svelte files. Each of these components can interact with each other via events, similar to how events in Javascript works, but events in Svelte won’t bubble (we need event forwarding for that). Compositions between components can be done with importing, slots and props.
  • Each of .svelte file (a component) will have its own html tags, css styles and js/typescript codes, most of the times, communication between these things can be done with reactivity and bindings, bindings can even be done with different components.
  • In Svelte we can have states that can be accessed by other components by subscribing, it is called as store, this can be useful for example clicking on a product detail component’s “add to cart” button will notify the cart component to update its quantity, it will be discussed in future articles.

Setting up Svelte Application

We can start our Svelte application by downloading it together with separate libraries (routing, server side rendering, etc), but it is recommended that for starting our app even faster we can use svelte-kit: https://kit.svelte.dev/. According to the documentation, we can run our first Svelte app right away with these commands:

$ npm init svelte svelte-mini-commerce
$ cd my-app
$ npm install
$ npm run dev -- --open

When running npm-init we’ll be asked a few questions:

We’ll be using TypeScript, which is an improvement over Javascript by adding type safety.

After successfully created, our new project workspace contains src directory, this is where we put most of our code base.

Mini-Commerce Web Application Overview

Before we move to our first component creation, we’ll need some goals. In this article, we’re going to create a product list page, comprising of mock (not real) products (in the future, this will be replaced by call to an API with fetch). Such product have some fields, let’s create it in src/types/ProductCard.ts:

export class ProductCard {
    id:Number;
    productName:string;
    imageURL:string;
    description:string;
    price:Number;
    unit:string;

    constructor(
        id:Number,
        productName:string,
        imageURL:string,
        description:string,
        price:Number,
        unit:string,
    ) {
        this.id = id,
        this.productName = productName
        this.description = description
        this.imageURL = imageURL
        this.price = price
        this.unit = unit
    }
}

This ProductCard class will be used by many Svelte components in the future.

The Components

Our first component will be a product card, src/components/ProductCardComponent.svelte. This component represents a single card of product.

Here we’ll create a Svelte component, comprising of 3 things:

  • HTML tags which is wrapped by a single div tag, then wrapped by an a tag.
  • style tag for styling the html tags.
  • script tag which has TypeScript code indicated by lang="ts".
<script lang="ts">
    import type { ProductCard } from "../types/ProductCard";
    export let productCard: ProductCard
    let highlighted = false;

    function highlight() {
        highlighted = true;
    }

    function revertHighlight() {
        highlighted = false;
    }
</script>

<a href="/products/{productCard.id}" class="product-link">
    <div class="product-card" class:highlighted on:mouseover={highlight} on:focus={highlight} on:blur={revertHighlight} on:mouseout={revertHighlight}>
        <p class="product-name">{productCard.productName}</p>
            <img src={productCard.imageURL} alt="imageAlt" class="product-image pure-img">
        <p class="product-price">Rp. {productCard.price} per {productCard.unit}</p>
    </div>
</a>

<style>
    .product-link {
        text-decoration: none; 
    }

    .product-card {
        font-size: 14px;
        border: 1px;
        padding: 6px 12px 6px 12px;
        border-style: solid;
        border-color: grey;
        margin: 8px;
        box-shadow: 2.5px 5px 4px #888888;
    }

    .highlighted {
        background: beige;
        transform: scale(1.05);
    }

    .product-name {
        text-transform: capitalize;
        font-weight: 700;
        font-size: 120%;
    }

    .product-image {
        width:100%;
        max-width:140px;
        max-height: 140px;
    }
</style>

There are some fascinating things here:

  • import type { ProductCard } will import ProductCard class from previous section. This class is needed to become the props of this component.
  • Such props is exposed (exported) for other components to use by writing export let productCard: ProductCard, which says “hey, you can use this component, if you want to use this, please fill in this props, please define the product id, product name, etc”.
  • let highlighted = false; is a variable that started as false, this will be toggled to true when function highlight() is called, such function is called reactively when we do mouseover and focus on div class="product-card". onmouseover and onfocus are standard Javascript events, Svelte uses simple syntax to do the reactivity.
  • highlighted will be set back to false when blur and mouseout happens to div class="product-card.
  • class:highlighted is class directive, a special feature in Svelte, it’s a bit advanced topic, but to explain in shortly, we can create some expressions in place of class value in html tag. This one tells that whenever highlight variable is true, set the class to .highlighted which will change the background color and scale the div by 1.05.
  • Svelte has special templating syntax like Mustache, {productCard.productName}, this syntax inject the value of product card’s name to the html.

Since we’re using TypeScript, everything related to script codes will be safely typed, it will make sure that injected props have the proper type.

Now, let’s create another component src/components/ProductsListComponent.svelte, this time, it will receive an array of ProductCard.

<script lang="ts">
    import type { ProductCard } from "../types/ProductCard";
    import ProductCardComponent from './ProductCardComponent.svelte';

    export let products: Array<ProductCard>;
</script>

<div class="pure-g">
    {#each products as productCard (productCard.id)}
        <div class="pure-u-1-6">
            <p> <ProductCardComponent productCard={productCard}/> </p>
        </div>
    {/each}
</div>

As we can see above, we’re importing the ProductCard type and not to forget, the product card component as well. Simply put, this component will accept an array (collection) of product cards, and export it for other components to fill in: export let products: Array<ProductCard>;.

Inside that component, there’s a Svelte templating syntax #each, to keep it simple, it’s iterating over products to render the product card component and fill the props, one by one. There are also other syntax like if else as well.

We’re using purecss for the responsive grid system, that’s why we put class="pure-g" and class="pure-u-1-6" in div tags above.

For adding purecss library, we can do that easily by adding it to src/app.html:

	<head>
        <!-- skipped for brevity --> 
		%svelte.head%
		<link rel="stylesheet" href="https://unpkg.com/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous">
	</head>

The Routes

Svelte kit provides us with some conveniences, one of them is by providing us built-in routing mechanism based on files conventions inside src/routes. There’s src/routes/index.svelte already containing some elements that will be rendered if we visit the base url of our web application. This section will also demonstrate on how to pass URL param to a svelte component for further processing (rendering by HTML template for example).

If we create a new file src/routes/products/index.svelte, Svelte kit will generate a new URL route for us: /products/.

Let’s create a new file src/routes/products/[id].svelte, this file convention will generate a new URL route like this: /products/1. But how do we get the [id] value from URL? By convention as well, we must create src/routes/products/[id].ts containing the following TypeScript code:

import type { RequestHandler } from './[id].d' 

type Output = { id: string }
export const get: RequestHandler<Output> = async({ params }) => {
    return {
        body: params
    }
}

n the future, we will replace this with call a to an endpoint providing the product detail data. This TypeScript function will be used by Svelte kit internal to pass return value (body) to [id].svelte for it to use (for rendering for example).

<script lang="ts">
    export let id:string;
</script>

<p>{id}</p>

Where does this RequestHandler come from? It is freely generated by Svelte kit, we fully utilize this for type safety, as we use TypeScript. Such type is generated in .svelte-kit/types. We can refer such file only by './[id].d', because there’s some built-in configurations in tsconfig.json.

export type RequestHandler<Output = ResponseBody> = GenericRequestHandler<{ id: string }, Output>;

For now, we only pass [id], so the RequestHandler only consists of id. Both the svelte file [id].svelte and the TypeScript file [id].ts share the same [id] so returned body can be used immediately by Svelte file.

Sample Data

And now, we’re ready to create some data sample for rendering purpose, this data will be constructed from ProductCard constructor, and collected into one array variable ready to be passed to ProductsListComponent component.

<script lang="ts">
    import ProductsListComponent from "../components/ProductsListComponent.svelte";
    import { ProductCard } from "../types/ProductCard";

    let products = [
        new ProductCard(1, "apple", "https://images.unsplash.com/photo-1619546813926-a78fa6372cd2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTF8fGFwcGxlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=900&q=60", "delicious apple", 44000, "kg"),
        new ProductCard(2, "orange", "https://images.unsplash.com/photo-1603664454146-50b9bb1e7afa?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8b3JhbmdlfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=900&q=60", "sweet orange", 38000, "kg"),
        new ProductCard(3, "watermelon", "https://images.unsplash.com/photo-1581074817932-af423ba4566e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1480&q=80", "red watermelon", 28000, "kg"),
        new ProductCard(4, "tomato", "https://images.unsplash.com/photo-1571680322279-a226e6a4cc2a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1486&q=80", "ripe tomato", 32000, "kg"),
        new ProductCard(5, "potato", "https://images.unsplash.com/photo-1578594640334-b71fbed2a406?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTF8fHBvdGF0b3xlbnwwfHwwfHw%3D&auto=format&fit=crop&w=900&q=60", "potato", 22400, "kg"),
        new ProductCard(6, "onion", "https://images.unsplash.com/photo-1585849834908-3481231155e8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8b25pb258ZW58MHx8MHx8&auto=format&fit=crop&w=900&q=60", "onion", 18000, "kg"),
        new ProductCard(7, "ginger", "https://images.unsplash.com/photo-1589707790848-9097c28e8569?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8N3x8Z2luZ2VyfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=900&q=60", "bornean ginger", 31000, "kg"),
        new ProductCard(8, "chicken egg", "https://images.unsplash.com/photo-1587486913049-53fc88980cfc?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8OXx8ZWdnfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=900&q=60", "chicken egg", 29000, "lusin"),
        new ProductCard(9, "strawberry", "https://images.unsplash.com/photo-1568966299181-bb7282cc84f0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80", "red strawberry", 27500, "kg"),
        new ProductCard(10, "dragon fruit", "https://images.unsplash.com/photo-1615817989745-ff7dffa0fefa?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8ZHJhZ29uJTIwZnJ1aXR8ZW58MHx8MHx8&auto=format&fit=crop&w=900&q=60", "indonesian dragon fruit", 37000, "kg"),
        new ProductCard(11, "red chillies", "https://images.unsplash.com/photo-1526346698789-22fd84314424?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8cGVwcGVyfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=900&q=60", "traditional red chillies", 25700, "kg"),
    ]
</script>

<h2>Popular Products</h2>

<ProductsListComponent products={products}/>

We’re using some free-royalty images for mocking the products data. In the future, these will be replaced by data from backend API.

Testing

Let’s run our Svelte project here, by running $ npm run dev, and visit http://localhost:3000/.

As we hover our mouse over a product card, such product card will be highlighted.

When we click one of them we’ll verify that such URL param to be successfully passed inside Svelte component.

Clicking the product will still be in a single HTML page (SPA application) without moving to another html, but Svelte kit also provides us with server rendering of such product detail URL route, this can be convenience for doing SEO.