Large Scale Apps With Svelte and TypeScript by Dam
Large Scale Apps With Svelte and TypeScript by Dam
TypeScript
Build Large and Scalable front-ends that leverage
component isolation, a centralized state manager,
internationalization, localization, Custom Component
Libraries, API-client code that easily can switch
between mocked data and live data and more.
Damiano Fusco
This book is for sale at http://leanpub.com/svelte-typescript
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean
Publishing process. Lean Publishing is the act of publishing an in-progress ebook using
lightweight tools and many iterations to get reader feedback, pivot until you have the right
book and build traction once you do.
Chapter 4 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Chapter 5 - Intro to Unit Testing While Refactoring a Bit . . . . . . . . . . . . . . . . . 29
ItemComponent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Add unit tests support to our project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
ItemComponent Unit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
ItemsList component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Chapter 5 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Chapter 6 - State Management . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Store Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Store Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Items.view.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
App.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Web Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
ItemsList.component.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Back to the Web Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Loader Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Chapter 6 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Chapter 7 - Api Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
API Client Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
Domains . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
The Main ApiClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Items domain Api Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Mock and Live Api Clients . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Environment Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Api Client Provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
Store Instance updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Alternatives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Chapter 7 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
Chapter 8 - Enhance the Api Client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
HttpClient Interfaces and Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
UrlUtils Unit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
HttpClient: Unit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
ItemsApiClientModel Update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
Chapter 8 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Chapter 9 - App Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
vite-env.d.ts updates (or env.d.ts) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
CONTENTS
Websites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
Tutorials . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
Blogs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
Books . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
LARGE SCALE APPS WITH SVELTE
AND TYPESCRIPT
This book is a guide for developers looking to build large-scale front-end applications with
Svelte and TypeScript. With the growth of the web and mobile app development, there is
an increasing demand for robust, scalable, and maintainable front-end solutions. This book
provides a comprehensive approach to building large scale code bases that use Svelte and
TypeScript.
The book covers key concepts and best practices like:
• Building front-ends that can grow to a large code base that is organized and easy to
expand and maintain.
• Development of UI components in isolation using an API client that can easily serve
live data or mocked data.
• Centralized State Manager organized into domain/area modules, providing a unified
and consistent way to manage the application state.
• Internationalization and Localization for language translation and number/dates
formatting according to a specific culture, making it easier to reach a global audience.
• TypeScript type-checking at development time to decrease run-time bugs or errors,
reducing the risk of costly bugs and enhancing the overall quality of the code.
• Directory structure, file, and code naming conventions, ensuring a consistent and
organized project structure for both developers and future maintainers.
• Hooks and Compositional Patterns, providing a flexible and reusable way to imple-
ment functionality in components.
• Components Libraries, allowing developers to build a library of reusable components,
reducing development time and increasing code quality.
• Unit tests for models and components, ensuring code quality and reducing the risk of
regressions.
The book is designed for developers with intermediate to advanced Svelte and TypeScript
skills who are looking to take their applications to the next level. Whether you are building
a new large-scale app or optimizing an existing one, this book will provide you with the
tools and knowledge to achieve your goals. Throughout the book, practical examples and
real-world scenarios are used to illustrate key concepts and best practices, providing you
with a solid understanding of how to build large scale apps with Svelte and TypeScript.
LARGE SCALE APPS WITH SVELTE AND TYPESCRIPT 2
Vite is a modern build tool for JavaScript projects that aims to provide fast and efficient
builds. It offers several benefits, including:
When we refer to “large scale apps”, we mean applications that have a large code base, a
large number of users, and a wide range of functionality. These applications typically require
efficient and scalable code that can handle high traffic and large amounts of data.
In this kind of projects there are several common concerns that arise, such as:
To address these concerns, here we will outline best practices for code organization and
structure, such as using a centralized state manager and implementing strong-type checking
with TypeScript. Additionally, we will focus on writing unit tests for our models and
components, which will help improve code quality and catch bugs early in the development
process.
Our ultimate goal is to build a foundation that can handle the demands of our app and be
easy to expand and maintain as the code base grows.
Preface 4
Goal
The primary aim of this book is to guide you through the process of building a scalable Svelte
application by following best practices for project structure, file organization, naming con-
ventions, state management, type checking with TypeScript, and compositional approaches
using hooks.
Throughout the chapters, we will grow our simple project into a robust, large-scale appli-
cation that is easy to expand and maintain, showcasing how patterns, conventions, and
strategies can lay a solid foundation and keep the code organized and uncluttered.
We will build a TypeScript API client that can seamlessly switch between serving static
mock data and communicating with a live API, allowing for front-end development to
commence even before the back-end API is fully functional. Additionally, we will delve
into topics such as internationalization, localization, and advanced techniques, to round out
our comprehensive guide to building a scalable Svelte application.
IMPORTANT: We will initially write code that allows us to achieve the desired functionality
quickly, even if it requires more code, but then we constantly “rework” it (refactoring) to
improve it and find solutions that allow us to reduce the amount of code used, or to organize
it in a clear and easy way that is easy to expand and maintain. So arm yourself with a lot of
patience!
Audience
The audience for this book is from beginners with some experience in MV* applications,
to intermediate developers. The format is similar to a cookbook, but instead of individual
recipes we’ll go through creating a project and keep enhancing, refactoring, and make
it better as we move forward to more advanced chapters to show different patterns,
architectures, and technologies.
Note: Some of the patterns illustrated here are not specific to Svelte, but can applied in any
application written in TypeScript or JavaScript. For example, most code from Chapters 3, 7,
and others can also be used in Vue.js/React/Angular or other front-end apps. Similarly, code
from Chapters 3 and 14 can also be used in NodeJS apps.
Text Conventions
I will highlight most terms or names in bold, rather than define different fonts/styles
depending on whether a term is code, or a directory name or something else.
Preface 5
Thanks
I would like to thank first and foremost Rich Harris¹ for having created Svelte²³⁴.
I want to thank my son for helping me proof read and validate the text and sample code in
this book.
I also would like to thank all the developers that over the time helped me correct things in
the book and provided valuable feedback.
About me
I have worked as a software developer for more than 20 years. I switched career from being a
full time musician when I was 30 years-old and then became a graphic designer, then transi-
tion to a web designer when internet became “a thing”, and for many years after that worked
as full-stack developer using Microsoft .NET, JavaScript, Node.js and many other technolo-
gies. You can read more about me on my personal website https://www.damianofusco.com
and LinkedIn profile https://www.linkedin.com/in/damianofusco/. You will find me also on
Twitter, @damianome, and GitHub github.com/damianof
¹Twitter: https://twitter.com/Rich_Harris
²Official Website: https://svelte.dev
³Wikipedia: https://en.wikipedia.org/wiki/Svelte
⁴GitHub: https://github.com/sveltejs
Prerequisites
This book assumes that you are familiar with the terminal (command prompt on Windows),
have already worked with the Node.js and NPM (Node Package Manager), know how to
install packages, and are familiar with the package.json file.
It also assumes you are familiar with JavaScript, HTML, CSS and in particular with HTML
DOM elements properties and events.
It will also help if you have some preliminary knowledge of TypeScript⁵ as we won’t get
into details about the language itself or all of its features but mostly illustrate how to enforce
type checking at development time with it.
You will need a text editor like VS Code or Sublime Text, better if you have extensions/plu-
gins installed that can help specifically for Svelte code. For VS Code for example, you could
use extensions like Svelte for VS Code⁶ (just search for it within the VS code extensions
tab).
⁵https://www.typescriptlang.org
⁶svelte.svelte-vscode
Companion Code
The entire companion code for the book can be found on GitHub at:
github.com/damianof/large-scale-apps-my-svelte-project
If you find any errors, or have difficulty completing any of the steps described
in the book, please report them to me through the GitHub issues section here:
github.com/damianof/large-scale-apps-my-svelte-project/issues
You are also free to reach out to me directly through Twitter at: @damianome
Chapter 1 - Setting Up The Project
IMPORTANT: This chapter assumes that you already have installed a recent version
of Node.js on your computer. If you do not have it yet, you can download it here:
https://nodejs.org/en/download/
There are many different ways to create a Svelte app. Here we’ll be leveraging TypeScript
and therefore will need to setup an project with a build/transpile process that will let us
make changes and verify them in real time. You could manually create this project, install
all the npm packages required, create each individual file. However, it is just much easier to
do this by leveraging vite⁷
If you do not have already installed the package create-vite@latest⁸ it will prompt you to
install it. In this case, type y and then enter to proceed:
The create-vite wizard will start and will ask you the name of the project. The default is
vite-project, so change this to my-svelte-project and hit enter:
The second step will ask to select a framework. Use the keyboard arrows to scroll down the
list and stop at svelte, then hit enter:
⁷https://vitejs.dev
⁸https://www.npmjs.com/package/create-vite/v/2.6.6
Chapter 1 - Setting Up The Project 9
The third step will asking which “variant” you want o use. Scroll down to svelte-ts (this is
for the version that uses TypeScript) and hit enter:
NOTE: in most recent version of Site the options might be slightly different, make sure you
select svelte-ts or TypeScript.
This will create a folder called my-svelte-project which is also the name of our project.
At the end it should display a message similar to this:
cd my-svelte-project
npm install
npm run dev
The first command will navigate to the current sub-directory called my-svelte-project, the
second one will install all the npm dependencies, the third one will serve the app locally.
You’ll see a message similar to this displayed:
From the web browser, navigate to the http://localhost:3000 address and you’ll see application
home page rendered:
Chapter 1 - Setting Up The Project 10
The my-svelte-project has been created with the main view called App.svelte and a
component called Counter.svelte.
Chapter 1 - Setting Up The Project 11
Chapter 1 Recap
What We Learned
How to create the basic plumbing for a Svelte app using the vite and create-vite@latest
• How to serve the app locally through the command npm run dev
Observations
• The app has been created with only preliminary code
• The app does not do much yet, has only the main App view with some static html in it
and a reference to a simple component called Counter
Based on these observations, there are a few improvements that will be making into the next
chapter:
Improvements
• Expand our app functionality by creating our first component
Chapter 2 - Your First Component
The Items List
Let’s now pretend we have been giving requirements for our app to have a component
that displays a list of ”items”. We will keep this simple initially and as we move towards
more advanced chapter expand on it to show how we can better structure our application to
support:
* id
* name
* selected
// file: src/components/items/ItemsList.component.svelte
<script lang="ts">
// expose a property called items with a default value of a blank array
export let items: any[] = [] // explicetely using any[] as we'll replace this with\
an interface in the next chapters
</script>
<div>
<h3>Items:</h3>
<ul>
⁹We are following a file naming convention where higher level components’ names are pascal-case and follow this format [Component-
Name].component.svelte (Reference: Naming Conventions section at the end of this book)
Chapter 2 - Your First Component 14
A few things to notice here. First, we specify the lang attribute on the <script> element with
the value ts so we can use TypeScript. We export a variable called items. This is how we
create a component property in Svelte¹⁰. Note that items is just using the type any¹¹ for now
(later we’ll replace any with an interface we’ll create).
For our html template, we added a <h3> element with hard-coded text just saying ”Items:”.
Then a <ul> with a #each binding that will render all our items within <li> elements.
App.svelte
Open the existing App.svelte file. Replace the existing code with this:
// file: src/App.svelte
<script lang="ts">
// TODO ....
</script>
<main>
<div class="home">
... TODO
</div>
</main>
¹⁰https://svelte.dev/docs#component-format-script-1-export-creates-a-component-prop
¹¹With ‘any’, TypeScript does not enforce type-checking on a property or variable. However, this is considered a bad practice as we lose the main
benefit of TypeScript. There might be exceptions to this rule when using older 3rd party packages/libraries/plugins that do not offer type definitions.
However, even in those cases it would be strongly recommended to provide interfaces and types so that you can still avoid using ‘any’.
Chapter 2 - Your First Component 15
// file: src/App.svelte
<script lang="ts">
// import a reference to our ItemsList component
import ItemsListComponent from './components/items/ItemsList.component.svelte'
...
For now, also mock some data for our list of items that we will feed to our ItemsListCompo-
nent. For this we instantiate a local const called items and initialize it with some hard-coded
data¹². Let;s just add it after the import statement:
// file: src/App.svelte
<script lang="ts">
// import a reference to our ItemsList component
import ItemsListComponent from './components/items/ItemsList.component.svelte'
¹²Note: using hard-coded data is a bad practice and here we are only doing it to first illustrate how things flow, and later in the next chapters will
remove in favor of best practices and patterns (see Chapter 5)
¹³https://svelte.dev/docs#template-syntax-attributes-and-props
Chapter 2 - Your First Component 16
// file: src/App.svelte
<script lang="ts">
// import a reference to our ItemsList component
import ItemsListComponent from './components/items/ItemsList.component.svelte'
<main>
<div class="home">
<ItemsListComponent items={items}/>
</div>
</main>
Comment out or remove the app.css import reference from the src/main.ts file:
Save the file. The web browser will refresh and display our preliminary items list being
rendered more or less like this:
Chapter 2 - Your First Component 17
Chapter 2 - Your First Component 18
Chapter 2 Recap
What We Learned
• How to create a basic component that displays a list of items
• How to consume that component from another component/view
Observations
• The items property within the ItemsList.component.svelte is declared as an array of
type any
• The App.svelte view contains hard-coded data (items) which is also declared as an
array of any
• This means we are not leveraging strong-type checking at development time using
TypeScript interfaces/models/types
Based on these observations, there are a few improvements that we will make in the next
chapters:
Improvements
• Create a TypeScript interface called ItemInterface for enforcing type checking at
development time for our items data
• Update our code so it uses the new ItemInterface interface
Chapter 3 - Data Model Interfaces
In this chapter, we will delve into the power of TypeScript by leveraging its strong-type
checking capabilities through the use of interfaces. One of the main challenges with pure
JavaScript is its loosely typed nature, which can lead to unexpected behavior and bugs at
run-time. This is due to the lack of checks on the type or properties of values or objects being
passed around in the code. TypeScript solves this problem by providing developers with the
ability to enforce strict type checking at development time through the use of interfaces,
types, classes, and more.
By incorporating TypeScript into our project, we’ll be able to catch potential issues and bugs
before they reach the production environment, saving us time and resources in debugging
and fixing. Moreover, strong-typing also improves the readability and maintainability of
our code, making it easier for developers to understand the purpose and usage of values
and objects in the codebase. In this chapter, we’ll explore how to use interfaces and types
to implement strong-typing in our project, and how it can help us ensure the reliability and
quality of our code.
Models Directory
To lay the foundation for building large-scale applications, we will start by creating a new
sub-directory under src called models. The organization of files and directories plays a
critical role in the success of large-scale code bases. As such, it’s essential to establish a
consistent naming convention and directory structure from the outset. This will help ensure
that the code remains organized, easy to understand, and maintainable as the application
grows and the number of source files and directories increases.
You and your team are free to determine the standards that work best for you, but it’s crucial
to establish a set of conventions and stick to them. This will save you a significant amount
of time and effort in the long run and prevent confusion and headaches as the application
grows and evolves.
Interface ItemInterface
To create the interface for our items, we will create a new directory called src/models/items
and add a TypeScript file named Item.interface.ts.
Chapter 3 - Data Model Interfaces 20
It’s worth noting that there are different naming conventions for TypeScript interfaces, with
some preferring to use a suffix like Interface, while others use a prefix like I. In this book, we
will follow the suffix convention, using Item.interface.ts as the file name. However, you are
free to choose your preferred naming convention or align with your organization’s coding
standards.
It’s important to keep each interface in its own file, as this makes it easier to maintain
and manage. For more information on naming conventions, please refer to the Naming
Conventions section at the end of this book.
Your directory structure should now look similar to this:
Let’s write an interface that represents one item that will be rendered in our Item component.
Our interface will have three properties:
// file: src/models/items/Item.interface.ts
For now, that is all we need. Since this will only represent a piece of data, we do not need to
implement a class.
NOTE: In this case our ItemInterface only holds fields, but no methods. You can think of this
more like the type struct in language like C or C#. Unfortunately TypeScript does not have
an explicit struct type¹⁴ and their guidance is to use interfaces for this.
ItemsList Component
Now that we have our interface, we can finally leverage TypeScript type checking ability by
changing our items property on the items component from any[] to ItemInterface[]. First,
import a reference for ItemInterface and modify our items property declaration from type
any[] to type ItemInterface[]:
// file: src/components/items/ItemsList.component.svelte
<script lang="ts">
// import a reference to our ItemInterace
import type { ItemInterface } from '../../models/items/Item.interface'
// expose a property called items with a default value of a blank array
export let items: ItemInterface[] = [] // here replace any[] with ItemInterace[]
</script>
...
¹⁴There have been suggestions presented, but I do not think they will ever add a struct type. See the TypeScript team answers here:
https://github.com/microsoft/TypeScript/issues/22101
Chapter 3 - Data Model Interfaces 22
// file: src/components/items/ItemsList.component.svelte
<script lang="ts">
// import a reference to our ItemInterace
import type { ItemInterface } from '../../models/items/Item.interface'
// expose a property called items with a default value of a blank array
export let items: ItemInterface[] = []
</script>
<div>
<h3>My Items:</h3>
<ul>
{#each items as item}
<li>
{item.name}
</li>
{/each}
</ul>
</div>
Make sure the terminal does not display any error, and that the web browser refreshed and
no error are displayed in the browser console.
App.svelte
We should also update the App.svelte code so it uses the ItemInterface interface for the
locally private property also called items.
Please note, that as soon as you change the items property from any[] to ItemInterface[] it will
complain that each item does not correctly implement the interface. This is because we did not
initially include the selected property required by the interface. This is one of the powerful
things of using TypeScript correctly. It will help catch errors like this at development time
rather than run time, increase the code quality and make it less prone to bugs. So make sure
each item has now also a selected field with a default of false.
Chapter 3 - Data Model Interfaces 23
// file: src/App.svelte
<script lang="ts">
// import a reference to our ItemInterace
import type { ItemInterface } from './models/items/Item.interface'
// import a reference to our ItemsList component
import ItemsListComponent from './components/items/ItemsList.component.svelte'
...
Again, make sure the terminal does not display any errors, and that the web browser
refreshed and no error are displayed in the browser console. As you make changes is also
a good idea occasionally to do an Empty Cache and Hard Reload by right clicking on the
Chrome refresh icon and selecting the last option:
Chapter 3 - Data Model Interfaces 24
Chapter 3 Recap
What We Learned
• It’s important to follow files and directories naming convention and structure conven-
tion
• How to leverage TypeScript interfaces and avoid using any so that strong-type
checking is enforced at development time and avoiding potential runtime errors or
hidden bugs
Observations
• The App.svelte contains a local variable that holds hard-coded mocked data that
enabled us to prototype our component quickly
• ItemsList.component.svelte just displays the list of items, but the user has still no
ability to click on them to change their selected property
Based on these observations, there are a few improvements that we will make into the next
chapter:
Improvements
• Update our component so that when a user clicks on an item displayed on the page, the
item selected property will toggle from false to true (and vice versa)
Chapter 4 - Adding Events To the
Items Component
In this chapter we keep building our ItemsList.component.svelte so we can handle when
the user clicks on an item in the list.
ItemsList Component
Start by adding a function called handleClick . This function will handle a click on each of
the <li> elements and will toggle the item.selected property from true to false or vice versa.
It will also logs the item id and selected properties to the console for preliminary debugging:
// file: src/components/items/ItemsList.component.svelte
<script lang="ts">
// import a reference to our ItemInterace
import type { ItemInterface } from '../../models/items/Item.interface'
// expose a property called items with a default value of a blank array
export let items: ItemInterface[] = []
And update the html by adding an on:click¹⁵ attribute to the <li> element, pointing to our
handler handleClick and passing a reference to the item as the argument:
¹⁵https://svelte.dev/tutorial/dom-events
Chapter 4 - Adding Events To the Items Component 26
// file: src/components/items/ItemsList.component.svelte
...
<div>
<h3>My Items:</h3>
<ul>
{#each items as item}
<li on:click={() => handleClick(item)}><!-- add on:click here -->
{item.name}
</li>
{/each}
</ul>
</div>
Note the syntax we use in the on:click attribute. We have an inline anonymous functoin here
because we need to pass our item within handleClick.
on:click={ handleClick }
Then, the web browser should have refreshed. Now, when clicking on the items in the list you
should see the message being displayed in the browser console, and when clicking multiple
times on the same item it should print true then false etc showing that toggling is working:
Now, we learned how to add a click handler to our component and changing the data item
selected property that way. However, updating the selected property within the handleClick
will not cause to re-render the html.
Let’s verify this. Start by slightly modifying the text output by our list element, outputting
also the selected value within [] (square brackets) like “[]”:
Chapter 4 - Adding Events To the Items Component 27
// file: src/components/items/ItemsList.component.svelte
...
{#each items as item}
<li on:click={() => handleClick(item)}>
{item.name} [{item.selected}] <!-- output selected next to the name -->
</li>
{/each}
...
Save and check the browser again. Notice that even though after clicking on the list items
you see a message in the console with the updated value for the selected property, each item
in the list always renders [false] next to the name.
Svelte is peculiar in this. In order to re-render our list, we need to update the items array by
setting to itself¹⁶:
...
// item click handler
function handleClick (item: ItemInterface) {
item.selected = !item.selected
items = items // add this line here to set items to itself to force a refresh
console.log('handleItemClick', item.id, item.selected)
}
...
Save once more and check the browser again. This time next to the name the value will
change from [false] to [true] and viceversa:
In the next chapter we’ll talk more in depth on how to better manage the application state
using centralized place and a State Manager
¹⁶https://svelte.dev/tutorial/updating-arrays-and-objects
Chapter 4 - Adding Events To the Items Component 28
Chapter 4 Recap
What We Learned
• How to add a click handler to our ItemsList component
• How to manipulate the item.selected property through our click handler
Observations
• The items selected property is being manipulated directly within our component
• We need a more centralized way to handle changes on the data and state of the
application
Based on these observations, there are a few improvements that we will make in the next
chapters:
Improvements
• Implement a state manager to control our application state from a centralized place
Chapter 5 - Intro to Unit Testing
While Refactoring a Bit
We will now delve into writing unit tests for our project. Unit tests serve as a critical aspect
of ensuring the stability and reliability of our code. In this book, we will cover two main
categories of unit tests:
• Unit tests for models, classes, structures, and interfaces (such as the API client and
helpers)
• Unit tests for Svelte components
Note: It’s worth mentioning that there is a third category of tests, known as end-to-end (e2e)
tests, but we will not be covering those in this book.
Our first step will be to write unit tests for our Svelte components. We will start with
the ItemsList component and while doing so, we will make some refactors to improve its
implementation. The unit tests will validate the changes we make, ensuring that our code
remains functional and free of bugs.
ItemComponent
Remember how in our ItemsList component we have a loop that creates <li> elements, one
for each item in our items property? Let’s extract the code for the <li> element and create a
child component just for that. Let’s start by adding a new file called Item.component.svelte
under the src/components/items/children directory:
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 30
// file: src/components/items/children/Item.component.svelte
<script lang="ts">
// import createEventDispatcher from Svelte:
import { createEventDispatcher } from 'svelte'
// import a reference to our ItemInterace
import type { ItemInterface } from '../../../models/items/Item.interface'
// expose a property called testid. This will be useful for the unit tests (or aut\
omation testing)
export let testid: string = 'not-set'
// a computed property to return a different css class based on the selected value
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 31
<style>
li.item {
padding: 5px;
outline: solid 1px #eee;
display: flex;
align-items: center;
height: 30px;
cursor: pointer;
transition: background-color 0.3s ease;
}
li.item .name {
margin-left: 6px;
}
li.item .selected-indicator {
font-size: 2em;
line-height: 0.5em;
margin: 10px 8px 0 8px;
color: lightgray;
}
li.item.selected .selected-indicator {
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 32
color: skyblue;
}
li.item:hover {
background-color: #eee;
}
</style>
We just created a template for a single <li> element. We also enhanced this a bit by replacing
the rendering of the name with binding { item.name } with two child <div> elements:
- one to display the Item name
- one that will show a star icon (we are just using a char here, but in the next chapters we’ll
be replacing this with real icons)
Then we added a computed property called cssClass that will return the string ”item”
or ”item selected”. We then bind this to the <li> class attribute, based on whether the
model.selected property is true or false: <li class= on:click=>
This will have the effect to render the <li> element in two possible ways:
- <li class=”item”> (when not selected)
- <li class=”item selected”> (when selected)
We also bind to the click event with on:click binding and in the local handleClick handler
we just invoke the parent handler by dispatching the event “selectItem” with dispatch (Svelte
event dispatcher¹⁷) and passing the item as the event argument. We will then handle this in
the parent component (ItemsList component).
Note that we also added a <style> section with some css to render our <li> element a little
better. The css above is just a quick-and-dirty bit of styling so we can make our list look a bit
prettier for now. In later chapters we’ll introduce TailwindCSS and keep working with that
instead of writing our own css.
Let’s also add some css at the end of the ItemsList.component.svelte component:
¹⁷https://svelte.dev/docs#run-time-svelte-createeventdispatcher
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 33
// file: src/components/items/ItemsList.component.svelte
...
<style>
ul {
padding-inline-start: 0;
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 0px;
}
</style>
// file: src/App.svelte
...
<style>
.home {
padding: 20px;
}
</style>
Note: we are not consuming our new Item.component.svelte anywhere yet. Let’s proceed first
to create a unit test against it and validate that it renders and behaves as we expect.
Dependencies
Let’s start installing our npm dependencies first.
Install Vitest¹⁸ npm package:
npm i -D vitest
npm i -D @testing-library/svelte
Configuration
Now we need to configure a few things to be able to run unit tests.
tsconfig.json file
// file: my-svelte-project/tsconfig.json
...
"compilerOptions": {
...,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
},
"types": [
"svelte",
"vite/client",
"vitest/globals"
]
...
¹⁸https://vitest.dev
¹⁹https://testing-library.com/docs/svelte-testing-library/intro
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 35
vite.config.js files
Add “test” section with the following settings to the vite.config.js files:
...
package.json
Within the package.json file, add the following command shortcuts within the script section:
...
"scripts": {
...
"test": "vitest run",
"test-watch": "npm run test -- --watch",
}
...
Item.rendering.test.ts
Open the file Item.rendering.test.ts and paste the following code in it:
//file: src/components/items/children/Item.rendering.test.ts
// render component
render(component, {
testid,
item
})
// test
expect(liElement).not.toBeNull()
// check that the innterHTML text is as expected
expect(liElement.innerHTML).toContain('Unit test item 1')
})
})
...
Here we test that the component renders the data model properties as expected.
Note: These example are just to get you started. Later you can look at more precise ways to
test what our component has rendered or even trigger events on them.
Run our unit tests from the terminal with this command:
It should run the unit tests and print the results on the terminal, similar to this:
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 38
...
√ src/components/items/children/Item.rendering.test.ts (1)
...
Let’s add two more tests within the same file to check that the component has the expected
CSS classes.
Test to check that it has the class ”selected” when item.selected is true, and that does NOT
have the css class ”selected” when item.selected is false:
// file: src/components/items/children/Item.rendering.test.ts
...
...
// render component
render(component, {
testid,
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 39
item
})
// test
expect(liElement).not.toBeNull()
// check that the element className attribute has the expected value
expect(liElement.className).toContain('selected')
})
// render component
render(component, {
testid,
item
})
// test
expect(liElement).not.toBeNull()
// check that the element className attribute has the expected value
expect(liElement.className).not.toContain('selected')
})
})
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 40
Item.behavior.test.ts
We can also test the behavior of our component by programmatifcally triggering the click
event. Open the file Item.behavior.test.ts and paste the following code in it:
// file: src/components/items/children/Item.behavior.test.ts
// Note: In svelte testing library we have to use await when firing events
// because we must wait for the next `tick` to allow for Svelte to flush all pen\
ding state changes.
await fireEvent.click(liElement)
})
Save and check the test results and make sure all pass (if you had stopped it, run npm run
test again).
ItemsList component
Now we can finally modify our ItemsList.component.svelte to consume our newly created
Item component. Import a reference to ItemComponent, then replace the <li> element within
the loop with our <ItemComponent>:
// file: src/components/items/ItemsList.component.svelte
<script lang="ts">
// import a reference to our ItemInterace
import type { ItemInterface } from '../../models/items/Item.interface'
// import a reference to our Item component
import ItemComponent from './children/Item.component.svelte'
<div>
<h3>My Items:</h3>
<ul>
{#each items as item}
<!-- begin: remove code block -->
<!--li on:click={() => handleClick(item)}>
{item.name} [{item.selected}]
</li-->
<!-- end: remove code block -->
<style>
ul {
padding-inline-start: 0;
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 0px;
}
</style>
selected)
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 44
Chapter 5 Recap
What We Learned
• How to write unit tests against a component
• How to test that components render specific DOM elements, or have specific text, or
attributes like CSS classes, etc.
• How to test events on our components by programmatically triggering them with
fireEvent (from Svelte Testing Library)
• How to re-factor parts of a component to create a child component and use unit tests
to validate our changes
Observations
• We did not test our ItemsList.component.svelte or more advanced behaviors
Based on these observations, there are a few improvements that you could make:
Improvements
• Add additional unit tests for ItemsList.component.svelte as well
Chapter 6 - State Management
One of the most important part of an app that will grow large is to decided how to manage
its state.
For many years in MV* frameworks like React²⁰ or Vue²¹ etc. that meant using a state
manager that usually implemented the Flux²² State Management pattern.
With React that usually meant using Redux²³, while with Vue it meant using Vuex²⁴, even
though nowadays there are other alternatives (inluding building your own custom state In
Vue using just Vue reactive²⁵, or the useState²⁶ hooks in React, etc).
Flux offers an architectural pattern that is a slight modification of the observer-observable
pattern and it is not a library or a framework.
Single source of truth:
The most important reason to implement a centralized state manager is to have a “single
source of truth” for the application state/data. This simply means that our application state
has only one global, centralized source. The responsibility of changing that state is only in
the hand of our state manager. That means you can expect a consistent behavior in your app
as the source of your data cannot be changed outside the state manager.
In this book, we’ll implement our own peculiar centralized state manager that will help us
deliver the goals of this book. For this, we’ll create a set of interfaces and a structure that
will allow use to keep our state manager organized into modules/domains.
Note: Just remember to be open minded to different ideas, but also challenge them, and take
time to explore your own ideas as well. Different patterns and code organization strategy can
be implemented, and some might be better or worse than others. This is always the case when
writing code in general, but even more important when writing state managements patterns.
Now let’s proceed creating our store interfaces and implementations.
²⁰https://reactjs.org
²¹https://vuejs.org
²²https://facebook.github.io/flux
²³https://redux.js.org
²⁴https://vuex.vuejs.org
²⁵https://vuejs.org/v2/guide/reactivity.html
²⁶https://reactjs.org/docs/hooks-state.html
Chapter 6 - State Management 46
Store Interfaces
One thing I learned from my past experience using React, Angular, Vue.js, Svelte, and more,
is that there are some advantages adopting a certain flow that is closer to Flux, but does not
have to follow it to the letter. We definitely won’t need this in every component, as in some
cases using just local state is the right thing to do. But we’ll need it for global state changes
on which many components within the same app will depend on.
One things that often drives the code pattern is the framework itself. Especially React
has a peculiar way as a lot of plumbing has to happen within the React context itself for
React to be aware of changes. Other frameworks are more flexible in this (Vue 3 reactive
for example) and are less prone to drive your architectural and patterns decisisions, thus
allowing more easily to decouple your state manager from the actual framework. In Svelte,
we have definitely much more flexibility than in React as you will see shortly.
In this chapter we’ll offer a bit of an opinionated structure, but I found that is helps better
understanding how the data and events flow, especially to beginners.
Let’s try to implement a state manager that follow more or less this pattern:
Start creating all the interfaces we need so we can better understand the abstraction of what
we are trying to accomplish.
ItemsState.interface.ts
Here add a file called ItemsState.interface.ts and paste the following code in it:
// file: src/store/items/models/ItemsState.interface.ts
/**
* @name ItemsStateInterface
* @description Interface represnets our Items state
*/
export interface ItemsStateInterface {
loading: boolean
items: ItemInterface[]
}
Chapter 6 - State Management 48
In the code above we just export an interface that represents our Items domain state. This
will be an object with a property called items which will contain an array of objects of type
ItemInterface., and one called loading which is a boolean and will indicate if we are loading
data or not (so that we can eventually display a loading indicator/component in the UI).
ItemsStore.interface.ts
Let’s add a second file called ItemsStore.interface.ts and paste the following code in it:
// file: src/store/items/models/ItemsStore.interface.ts
...
In the code above, we declare an interface called ItemsStoreInterface. This one will
represent our Items store module.
For following the pattern we established earlier, we need to add two things to our Items
store:
Let’s keep expanding the code within our ItemsStore.interface.ts file. Let’s add another
interface that will represent our state manager actions, called ItemsStoreActionsInterface:
// file: src/store/items/models/ItemsStore.interface.ts
/**
* @name ItemsStoreActionsInterface
* @description Interface represents our Items state actions
*/
Chapter 6 - State Management 49
...
We are going to need an action to load our items, and one action to toggle the selected value
on a specific item. So let’s complete the actions interface as follow:
// file: src/store/items/models/ItemsStore.interface.ts
...
/**
* @name ItemsStoreActionsInterface
* @description Interface represents our Items state actions
*/
export interface ItemsStoreActionsInterface {
loadItems (): Promise<void>
toggleItemSelected (item: ItemInterface): Promise<void>
}
...
Note: we are declaring our actions with a return type of Promise as we’ll attempt to keep the
implementation of this async later.
Now, let’s add an interface that will represents our store getters, called ItemsStoreGettersIn-
terface:
// file: src/store/items/models/ItemsStore.interface.ts
...
/**
* @name ItemsStoreGettersInterface
* @description Interface represents our store getters
* Getters will be used to consume the data from the store.
*/
export interface ItemsStoreGettersInterface {
// note: we have to use type SvelteStore.Readable on these properties
loading: SvelteStore.Readable<boolean>
Chapter 6 - State Management 50
items: SvelteStore.Readable<ItemInterface[]>
}
...
Our getters interface will have a loading property, and an items property (similar to your
Items state interface, but feel free to use a different naming convention for your properties
in the getters).
Let’s now add two properties to our ItemsStoreActionsInterface, called actions and getters
using the last two interface we just created as their types:
// file: src/store/items/models/ItemsStore.interface.ts
...
/**
* @name ItemsStoreInterface
* @description Interface represents our Items store module
*/
export interface ItemsStoreInterface {
actions: ItemsStoreActionsInterface
getters: ItemsStoreGettersInterface
}
// file: src/store/items/models/ItemsStore.interface.ts
/**
* @name ItemsStoreActionsInterface
* @description Interface represents our Items state actions
*/
export interface ItemsStoreActionsInterface {
loadItems (): Promise<void>
toggleItemSelected (item: ItemInterface): Promise<void>
}
Chapter 6 - State Management 51
/**
* @name ItemsStoreGettersInterface
* @description Interface represents our store getters
* Getters will be used to consume the data from the store.
*/
export interface ItemsStoreGettersInterface {
// note: we have to use type SvelteStore.Readable on these properties
loading: SvelteStore.Readable<boolean>
items: SvelteStore.Readable<ItemInterface[]>
}
/**
* @name ItemsStoreInterface
* @description Interface represents our Items store module
*/
export interface ItemsStoreInterface {
actions: ItemsStoreActionsInterface
getters: ItemsStoreGettersInterface
}
// file: src/store/items/models/index.ts
RootStore.interface.ts
Here add a file called RootState.interface.ts and paste the following code in it:
// file: RootStore.interface.ts
/**
* @name RootStateInterface
* @description Interface represents our global state manager
*/
export interface RootStoreInterface {
itemsStore: ItemsStoreInterface
// additional domain store modules will be eventually added here
}
Chapter 6 - State Management 53
Note that this interface will represent an object that wrap references to each individual
domain module. In this case, we only have one for now called itemsStore.
Here too add a barrel index.ts file to just export the RooStore interface:
// file: src/store/root/models/index.ts
Store Implementation
Now let’s write the implementations for our interfaces.
Items.store.ts
Add a file called Items.store.ts and paste the following code in it:
// file: src/store/items/Items.store.ts
Here too add a barrel index.ts file to export the useItemsStore hook:
// file: src/store/items/index.ts
Root.store.ts
Inside the directory src/store/root add a file called Root.store.ts and paste the following code
in it:
// file: src/store/root/Root.store.ts
// hook that returns our root store instance and will allow us to consume our app st\
ore from our components
export function useAppStore(): RootStoreInterface {
return {
itemsStore: useItemsStore(),
// additional domain store modules will be eventually added here
}
}
Here too add a barrel index.ts file to export the global useAppStore hook:
// file: src/store/root/index.ts
Up one directory, finally add one last barrel index.ts file at src/store/index.ts to export the
root store:
// file: src/store/index.ts
Let’s now go back to our components and start consuming our state.
Chapter 6 - State Management 57
Items.view.svelte
Add a new directory called views under src.
Here we add a new higher-level component called Items.view.svelte. Your directory
structure will be like this:
Note that in Svelte anything is a component and we could have just called this
Items.component.svelte and put it under component/items. This is only for organizational
purposes. We are really free to organize the code as we see fit. In this case I also wanted to
better separate what the lower components are doing and accessing the global state only in
the higher-level component.
Paste the following code within the file Items.view.svelte:
Chapter 6 - State Management 58
<script lang="ts">
// import reference to Svelte lifecycle hook onMount:
import { onMount } from 'svelte'
// get a reference to the items state data through our itemsStore getters:
const {
loading,
items
} = itemsStore.getters
// lifecycle onMount hook: use to dispatch our loadItems action to our itemsStore
onMount(async () => {
// invoke our store action to load the items
itemsStore.actions.loadItems()
})
</script>
<div>
<ItemsListComponent
loading={$loading}
items={$items}
selectItem={onSelectItem} />
</div>
Chapter 6 - State Management 59
In the code above we are basically rendering the same itemsListComponent as we did earlier
in the App.svelte file. However, here we are consuming the data from our items store and
invoking our store actions that will mutate our data.
App.svelte
Finally, replace the entire code within App.svelte so we can consume our new
Items.view.svelte here:
<script lang="ts">
// import a reference to our ItemsView component
import ItemsView from './views/Items.view.svelte'
</script>
<main>
<div class="home">
<ItemsView />
</div>
</main>
<style>
.home {
padding: 20px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 12px;
}
</style>
Web Browser
The web browser should refresh and display the content similar to before. Notice that now it
will take about 1 second before the items will be rendered. This is because in our loadItems
action implementation we used a setTimeout with a 1 second delay to simulate a possible
call to an API for example.
Chapter 6 - State Management 60
ItemsList.component.svelte
Add a loading property of type boolean with a default value of false, and a selectItem
property of a function type (we will use this to bubble up the onSelectItem to the Items
view):
// file: src/components/items/ItemsList.component.svelte
<script lang="ts">
...
...
</script>
...
Now within the <h3> element, add a one-way binding using the single curly
braces to print out the value of the loading property and also change the binding
on:selectItem={onSelectItem} to on:selectItem={selectItem}:
Chapter 6 - State Management 61
// file: src/components/items/ItemsList.component.svelte
...
<div>
<h3>My Items - loading: {loading}</h3> <!-- add "- loading: {loading}" -->
<ul>
{#each items as item}
<ItemComponent item={item} on:selectItem={selectItem} /> <!-- change {onSelect\
Item} to {selectItem} -->
{/each}
</ul>
</div>
After 1 second the items will render and the h3 element will display the text My items -
loading: false:
Loader Component
Let’s create a quick-and-dirty loader component that we can show to indicate a loading
operation.
Chapter 6 - State Management 62
Create the directory src/components/shared. Within this directory create a file called
Loader.component.svelte. Within the file, paste the following code:
// file: src/components/shared/Loader.component.svelte
<div class="loader">
<div class="bounceball"></div>
</div>
<style>
.loader {
display: inline-block;
}
.loader .bounceball {
position: relative;
width: 30px;
}
.loader .bounceball:before {
position: absolute;
content: '';
top: 0;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #ff3e00;
transform-origin: 50%;
animation: bounce 500ms alternate infinite ease;
}
@keyframes bounce {
0% {
top: 60px;
height: 10px;
border-radius: 60px 60px 20px 20px;
transform: scaleX(2);
}
25% {
height: 60px;
border-radius: 50%;
transform: scaleX(1);
}
100% {
top: 0;
}
Chapter 6 - State Management 63
}
</style>
This provides a basic loader that uses pure CSS for the animation. You are free to use an
animated gif, or svg image, or font-icon etc. In later chapter we might modify this to
implement a versin that uses TailwindCSS.
Now, lets go back into our ItemsList.component.svelte code and import a reference to our
new Loader component, and update our code as follow (complete code):
<script lang="ts">
// import a reference to our ItemInterace
import type { ItemInterface } from '../../models/items/Item.interface'
// import a reference to our Item component
import ItemComponent from './children/Item.component.svelte'
// import a reference to our Loader component:
import Loader from '../shared/Loader.component.svelte'
<div>
<h3>My Items:</h3>
{#if loading}
<Loader />
{/if}
{#if !loading}
<ul>
{#each items as item}
<ItemComponent item={item} on:selectItem={selectItem} />
{/each}
</ul>
{/if}
</div>
<style>
ul {
padding-inline-start: 0;
Chapter 6 - State Management 64
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 0px;
}
</style>
Note: we also removed the - loading: {loading} from within the <h3> element
Save the file and the refreshed the web page will show the loader bouncing for about 1 second
before it renders the items:
Then the loader will hide and the items list is rendered:
Congratulations on completing this chapter and learning how to build a state manager
organized into domains to easily manage the application state in a consistent and predictable
way. It’s a long chapter, the concepts outlined here require a lot of code to implement, and
not everyone gets through it in a straightforward fashion the first time around. In the next
chapters we will try to improve this code even more so arm yourself with a lot of patience!
Chapter 6 - State Management 65
Chapter 6 Recap
What We Learned
• How to create a centralized state manager organized into modules, leveraging Svelte
writable store
• How to use our state manager to update our Items state
• How to create actions that will update our state
• How to invoke state actions from our components
• How to use a loading property on our state to provide feedback to the user about long-
running processes through a loader (animation)
• How to create a simple and reusable Loader component
Observations
• We are still using hard-coded data (mockItems within the actions in the store/item-
s/Items.store.ts file), instead of loading the data through an API client
Based on these observations, there are a few improvements we will make in the next chapters:
Improvements
• Create an API client that can serve mocked data for quick front-end development and
prototyping, and an API client that can communicate with real API end-points
Chapter 7 - Api Client
So far we have worked by manipulating the app state/data through our state manager (store).
However, we are still ”pretending” to load data by using a mockItems variable with hard-
coded mock data within our loadItems action, and using the setTimeout trick to add a bit
of delay before returning the data (so we have at least 1 second to show our Loader to the
user).
In the real world, we’ll be most likely writing a component that has to load the data from
a server-side API end-point. At the same time, we do not want to lose our ability to do
quick prototyping and development of our front-end, even if the server-side API has not
been developed yet. Now there are different ways of accomplishing this. Some people like
to use mock data returned by a real API (there are packages and services out there that do
just this²⁸). Others prefer to have 2 different implementations for each API client, one that
returns the mocked data (either by loading from disk or invoking a mocked API service),
and one that returns the live data from the real server API. We’ll be implementing the latter
pattern in this chapter so we have better control on our data and also have better control on
different scenarios.
Another pattern is to create a separate API client for each area of our application. This will
enable for better separation of concerns, avoid code cluttering, more easily write unit tests
against each client. This is the pattern we’ll be following in this book, but remember this is
not the only way to accomplish this. You should always evaluate what is the best solution
for your specific requirements and evaluate that it fits your needs.
You should also read about Domain Driver Design, even though this book is not strictly
following DDD principles, still the overall idea here is to try to keep code organized by
application domain.
API Client module will read the custom environment variable called VITE_API_CLIENT
and there are two possible outcomes:
Domains
We’ll create a global ApiClient that wraps additional clients organized by application
domain. Our ApiClient will have for example a property called items which is the actual
API client for the Items domain. As our application grows, we’ll be adding more domains
specific API clients.
Our goal is to eventually consume our API client code from our store in this way:
apiClient
.items
.fetchItems()
Here we have an instance of our main ApiClientInterface. We then access its items property
which is the domain-specific API client (of type ItemsApiClientInterface) and call its
methods or access its properties.
Later, if for example need to add a new people domain, we will add a people property to our
main ApiClientInterface that points to an instance of PeopleApiClientInterface. Then we
will be able to call its methods like this:
apiClient
.people
.fetchPeople()
As you can see, this makes the code much more concise and readable.
NOTE: This might seem to complicate things at first. However, remember that the scope of
this book is to build a foundation for large-scale applications. Our primary goal is a solid
code organization and structuring to avoid cluttering as the code might grow very large with
many files.
// file: src/api-client/models/ApiClient.interface.ts
/**
* @Name ApiClientInterface
* @description
* Interface wraps all api client modules into one places for keeping code organized.
*/
export interface ApiClientInterface {
items: ItemsApiClientInterface
}
As you can see in the code above, our ApiClient will have a property called items of type
ItemsApiClientInterface, which will be the API client specific to the Items domain.
Now let’s create the the Items API client.
• index.ts
• ItemsApiClient.interface.ts
• ItemsApiClient.model.ts
• ItemsApiClientOptions.interface.ts
Following is the the description and code for each of the files.
ItemsApiClientOptions.interface.ts
In order to avoid using hard-coded strings, and to enforce type-checking at development
time, we’ll be using interface ItemsApiClientOptionsInterface for the values that indicates
the API end-points consumed by the ItemsApiClient. Also, we’ll have a mockDelay
parameter that we can use to simulate the delay when loading data from static json files.
Here is the code:
Chapter 7 - Api Client 71
// file: src/api-client/models/items/ItemsApiClientOptions.interface.ts
/**
* @Name ItemsApiClientEndpoints
* @description
* Interface for the Items urls used to avoid hard-coded strings
*/
export interface ItemsApiClientEndpoints {
fetchItems: string
}
/**
* @Name ItemsApiClientOptions
* @description
* Interface for the Items api client options (includes endpoints used to avoid hard\
-coded strings)
*/
export interface ItemsApiClientOptions {
mockDelay?: number
endpoints: ItemsApiClientEndpoints
}
ItemsApiClient.interface.ts
This is the interface for our ItemsApiClient. Our interface requires implementing a method
called fetchItems the will return a list of items. Here is the code to paste into ItemsApi-
Client.interface.ts:
// file: src/api-client/models/items/ItemsApiClient.interface.ts
/**
* @Name ItemsApiClientInterface
* @description
* Interface for the Items api client module
*/
export interface ItemsApiClientInterface {
fetchItems: () => Promise<ItemInterface[]>
}
Chapter 7 - Api Client 72
ItemsApiClient.model.ts
This is the model (class) for our ItemsApiClient which implements our Items API client
interface.
For the initial version of this, we will be using a third-part open-source NPM package called
axios. This is just a library that allows to make Ajax call in a much easier way. Let’s go
back to the terminal, from within my-svelte-project directory, and install axios with the
command:
NOTE: we will improve this even more later to avoid having references to a third-party
NPM package spread throughout the code. Also note, we are showing here to use a 3rd
party package like axios on purpose, instead of the browser built-in fetch api, to show in
later chapters how we should always try to abstract and encapsulate dependencies to avoid
polluting our code.
Back to the editor, open ItemsApiClient.model.ts and start importing all the things we need:
// file: src/api-client/models/items/ItemsApiClient.model.ts
...
// file: src/api-client/models/items/ItemsApiClient.model.ts
...
/**
* @Name ItemsApiClientModel
* @description
* Implements the ItemsApiClientInterface interface
*/
export class ItemsApiClientModel implements ItemsApiClientInterface {
private readonly endpoints!: ItemsApiClientEndpoints
private readonly mockDelay: number = 0
constructor(options: ItemsApiClientOptions) {
this.endpoints = options.endpoints
if (options.mockDelay) {
this.mockDelay = options.mockDelay
}
}
fetchItems(): Promise<ItemInterface[]> {
return new Promise<ItemInterface[]>((resolve) => {
const endpoint = this.endpoints.fetchItems
// axios options
const options: AxiosRequestConfig = {
headers: {
}
}
axios
.get(endpoint, options)
.then((response: AxiosResponse) => {
if (!this.mockDelay) {
resolve(response.data as ItemInterface[])
} else {
setTimeout(() => {
resolve(response.data as ItemInterface[])
}, this.mockDelay)
}
})
.catch((error: any) => {
console.error('ItemsApiClient: HttpClient: Get: error', error)
Chapter 7 - Api Client 74
})
})
}
}
// file: src/api-client/models/items/index.ts
• mock (this will contain our mock implementations to return static json data)
• live (this will contain the implementation that call the real API end-points)
We’ll be writing a mock implementation of our ApiClientInterface and its child Item-
sApiClientInterface. We’ll be also instantiating either the mock or live api-client based
on config..
Within the mock directory, add a child directory called items, and within that one create a
new file named index.ts. Your directory structure should look like this:
Chapter 7 - Api Client 75
// file: src/api-client/mock/items/index.ts
import {
ItemsApiClientOptions,
ItemsApiClientInterface,
ItemsApiClientModel
} from '../../models/items'
// instantiate the ItemsApiClient pointing at the url that returns static json mock \
data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(options)
Here we import all our interfaces and models, then we instantiate a variable called options
Chapter 7 - Api Client 76
of type ItemsApiClientOptions that holds the API end-points values and the mockDelay
option. In this case, since this is the mock implementation, for fetchItems we will point
to some static json file with the mock data. Note that we have only fetchItems, but we
could have multiple end-points. For now we’ll focus only on returning data. Later, in more
advanced chapter I’ll show you how to do something similar for CRUD operations.
We then create an instance of our ItemsApiClient class by passing our options instance into
the constructor (as you can see, later in our live implementation we’ll pass an instance of
ItemsApiClientOptions that contains the paths/urls to the real end-points)
Finally, we just export our instance called itemsApiClient.
Now let’s move one directory up, under src/api-client/mock and create another index.ts
file here. Your directory structure should look like this:
// file: src/api-client/mock/index.ts
// create an instance of our main ApiClient that wraps the mock child clients
const apiMockClient: ApiClientInterface = {
items: itemsApiClient
}
export {
apiMockClient
}
This is the mock implementation of our main ApiClient that wraps that items client.
Here we import our ApiClientInterface interface, and our mock instance of ItemsApiClient.
We then create an instance of our ApiClientInterface that is called apiMockClient because
it will use the mock implementation of the ItemsApiClient.
Similar to what we did with our mock api client, we’ll be implementing the live api client
now. Note that the live directory structure will be the same as the mock directory structure.
Create directory src/api-client/live/items and here add a new file named index.ts. Your
directory structure should look like this:
// file: src/api-client/live/items/index.ts
import {
ItemsApiClientOptions,
ItemsApiClientInterface,
ItemsApiClientModel
} from '../../models/items'
// instantiate the ItemsApiClient pointing at the url that returns live data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(options)
NOTE: this code is almost exactly the same as the mock client. The only difference is the
fetchItems property that here says for now ”/path/to/your/real/api/end-point”. You’ll replace
this with the actual value of your real server API end-point url/path. If you do not have one
yet, leave the current value as a place holder and updated once in the future you’ll have your
server API ready.
Now let’s move one directory up, under src/api-client/live and create another index.ts file
here. Your directory structure should look like this:
Chapter 7 - Api Client 79
// file: src/api-client/live/index.ts
// create an instance of our main ApiClient that wraps the live child clients
const apiLiveClient: ApiClientInterface = {
items: itemsApiClient
}
This code is also almost identical to the related mock index.ts file. The only exceptions are:
Environment Variables
Since Vite uses dotenv²⁹ to load environment variables, we’ll have to create two .env files³⁰
at root of your src directory:
# file src/.env.dev
VITE_API_CLIENT=mock
# file src/.env.production
VITE_API_CLIENT=live
You might have to add declarations for the import.meta.env types within the src/vite-
env.d.ts file³¹:
// file: src/vite-env.d.ts
interface ImportMeta {
readonly env: ImportMetaEnv
}
²⁹https://github.com/motdotla/dotenv
³⁰https://vitejs.dev/guide/env-and-mode.html#production-replacement
³¹https://vitejs.dev/guide/env-and-mode.html#intellisense-for-typescript
Chapter 7 - Api Client 81
NOTE: Only variables prefixed with VITE_ are exposed to the Vite-processed code.³²
We’ll be now able to access the value of our environment variables in TypeScript with
import.meta.env (i.e. import.meta.dev.VITE_API_CLIENT). Before we can do this, we need
to do one final change in our package.json scripts configurationso it will correctly set the
expected environment variables when running locally for development with npm start, or
when building for production with npm run build. The current content of your script section
should be like this:
file: package.json
...
"scripts": {
"start": "npm run dev",
"dev": "vite --mode mock", // here add --mode mock
"build": "vite build --mode production", // here add --mode production
...
},
...
Optional: You could also add a build-mock command that uses the mock api client, if you are
do not plan to have a real API in your project, or maybe to test new front-end functionality
in production when the server API is not yet ready:
Note: when running the app, if you make a change to the –mode value in the package.json,
or the values within the .env files, you’ll have to stop it with CTRL+C and restart with npm
start for changes to take into effect.
One last thing: we put our .env files within the src/ directory for now. To make sure Vite is
aware of where they are, open the vite.config.js file and add the envDir option with the
following value:
³²import.meta.env
Chapter 7 - Api Client 82
// file: vite.config.js
/// <reference types="vite/client" />
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
envDir: './src/' // <-- make sure this is there
})
To test that the configuration is working, temporarily modify the App.svelte code to ourput
all the content of the import.meta.env like this:
// file: src/App.svelte
...
<main>
<div class="home">
[{JSON.stringify(import.meta.env)}] <!-- add this to output the current content \
of import.meta.env -->
...
Stop the app with CTRL+C and run it again with npm start. Verify that in the browser our
App.svelte renders something like this at the top:
[{"VITE_API_CLIENT":"mock","BASE_URL":"/","MODE":"mock","DEV":true,"PROD":false, ...
As you can see, our VITE_API_CLIENT environment variable contains the correct value
“mock” and we are able to read this in our views or other client-side code.
Now remove the code we just added to App.svelte and let’s proceed creating our Api Client
Provider.
find easier to drive this with different configuration files). Create an the file at the root of
src/api-client:
// file: src/api-client/index.ts
Now we will add some code that will export either the mock or live clients based on the
VITE_API_CLIENT environment variable. The complete code will look like this:
// file: src/api-client/index.ts
...
} else {
// default is always apiMockClient
apiClient = apiMockClient
}
export {
apiClient
}
Now let’s proceed to update our store to consume the data from our newly created Api
Client.
// src/store/items/Items.store.ts
...
// import a reference to our apiClient instance
import { apiClient } from '../../api-client'
...
Then, within the loadItems action, remove the hard-coded mockItems variable and its data.
Then remove the setTimeout lines with the call to commit(setItems(mockItems)).
Replace the loadItems code with a call to apiClient.items.fetchItems and this time dispatch/-
commit setItems passing it the data returned by our fetchItems:
Chapter 7 - Api Client 85
// src/store/items/Items.store.ts
...
We also need to create a data folder from where our mock api-client will load the static json
files.
If you remember, earlier during our Mock Api Client implementation we set the urls
fetchItems end-point path to be /static/mock-data/items/items.json.
We need to create a directory called static under our public folder, because that is what app
considers our root directory to be when running the application. Within the static directory
create a sub-directory called mock-data, within mock-data add one more sub-directory
called items. Here create a file called items.json.
Within the items.json files and paste in the following data:
# File: public/static/mock-data/items/items.json:
[{
"id": 1,
"name": "Item 1",
"selected": false
}, {
"id": 2,
"name": "Item 2",
"selected": false
}, {
"id": 3,
"name": "Item 3",
"selected": false
}, {
"id": 4,
"name": "Item 4",
Chapter 7 - Api Client 87
"selected": false
}, {
"id": 5,
"name": "Item 5",
"selected": false
}]
Make sure there are no errors in the terminal. If needed stop it with CTRL-C and run again
with npm start. The browser should display a loader, then render our items list as before,
but this time should display 5 items (because the data now is loaded through our Api client
from the file public/static/mock-data/items/items.json):
Notice how powerful is this pattern we just implemented as it allows us to easily build our
front-end components in isolation without a real API, and later everything we’ll just work
with a live API client that returns the same data structure as our static json data.
Alternatives
There are other ways in which you could use a mocked API. There are services or libraries
out there that can help you build a mocked API like Miragejs or JSONPlaceHolder³³, and
you could simplify the code here by having only one apiClient that uses either mock or live
API end-points based on environment variables only etc. Some of these alternatives require
running an additional server app that will serve your mocked API.
I opted to show you how you can do this using static .json files that are located in the same
project under public/static/mock-data as this gives you a lot of flexibility to play around
with different things when you are starting out. The other thing is that by having a specific
³³JsonPlaceHolder or miragejs for example
Chapter 7 - Api Client 88
implementation of the mock apiClient you do not have to necessarily return the .json files,
but you could simulate fake responses or pretend to have saved or deleted an item without
actually modifying any static data (so it will be just in memory, and when you refresh the
web browser the data would be reloaded as in its original state).
Additionally, this gives you the flexibility to use either: static JSON files, or maybe for the
url end points use something like Miragejs etc for some of the API clients.
You can research alternatives as you see fit and make the decision you feel works better
for you, but remember you are not confined to one way or another if you keep following
the patterns I am showing you in this book. Indeed, let me finish by adding a few more
instructions on how to use for example an NPM package called json-server.
Now let’s rename the vite.config.ts file to vite.config.jsonserver.ts. Make 2 more copies of
this file and name one vite.config.mock.ts and the other vite.config.production.ts:
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
envDir: './src/',
Chapter 7 - Api Client 89
resolve: {
alias: {
// @ts-ignore
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
origin: 'http://localhost:3000',
open: 'http://localhost:3000'
},
test: {
globals: true,
environment: 'jsdom',
exclude: [
'node_modules'
]
}
})
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
envDir: './src/',
resolve: {
alias: {
// @ts-ignore
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
globals: true,
environment: 'jsdom',
exclude: [
'node_modules'
Chapter 7 - Api Client 90
]
}
})
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
envDir: './src/',
resolve: {
alias: {
// @ts-ignore
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
origin: 'http://localhost:3000',
open: 'http://localhost:3000',
proxy: {
'/jsonserver': {
target: 'http://localhost:3111',
changeOrigin: true,
secure: false,
ws: false,
rewrite: (path) => path.replace(/^\/jsonserver/, '')
}
}
},
test: {
globals: true,
environment: 'jsdom',
exclude: [
'node_modules'
]
Chapter 7 - Api Client 91
}
})
Note how the main difference in the vite.config.jsonserver.ts is the addition of the proxy
section:
...
proxy: {
'/jsonserver': {
target: 'http://localhost:3111',
changeOrigin: true,
secure: false,
ws: false,
rewrite: (path) => path.replace(/^\/jsonserver/, '')
}
}
...
This is telling Vite to proxy all the requests for endpoints that start with /jsonserver to the
url http://localhost:3111 (this is where the json-server API will run from)
Modify tsconfig.node.json include section like this (if you don’t have this file, please create
it):
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": [
"vite.config.jsonserver.ts",
"vite.config.mock.ts",
"vite.config.production.ts"
]
}
{
...
Modify the script section of the package.json file to have two additional commends:
• with-jsonserver (we’ll use this to run the app using the vite.config.jsonserver.ts)
• json-server-api (with this we’ll start json-server on port 3111)
like this:
"scripts": {
"dev": "vite --config vite.config.mock.js --mode mock",
"build": "vite build --config vite.config.production.js --mode production",
"build-beta": "vite build --config vite.config.production.js --mode beta",
"build-mock": "vite build --config vite.config.mock.js --mode mock",
"preview": "vite preview --config vite.config.mock.js --mode mock",
"start": "npm run dev",
"start-local": "vite --config vite.config.production.js --mode localapis",
"with-jsonserver": "vite --config vite.config.jsonserver.js --mode mock",
"json-server-api": "json-server --port 3111 --watch json-server/db.json",
...
// file: src/json-server/db.json
{
"items": [
{
"id": 1,
"name": "Item 1 from json-server",
"selected": false
},
{
"id": 2,
"name": "Item 2 from json-server",
"selected": false
Chapter 7 - Api Client 93
},
{
"id": 3,
"name": "Item 3 from json-server",
"selected": false
},
{
"id": 4,
"name": "Item 4 from json-server",
"selected": false
},
{
"id": 5,
"name": "Item 5 from json-server",
"selected": false
}
]
Now to finally test it, temporarily modify the file src/api-client/mock/items/index.ts to use
/jsonserver/items for the fetchItems url:
// file: src/api-client/mock/items/index.ts
...
...
Note: we’ll drive the API urls end-points through a much better configuration strategy in the
next chapters.
Now stop the app, and this time open 2 terminal windows:
• in terminal one, execute npm run json-server-api (this will run json-server API on
port 3111)
Chapter 7 - Api Client 94
• in therminal two, execute npm run with-jsonserver (this will start our app but tell
Vite to use the vite.config.jsonserver.ts which contains our proxy configuration)
NOTE: Do not forget to revert your change for the URL end-point within the file
src/api-client/mock/items/index.ts. Later, when we introduce the apllication configuration
in the next chapters, we’ll drive the end-points from configuration and will not have to
modify eny code to test different environments.
Chapter 7 - Api Client 95
Chapter 7 Recap
What We Learned
• How to implement an apiClient that automatically can serve either mock or real data
depending on environment variables configuration
• How to continue enforcing type checking at development time with TypeScript inter-
faces and models
• How to structure directories and files in an organized way
• How to invoke our api client from the store
Observations
• We have a reference to a third NPM package (axios) in our ItemsApiClient mode and if
we keep following this pattern we’ll keep polluting new api client implementations for
different areas with references to this NPM package in several parts of our code. This
will cause a build up in technical debt that will make it harder to later replace axios
with something else one day we’ll have to. This might happen either because axios will
no longer be supported, or maybe better NPM packages will be available that we want
o use in its place. Either way, we should structure our code in a way so that we can
more easily replace axios with something else without having to change a lot of code
in too many places.
Based on these observations, there are a few improvements that will be making into the next
two chapters:
Improvements
• Create an HttpClient model that implements an HttpClientInterface where we can
encapsulate the reference to axios all in one place and make it easier to change later if
we find the need to use a different NPM package.
Chapter 8 - Enhance the Api Client
From the previous chapter recap, we observed that the ItemsApiClient contains hard-coded
references to the axios NPM package. We understand that is not a good practice to follow
as, when adding more API clients, we do not want to have references to a 3rd party NPM
packages spread throughout our code.
Imagine if we had built a huge code base with many components and state modules and
now we wanted to using something like Fetch Api³⁴ or another library insteaf of axios. We
would have to replace all the calls that use axios in our entire code base.
What we need to do is abstract the http client methods into their own implementation that
we can then consume from our ItemsApiClient and future API clients implementations that
we’ll be adding later.
There are multiple ways we could do this, but the most straigh-forward way is to create a
class that wraps our calls done with axios in one place. We’ll call this the HttpClient class
and here we’ll implement code that allow us to perform http requests using axios for now. If
later we have to switch to a different NPM library or use the Fetch API etc, we’ll jsut need
to update the code without our HttpClient. Ass long as we do not change the signature of
our HttpClient methods, everything should still work as before without having to change
the code that consumes our HttpClient throughout our application.
Here I will show you how this pattern works by offering both an implementation that uses
axios and one that uses the browser Fetch API. Then in the net chapter will drive which
client we use through the app configuration.
• Constants.ts
• HttpRequestParams.interface.ts
• UrlUtils.ts
• HttpClient.interface.ts
• HttpClient.axios.ts
³⁴https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
Chapter 8 - Enhance the Api Client 97
• HttpClient.fetch.ts
• index.ts
Following is the the description and code for each of the files.
Constants.ts
Within the Constants.ts file, we’ll add an enum representing the type of http request we
want our HttpClient to execute. For now we just add the 4 most common http verbs:
get/post/put/delete:
Chapter 8 - Enhance the Api Client 98
// file: src/http-client/models/HttpRequestType.ts
/**
* @name HttpRequestType
* @description
* The type of http request we need to execute in our HttpClient request method
*/
export const enum HttpRequestType {
get,
post,
put,
delete,
patch
}
...
We’ll also add two readonly objects to avoid using hard-coded strings later:
// file: src/http-client/models/HttpRequestType.ts
...
HttpRequestParams.interface.ts
The HttpRequestParamsInterface will allow us to pass parameters to the HttpClient request
method. These are things like the type of request (GET/POST/etc), the API endpoint, an
Chapter 8 - Enhance the Api Client 99
optional payload (if POST or PUT), and a flag that indicates if the request must include an
authentication token.
// file: src/http-client/models/HttpRequestParams.interface.ts
/**
* @name HttpRequestParamsInterface
* @description
* Interface represents an object we'll use to pass arguments into our HttpClient re\
quest method.
* This allow us to specify the type of request we want to execute, the end-point ur\
l,
* if the request should include an authentication token, and an optional payload (i\
f POST or PUT for example)
*/
export interface HttpRequestParamsInterface<P = void> {
requestType: HttpRequestType
endpoint: string
requiresToken: boolean
headers?: { [key: string]: string }
payload?: P
mockDelay?: number
}
NOTE: With **P ** we are trying to enfore more type-checking with TypeScript when we’ll
consume this, at the same time we need to add as P = void as this is not always required.
UrlUtils.ts
This mainly contains an helper to dynamically build urls with parameters:
Chapter 8 - Enhance the Api Client 100
// file: src/http-client/models/UrlUtils.ts
export interface UrlUtilsInterface {
getFullUrlWithParams(baseUrl: string, params: { [key: string]: number | string }):\
string
}
Note: you could alternatively implement getFullUrlWithParams using the JavaScript built-in
Url.
HttpClient.interface.ts
The HttpClientInterface is the interface that defines the methods that the HttpClient will
have to implement. There will be only one method called request which can execute different
types of http request based on the parameters argument provided, and returns a Promise³⁵
with the results (if any):
³⁵https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
Chapter 8 - Enhance the Api Client 101
// files: src/http-client/models/HttpClient.interface.ts
/**
* @name HttpClientConfigInterface
* @description
* We'll drive the HttpClient from configuration in later chapters.
*/
export interface HttpClientConfigInterface {
tokenKey: string
clientType: string
}
/**
* @name HttpClientInterface
* @description
* Represents our HttpClient.
*/
export interface HttpClientInterface {
/**
* @name request
* @description
* A method that executes different types of http requests (i.e. GET/POST/etc)
* based on the parameters argument.
* The type R specify the type of the result returned
* The type P specify the type of payload if any
* @returns A Promise<R> as the implementation of this method will be async.
*/
request<R, P = void>(parameters: HttpRequestParamsInterface<P>): Promise<R>
}
Note: in the code above the request method can take 2 generic types. The first one, R, define
the type of the result/data returned. The second, P, is optional and defines the type of the
payload (if any) passed with the parameters argument.
HttpClient.axios.ts
The HttpClientAxios is the class that implements our HttpClientInterface using axios.
Since the code here is longer, let me split in multiple parts.
First the import section:
Chapter 8 - Enhance the Api Client 102
// file: src/http-client/models/HttpClient.axios.ts
import axios, {
AxiosRequestConfig,
AxiosResponse
} from 'axios'
/**
* @name HttpClientAxios
* @description
* Wraps http client functionality to avoid directly using a third party npm package\
like axios
* and simplify replacement in the future if such npm package would stop being devel\
oped or other reasons
*/
export class HttpClientAxios implements HttpClientInterface {
constructor() {
// OPTIONAL for now: Add request interceptor to handle errors or other things fo\
r each request in one place
}
/**
* @name request
* @description
* A method that executes different types of http requests (i.e. GET/POST/etc)
* based on the parameters argument.
* The type R specify the type of the result returned
* The type P specify the type of payload if any
* @returns A Promise<R> as the implementation of this method will be async.
*/
async request<R, P>(parameters: HttpRequestParamsInterface<P>): Promise<R> {
// use destructuring to extract our parameters into local variables
const { requestType, endpoint, requiresToken, payload, headers, mockDelay } = pa\
rameters
// use helper to build the fullUrl with request parameters derived from the payl\
Chapter 8 - Enhance the Api Client 103
oad
const fullUrl = UrlUtils.getFullUrlWithParams(endpoint, payload as any)
console.log('HttpClientAxios: fullUrl: ', fullUrl, payload)
if (headers) {
options.headers = {
//...options.headers,
...headers
}
}
let result!: R
try {
switch(requestType) {
default: {
console.warn('HttpClientAxios: invalid requestType argument or request typ\
e not implemented')
}
}
} catch (e) {
console.error('HttpClientAxios: exception', e)
throw Error('HttpClientAxios: exception')
}
if ((mockDelay || 0) > 0) {
Chapter 8 - Enhance the Api Client 104
return result
}
}
Note how we added a constructor placeholder, but not doing anything with it yet. Later, you
could add things like request interceptors within the contructor so you can log or capture
errors in one place. One more thing to notice is that we are using a try/catch block and just
log the error in the console, but we are not gracefully rejecting the promise return by our
request method. You are welcome to enhance and improve this code as you see fit based on
your sepcific requirements.
The implementation of the request method starts by destructuring our request parameters,
creates the fullUrl, setting some axios options, optionally setting an Authorization header
(commented out for now, but to show how you can do that if you need it), and a switch
statement that will execute the type of request we want. Let’s implement now the different
type of requests within each case block of our switch statement.
The get implementation:
// file: src/http-client/models/HttpClient.axios.ts
...
...
// file: src/http-client/models/HttpClient.axios.ts
...
...
...
...
...
...
// file: src/http-client/models/HttpClient.axios.ts
...
...
HttpClient.fetch.ts
The HttpClientFetch is the class that implements our HttpClientInterface using fetch. Since
the code here is longer, let me split in multiple parts.
// file: src/http-client/models/HttpClient.fetch.ts
import { HttpRequestParamsInterface } from './HttpRequestParams.interface'
import { HttpClientInterface, HttpClientConfigInterface } from './HttpClient.interfa\
ce'
import { HttpRequestType, HttpRequestMethods, HttpContentTypes } from './Constants'
import { UrlUtils } from './UrlUtils'
/**
* @name HttpClientFetch
* @description
* Wraps http client functionality to avoid directly using fetch
* and simplify replacement in the future if such npm package would stop being devel\
oped or other reasons
*/
export class HttpClientFetch implements HttpClientInterface {
constructor() {
// OPTIONAL for now: Add request interceptor to handle errors or other things fo\
r each request in one place
}
/**
* @name request
* @description
Chapter 8 - Enhance the Api Client 107
// use helper to build the fullUrl with request parameters derived from the payl\
oad
const fullUrl = UrlUtils.getFullUrlWithParams(endpoint, payload as any)
console.log('HttpClientFetch: fullUrl: ', fullUrl, payload)
if (headers) {
options.headers = {
...headers
}
}
if (!options.headers?.hasOwnProperty('Content-Type')) {
// default to content-type json
options.headers = {
...headers,
'Content-Type': HttpContentTypes.applicationJson
}
}
let result!: R
try {
switch (requestType) {
default: {
console.warn('HttpClientFetch: invalid requestType argument or request typ\
e not implemented')
}
}
} catch (e) {
//console.error('HttpClientFetch: exception', e)
throw Error('HttpClientFetch: exception')
}
if ((mockDelay || 0) > 0) {
return new Promise<R>((resolve) => {
setTimeout(() => {
resolve(result)
}, mockDelay)
})
}
return result
}
}
The implementation of the request method starts by destructuring our request parameters,
creates the fullUrl, setting some fetch options, optionally setting an Authorization header
(commented out for now, but to show how you can do that if you need it), and a switch
Chapter 8 - Enhance the Api Client 109
statement that will execute the type of request we want. Let’s implement now the different
type of requests within each case block of our switch statement.
The get implementation:
// file: src/http-client/models/HttpClient.fetch.ts
...
...
// file: src/http-client/models/HttpClient.fetch.ts
...
...
// file: src/http-client/models/HttpClient.fetch.ts
...
...
// file: src/http-client/models/HttpClient.fetch.ts
...
...
// file: src/http-client/models/HttpClient.fetch.ts
...
...
// file: src/http-client/models/index.ts
This file contains the export of a hook called useHttpClient which will return a singleton
instance of our HttpClient. This is what we’ll be consuming in our API client. For now, we’ll
create an instance of the HttpClient.fetch implementation, but in later chapters we’ll drive
this from configuration (appConfig):
// file: src/http-client/index.ts
// if you'd like to use axios, set "clientType": "axios" within the config files\
httpClient section
if (clientType === 'fetch') {
_httpClient = new HttpClientFetch()
} else if (clientType === 'axios') {
Chapter 8 - Enhance the Api Client 113
// file: src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts
expect('https://unit-test-api/v1/domain/5346782/abcde23').toEqual(result)
})
expect('https://unit-test-api/v1/domain/USA/NY/gtref345ytr').toEqual(result)
})
})
Chapter 8 - Enhance the Api Client 114
HttpClientAxios tests
// file: src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts
httpClient
.request(mockRequestParams)
.then((response) => {
//console.debug('response:', response)
expect(response).toEqual(`request completed: ${mockRequestParams.endpoint}`)
})
.catch((error) => {
Chapter 8 - Enhance the Api Client 115
...
// file: src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts
...
...
httpClient.request(mockRequestParams).catch((error) => {
expect(error).toBeDefined()
expect(error.toString()).toEqual('Error: HttpClientAxios: exception')
})
})
})
Within the directory tests/unit/http-client/axios-client directory and add a new file called
AxiosClient.request.post.test.ts. Within the file, paste the following code:
Chapter 8 - Enhance the Api Client 116
// file: src/tests/unit/http-client/axios-client/AxiosClient.request.post.test.ts
httpClient
.request<string, P>(mockRequestParams)
.then((response) => {
//console.debug('response:', response)
expect(response).toEqual(`request completed: ${mockRequestParams.endpoint}`)
})
.catch((error) => {
console.info('AxiosClient.request.post.test.ts: post error', error)
})
})
})
Note: you can keep adding more test in a similar way for the rest of the request type like
PUT/DELETE/etc
Chapter 8 - Enhance the Api Client 117
HttpClientFetch tests
// file: src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts
// could not find an easy way to use spyOn for fetch so overriding global.fetch
// save original fetch
const unmockedFetch = global.fetch || (() => {})
global.fetch = unmockedFetch
const expectedResult = {
result: `request completed: ${mockRequestParams.endpoint}`
}
vitest
.spyOn(global, 'fetch')
.mockImplementation(async () => Promise.resolve({
redirected: false,
json: () => Promise.resolve(JSON.stringify(expectedResult))
} as any))
try {
const response = await httpClient.request(mockRequestParams)
Chapter 8 - Enhance the Api Client 118
expect(response).not.toBeNull()
expect(response).toEqual(expectedResult)
} catch (error) {
console.info('AxiosClient.request.get.test.ts: error', error)
}
// restore globa.fetch
global.fetch = unmockedFetch
})
vitest
.spyOn(global, 'fetch')
.mockImplementation(async () => Promise.reject())
httpClient.request(mockRequestParams).catch((error) => {
expect(error).toBeDefined()
expect(error.toString()).toEqual('Error: HttpClientFetch: exception')
})
})
})
And so on, you can keep adding more unit tests for each request type like you did for the
axios-client.
We can finally change our ItemApiClient so it uses our newly implemented HttpClient
instead of axios.
ItemsApiClientModel Update
Open the file src/api-client/models/items/ItemsApiClient.model.ts.
Remove the import axios line and replace it with an import for our HttpClient instance and
the HttpRequestParamsInterface:
Chapter 8 - Enhance the Api Client 119
// file: src/api-client/models/items/ItemsApiClient.model.ts
// file: src/api-client/models/items/ItemsApiClient.model.ts
...
fetchItems(): Promise<ItemInterface[]> {
const requestParameters: HttpRequestParamsInterface = {
requestType: HttpRequestType.get,
endpoint: this.endpoints.fetchItems,
requiresToken: false,
mockDelay: this.mockDelay
}
return useHttpClient().request<ItemInterface[]>(requestParameters)
}
...
This creates a const variable to hold our HttpRequestParamsInterface parameters, and then
return the call to HttpClient.request (which is already a Promise, so we do not have to do
anything else here):
Now, make sure there are no errors in the terminal and the browser refreshes correctly and
load the data correctly.
Chapter 8 - Enhance the Api Client 120
Chapter 8 Recap
What We Learned
• How to abstract an http client into interfaces and models that are generic
• How to implement the HttpClientInterface into a model that encapsulate the use of
a 3rd party package in one place. We show this by implement two different clients:
HttpClientAxios and HttpClientFetch.
• How to use vitest.spyON for stubs so we can test the HttpClient request method
responses for different scenarios.
Observations
• We did not write unit tests against the HttpClient put/delete/patch methods
• We did not write unit tests against the ItemsApiClientModel
Based on these observations, there are a few improvements that you could make on your
own:
Improvements
• Add unit tests against the HttpClient put/delete/patch methods as well
• Add unit tests against the ItemsApiClient methods as well
• Experiment by adding another HttpClient implementation that uses another Ajax
library other than axios or fetch and then and modify the file src/http-client/index.ts
so that it instantiate this one instead of the axios or fetch implementation. Then verify
that the app still run as expected.
Chapter 9 - App Configuration
We need now to add a way to configure our app through configuration files for different
environments (i.e. mock, beta, production, etc).
NOTE: The code in this chapter is not just specific to Svelte. These concepts can be applied
to any front-end app (i.e. React/Vue/Svelte/Angular/etc).
As you recall from Chapter 7, we extended import.meta.env declaration types (file src/vite-
env.d.ts) to include a new variable called VITE_API_CLIENT. This currently drives the
selection of the API client at run time (mock or live). As you can imagine, as we add more
configuration, we might end adding a lot of new variables prefixed with VITE_. This works,
but can quickly become very hard to manage, especially for large configurations that will
drive many settings.
A better approach is to drive the entire configuration through only one variable that we
are going to call VITE_APP_CONFIG. We’ll store all the settings in dedicated JSON files.
We’ll have one configuration file for each environment (mock/beta/production/etc) and then
load that dynamically at run-time (or build time) based on our new VITE_APP_CONFIG
environment variable.
...
interface ImportMeta {
readonly env: ImportMetaEnv
}
// file: src/.env.mock
VITE_APP_CONFIG=mock
// file: src/.env.production
VITE_APP_CONFIG=production
// file: src/.env.jsonserver
VITE_APP_CONFIG=jsonserver
// file: src/.env.localapis
VITE_APP_CONFIG=localapis
// file: src/.env.beta
VITE_APP_CONFIG=beta
Note: remember that VITE read from the .env files based on the –mode flag specified in
the scripts shortcut (within the package.json file). Here we did not add any command for
localapis or beta. But you could add things like start-local or build-beta etc:
Chapter 9 - App Configuration 123
// file: package.json
...
"scripts": {
"start": "npm run dev",
"dev": "vite --config vite.config.mock.ts --mode mock",
"build": "vite build --config vite.config.production.ts --mode production",
"build-mock": "vite build --config vite.config.mock.ts --mode mock",
"build-beta": "vite build --config vite.config.production.ts --mode beta", /* yo\
u could add this */
"start-local": "vite --config vite.config.production.ts --mode localapis", /* yo\
u could add this */
"preview": "vite preview --config vite.config.mock.ts --mode mock",
"test": "vitest run --config vite.config.mock.ts --mode mock",
"test-watch": "vitest watch --config vite.config.mock.ts --mode mock",
"test-coverage": "vitest run --coverage --config vite.config.mock.ts --mode mock\
",
}
...
Config Interface
Create the directory src/config/models/ and under this directory create a file called
Config.interface.ts. This contains the declaration for our config interface. You will keep
expanding this as you add more settings or app domains (i.e. like Items), for now let’s just
have the interface contain four sections:
For the items section, we’ll have only the apiClientOptions child section for now. This will
be of type ItemsApiClientOptions.
Here is the code for the src/config/models/Config.interface.ts file:
Chapter 9 - App Configuration 124
// file: src/config/models/Config.interface.ts
import {
ItemsApiClientOptions // NOTE: we'll create this a bit later
} from '@/api-client/models'
/**
* @Name ConfigInterface
* @description
* Describes the structure of a configuration file
*/
export interface ConfigInterface {
global: {
// ... things that are not specific to a single app domain
version: number
}
httpClient: HttpClientConfigInterface,
apiClient: {
type: string
}
items: {
apiClientOptions: ItemsApiClientOptions
}
Config files
Now create a sub-directory called config-files under this directory. The full path for this
will be src/config/config-files/
Inside this directory, add 4 JSON files with the following names:
• mock.json
Chapter 9 - App Configuration 125
• jsonserver.json
• localapis.json
• beta.json
• production.json
The content of each file will have to match what is required by our ConfigInterface. In
a little bit we’ll be also adding some unit tests against this files to make sure they are as
expected.
Here is the content of each file:
mock.json
// file: src/config/config-files/mock.json
{
"global": {
"version": 0.103
},
"httpClient": {
"tokenKey": "myapp-token",
"clientType": "fetch"
},
"apiClient": {
"type": "mock"
},
"items": {
"apiClientOptions": {
"endpoints": {
"fetchItems": "/static/mock-data/items/items.json"
},
"mockDelay": 250
}
}
}
jsonserver.json
Chapter 9 - App Configuration 126
// file: src/config/config-files/jsonserver.json
{
"global": {
"version": 0.1
},
"httpClient": {
"tokenKey": "myapp-token",
"clientType": "fetch"
},
"apiClient": {
"type": "live"
},
"items": {
"apiClientOptions": {
"endpoints": {
"fetchItems": "/jsonserver/items"
},
"mockDelay": 0
}
}
}
localapis.json
// file: src/config/config-files/localapis.json
{
"global": {
"version": 0.1
},
"httpClient": {
"tokenKey": "myapp-token",
"clientType": "fetch"
},
"apiClient": {
"type": "live"
},
Chapter 9 - App Configuration 127
"items": {
"apiClientOptions": {
"endpoints": {
"fetchItems": "http://api.localhost:4111/items"
},
"mockDelay": 0
}
}
}
beta.json
// file: src/config/config-files/beta.json
{
"global": {
"version": 0.1
},
"httpClient": {
"tokenKey": "myapp-token",
"clientType": "fetch"
},
"apiClient": {
"type": "live"
},
"items": {
"apiClientOptions": {
"endpoints": {
"fetchItems": "/path/to/your/real/BETA/api/and-point"
},
"mockDelay": 0
}
}
}
production.json
Chapter 9 - App Configuration 128
// file: src/config/config-files/production.json
{
"global": {
"version": 0.1
},
"httpClient": {
"tokenKey": "myapp-token",
"clientType": "fetch"
},
"apiClient": {
"type": "live"
},
"items": {
"apiClientOptions": {
"endpoints": {
"fetchItems": "/path/to/your/real/PRODUCTION/api/and-point"
},
"mockDelay": 0
}
}
}
tsconfig.json updates
In the next section, we’ll be loading the individual config files through import statements.
In order to enable this in TypeScript, we have to modify the tsconfig.json file located
in the root of your project. We need to add option resolveJsonModule with true to the
compilerOptions section:
Chapter 9 - App Configuration 129
{
"compilerOptions": {
...
"resolveJsonModule": true, /* this allows to import .json file as if they were .\
ts files: using to load config files */
}
...
NOTE: your tsconfig.json might already have the resolveJsonModule flag, if so just make sure
that is set to true.
// file: src/config/config-files-map.ts
Config provider
File utils.ts
Add a new file called utils.ts under src/config. Here we implement an helper function called
getAppConfigKey that will return the value of our VITE_APP_CONFIG environment
variable.
// file: src/config/utils.ts
Note that by wrapping this in one place, we could easily re-use this code in a project created
with webpack. Since that uses process.env instead of Vite’s import.meta.env we just need to
uncomment the related code above and comment out the one that uses import.meta.env.
// file: src/config/index.ts
// returns appropriate config based on env VITE_APP_CONFIG
...
Then add a check and throw and error if our map does not contain an entry for the current
environment key:
// file: src/config/index.ts
...
if (!configFilesMap.has(getAppConfigKey())) {
throw Error(`Could not find config for VITE_APP_CONFIG key "${ getAppConfigKey() }\
"`)
}
...
// file: src/config/index.ts
...
// file: src/config/index.ts
// returns appropriate config based on env VITE_APP_CONFIG
// optional: you can console.log the content of import.meta.env to inspect its value:
console.log(`------ env ---- "${getAppConfigKey()}"`)
if (!configFilesMap.has(getAppConfigKey())) {
throw Error(`Could not find config for VITE_APP_CONFIG key "${getAppConfigKey()}"`)
}
Unit Tests
Le’s now write a few unit tests to validate that our config is being set as expected. This will
also validate that the config JSON files contains the expected data structure.
// file: src/tests/unit/config/config-files-map.test.ts
describe('configFilesMap', () => {
Note: Jest does not understand VITE’s import.meta.dev our of the box. There are discussion
on the web if you google this and some people are using plugins or specific babel config etc.
Here will simply do not test the config instance from the src/config/index.ts file but create a
new one in our unit tests
Vitest does not have this issue but I thought it was worth mentioning it.
To be safe in either case, we will write our unit tests by creating an instance of ConfigIn-
terface within the unit tests. We can easily do this by specifying the environment key as a
hard-coded string to the map.get method, i.e. configFilesMap.get('mock').
Chapter 9 - App Configuration 134
Tests config.mock.test.ts
Still under directory tests/unit/config add another file called config.mock.test.ts with the
following code:
// file: src/tests/unit/config/config.mock.test.ts
describe('global', () => {
const section = config.global
it('section should have "version" property', () => {
expect(section).toHaveProperty('version')
expect(typeof section.version).toBe('number')
expect(section.version).toBeGreaterThan(0)
})
})
describe('httpClient', () => {
const section = config.httpClient
it('section should have "tokenKey" property', () => {
expect(section).toHaveProperty('tokenKey')
})
expect(section).toHaveProperty('clientType')
})
})
describe('apiClient', () => {
const section = config.apiClient
it('section should have "type" property', () => {
expect(section).toHaveProperty('type')
})
})
describe('items', () => {
const section = config.items
describe('apiClientOptions', () => {
const apiClientOptions = section.apiClientOptions
describe('endpoints', () => {
const endpoints = apiClientOptions.endpoints
Run the unit tests with npm run test: and verify all succeed:
Chapter 9 - App Configuration 136
// terminal output:
...
...
Please keep adding additional unit tests for each environment (i.e. config.jsonserver.test.ts,
config.production.test.ts etc).
file src/http-client/index.ts
Now we need to update the file src/http-client/index.ts and remove the hard-coded value
for the clientType variable. We’ll be instead reading the value from the config instance
(config.httpClient.type):
// file: src/http-client/index.ts
// if you'd like to use axios, set "clientType": "axios" within the config files\
(section "httpClient")
if (clientType === 'fetch') {
_httpClient = new HttpClientFetch()
} else if (clientType === 'axios') {
_httpClient = new HttpClientAxios()
}
}
file src/api-client/index.ts
Update also the file src/api-client/index.ts and remove the code currently using the
previous env variable and replace it by consuming our value from the config instance
(config.apiClient.type). Here is the updated code:
// file: src/api-client/index.ts
export { apiClient }
Chapter 9 - App Configuration 138
file src/api-client/mock/items/index.ts
Update the code that returns the mock Items API client instance to use the apiClientOptions
from the config:
// file: src/api-client/mock/items/index.ts
import {
ItemsApiClientInterface,
ItemsApiClientModel
} from '../../models/items'
// instantiate the ItemsApiClient pointing at the url that returns static json mock \
data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(config.items\
.apiClientOptions) // <-- this time we'll pass the options from the config
file src/api-client/live/items/index.ts
Similarly, update the code that returns the live Items API client instance to use the
apiClientOptions from the config:
Chapter 9 - App Configuration 139
// file: src/api-client/live/items/index.ts
// instantiate the ItemsApiClient pointing at the url that returns static json live \
data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(config.items\
.apiClientOptions)
IMPORTANT: At this point, thanks to the new way of driving things through the
config, the code in both files src/api-client/live/items/index.ts and src/api-
client/mock/items/index.ts is basically identical. In later chapters we will simplify
and reduce the amount of code we initially created to serve either mock or live data. But for
now, we’ll keep the duplicated code to avoid making this chapter too long.
Now make sure you run all the unit tests again, then serve the app again to make sure all
compiles and works as before.
Chapter 9 - App Configuration 140
Chapter 9 Recap
What We Learned
• We learned how to use static JSON files to have multiple configuration settings, one for
each environment
• How to dynamically return the appropriate config file based on the new environment
variable VITE_APP_CONFIG
• How to add option resolveJsonModule to the the TypeScript tsconfig.json file, section
compilerOptions to allow importing static JSON files through import statement
• How to write unit tests against our configuration code
Observations
• For now our configuration is pretty small, but might grow larger as the application itself
grows and we need to add more configurable options.
• We did not write unit tests again each config file like we did for config.mock.test.ts
Improvements
• Going forward we’ll be expanding the configuration as we keep growing our application
components and logic.
• You can write additional unit tests similar to config.mock.test.ts for beta and production
as well (i.e. config.beta.test.ts, config.mock.production.ts, etc)
Chapter 10 - Localization and
Internationalization - Language
Localization
”Localization refers to the adaptation of a product, application or document content to meet
the language, cultural and other requirements of a specific target market (a locale)…”
”…Internationalization is the design and development of a product, application or document
content that enables easy localization for target audiences that vary in culture, region, or
language”³⁶
NOTE: This chapter applies to you only if the application you are working on will be used
or marketed to multiple countries and it is desired to present labels in the local language, as
well as date/number formats in the local culture.
Most modern applications that target multiple countries or cultures are architected in a way
that is easy to present the UI in different languages and also present values like numbers or
dates formatted as expected by the culture specific to that country (hence, localized).
In this book we’ll first leverage plugins that allows us to present labels in different languages
(i18next, svelte-i18n) and later we’ll add also a custom plugin based on the Intl API
(supported by most modern browsers) to provide for numbers/date formatting functionality
based on different locales (cultures).
In this book we’ll be creating wrapping additional code around the i18n initialization which
will allow us to avoid code cluttering and greatly simplify how we localize our components
in our Svelte application.
Let’s start by first adding the i18next and svelte-i18n NPM packages to our application.
We need to use the -save option as we want this to be saved as part of the app ”dependencies”
in the package.json:
Config updates
ConfigInterface
We will be introducing a concept of versioning here to dynamically drive different versions of
data, introduce/retire views and components overtime, or expire cached data on the browser.
You will see this in action first shortly when we’ll use to expire our translation data stored
in the browser cache.
Let’s add a field called version to our global section (note: we might have added this already
in the one of previous chapters):
// file: src/config/models/Config.interface.ts
...
...
// file: src/config/models/Config.interface.ts
...
...
// add this block:
localization: {
apiClientOptions: LocalizationApiClientOptions
locales: { key: string, isDefault: boolean }[]
localStorageCache: { enabled: boolean, expirationInMinutes: number }
}
...
Here we reference a apiClientOptions. We don’t have this yet. We’ll add a new API client
module called localization shortly.
First, let’s finish updating the code related to the configuration.
file mock.json
Let’s add the data we need in the src/config/files/mock.json as per our ConfigInterface
updates:
// file: src/config/config-files/mock.json
{
"global": {
"version": 0.1 // add this line
},
...
"fetchTranslation": "/static/mock-data/localization/[namespace]/[key].json"
},
"mockDelay": 250
},
"locales": [
// each of this objects represent a locale available in our app
{ "key": "en-US", "isDefault": true },
{ "key": "it-IT", "isDefault": false },
{ "key": "fr-FR", "isDefault": false },
{ "key": "es-ES", "isDefault": false }
],
"localStorageCache": {
// these are settings we'll use to cache JSON locale translation data into loc\
aleStorage
"enabled": true,
"expirationInMinutes": 60
}
}
// end: add the localization section
}
Please feel free to also update the beta.json/production.json/localapis.json files as well, and
possibly add unit tests to validate your changes. Note that we have also an array called
locales which hold a list of object that represent each of the locales available in or application.
We also have a section caled localeStorageCache that we’ll use to drive how we cache the
locale translation JSON data into the browser localStorage.³⁹
• en-US.json
• es-ES.json
• fr-FR.json
³⁹https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
Chapter 10 - Localization and Internationalization - Language Localization 145
• it-IT.json
// file: public/static/mock-data/localization/translation/en-US.json
{
"locale.selector.en-US": "English",
"locale.selector.it-IT": "Italian",
"locale.selector.fr-FR": "French",
"locale.selector.es-ES": "Spanish",
"navigation.home": "Home",
"navigation.about": "About",
// file: public/static/mock-data/localization/translation/es-ES.json
{
"locale.selector.en-US": "Inglés",
"locale.selector.it-IT": "Italiano",
"locale.selector.fr-FR": "Francés",
"locale.selector.es-ES": "Español",
"navigation.home": "Inicio",
"navigation.about": "Acerca de",
// file: public/static/mock-data/localization/translation/fr-FR.json
{
"locale.selector.en-US": "Anglais",
"locale.selector.it-IT": "Italien",
"locale.selector.fr-FR": "Français",
"locale.selector.es-ES": "Espagnol",
"navigation.home": "Accueil",
"navigation.about": "À propos de nous",
// file: public/static/mock-data/localization/translation/it-IT.json
{
"locale.selector.en-US": "Inglese",
"locale.selector.it-IT": "Italiano",
"locale.selector.fr-FR": "Francese",
"locale.selector.es-ES": "Spagnolo",
"navigation.home": "Home",
"navigation.about": "Chi Siamo",
We now have to add a new API client module for loading our localization data.
• LocalizationApiClient.interface.ts
Chapter 10 - Localization and Internationalization - Language Localization 147
• LocalizationApiClientOptions.interface.ts
• LocalizationApiClient.model.ts
• index.ts
file LocalizationApiClient.interface.ts
Our localization API client will exposes one method called fetchTranslation:
// file: src/api-client/models/localization/LocalizationApiClient.interface.ts
/**
* @Name LocalizationApiClientInterface
* @description
* Interface for the Localization api client module
*/
export interface LocalizationApiClientInterface {
fetchTranslation: (namespace: string, key: string) => Promise<{ [key: string]: str\
ing }>
}
file LocalizationApiClientOptions.interface.ts
Here we have the itnerfaces for the API client configuration:
// file: src/api-client/models/localization/LocalizationApiClientOptions.interface.ts
/**
* @Name LocalizationApiClientOptions
* @description
* Interface for the Localization api client options (includes endpoints used to avo\
id hard-coded strings)
*/
export interface LocalizationApiClientOptions {
mockDelay?: number
endpoints: LocalizationApiClientEndpoints
}
Chapter 10 - Localization and Internationalization - Language Localization 148
file LocalizationApiClient.model.ts
here is the implementation of our localization API client:
// file: src/api-client/models/localization/LocalizationApiClient.model.ts
/**
* @Name LocalizationApiClientModel
* @description
* Implements the LocalizationApiClientInterface interface
*/
export class LocalizationApiClientModel implements LocalizationApiClientInterface {
private readonly endpoints!: LocalizationApiClientEndpoints
private readonly mockDelay: number = 0
constructor(options: LocalizationApiClientOptions) {
this.endpoints = options.endpoints
if (options.mockDelay) {
this.mockDelay = options.mockDelay
}
}
}
}
file src/api-client/models/localization/index.ts
Just a barrel file:
// file: src/api-client/models/localization/index.ts
Updates to ApiClient.interface.ts
Import and add our localization module:
// file: src/api-client/models/ApiClient.interface.ts
/**
* @Name ApiClientInterface
* @description
* Interface wraps all api client modules into one places for keeping code organized.
*/
export interface ApiClientInterface {
localization: LocalizationApiClientInterface
items: ItemsApiClientInterface
}
// file: src/api-client/models/index.ts
// file: src/api-client/mock/localization/index.ts
// instantiate the LocalizationApiClient pointing at the url that returns static jso\
n mock data
const localizationApiClient: LocalizationApiClientInterface = new LocalizationApiCli\
entModel(config.localization.apiClientOptions)
// file: src/api-client/mock/index.ts
// create an instance of our main ApiClient that wraps the mock child clients
const apiMockClient: ApiClientInterface = {
localization: localizationApiClient,
items: itemsApiClient
}
// file: src/api-client/live/index.ts
import {
ApiClientInterface,
LocalizationApiClientModel,
ItemsApiClientModel
} from '../models'
// create an instance of our main ApiClient that wraps the live child clients
const apiLiveClient: ApiClientInterface = {
localization: new LocalizationApiClientModel(config.localization.apiClientOptions),
items: new ItemsApiClientModel(config.items.apiClientOptions)
}
// export our instance
export { apiLiveClient }
Note: for the live instance going forward we’ll just initialize the client modules within this
file so you can go ahead and delete theapi-client/live/items sub-directory.
Chapter 10 - Localization and Internationalization - Language Localization 152
• utils.ts
• useLocalization.ts
• index.ts
file utils.ts
Here we’ll have three helper methods:
// file: src/localization/utils.ts
if (!preferredLocale) {
// if not, use the default locale from config
const defaultLocale = getDefaultLocale().key
return defaultLocale
}
return preferredLocale
}
Note that in getUserPreferredLocale we return the default locale from config if there is not
preferred locale in localStorage yet.
file useLocalization.ts
For our hook, let’s start importing a few types and utils from both svelte-i18n and
svelte/store. Also import reference to our config and apiClient and the newly created
helpers from our utils above:
// file: src/localization/useLocalization.ts
...
// file: src/localization/useLocalization.ts
...
...
// file: src/localization/useLocalization.ts
...
...
Add an helper called changeLocale that will help us switch local but also load JSON
translation data for a specific locale from an API and cache it into localStorage (so subsequent
calls to this method will retrieve the JSON data from cache):
// file: src/localization/useLocalization.ts
...
// if localeStorage is enabled through config, then proced trying parsing the cac\
heEntryStr
if (localStorageConfig.enabled) {
try {
cacheEntry = JSON.parse(cacheEntryStr)
} catch (e) {
console.warn('error parsing data', cacheEntryStr)
}
}
// check if we have cacheEntry and if matches app version and also did not expire
if (cacheEntry && cacheEntry.appVersion === config.global.version && cacheEntry.ex\
piresAt - Date.now() > 0) {
// use value from cache and pass it to svelte-i18n dictionary.set():
SvelteI18N.dictionary.set({ [lcid]: cacheEntry.json as any })
// then switch locale by invoking svelte-i18n locale.set()
SvelteI18N.locale.set(lcid)
} else {
// set our loading flag to true:
isLoadingLocale.set(true)
// retrieve data from API end point (or CDN etc)
const translationData = await apiClient.localization.fetchTranslation('translati\
on', lcid)
// use the data returned y the API and pass it to svelte-i18n dictionary.set():
SvelteI18N.dictionary.set({ [lcid]: translationData })
// then switch locale by invoking svelte-i18n locale.set()
SvelteI18N.locale.set(lcid)
appVersion: config.global.version,
expiresAt: expiresAt,
json: translationData
})
)
}
// set our loading flag to false
isLoadingLocale.set(false)
}
...
Finally we’ll just export a useLocalization hook that has all we need, and enable us to easily
consume the different locale translations in our components:
// file: src/localization/useLocalization.ts
...
return {
locales: availableLocales,
changeLocale,
currentLocale,
isLocaleLoaded,
isLoadingLocale: derived(isLoadingLocale, ($state) => $state),
t: SvelteI18N._, // expose the svelte-i18n underscore function which is the tran\
slation function
getUserPreferredLocale
}
}
App.svelte updates
Let’s now consume our useLocalization within the App.svelte file by adding a quick way
to change locale and also display the translated home welcome message:
Chapter 10 - Localization and Internationalization - Language Localization 157
// file: src/App.svelte
<script lang="ts">
...
<main>
<div class="home">
{#if $isLocaleLoaded && !$isLoadingLocale}
<div class="locale-selector">
{#each locales as item} <!-- loop through the locales and create a radio but\
ton for each locale -->
<label class="cursor-pointer">
<input type="radio" group={$currentLocale} name="locale" value={item.key\
} checked={ $currentLocale === item.key } on:click={() => onLocaleClick(item.key)} /\
>
<!-- use the t function to translate the label of this radio -->
{ $t(`locale.selector.${ item.key }`) }
</label>
{/each}
</div>
<h1>{$t('home.welcome')}</h1> <!-- update this to use the t function to transl\
ate our welcome message -->
<ItemsView />
Chapter 10 - Localization and Internationalization - Language Localization 158
{:else}
<p>Loading...</p>
{/if}
</div>
</main>
You can similarly update the ItemsList.component.svelte code as well to use the translations.
I’ll let you do that on your own. You can always refer tot he github repo if you need help.
Browser
Now run the app and you will see something like this:
Now right-click and select inspect to open the Chrome dev tools, then select the Application
tab, then Local storage > http://localhost:3000 note how our code has cached the en-US JSON
data in localStorage and saved the current locale id under user-lcid:
Chapter 10 - Localization and Internationalization - Language Localization 159
If you select a different locale, i.e. French, it should display the translated labels:
Now in the Local Storage inspector in the Chrome console, again notice how the fr-FR locale
data has been also saved to localeStorage and the user preferred locale saved under user-lcid
has been also updated:
Chapter 10 - Localization and Internationalization - Language Localization 160
If you switch on the Netwrok tab in the Chrome console, you can see how our API client
has loaded the data from static/mock-data/localization/translation/fr-FR.json:
To test that our code will load the user-preferred locale from local storage, along with the
save JSON translation data, clear the Network tab and then refresh the Chrome tab with F5.
Note how the network tab will NOT show a call to the static JSON file this time because our
code is actually loading the data from localStorage this time:
Chapter 10 - Localization and Internationalization - Language Localization 161
Important
Remember that if you add additional keys to your translation files, or modify the data
in any way, you’ll have to either clear your local storage (if you are just playing with
things locally), or increment that Application Configuration version in the config files
(global section). The localStorage cache will also expire eventually based on the con-
fig.localization.localStorageCache.expirationInMinutes value.
For a new deploy, incrementing the version will make sure that the logic we added in our
useLocalization code will ignore the currently cached data from localStorage and re-load
fresh data through the AP. Here for example I increase it from 0.102 to 0.103:
Chapter 10 - Localization and Internationalization - Language Localization 162
Note: you dont have to use decimals for your version. You are free to just use integer2 like 1,
2, etc.
Chapter 10 - Localization and Internationalization - Language Localization 163
Chapter 10 Recap
What We Learned
• How to add the i18next and svelte-i18n plugins to our application
• How to wrap code for svelte-i18n and lazy-loading of JSON translation data through
our API client
• How to cache translation JSON data into localStorage with versioning and expiration
• How to drive available locales through configuration
• How to use multiple locale settings for text translation in order to localize our UI labels
• How to switch to different locales
Observations
• We did not add unit tests around switching locale through the radio buttons
• We did not create a component for switching locale
Improvements
• Add additional unit tests
• Extract the code that loops through each locale and adds a radio button (div with
className set to locale-selector) into its own component and add unit tests against
this. Maybe in your application requirements this has to be a dropdown instead of a
radio group, so it is up to you how you will implement this.
Chapter 11 - Localization and
Internationalization - Number and
DateTime Formatters
In this chapter we are going to expand our support for localization by adding Number and
DateTime value formatters. We’ll leverage the Intl API⁴⁰ which is supported by all major
web browsers.
We’ll build a hook called useFormatters that will make it easier to consume different kind of
formatters based on the currently selected locale.
Note: this is the same code from a plugin I published here and you could use it in other apps
in the future without coding it yourself if you prefer: @builtwithjavascript/formatters
Note also that the code in this chapter is re-usable in any framework, not just Svelte, as it
does not have any dependency on Svelte.
Directory localization/formatters
Start by creating a directory under src/localization called formatters. Inside this directory
create the following files:
• useDateTimeFormatters.ts
• useNumberFormatters.ts
• index.ts
File: useDateTimeFormatters.ts
For date-time formatters we’ll wrap around Intl.DateTimeFormat and make it easier to
consume it. We’ll cache each instance of Intl.DateTimeFormat by localeId and the different
options to avoid keeping re-instantiating it everytime (this is for performance reasons).
⁴⁰https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 165
For this reason, we need to first add a method that return a valid and unique cache key that
uses localeId and the different options that might be passed when consuming it. The cache
key will be a string in the format [localeId]-[dateStyle]-[timeStyle]:
// file: src/formatters/useDateTimeFormatters.ts
// helper to calculate the cache key for the datetime Intl.DateTimeFormat instances
export const getDateTimeFormattersCacheKey = (params: { lcid: string; dateStyle?: st\
ring; timeStyle?: string }) => {
let { lcid, dateStyle, timeStyle } = params
dateStyle = (dateStyle || defaultDateStyle).trim().toLowerCase()
timeStyle = (timeStyle || '').trim().toLowerCase()
...
We can then add the code for the useDateTimeFormatters hook. This will return an
object with 3 methods: datetime, dayNames, monthNames. Here is the preliminary
implementation with just datetime:
// file: src/formatters/useDateTimeFormatters.ts
...
return {
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 166
if (!_cache.has(cacheKey)) {
// if not in our cache yet, create it and cache it
let options: { dateStyle?: string; timeStyle?: string } = {}
if (dateStyle.length) {
options.dateStyle = dateStyle
}
if (timeStyle.length) {
options.timeStyle = timeStyle
}
// cache instance
const instance = new Intl.DateTimeFormat(_lcid, options as Intl.DateTimeForm\
atOptions)
_cache.set(cacheKey, instance)
}
// return instance from cache
return _cache.get(cacheKey) as Intl.DateTimeFormat
},
}
}
// file: src/formatters/useDateTimeFormatters.ts
...
return {
dateTime: (dateStyle?: string, timeStyle?: string) => {
...
},
}
}
As you can see in the code above we leverage date.toLocaleString to get either the day or
month name. We use a calculated date from March 1st 1970 (which is a Sunday) to get the
correct weekday name, and from January 1st 1970 to get the correct month name (irrelevant
of the current user time zone).
File: useNumberFormatters.ts
For number formatters, similar to what we did for datetime in the previous section, we’ll
wrap around Intl.NumberFormat . We’ll cache each instance of Intl.NumberFormat by
localeId and the different options to avoid keeping re-instantiating it everytime (again for
performance reasons).
Similaor to the datetime formatters hook, we’ll need a function here as well that calculate
the cache key dynamically. This is a bit more complex as it takes into account a few more
parameters:
// file: src/formatters/useNumberFormatters.ts
// helper to calculate the cache key for the datetime Intl.NumberFormat instances
export const getNumberFormattersCacheKey = (params: {
lcid: string
style?: string
currency?: string
currencyDisplay?: string
minimumFractionDigits: number
maximumFractionDigits: number
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 169
}) => {
let { lcid, style, currency, currencyDisplay, minimumFractionDigits, maximumFracti\
onDigits } = params
style = (style || 'decimal').trim().toLowerCase()
currency = (currency || '').trim()
currencyDisplay = (currencyDisplay || defaultcurrencyDisplay).trim()
...
We can then add the code for the useNumberFormatters hook. This will return an object
with 4 methods: whole, decimal, currency, percent. Here we start by implementing a
private method called _privateGetFormatter that will use to avoid code duplication. This
method also contains the logic to retrieve/set the instance into the cache:
// file: src/formatters/useNumberFormatters.ts
...
if (!_cache.has(cacheKey)) {
// if not in our cache yet, create it and cache it
let options: Intl.NumberFormatOptions = {
style,
minimumFractionDigits,
maximumFractionDigits
}
if (currency.length > 0) {
options.currency = currency
if (currencyDisplay.length > 0) {
options.currencyDisplay = currencyDisplay
}
}
// cache instance
const instance = new Intl.NumberFormat(_lcid, options)
_cache.set(cacheKey, instance)
}
// return instance from cache
return _cache.get(cacheKey) as Intl.NumberFormat
}
...
Then we can add the code to export our 4 utility methods by using the private method to
construct each instance with the various options:
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 171
// file: src/formatters/useNumberFormatters.ts
...
return {
whole: () => {
return _privateGetFormatter({
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
},
decimal: (minimumFractionDigits: number = 0, maximumFractionDigits: number = 2) \
=> {
return _privateGetFormatter({
style: 'decimal',
minimumFractionDigits,
maximumFractionDigits
})
},
currency: (
currency: string,
currencyDisplay?: string,
minimumFractionDigits: number = 0,
maximumFractionDigits: number = 2
) => {
return _privateGetFormatter({
style: 'currency',
currency,
currencyDisplay,
minimumFractionDigits,
maximumFractionDigits
})
},
percent: (minimumFractionDigits: number = 0, maximumFractionDigits: number = 2) \
=> {
return _privateGetFormatter({
style: 'percent',
minimumFractionDigits,
maximumFractionDigits
})
},
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 172
unescapeResult(result: string) {
return (result || '').replace(/\xa0/g, ' ').replace(/\u202f/g, ' ')
}
}
}
File: index.ts
For convenience, here we just export a global hook called useFormatters:
// file: src/formatters/index.ts
Note: this step is optional. You could just import individually either useDateTimeFormatters
or useNumberFormatters when consuming them.
Later, when we need to consume our formatters, we can just import them as:
import {
useLocalization,
useDateTimeFormatters,
useNumberFormatters
} from '@/localization/formatters'
Component DebugFormatters.component.svelte
Let’s now create a component that we can just use to visually debug the output of the
formatters. Here will also use the useLocalization hook to get the currentLocale. Then
we’ll have some computed properties that will return the correct formatters based on the
currentLocale:
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 173
// file: src/components/shared/DebugFormatters.component.svelte
<script lang="ts">
import {
useLocalization,
useDateTimeFormatters,
useNumberFormatters
} from '@/localization/formatters'
$: wholeNumberFormatter = () => {
return useNumberFormatters($currentLocale).whole()
}
$: decimalNumberFormatter = () => {
return useNumberFormatters($currentLocale).decimal()
}
$: currencyNumberFormatter = (currency: string = 'USD') => {
return useNumberFormatters($currentLocale).currency(currency)
}
$: percentNumberFormatter = () => {
return useNumberFormatters($currentLocale).percent()
}
</script>
{#if show}
<div>
<h3>Debugging formatters:</h3>
<div>Whole: { wholeNumberFormatter().format(123456789.321654) }</div>
<div>Decimal: { decimalNumberFormatter().format(123456789.321654) }</div>
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 174
Updates to App.svelte
Now we can import the DebugFormatters component and render it within our App.svelte to
quickly visually debug that the formatters are working as expected:
<script lang="ts">
...
...
<main>
<div class="home">
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 175
If you now run the app you will see the DebugFormatters rendering information at the
bottom of the page:
And of course, if you select a different locale (i.e. French) you’ll see the formatters displaying
the value as per the current locale culture:
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 176
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 177
Chapter 11 Recap
What We Learned
• How to add code that wraps around Intl API DateTimeFormat and NumberFormat
• How to format values according to the current locale using the formatters we created
Observations
• We did not add unit tests against our formatters
Improvements
• Add unit tests again the formatters hooks
Chapter 12 - Adding Tailwind CSS
Going forward, we are going to use Tailwind CSS⁴¹ as it makes it so easy to design
components without having to mess with the CSS directly. We would eventually need to
remove also the older CSS we wrote during the previous chapter, but there is no harm for
now in leaving that there. But for our primitives library and new higher-level components,
it will be written exclusively using Tailwind CSS.
The easiest way to add TailwindCSS to our existing project is through svelte-add⁴². Let’s do
it with this command:
It might prompt you to install the package svelte-add@latest if you don’t have it already.
Choose y and press enter:
This will create the tailwind and postcss config files for you, a preliminary app.css file, and
modify the svelte.config.js to add sveltePreprocess for postcss to the preprocess section.
Open the file svelte.config.cjs and verify it has been changed like this:
//file: src/svelte.config.cjs
import sveltePreprocess from 'svelte-preprocess'
export default {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
sveltePreprocess({
postcss: true
})
]
}
Opent the file tailwind.config.cjs and verify it has been created with this content:
⁴¹Tailwind CSS Official Website
⁴²npmjs.com: svelte-add
Chapter 12 - Adding Tailwind CSS 179
//file: src/tailwind.config.cjs
const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: []
}
module.exports = config
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
}
module.exports = config
Finally, create a new directory called tailwind and move the app.css file into it. The final
path of this will be src/tailwind/app.css. Open the file and make sure contains this code:
/* file: src/tailwind/app.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
// file: src/main.ts
import App from './App.svelte'
// import tailwind main css file
import './tailwind/app.css'
Finally, to test that Tailwind CSS has been added and it is working, add this classes to the
first div child of the <main> element within the App.svelte file:
// file: App.svelte
...
<main>
<div class="home m-2 p-2 border-2 border-red-500">
...
and also remove the <style> block found at the end of the App.svelte code:
...
To confirm that Tailwind CSS is being add correctly, run the application and verify that it
renders like this:
Chapter 12 - Adding Tailwind CSS 181
Chapter 12 - Adding Tailwind CSS 182
Chapter 12 Recap
What We Learned
• We learned how to add Tailwind CSS to our existing project
Observations
• We did not talk about how to add support for something like Sass/Scss
Based on these observations, there are a few improvements that can be done:
Improvements
• You could learn how to add Sass/Scss if you do not want to use Tailwind CSS
Chapter 13 - Intro to Primitives
This chapter covers concepts on how to write and organize the most primitive components
(i.e. Inputs, Buttons etc) in order to create a foundation for higher-level and more complex
components.
Conventions
One of the convention we will follow is to put all our primitive components under the
directory src/components/primitives
⁴³Brad Frost - Atomic Design https://bradfrost.com/blog/post/atomic-web-design/
⁴⁴Alba Silvente - How to structure a Vue.js app using Atomic Design and TailwindCSS https://vuedose.tips/how-to-structure-a-vue-js-app-using-
atomic-design-and-tailwindcss
Chapter 13 - Intro to Primitives 184
Within this directory we’ll have sub-directories (folders) that leep the components organized
by category. I.e. buttons will be under src/components/primitives/buttons, inputs will be
under src/components/primitives/inputs etc.
We’ll follow also a naming convention where each .vue file that represents a primitive will
start with the El prefix. I.e. ElText.svelte, ElIcon.svelte, etc. In this case El is for “element”.
You are of course free to decide your own naming convention. But I strongly suggest using
some kind of prefix to more quickly identify a primitive by just looking at its file name
when it is open in your editor.
Create the following 5 sub-directories to get started:
• src/components/primitives/buttons
• src/components/primitives/text
• src/components/primitives/modals
• src/components/primitives/inputs
• src/components/primitives/icons
General Strategies
One of the things we are going to consistently need in each primitive is the main CSS class
property. Often, this will have to be a computed property that returns the appropriate value
based on other conditions. For example, a Button might have to render with an additional
“disabled” CSS class if its disabled property is true.
For consistency, every time a primitive needs a dynamic CSS class, we’ll add a computed
property called cssClass that will return the appropriate value based on various conditions.
Here is a code example for an hypotethical Button component:
<script lang="ts">
// a computed property to return a different css class based on the selected value
$: cssClass = (): string => {
// here we concatenate the default CSS with 'disabled' only if disabled is true
const defaultClasses = 'p-6' // in TailwindCSS this means we want a padding of 6
return `${ defaultClasses } ${ this.disabled ? 'disabled' : '' }`.trim()
Text Elements
Let’s start creating one element for each group as a starting point and then we’ll keep building
more elements from there.
Create a file called ElText.svelte under directory src/components/primitives/text.
For the code, use the following:
// file: src/components/primitives/text/ElText.svelte
<script lang="ts">
// expose a property called testid
export let testid: string = 'not-set'
// expose a property called tag
export let tag: string = 'span'
// expose a property called text
export let text: string = 'text-not-set'
// expose a property called addCss
export let addCss: string = ''
return `<${ tag } data-testid="${ testid }" class="${ cssClass() }">${ text }</$\
{ tag }>`
}
</script>
{ @html render() }
Here our logic within the computed cssClass property is a bit more complex. Our component
also has a property called addCss (for additional Css) that can be used to specify CSS classes
for our component in addition to the ones internally set initially to the const cssClasses
variable. Within the cssClass computed property we check if a value for the property addCss
has been provided. If so, we add its value to our computed value.
Here is an example on how we’ll consume our ElText component:
As you can see we specified the value “text-red-500” for the addCss property. Thus, the final
computed value for the cssClass will be “p-1 text-red-500”.
Furthermore, since we are rendering the component dynamically based on the tag property
specified, we can render our text as any valid Html element we wish for. In the example
above, we specified the tag property to be “h2” and thus it will render an <h2> element. Or
we could have specified a tag value of “p” and it will render as a <p> element etc. We are
able to do this thanks to the Svelte’s @html⁴⁵ binding directive that lets us render any html
from a string
Primitives View
Let’s create a view where we can consume our primitives so that we can visually debug and
prototype them as we develop them. This view can become apoint of reference for our basic
library of primitives from which we will build more complex components later.
Create the following file:
• src/views/Primitives.view.svelte
//file: src/views/Primitives.view.svelte
<script lang="ts">
// import a reference to the ElText primitive
import ElText from '@/components/primitives/text/ElText.svelte'
</script>
<div>
<div class="about">
<ElText tag="h1" addCss="text-gray-500" text="Primitives"/>
<ElText tag="h2" addCss="text-gray-500" text="ElText examples:"/>
<div class="p-6 border">
<ElText tag="h2" addCss="text-red-500" text="Here ElText will render a <h2&g\
t element"/>
<ElText tag="p" addCss="text-red-700" text="Here ElText will render a <p> \
element"/>
</div>
</div>
</div>
Let’s now temporarily import the Primites view in our App.svelte and replace the ItemsList
with it so we can verify it renders correctly:
// file: rc/App.svelte
<script lang="ts">
// import a reference to our ItemsView component
import ItemsView from '@/views/Items.view.svelte'
import PrimitivesView from '@/views/Primitives.view.svelte'
....
<h1>{$t('home.welcome')}</h1>
<PrimitivesView /> <!-- put it here for now -->
Now run the application and navigate to the Primitives view to see what we got. If all
worked as expected, you should see something like this:
Chapter 13 - Intro to Primitives 188
As you can see, the two ElText elements are rendered with different HTML tags. The first one
as <h2> while the second one as a <p> element. We also specified some additional CSS class
through their addCss property. The first has “text-red-500” which is a red, and the second
one “text-red-700” which is a darker red in the default TailwindCSS colors.
Chapter 13 - Intro to Primitives 189
Chapter 13 Recap
What We Learned
• We talked a bit about about atomic design
• We learned how to structure a directory of primitive elements from which we’ll build
higher-level components that are more complex
Observations
• We only created one primitive called ElText
Based on these observations, there are a few improvements that can be done:
Improvements
• We need to create a few more primitives
• We need to start consuming these primitives in higher-level components
Chapter 14 - More Primitives
Let’s add now a few more primitives. This is just to give you some idea about the direction
to take to build your own foundation library from which you can then derive all your higher
level components.
Button Elements
Let’s start for now by creating a button element for our primitives library, similar to how we
created the ElText in the previous chapter.
Create a file called ElButton.svelte under directory src/components/primitives/buttons.
For the code, use the following:
<script lang="ts">
// import createEventDispatcher from Svelte:
import { createEventDispatcher } from 'svelte'
// a computed property to return a different css class based on the selected value
$: cssClass = (): string => {
const result = ['font-bold py-1 px-2 border rounded']
if (disabled) {
// these are the button CSS classes when disabled
Chapter 14 - More Primitives 191
// click handler
function handleClick () {
// proceed only if the button is not disabled, otherwise ignore the click
if (!disabled) {
// dispatch a 'clicked' even through Svelte dispatch
dispatch('clicked', id)
}
}
</script>
<button type="button"
aria-label={ label }
data-testid={ testid }
disabled={disabled}
class={cssClass()}
on:click={() => handleClick()}>
<span class="name">{ label }</span>
</button>
Here as you can see we start having a bit more complexity than what we had in ElText.
First, we have an handleClick method that emits the clicked event when the button is
clicked. This way, we can handle the click in the parent component where we will consume
this primitive.
We have an addCss property (like we have in the ElText primitive), and a label property
which is for the text of the button label. We also have a disabled boolean property to render
the button either as enabled or disabled. Then we use this property in two places:
• Within the handleClick method, we make sure we proceed only if the button is not
Chapter 14 - More Primitives 192
disabled
• Within the computed cssClass property, we check if the disabled property value is true
to render a different set of CSS classes (with TailwindCSS here we set the text to gray
with text-gray-300 for example, and a few other changes)
NOTE: We emit (dispatch) a custom event called clicked (not “click”) so we can consume with
on:clicked binding. If we were to emit “click” and consume with on:click there is a chance
that the event would fire twice as “click” is a native Svelte event and therefore would fire both
inside ElButton and on the actual <ElButton> instance itself.
I hope you start seeing the power of organizing and building primitives this way. Ahead
we’ll soon compose higher-level components out of these primitives and you will see how
easier to manage this will be, plus the code will be much cleaner and encapsulated.
// file src/views/Primitives.view.svelte
<script lang="ts">
import ElButton from '@/components/primitives/buttons/ElButton.svelte'
import ElText from '@/components/primitives/text/ElText.svelte'
<div>
<div class="about">
<ElText tag="h1" addCss="text-gray-500" text="Primitives"/>
<ElText tag="h2" addCss="text-gray-500" text="ElText examples:"/>
<div class="p-6 border">
Chapter 14 - More Primitives 193
Now run the application and navigate to the Primitives view to see what we got. If all
worked as expected, you should see something like this:
As you can see, the buttons are rendered with our specified label text, and the one on the
right is rendering as “disabled” (Note that we also specified a margin-left with the addCss
property using TailwindCSS ml-2 value).
NOTE: I did not add a handler for @clicked event yet, but you are welcome to add one in the
Primitives view and log a message to the console to test that is working
Chapter 14 - More Primitives 194
Toggle/Checkbox Elements
Let’s add one more primitive called ElToggle that will behave like a checkbox but looks like
a toggle.
Create a file called ElToggle.svelte under directory src/components/primitives/toggles.
For the code, use the following:
// file: src/components/primitives/toggles/ElToggle.svelte
<script lang="ts">
// import createEventDispatcher from Svelte:
import { createEventDispatcher } from 'svelte'
// a computed property that returns the css class of the outer element
$: cssClass = (): string => {
const result = ['relative inline-flex flex-shrink-0 h-6 w-12 border-1 rounded-fu\
ll cursor-pointer transition-colors duration-200 focus:outline-none']
if (checked) {
result.push('bg-green-400')
} else {
result.push('bg-gray-300')
}
if (disabled) {
result.push('opacity-40 cursor-not-allowed')
}
if ((addCss || '').trim().length > 0) {
result.push(addCss.trim())
}
Chapter 14 - More Primitives 195
// click handler
function handleClick () {
// proceed only if the button is not disabled, otherwise ignore the click
if (!disabled) {
// dispatch a 'clicked' even through Svelte dispatch
dispatch('clicked', {
id
})
}
}
</script>
<button type="button"
role="checkbox"
data-testid={ testid }
aria-checked={ checked }
disabled={disabled}
class={cssClass()}
on:click={() => handleClick()}>
<span class={innerCssClass()} ></span>
</button>
As you can see this looks a lot similar to the ElButton primitive we created earlier.
Here too we have an handleClick method that emits the clicked event when the toggle is
clicked. This way, we can handle the click in the parent component where we will consume
this primitive.
We have an addCss property (like we have in the ElButton primitive), and a checked
property which we’ll use to specify whether the toggle is on or off. We also have a disabled
Chapter 14 - More Primitives 196
boolean property to render the toggle as either enabled or disabled. Then we use this
property in two places:
• Within the handleClick method, we make sure we proceed only if the toggle is not
disabled
• Within the computed cssClass property, we check if the disabled property value is true
to render a different set of CSS classes, we do the something similar for the checked
property as well to change the background color of the toggle track
• We have an additional computed property called innerCssClass (this is for the inner
<span> of the toggle which will render as a white circle). Inside here we check for the
checked property value to determine how much we have to shift the circle horizontally
(through the TailwindCSS translate-x property)
// file: src/views/Primitives.view.svelte
<script lang="ts">
import ElText from '@/components/primitives/text/ElText.svelte'
import ElButton from '@/components/primitives/buttons/ElButton.svelte'
import ElToggle from '@/components/primitives/toggles/ElToggle.svelte'
import * as SvelteStore from 'svelte/store'
}, {
id: 'toggle-b',
checked: false
}, {
id: 'toggle-c',
checked: false
}
]
})
const state = SvelteStore.derived(writableStore, ($writableStore) => $writableStor\
e)
<div>
<div class="about">
<ElText tag="h1" addCss="text-gray-500" text="Primitives"/>
<ElText tag="h2" addCss="text-gray-500" text="ElText examples:"/>
<div class="p-6 border">
<ElText tag="h2" addCss="text-red-500" text="Here ElText will render a <h2&g\
t element"/>
<ElText tag="p" addCss="text-red-700" text="Here ElText will render a <p> \
element"/>
</div>
Note how here we are using Svelte’s writable store to create a simple local state to track
the checked state of all toggles. Then in the onToggleClicked method we retrieve the state
information for that toggle and invert its current checked value.
Now run the application and navigate to the Primitives view to see what we got. If all
worked as expected, you should see something like this:
Chapter 14 - More Primitives 199
You are welcome to keep following this pattern and start creating more complex primitives
like icons, textboxes, dropdowns, lists etc. For now we’ll stop here, and in the next chapter
we’ll start consuming the primitives we have created to compose higher level components.
Optional: You might also want to add a barrel index.ts file under components/primitives and
export all primitives in an organized fashion so that you can simplify your imports like, i.e.
import { ElText, ElButton, ElToggle } from '@/components/primitives':
// file: src/components/primitives/index.ts
// text
import ElText from './text/ElText.svelte'
// buttons
import ElButton from './buttons/ElButton.svelte'
// toggles
import ElToggle from './toggles/ElToggle.svelte'
export {
// text
ElText,
// buttons
ElButton,
// toggles
Chapter 14 - More Primitives 200
ElToggle
}
Chapter 14 - More Primitives 201
Chapter 14 Recap
What We Learned
• We learned how to add additional components to our custom library by adding an
ElButton and ElToggle primitives
• We learned how to render these primitives with different CSS classes conditionally to
other properties like disabled, selected etc
Observations
• We did not consume these primitives in higher level components yet (besides the
Primitive vue use to visually prototype them)
Based on these observations, there are a few improvements that can be done:
Improvements
• We need to start consuming these primitives in higher-level coponents
• We need to start consuming the primitives and higher-level components in other
existing component like our initial ItemsList and Item components
Chapter 15 - A Primitive Modal
I wanted to dedicate an additional chapter to creating a Modal component. There are many
ways to create modals in Svelte. There are also plug-ins created by various authors out there.
You are free to choose anything you like of course and skip this chapter completely.
Here, I wanted to introduce a way of creating it a modal component that, in my experience
over the years, has worked out to be one of the best ways in any front-end frameworks,
including Vue.js or React.
One of the main difference between a Modal component and a traditional component is that
a Modal must prevent interaction with the rest of the application until the user dismisses the
dialog.
The main use case for a dialog is to prompt the user to confirm an action, which might be
usually destructive, like deleting a record or updating data (thus overwriting existing data,
etc). The Modal will usually present a dialog box with a message and two buttons: one to
confirm the action (primary) and one to cancel the action (secondary).
Our goal is to have a hook called useModal that will return a reference to a shared instance
of a Modal component and we can consume like:
...
We’ll expect the prompt() method to be async and block execution of our code at that line
till a result is returned. Similar to how the native JavaScript prompt works.
We also want to pass an icon to our dialog that will be rendered thanks to Svelte’s dynamic
component directive <svelte:component...>⁴⁶. The icon will be an optional parmaeter and
if no icon is passed, then no icon will be rendered.
⁴⁶https://svelte.dev/tutorial/svelte-component
Chapter 15 - A Primitive Modal 203
Icon: ElIconAlert
Before we start working on the modal code itself, let’s create a preliminary icon icon primi-
tive. Create the file ElIconAlert.svelte under directory src/components/primitives/icons/
and put the following code in it:
// file: src/components/primitives/icons/ElIconAlert.svelte
<script lang="ts">
export let addCss = ''
$: cssClass = () => {
const result = ['h-6 w-6 ']
if ((addCss || '').trim().length > 0) {
result.push(addCss)
}
return result.join(' ').trim()
}
</script>
<svg class={cssClass()} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 \
24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0\
4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.\
464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
Create also a barrel indexts file within the icons directory with this:
// file: src/components/primitives/icons/index.ts
import ElIconAlert from './ElIconAlert.svelte'
export {
ElIconAlert
}
We’ll be dynamically adding the icon as one of the possible properties passed to our modal
component.
Interface ModalProps
Within the directory src/components/primitives/modals add a new file called Modal-
Props.interface.ts with the following code:
Chapter 15 - A Primitive Modal 204
// file: src/components/primitives/modals/ModalProps.interface.ts
/**
* @name ModalProps
* @desrciption Interface that represents the public properties of the Modal compone\
nt
*/
export interface ModalProps {
testid?: string // optional
cancelLabel: string
confirmLabel: string
title?: string
longDesc?: string // optional
primaryButtonType?: string // optional, defaults to 'primary'
icon?: string // the icon and iconAddCss props are optional
iconAddCss?: string
}
File ElModal.svelte
Within the same directory, create a file called ElModal.svelte with the following code for
the <script> section:
<script lang="ts">
import { fade } from "svelte/transition"
import { ModalProps } from './ModalProps.interface'
import ElButton from '../buttons/ElButton.svelte'
import ElText from '../text/ElText.svelte'
// public setProps() method used to set the private props from our useModal hook
export const setProps = (updatedProps: ModalProps) => {
props = {
...getDefaultProps(),
...updatedProps
}
}
$: cssClass = () => {
const result = ['fixed z-10 inset-0 overflow-y-auto transform transition-all']
// might add additional css based on conditions...
return result.join(' ').trim()
}
</script>
The core concept here is to return a Promise from the prompt() method that will be awaited
in the consuming code till the user clicks on either Cancel or Confirm. The promise will
be resolved when the user clicks on Cancel or Confirm and the result will be either false
(cancelled) or true (confirmed).
In additional to that, we will initialize the props with custom text labels for the Cancel and
Confirm buttons. We can also initialize the title, or optionally set the title when we call
prompt().
Now, for the html part, we’ll just render the content only if the open flag is true:
...
{#if open}
<div transition:fade={fadeOptions} ... >
...
</div>
{/if}
Note that we are also using Svelte transition binding, specifically to create a fade transition.
You can learn more about Svelte transitions here⁴⁷
The complete html section of our component is this:
⁴⁷https://svelte.dev/tutorial/transition
Chapter 15 - A Primitive Modal 207
...
{#if open}
<div transition:fade={fadeOptions} data-testid={props.testid} class={cssClass()} a\
ria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-cent\
er sm:block sm:p-0">
<!-- Background overlay -->
<div class="fixed inset-0 bg-gray-400 bg-opacity-75" aria-hidden="true"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="\
true">​</span>
<!-- Modal panel -->
<div class="relative inline-block align-bottom bg-white rounded-lg px-4 pt-5 p\
b-4 text-left overflow-hidden shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-ful\
l sm:p-6">
<div>
{#if props.icon}
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-ful\
l bg-green-100">
<svelte:component this={props.icon} addCss={props.iconAddCss}/>
</div>
{/if}
<div class="mt-3 text-center sm:mt-5">
<ElText id="modal-title" tag="h3" text={props.title} addCss="text-lg lea\
ding-6 font-medium"/>
{#if props.longDesc}
<div class="mt-2">
<ElText tag="p" text={props.longDesc} addCss="text-sm text-gray-500"/>
</div>
{/if}
</div>
</div>
<div class="mt-5 sm:mt-6 grid gap-3 sm:grid-cols-2 sm:grid-flow-row-dense">
<ElButton id="modal-cancel" buttonType="secondary" disabled={false} label=\
{props.cancelLabel} on:clicked={onCancelClick}/>
<ElButton id="modal-confirm" buttonType={props.primaryButtonType} disabled\
={false} label={props.confirmLabel} on:clicked={onConfirmClick}/>
</div>
</div>
</div>
</div>
{/if}
Chapter 15 - A Primitive Modal 208
Note: I made some enhancements to the ElButton to render with different css based on a type
classification like primary/secondary/danger etc. Please see the public GitHub repository for
the additional changes
One more thing to notice is how in our ElModal we make use of “Svelte:component” to
dynamically render the icon from the props.icon passed into our ElModal. Here we also
pass the iconAddCss dynamically to the icon. Mind you, “svelte:component” will render any
component, not just an icon. In this case however, we most likely always use it to render an
optional icon within our modal:
File useModal.ts
Create a file called useModal.ts under the same directory (src/components/primitives/-
modals/) with the following code:
// file: src/components/primitives/modals/useModal.ts
import ElModal from './ElModal.svelte'
import { ModalProps } from './ModalProps.interface'
/**
* @name useModal
* @param props The modal props
* @returns the Modal component instance
*/
export const useModal = (props: ModalProps) => {
if (!instance) {
// get the modal target dom element by id
let domTarget = document.getElementById(domTargetId)
// if not existing yet, create it with vanilla JS
if (!domTarget) {
domTarget = document.createElement('div')
domTarget.setAttribute('id', domTargetId)
document.body.appendChild(domTarget)
}
// create the ElModal instance
instance = new ElModal({ target: domTarget })
Chapter 15 - A Primitive Modal 209
The code here just makes sure we create only one instance of the ElModal component
(singleton pattern) and also only create a <div> element as the target for the modal. Then
create the modal instance programmatically, invoke its setProps method to set its properties,
and return the instance to the consuming code.
An example on how we will consume our ElModal through the useModal hook is this:
// example:
const modal = useModal({
cancelLabel: 'Cancel',
confirmLabel: 'Confirm',
longDesc: 'This has also a longer description and an icon',
primaryButtonType: 'danger',
icon: ElIconAlert, // here we can use an optional icon
iconAddCss: 'text-red-600' // additional css classes for the icon
})
Let’s now modify the Primitives.view.svelte so we can test a couple of different scenarios,
for two different modals.
Updates to Primitives.view.svelte
Now let’s consume our useModal hook. Open the Primitives.view.svelte file and make the
following changes:
Chapter 15 - A Primitive Modal 210
<script lang="ts">
...
// import a reference to ElIconAlert
import { ElIconAlert } from '@/components/primitives/icons/'
// import a reference to useModal
import { useModal } from '@/components/primitives/modals/useModal'
...
const onButtonClicked = async (event: CustomEvent<{ id: string }>) => {
console.log('PrimitivesView: onButtonClicked', event.detail.id)
}
// add this new handler for the two new Open Modal X buttons we'll add shortly
const onOpenDialogClicked = async (event: CustomEvent<{ id: string }>) => {
console.log('PrimitivesView: onOpeanDialogClicked', event.detail.id)
Cancel
console.log('----- PrimitivesView: onButtonClicked: modal-2 prompt result', re\
sult)
}
}
...
...
Browser
The app will now render our 3rd button:
Chapter 15 - A Primitive Modal 212
After clicking on the “Open modal 1” button, you will see a modal rendered without an
icon:
The Modal will block the execution at the await line where we call modal.prompt(). After
clicking Ok, you should see it logging “result true” in the console:
Chapter 15 - A Primitive Modal 213
If you click on “Open modal 2” button, you will see a modal rendered with the alert icon:
Note: here the Confirm button type is “primary”, which is teh default for Modal property
“primaryButtonType” when we do not explicitely pass a value for it.
Here too click on Cancel/Confirm and make sure that the console logs “modal-2 result
true/false” as well:
Chapter 15 - A Primitive Modal 214
Chapter 15 - A Primitive Modal 215
Chapter 15 Recap
What We Learned
• We learned how to build a Modal component that leverages some of our previous
primitives like ElText and ElButton and uses a technique with Promises to block the
execution when we invoke modal.prompt() from the consuming code and return true
or false once the Promise is resolved by clicking on Cancel or Confirm
Observations
• We did not add unit tests against the Modal component
Based on these observations, there are a few improvements that can be done:
Improvements
• You could write unit tests against the Modal component to verify it renders and behaves
as expected
Chapter 16 - Higher-level
components
Let’s now consume the primitives we created so far within the Item component.
As we do this, we’ll make any additional adjustment we discover necessary.
Finally, if needed, we might be creating additional primitives that we do not have yet (i.e. a
list)
// file: src/components/items/children/Item.component.svelte
...
...
There are two elements that can be replaced with our primitives. We could use an ElToggle
for the selected indicator, and an ElText for the name.
First, let’s udpate our imports and also add a components block to the component definition
that includes ElButton and ElText:
Chapter 16 - Higher-level components 217
// file: src/components/items/children/Item.component.svelte
...
<script lang="ts">
...
...
// file: src/components/items/children/Item.component.svelte
...
...
Run the application and make sure it still renders the list of items without errors.
Now let’s finish updating the HTML template by replacing the selected indicator with our
ElToggle:
Chapter 16 - Higher-level components 218
// file: src/components/items/children/Item.component.svelte
...
...
NOTE: we do not have to handle the on:clicked event on the ElToggle here as we are already
handling a on:click event on the entire <li> element.
Again, refresh the browser and make sure everything still renders without errors.
It should look currently like this (the layout will be a bit off, so we’ll need to tweak the Item
component CSS and start using TailwindCSS here as well):
// file: src/components/items/children/Item.component.svelte
...
...
Let’s add a new property called isLast that we’ll use to better control the border style:
// file: src/components/items/children/Item.component.svelte
...
...
...
$: cssClass = (): string => {
// begin: remove code block
let css = 'item'
if (item.selected) {
css += ' selected'
}
// end: remove code block
// begin: add code block
let css = 'item flex items-center justify-between cursor-pointer border border-l\
-4 list-none rounded-sm px-3 py-3'
if (props.model?.selected) {
css += ' font-bold bg-pink-200 hover:bg-pink-100 selected'
} else {
css += ' text-gray-500 hover:bg-gray-100'
Chapter 16 - Higher-level components 220
}
if (!isLast) {
css += ' border-b-0'
}
// end: add code block
return css.trim()
}
...
Now, before we proceed, lets remove out all the custom SCSS we wrote at the beginning of
this book for the ItemsList and Item component by removing the entire <style> blocks from
both files.
// file: src/components/items/ItemsList.component.svelte
...
<ul>
{#each items as item, index} <!-- after item, add index here -->
<ItemComponent
testid={`items.list.item.${item.id}`}
{item}
<!-- add the following line: -->
isLast={index === items.length - 1}
on:selectItem={selectItem} />
{/each}
</ul>
...
Chapter 16 - Higher-level components 221
NOTE: You will have to update also the unit tests accordingly as they would be now failing.
Please see the GitHub repo for the updated unit tests if you need help.
Refresh the browser, and if everything is correct it should render like this:
Summary
Let’s reflect a little bit on what we just did. We replaced two HTML elements within the
Item.component.svelte with our new primitives. By doing this, we effectively “composed”
the higher-level component “Item” from those primitives. I hope you start seeing the pattern
here. Even though this was a very simple example, the sky is really the limit on how you
can better structure your primitives and copose more complex primitives out of those, and
ultimately the higher-level components that consume them.
Chapter 16 - Higher-level components 222
Chapter 16 Recap
What We Learned
• We started to learn how to compose higher-level components by putting together the
primitives we created in the previous chapters.
Observations
• We did not leverage localization and internationalization in our primitives.
• We did not write unit tests against our primitives
Based on these observations, there are a few improvements that can be done:
Improvements
• You can add localization and internationalization support through the i18n plugin as
shown in other chapters
• You can write unit tests against the primitives to further create a solid foundation for
your primitives library
Chapter 17 - Creating Component
Libraries
In this chapter we’ll leverage Vite to create a component library that can be shared across
different projects. Once you know how to create a component library, you could choose to
publish it as an NPM package (either public or private) for more easily sharing it between
different projects, or across departments in your organization.
When creating a component library, there are different approaches and architecture decision
to be made, depending on different factors. One of the main thing to keep in mind is the
dependencies that your library will have (i.e. web framework, state, css frameworks, other
frameworks, etc).
In this chapter we’ll worry about only creating a library with a couple of simple components,
we’ll learn how to build it and package it and how to consume it into our sample project.
In the next chapter we’ll build a more complex component that might require additional
things like state etc.
Create my-component-library
To setup the library project, use the terminal and execute the following command (make sure
you are at the same level of your my-svelte-project folder):
The create-vite wizard will start and will ask you the name of the project. The default is
vite-project, so change this to my-component-library and hit enter:
The second step will ask to select a framework. Use the keyboard arrows to scroll down the
list and stop at svelte, then hit enter:
Chapter 17 - Creating Component Libraries 224
The third step will asking which “variant” you want o use. Scroll down to svelte-ts (this is
for the version that uses TypeScript) and hit enter:
This will create a folder called my-component-library which is also the name of our project.
At the end it should display a message similar to this:
cd my-component-library
npm install
npm run dev
The first command will navigate to the current sub-directory called my-component-library,
the second one will install all the npm dependencies, and we do not need to run the third
one in this case.
Now let’s clean up a few things. We need to remove a few files that we are not going to need
since this is a library, and we’ll also need to update the project configuration so that it can
correctly build and bundle our library.
• index.html
• src/App.svelte
• src/app.css (or style.css)
Chapter 17 - Creating Component Libraries 225
• src/main.ts
• src/assets/logo.svg (or svelte.svg)
Update vite.config.ts
Update the Vite’s config file as follows:
// file: vite.config.ts
// https://vitejs.dev/config/
export default defineConfig({
plugins: [svelte()],
envDir: './src/', // <- add this line
resolve: { // <- add this block
alias: {
'@': path.resolve(__dirname, 'src/')
},
},
build: { // <- add this block
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: "MyComponentLib",
fileName: (format) => `my-component-lib.${format}.js`,
},
rollupOptions: {
// Svelte should not be bundled with the cmoponent library
// tell vite that this is an external dependency
external: ['svelte'],
output: {
// To expose global variables for use in the UMD builds
// for external dependencies
globals: {
svelte: 'Svelte'
}
}
}
Chapter 17 - Creating Component Libraries 226
}
})
• we are telling Vite that the environment directory for the source code is ./src/
• we added a “resolve” block so we can use the @ shortcut to point to src/ and avoid
imports with relative paths (i.e. ../../../)
• we added a “build” block, and this is the most important change for setting up the project
as a library. Here:
– we tell Vite which is the main entry file for our library (src/index.ts),
– set the name of our library to MyComponentLib
– set the name of the main built files to be my-component-lib.${format}.js (where
format will be set dynamically to es or umd)
– set the rollupOptions so that Svelte will not be bundled with our library (we’ll
assume this library is consumed in a project that already uses Svelte thus we do
not want to include it multiple times)
• svelte2tsx-shims.d.ts
• svelte2tsx.index.js
The content of svelte2tsx-shims.d.ts will be the following (Note: these are just shims to
allow svelte2tsx to do its work for generating types declarations. I grabbed this from the
svelte2tsx repository but reposting here to make it easier):
Chapter 17 - Creating Component Libraries 227
// file: svelte2tsx-shims.d.ts
// -- start svelte-ls-remove --
declare module '*.svelte' {
export default _SvelteComponent
}
// -- end svelte-ls-remove --
=> void;
/**
* Removes a component from the DOM and triggers any `onDestroy` handlers.
*/
$destroy(): void;
/**
* Programmatically sets props on an instance.
* `component.$set({ x: 1 })` is equivalent to `x = 1` inside the component's `<sc\
ript>` block.
* Calling this method schedules an update for the next microtask — the DOM is __n\
ot__ updated synchronously.
*/
$set(props?: Partial<Props>): void;
// From SvelteComponent(Dev) definition
$$: any;
$capture_state(): void;
$inject_state(): void;
}
type SvelteActionReturnType = {
update?: (args: any) => void,
destroy?: () => void
} | void
type SvelteTransitionConfig = {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
type SvelteAnimationReturnType = {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
// Forces TypeScript to look into the type which results in a better representation \
of it
// which helps for error messages and is necessary for d.ts file transformation so t\
hat
Chapter 17 - Creating Component Libraries 230
): any;
// v2
declare function __sveltets_2_createCreateSlot<Slots = Record<string, Record<string,\
any>>>(): <SlotName extends keyof Slots>(slotName: SlotName, attrs: Slots[SlotName]\
) => Record<string, any>;
declare function __sveltets_2_createComponentAny(props: Record<string, any>): _Svelt\
eComponent<any, any, any>;
type __sveltets_2_SvelteAnimationReturnType = {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
declare var __sveltets_2_AnimationMove: { from: DOMRect, to: DOMRect }
declare function __sveltets_2_ensureAnimation(animationCall: __sveltets_2_SvelteAnim\
ationReturnType): {};
type __sveltets_2_SvelteActionReturnType = {
update?: (args: any) => void,
destroy?: () => void
} | void
declare function __sveltets_2_ensureAction(actionCall: __sveltets_2_SvelteActionRetu\
rnType): {};
type __sveltets_2_SvelteTransitionConfig = {
Chapter 17 - Creating Component Libraries 234
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}
type __sveltets_2_SvelteTransitionReturnType = __sveltets_2_SvelteTransitionConfig |\
(() => __sveltets_2_SvelteTransitionConfig)
declare function __sveltets_2_ensureTransition(transitionCall: __sveltets_2_SvelteTr\
ansitionReturnType): {};
// Includes undefined and null for all types as all usages also allow these
declare function __sveltets_2_ensureType<T>(type: AConstructorTypeOf<T>, el: T | und\
efined | null): {};
declare function __sveltets_2_ensureType<T1, T2>(type1: AConstructorTypeOf<T1>, type\
2: AConstructorTypeOf<T2>, el: T1 | T2 | undefined | null): {};
// The following is necessary because there are two clashing errors that can't be so\
lved at the same time
// when using Svelte2TsxComponent, more precisely the event typings in
// __sveltets_2_ensureComponent<T extends new (..) => _SvelteComponent<any,||any||<-\
this,any>>(type: T): T;
// If we type it as "any", we have an error when using sth like {a: CustomEvent<any>}
// If we type it as "{}", we have an error when using sth like {[evt: string]: Custo\
mEvent<any>}
// If we type it as "unknown", we get all kinds of follow up errors which we want to\
avoid
// Therefore introduce two more base classes just for this case.
/**
* Ambient type only used for intellisense, DO NOT USE IN YOUR PROJECT
*/
declare type ATypedSvelteComponent = {
/**
* @internal This is for type checking capabilities only
* and does not exist at runtime. Don't use this property.
*/
$$prop_def: any;
/**
* @internal This is for type checking capabilities only
* and does not exist at runtime. Don't use this property.
*/
$$events_def: any;
/**
Chapter 17 - Creating Component Libraries 235
For the code for file svelte2tsx.index.js we’ll just create a simple function that leverage
svelte2tsx’s emitDts helper:
// file: svelte2tsx.index.js
(async () => {
await emitDts({
declarationDir: 'dist/',
svelteShimsPath,
...config,
libRoot: config.libRoot ? join(cwd, config.libRoot) : cwd
Chapter 17 - Creating Component Libraries 236
})
})()
Svelte version
One thing I had to do at the time of this writing is downgrade the Svelte version in my-
component-library in order to avoid an error when consuming the library in my-svelte-
project that said Uncaught (in promise) TypeError: Cannot read property '$$' of undefined
So, remove the current version and install version 3.39:
Finally, let’s proceed updating the package.json commands so we can correctly build our
library.
// file: src/package.json
{
"name": "my-component-library",
"version": "0.1.2",
"scripts": {
"clean": "rm -rf ./dist; rm -rf my-component-library-0.1.2.tgz; rm -rf ../my-com\
ponent-library-0.1.2.tgz",
"build-types": "node svelte2tsx.index",
"build-lib": "vite build",
"build": "npm run clean && npm run build-lib && npm run build-types",
"pack": "npm pack; mv my-component-library-0.1.2.tgz ../my-component-library-0.1\
.2.tgz",
"all": "npm run build && npm run pack",
"preversion": "npm run clean",
"version": "npm run build",
"postversion": "npm run pack",
"version-patch": "npm version patch -m \"Patch version\""
Chapter 17 - Creating Component Libraries 237
...
The most important thing to notice here is that we have a master command called “all” that
will run the build and then the pack command. The pack command is optional and will create
a single compressed (tgz) file with all our library code, then copy this up to one directory so
we could more easily consume it from our my-vue-project.
The sub-commands run buy the build command are:
• clean: this will just remove the dist/ folder and the previously packed tgx file
• build-types: this will build the TypeScript types declarations (note that here we are
running our code in the svelte2tsx.index.js created earlier)
• build-lib: this will build our Svelte library code
• build: this will run the clean + build-lib + build-types sub-commands
We need to also make changes to package.json so that it can correctly build the project as a
library. We need to add these sectoins/properties:
• files: this tells which directory is the destinatio for the built JavaScript files (dist in our
case)
• types: this indicates the entry file for the TypeScript definitions
• main: this indicates the main entry file for our library (umd module)
• module: this indicates the main entry file for our library (es module)
• exports: this section indicates what our package will export
// file: src/package.json
...
"files": [
"dist"
],
"types": "./dist/src/index.d.ts",
"main": "./dist/my-component-lib.umd.js",
"module": "./dist/my-component-lib.es.js",
"exports": {
".": {
"import": [
Chapter 17 - Creating Component Libraries 238
"./dist/my-component-lib.es.js"
],
"require": "./dist/my-component-lib.umd.js"
},
"./package.json": "./package.json"
},
...
// file: src/components/counter/Counter.svelte
<script lang="ts">
let count: number = 0
const increment = () => {
count += 1
}
</script>
<button on:click={increment}>
count is {count}
</button>
// file: src/components/sample-component/SampleComp.svelte
<script lang="ts">
</script>
// file: src/components/index.ts
export {
Counter,
SampleComp
}
Then open the file App.svelte and add the following imports at the top:
// file: src/App.svelte
<script lang="ts">
import {
SampleComp,
Counter
} from 'my-component-library'
...
// file: src/App.svelte
...
<main>
<div class="home m-2 p-2 border-2 border-red-500">
<SampleComp text="This is a sample component from my-component-library" />
<Counter />
...
Save and run the application. If everything worked and there are no errors, you should see
something like this in the browser (here shown after I clicked two times on the count button):
Chapter 17 - Creating Component Libraries 241
Chapter 17 - Creating Component Libraries 242
Chapter 17 Recap
What We Learned
• We create a new project called my-component-library that will export a couple of simple
components
• We then consumed these components in our my-vue-project
Observations
• We did not write unit tests against our components within my-component-library
• We did not publish our component library to NPM.
• We did not write more complex components that leverage other dependencies like
application state or other libraries
Based on these observations, there are a few more things that can be done:
Improvements
• You can add unit tests within the my-component-library and test your components
• You could keep adding more complex components to your library that use application
state or other dependencies
Chapter 18 - Creating a JavaScript
library
Similarly to what we discussed in Chapter 17, we can create a library that we can publish
as an NPM package that does not necessarily contains Svelte components. This might be a
collection of helpers, or a plugin, etc.
As you start building more complex application that will grow to a large code base, it starts
to make sense to be more strict about following principles like Single Responsibility and
Separation of Concerns⁴⁸.
Separating code that can be shared across different applications/projects into its own NPM
package has many advantages, and if you publish it as an open-source project with a
permissive license, other developers might start using it as well, providing more feedback
and reporting or even helping with bugs. This might result in your package growing even
stronger as time goes by.
There are a few downsides as well, like having to maintain a separate code base, having to
publish a new version whenever you add new functionality or fix a bug.
Unless you are working only on one small application, and/or the code within your NPM
package has not utility in other applications (or does not offer much benefits to other
developers), usually the advantages will make it worth it to have it as an NPM package.
Create my-js-helpers
We’ll create a new project called my-js-helpers by following similar to those as at the
beginning of Chapter 17 for my-component-library (just make sure you use the name my-js-
helpers this time).
Please note, this chapter will just illustrate how to create a simple NPM package that
exposes some simple JavaScript helpers, thus the name my-js-helpers. But, of course, you
are welcome to choose whatever name you wish for your NPM package.
One main difference after you run npm init vite@latest and set my-js-helpers as the name,
is to choose vanilla for the framework selection:
⁴⁸https://en.wikipedia.org/wiki/Separation_of_concerns
Chapter 18 - Creating a JavaScript library 244
After you are done creating the project and have run “npm install”, let’s continue by
removing unecessary files (similarly to Chapter 17)
• favicon.svg
• typescript.svg
• app.css (or style.css)
• index.html
• main.ts
• counter.ts
// file: src/index.ts
export * from './helpers'
Update vite.config.ts
Update the Vite’s config file similarly to what we did in Chapter 17 (just make sure to replace
my-component-library with my-js-library). Here is what it should look like:
// file: vite.config.ts
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
],
envDir: './src/',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src/')
},
},
test: {
globals: true,
environment: 'jsdom',
exclude: [
'node_modules'
]
},
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'MyJsHelpers',
fileName: (format) => `my-js-helpers.${format}.js`,
},
rollupOptions: {
external: [],
output: {
Chapter 18 - Creating a JavaScript library 246
// file: src/package.json
{
"name": "my-component-library",
"version": "0.1.2",
"scripts": {
"clean": "rm -rf ./dist; rm -rf my-js-helpers-0.1.2.tgz; rm -rf ../my-js-helpers\
-0.1.2.tgz",
"build-types": "tsc --declaration --emitDeclarationOnly --outDir ./dist",
"build-lib": "vite build",
"build": "npm run clean && npm run build-lib && npm run build-types",
"pack": "npm pack; mv my-js-helpers-0.1.2.tgz ../my-js-helpers-0.1.2.tgz",
"all": "npm run build && npm run pack",
"preversion": "npm run clean",
"version": "npm run build",
"postversion": "npm run pack",
"version-patch": "npm version patch -m \"Patch version\""
}
...
And similarly to Chapter 17, lets add additional configuration so that the project will build
as a library:
Chapter 18 - Creating a JavaScript library 247
// file: src/package.json
...
"files": [
"dist"
],
"types": "./dist/src/index.d.ts",
"main": "./dist/my-js-helpers.umd.js",
"module": "./dist/my-js-helpers.es.js",
"exports": {
".": {
"import": [
"./dist/my-js-helpers.es.js"
],
"require": "./dist/my-js-helpers.umd.js"
},
"./package.json": "./package.json"
},
...
random-id
Create the directory src\helpers\random-id and inside here add a file called random-id.ts
(the full location path will be src/helpers/random-id/random-id.ts) with the following code:
// file: src/helpers/random-id/random-id.ts
result = require('crypto').randomBytes(5).toString('hex')
}
// pad the result with zero to make sure is always the same length (11 chars in ou\
r case)
if (result.length < 11) {
result = result.padStart(11, '0')
}
return result
}
Note: this is a very simple function that leverage the web browser native crypto api to generate
a rabdin string. If the browser does not support crypto (or maybe you want to consume this
package in a node.js app), you could either throw an error (commented out in the above code)
or leverage node.js crypto library. We also make syre the string is always 11 chars long, and
if not we leverage the string padStart method to pad the start of the string with zeros
Add also a barrel index.ts file to just export the code from random-id/random-id.ts.
Create the directory src\tests\random-id and here add a file called randomid.test.ts with
the following:
// file: src/tests/random-id/randomid.test.ts
describe('id', () => {
results.push(value)
}
Here we have a unit test that ensure the result from randomid() is of the expected length,
which is 11 chars. We also have a unit test that invokes randomid() ten thousand times and
then checks that the distinct results count matches the results count. If these do not match
it means that randomid is in some cases returning a non-unique id and thus fail.
Note: we leverage the JavaScript Set to get rid of potential duplicates.⁴⁹
Before we try to run our tests, let’s install vitest and additional unit-test depedencies we
need with npm install -D vitest @types/jest jsdom and then add the following 2 new
commands to the package.json scripts section:
// file: package.json
{
"name": "@largescaleapps/my-js-helpers",
"version": "0.1.2",
"type": "module",
"scripts": {
...
...
Now finally execute the command npm run test and it should output something like this:
⁴⁹https://dev.to/soyleninjs/3-ways-to-remove-duplicates-in-an-array-in-javascript-259o
Chapter 18 - Creating a JavaScript library 250
✓ src/tests/random-id/randomid.test.ts (2)
Note that we are referencing our helpers library with a relative directory path so it is
important that oyu have create the my-js-helpers project at the same level of my-svelte-
project.
To test that we can consume our library without problems, open one of the views, maybe
App.svelte, and import a reference to randomid:
import {
randomid
} from 'my-js-helpers'
And then output the value in the UI with some HTML like:
Or maybe you could add the output to the text property of the SampleComp created in the
previous chapter:
Chapter 18 - Creating a JavaScript library 251
Chapter 18 Recap
What We Learned
• We created a new project called my-js-helpers that will export an helper method called
randomid that returns a unique id value
• We wrote some basic unit tests against our randomid helper function
• We then built this library and consumed it in our my-svelte-project to display in the UI
the value returned by the randomid() helper
Observations
• We did not publish our library to NPM yet.
Based on these observations, there are a few more things that can be done:
Improvements
• In the next chapter will learn how to publish our library to the NPM registry and then
consume it from there
Chapter 19 - Publish a library as a
NPM package
Publishing to the NPM registry is pretty straigh forward. However, there are many different
options like publishing private packages etc that might also interested you. For this, is a
good idea to review the official documentation here: https://docs.npmjs.com/packages-and-
modules/contributing-packages-to-the-registry.
Here we’ll explain only how to publish scoped public packages but will not cover private
packages or unscoped packages.
// file: package.json
{
"name": "@your-org-name/my-js-helpers", // prefix is in the form @username/ or @or\
gname/
"version": "0.1.21",
...
npm login
It will prompt you for username, password and email (careful: this email will be public so
feel free to use an email that is different from the one used in the NPM account):
Note: if you have 2FA (two-factor authentication) setup in NPM, it will also prompt you to
enter an OTP code:
npm notice Please use the one-time password (OTP) from your authenticator application
Enter one-time password: [yourOtpCode]
Now you can publish the my-js-helpers package by first navigating to the root of the my-js-
helpers directory, and then execute the command:
Chapter 19 - Publish a library as a NPM package 255
Then we can install the one form the NPM registry with:
Chapter 19 Recap
What We Learned
• We learned how to publish our library as an NPM package on the NPM registry using
a user-scope or organization-scope
• We learned how to install our NPM package from the NPM registry and consume it as
we did before when it was installed form the local directory
• We learned how to bump the version of our NPM package and publish a new version
on NPM
Observations
• We did not publish the other library we created in Chapter 17 which is a component
library (my-component-library)
Based on these observations, there are a few more things that you could try:
Improvements
• You can try to publish also the my-component-library as an NPM package and then
consume it from NPM
(More Chapters Coming Soon)
Naming Conventions
In this book we have been providing some direction on both naming standards for code
elements like interface, classes etc, as well as for directory and file names. Here is a detailed
description of the standard we followed in this book.
NOTE: These are mostly suggestions, a starting point. You should always agree with your
team on the naming conventions and directory structure standards that will work best for
your company. Then the whole team should commit to follow those conventions.
Coding Standards
TypeScript any
Directory Names
In general, we’ll use lower-case for naming directories. When this contains multiple words,
they will be separated by a hyphen (dash). I.e. order-history
We try to keep code files organized in directories by category (i.e. components, models,
plugins) and sub-directories
Sub-directories are organized by app domain for models, i.e. models/items, models/cus-
tomers, models/order-history, models/locales etc
For components, they are organized by component domain or functionality, i.e. compo-
nents/items, components/locales, components/order-history etc.
Naming Conventions 259
In general, if a model or a component is used across all domains, then the sub-directory name
is shared (or common if you prefer), i.e. components/shared
Primitive components will be under the directory primitives (src/primitives).
File Names
Higher-order components files will be under src/components directory. Their names follow
the convention [ComponentName].component.svelte. I.e. ItemsList.component.svelte
Primitive components will be under src/primitives. Their names follow the convention
El[ComponentName].svelte. I.e. ElButton.svelte, ElTextbox.svelte, etc
NOTE: If you have to write many unit tests against the same class or component to test specific
areas (i.e. security, permissions etc) might be a good idea to also split the code into additional
files named with additional suffixes (as long as you adopt a standard that makes sense and
it’s easy to follow).
This could be a convention to follow in that case: [ClassOrComponentBeingTested].[area-
tested].[condition].[value].test.ts and here are a couple of examples:
• ItemsList.permissions.view.no.ts (to test when user does not have View permisions)
• ItemsList.permissions.view.yes.ts (to test when user has View permisions)
Directory src
• src/assets: contains static assets like image files etc (organized in further sub-directories)
src/components: contains the higher order components (while primitives are within a sub-
directory)
src/components/primitives: contains all the primitives (i.e. buttons, inputs, etc) organized
in sub-directories by families:
• buttons
• icons
• etc // add more directories as you keep building your primitives foundation
Naming Conventions 261
src/views: contains all the views, except for the App.svelte which is located directly under
src/
Directory tests/unit
Contains all the unit tests that are not for components. (each component unit test will be
located at the same level of the corresponding component)
• tests/unit: contains the unit tests against TypeScript models (not components) orga-
nized in sub-directories by domain/area or however you see fit
Resources
Websites
Official Website:
https://svelte.dev
Official TailwindCSS Website:
https://tailwindcss.com
Svelte Testing Library:
https://testing-library.com/docs/svelte-testing-library/intro
Vitest:
https://vitest.dev
Resources 263
Tutorials
Official Svelte Tutorials
https://svelte.dev/tutorial
… more coming soon
Resources 264
Blogs
Atomic Design
… coming soon
Resources 265
Books
… coming soon