100% found this document useful (1 vote)
2K views273 pages

Large Scale Apps With Svelte and TypeScript by Dam

This document is the table of contents for a book about building large and scalable frontends with Svelte and TypeScript. The book covers topics like setting up a project, building components, adding data models and interfaces, unit testing components, implementing state management with a store, and creating an API client to fetch real and mock data. Each chapter introduces these concepts at a beginner level and includes code examples from a sample application to demonstrate the techniques.

Uploaded by

Tran quoc Huy
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
2K views273 pages

Large Scale Apps With Svelte and TypeScript by Dam

This document is the table of contents for a book about building large and scalable frontends with Svelte and TypeScript. The book covers topics like setting up a project, building components, adding data models and interfaces, unit testing components, implementing state management with a store, and creating an API client to fetch real and mock data. Each chapter introduces these concepts at a beginner level and includes code examples from a sample application to demonstrate the techniques.

Uploaded by

Tran quoc Huy
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 273

Large Scale Apps with Svelte and

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 version was published on 2023-01-30

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.

© 2022 - 2023 Damiano Fusco


Contents

LARGE SCALE APPS WITH SVELTE AND TYPESCRIPT . . . . . . . . . . . . . . . . . 1


Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Goal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Audience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Text Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Thanks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
About me . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Companion Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Chapter 1 - Setting Up The Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Create Project Wizard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Chapter 1 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Chapter 2 - Your First Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
The Items List . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
ItemsList Component Requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
ItemsList Component Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
App.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Chapter 2 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Chapter 3 - Data Model Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Models Directory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
Interface ItemInterface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
ItemsList Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
App.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Chapter 3 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
Chapter 4 - Adding Events To the Items Component . . . . . . . . . . . . . . . . . . . . 25
ItemsList Component . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
CONTENTS

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

.env files updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122


Config Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
Config files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
tsconfig.json updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
Config files map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Config provider . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
Unit Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
HttpClient code updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Api Client code updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
Chapter 9 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
Chapter 10 - Localization and Internationalization - Language Localization . . . . 141
Plugins: i18next, svelte-i18n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Config updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Translation JSON data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
API Client updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
Updates to ApiClient.interface.ts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
Updates to ApiClient instances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
i18n initialization and useLocalization hook . . . . . . . . . . . . . . . . . . . . . . . . . 152
App.svelte updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
Chapter 10 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
Chapter 11 - Localization and Internationalization - Number and DateTime
Formatters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Directory localization/formatters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
Chapter 11 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
Chapter 12 - Adding Tailwind CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Chapter 12 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Chapter 13 - Intro to Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Atomic Design and Similar Approaches . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
General Strategies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Text Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
Primitives View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
Chapter 13 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
Chapter 14 - More Primitives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Button Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 190
CONTENTS

Primitives View - update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192


Toggle/Checkbox Elements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Primitives View - one more update . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Chapter 14 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
Chapter 15 - A Primitive Modal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Icon: ElIconAlert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
Interface ModalProps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203
File ElModal.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
File useModal.ts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208
Updates to Primitives.view.svelte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
Browser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
Chapter 15 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Chapter 16 - Higher-level components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
Item Component - updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
ItemsList Component - updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Chapter 16 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Chapter 17 - Creating Component Libraries . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Create my-component-library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Chapter 17 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
Chapter 18 - Creating a JavaScript library . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Create my-js-helpers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Chapter 18 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
Chapter 19 - Publish a library as a NPM package . . . . . . . . . . . . . . . . . . . . . . . 253
Create an NPM user account . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253
Create an Organization under your NPM profile . . . . . . . . . . . . . . . . . . . . . . 253
Update my-js-helpers package.json . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Publishing the library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
Consuming your NPM package . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Chapter 19 Recap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
(More Chapters Coming Soon) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Naming Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
Coding Standards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
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

Copyright © 2022 by Damiano Fusco (first published in January 2022)


All rights reserved. No part of this publication may be reproduced, distributed, or transmitted
in any form or by any means, including photocopying, recording, or other electronic or me-
chanical methods, without the prior written permission of the author and publisher, except
in the case of brief quotations embodied in critical reviews and certain other noncommercial
uses permitted by copyright law. For permission requests, write to the author and publisher,
addressed “Attention: Permissions Coordinator,” at the email [email protected].
Preface
Why Svelte, Vite and what we mean by “large scale apps” in this book.
Svelte is a modern JavaScript framework for building user interfaces. It offers several
advantages over other frameworks, such as:

• Minimal boilerplate and size overhead


• Faster loading times due to code-splitting and lazy-loading
• Improved runtime performance through virtual DOM optimization

Vite is a modern build tool for JavaScript projects that aims to provide fast and efficient
builds. It offers several benefits, including:

• Faster build times and development experience compared to traditional bundlers.


• Lower initial load times, as only the essential code is loaded
• Improved build size, as Vite only includes the necessary code
• Lightweight and optimized for modern web development.

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:

• Code maintenance and scalability


• Code quality and performance
• Code organization and structure

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⁷

Create Project Wizard


To set up the project, use the terminal and execute the following node.js command:

npm init vite@latest

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:

Need to install the following packages:


create-vite@latest
Ok to proceed? (y)

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:

? Project name: › my-svelte-project

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

? Select a framework: › - Use arrow-keys. Return to submit.


vanilla
vue
react
preact
lit
� svelte

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:

? Select a variant: › - Use arrow-keys. Return to submit.


svelte
� svelte-ts

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:

Scaffolding project in /Volumes/projects/my-svelte-project...

Done. Now run:

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:

vite v3.1.0 dev server running at:

> Local: http://localhost:3000/


> Network: use `--host` to expose

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:

• Quick prototyping and development using mocked data


• Component Organization
• Unit Testing
• State Management
• Internationalization support so we can render our user interface using different lan-
guages
• Localization for number and date formatting for different cultures

ItemsList Component Requirements


Your initial version of the ItemsList component, will have to implement the following
requirements (later, in more advanced chapters, we will expand on these as we get into
more advanced topics):

• The component will display a list of items


• An item will have 3 properties:

* id
* name
* selected

• The item name will be displayed to the user


• The user should be able to select/deselect one or more item
• An icon will be shown next to the name to indicate if the item is selected
Chapter 2 - Your First Component 13

ItemsList Component Code


Within the src/components directory, create a sub-directory called items. Within this folder
add a new file called ItemsList.component.svelte⁹
Your directory structure will now look like this:

Within the ItemsList.component.svelte file, paste the following code:

// 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

{#each items as item}


<li>
{item.name}
</li>
{/each}
</ul>
</div>

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>

Within <script> the section, import a reference to the ItemsListComponent:

¹⁰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'

// mock some data:


const items: any[] = [{ // explicetely using any[] as we'll replace this with an i\
nterface in the next chapters
id: 1,
name: 'Item 1'
}, {
id: 2,
name: 'Item 2'
}, {
id: 3,
name: 'Item 3'
}]
</script>
...

Finally, we add an <ItemsListComponent> element within the <main> markup. Insert it


within the <div class=”home”> element. Add an attribute called items to our <ItemsList-
Component>. This is how we pass properties to from parent to child components in Svelte¹³.
The items data is fed into the component items property this way. The complete code within
the App.svelte file should now look like this:

¹²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'

// mock some data:


const items: any[] = [{
id: 1,
name: 'Item 1'
}, {
id: 2,
name: 'Item 2'
}, {
id: 3,
name: 'Item 3'
}]
</script>

<main>
<div class="home">
<ItemsListComponent items={items}/>
</div>
</main>

Comment out or remove the app.css import reference from the src/main.ts file:

// import './app.css' // <-- remove this line


import App from './App.svelte'

const app = new App({


target: document.getElementById('app')
})

export default app

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:

• id: this is a unique number for each item in the list


• name: is a string containing the name of the item
• selected: is a boolean value that shows if the user has selected the item

The code for your interface should look like this:


Chapter 3 - Data Model Interfaces 21

// file: src/models/items/Item.interface.ts

export interface ItemInterface {


id: number
name: string
selected: boolean
}

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>
...

The complete update code should look like this:

¹⁴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'

// mock some data:


const items: ItemInterface[] = [{ // here change any[] to ItemInterface[]
id: 1,
name: 'Item 1',
selected: false // add selected: false to each item
}, {
id: 2,
name: 'Item 2',
selected: false
}, {
id: 3,
name: 'Item 3',
selected: false
}]
</script>

...

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[] = []

// item click handler


const handleClick = (item: ItemInterface) => {
item.selected = !item.selected
console.log('handleItemClick', item.id, item.selected)
}
</script>
...

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(item) }

If we did not have to pass data we could just simply do:

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

Paste the following code in the file:

// 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'

// expose a property called items with a default value of a blank array


export let item: ItemInterface = {
id: -1,
name: '',
selected: false
}

// create an instance of Svelte event dispatcher


const dispatch = createEventDispatcher()

// 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

$: cssClass = (): string => {


let css = 'item'
if (item.selected) {
css += ' selected'
}
return css.trim()
}

// item click handler


function handleClick (item: ItemInterface) {
// dispatch a 'selectItem' even through Svelte dispatch
dispatch('selectItem', {
item
})
}
</script>

<li data-testid={testid} role="button" class={cssClass()} on:click={() => handleClic\


k(item)}>
<div class="selected-indicator">*</div>
<div class="name">{item.name} [{item.selected}]</div>
</li>

<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>

And add some css at the end of the App.svelte code:

// 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.

Add unit tests support to our project


We need to configure our project to be able to run unit tests. We need to add dependencies
on a few npm packages and some configuration in order to be able to add unit tests for our
components.
NOTE: In a preliminary version of this book we were using jest, but jest has become quite old
and going forward is much better to use Vitest. Therefore, changes have been made in the
latest version of this book. If you run into trouble please refer to the public GitHub repository
for the sample source code.
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 34

Dependencies
Let’s start installing our npm dependencies first.
Install Vitest¹⁸ npm package:

npm i -D vitest

Install Svelte Testing Library¹⁹:

npm i -D @testing-library/svelte

Install jsdom and @types/jest:

npm i -D jsdom @types/jest

Configuration
Now we need to configure a few things to be able to run unit tests.

tsconfig.json file

Add “vite/client” and “vitest/globals” to tsconfig.json compilerOptions types:

// 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:

// file: my-svelte-project/vite.config.js (and any other vite.config.xyz.js file)

...

export default defineConfig({


...
test: {
globals: true,
environment: 'jsdom',
exclude: [
'node_modules'
]
}
})

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",
}
...

ItemComponent Unit Tests


Add our first two unit tests again our newly created component ItemComponent.
Within the same directory where our Item.component.svelte is located, add two new files:

• one called Item.rendering.test.ts


• one called Item.behavior.test.ts
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 36

Your directory structure will look now ike this:

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

// import a reference to testing library "render"


import { render, screen } from '@testing-library/svelte'

// import reference to our interface


import type { ItemInterface } from '../../../models/items/Item.interface'
// import reference to your Item component:
import component from './Item.component.svelte'

describe('Item.component: rendering', () => {

it('renders an Item text correctly', () => {


// our data to pass to our component:
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 37

const item: ItemInterface = {


id: 1,
name: 'Unit test item 1',
selected: false
}

const testid = 'unit-test-appearance-2'

// render component
render(component, {
testid,
item
})

// get element reference by testid


const liElement = screen.getByTestId(testid)

// 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:

npm run test

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

...

> [email protected] test


> vitest run

RUN v0.23.4 ...

√ src/components/items/children/Item.rendering.test.ts (1)

Test Files 1 passed (1)


Tests 1 passed (1)
Start at 18:24:17
Duration 1.09s (transform 531ms, setup 0ms, collect 355ms, tests 10ms)

...

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

...

describe('Item.component: rendering', () => {

...

it('has expected css class when selected is true', () => {


// our data to pass to our component:
const item: ItemInterface = {
id: 1,
name: 'Unit test item 2',
selected: true // note this is true
}

const testid = 'unit-test-appearance-2'

// render component
render(component, {
testid,
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 39

item
})

// get element reference by testid


const liElement = screen.getByTestId(testid)

// test
expect(liElement).not.toBeNull()
// check that the element className attribute has the expected value
expect(liElement.className).toContain('selected')
})

it('has expected css class when selected is false', () => {


// our data to pass to our component:
const item: ItemInterface = {
id: 1,
name: 'Unit test item 3',
selected: false // note this is false
}

const testid = 'unit-test-appearance-3'

// render component
render(component, {
testid,
item
})

// get element reference by testid


const liElement = screen.getByTestId(testid)

// 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

// import references to testing library "render" and "fireEvent"


import { render, screen, fireEvent } from '@testing-library/svelte'

// import reference to our interface


import type { ItemInterface } from '../../../models/items/Item.interface'
// import reference to your Item component:
import ItemComponent from './Item.component.svelte'

describe('Item.component: behavior', () => {

// Note: This is as an async test as we are using `fireEvent`


it('click event invokes onItemSelect handler as expected', async () => {
// our data to pass to our component:
const item: ItemInterface = {
id: 1,
name: 'Unit test item 1',
selected: false
}

const testid = 'unit-test-behavior-1'

// using testing library "render" to get the element by text


const { component } = render(ItemComponent, {
testid,
item
})

// get element reference by testid


const liElement = screen.getByTestId(testid)

// create a spy function with vitest.fn()


const mockOnItemSelect = vitest.fn()
// wire up the spy function on the event that is dispatched as 'selectEvent"
component.$on('selectItem', mockOnItemSelect)
// trigger click on the <li> element:
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 41

// 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)

// check test result (should have been called once)


expect(mockOnItemSelect).toHaveBeenCalledTimes(1)
})

})

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'

// expose a property called items with a default value of a blank array


export let items: ItemInterface[] = []

// begin: remove code block:


// // item click handler
// const 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)
// }
// end: remove code block:

// begin: add code block


Chapter 5 - Intro to Unit Testing While Refactoring a Bit 42

// item select handler


function onSelectItem (event: CustomEvent<{ item: ItemInterface }>) {
const item = event.detail.item
item.selected = !item.selected
items = items
console.log('onSelectItem', item.id, item.selected)
}
// end: add code block
</script>

<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 -->

<!-- add a reference to the item component -->


<ItemComponent item={item} on:selectItem={onSelectItem} />
{/each}
</ul>
</div>

<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>

Note how we are


handling the dispatched ‘selectItem’ event using the
on:selectItem={onSelectItem} binding. This time our handler (onSelectItem) will receive an
event of type CustomEvent), and to access our item we’ll ahve to use event.detail.item
If you are not already running the app, run it. In the web browser, the list should now render
similar to this (here we are showing it after we clicked on the 2nd item element and is now
Chapter 5 - Intro to Unit Testing While Refactoring a Bit 43

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:

• we will invoke an action on our state manager from a component


• the state manager will perform some tasks within that action
• the state manager will commit a change to our state
• the state manager will be organized into modules (each module will represent a
odmain/area of the application. I.e. items, authors, companies, projects, products,
categories, etc)

Start creating all the interfaces we need so we can better understand the abstraction of what
we are trying to accomplish.

Items Store Interfaces


We will create first the interfaces for the items store module. Create the directory src/s-
tore/items. Inside here, create a directory called models.
Chapter 6 - State Management 47

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

import { ItemInterface } from '../../../models/items/Item.interface'

/**
* @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

// import a reference to Svelte's writable from 'svelte/store'


import * as SvelteStore from 'svelte/store'
// import a reference to our ItemInterface:
import { ItemInterface } from '../../../models/items/Item.interface'

...

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:

• actions (used to initiate a state change)


• getters (used to retrieve data from the 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

// import a reference to Svelte's writable from 'svelte/store'


import * as SvelteStore from 'svelte/store'
// import a reference to our ItemInterface:
import { ItemInterface } from '../../../models/items/Item.interface'

/**
* @name ItemsStoreActionsInterface
* @description Interface represents our Items state actions
*/
Chapter 6 - State Management 49

export interface ItemsStoreActionsInterface {


// TODO: we'll add code here in a second
}

...

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
}

The complete code for our ItemsStore.interface.ts is as follows:

// file: src/store/items/models/ItemsStore.interface.ts

// import a reference to Svelte's writable from 'svelte/store'


import * as SvelteStore from 'svelte/store'
// import a reference to our ItemInterface:
import { ItemInterface } from '../../../models/items/Item.interface'

/**
* @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
}

Finally, let’s add an index.ts²⁷ to just export both interfaces:

// file: src/store/items/models/index.ts

export * from './ItemsState.interface'


export * from './ItemsStore.interface'

Root Store Interfaces


Create now the root store interfaces. Create the directory src/store/root. Inside here, create
a directory called models.
²⁷index.ts files that just export code from the same folder are called “Barrels” files
Chapter 6 - State Management 52

RootStore.interface.ts

Here add a file called RootState.interface.ts and paste the following code in it:

// file: RootStore.interface.ts

import { ItemsStoreInterface } from '../../items/models/ItemsStore.interface'

/**
* @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

export * from './RootStore.interface'

Store Implementation
Now let’s write the implementations for our interfaces.

Items Store instance


Let’s implement the items store module.

Items.store.ts

Add a file called Items.store.ts and paste the following code in it:

// file: src/store/items/Items.store.ts

// import a reference to Svelte's writable from 'svelte/store'


import * as SvelteStore from 'svelte/store'

// import references to our itesms tore and actions interfaces


import {
ItemsStateInterface,
ItemsStoreInterface,
ItemsStoreActionsInterface,
ItemsStoreGettersInterface
} from './models'
// import a reference to our ItemInterface
import { ItemInterface } from '../../models/items/Item.interface'

const writableItemsStore = SvelteStore.writable<ItemsStateInterface>({


loading: false,
items: []
})
Chapter 6 - State Management 54

// hook to allows us to consume the ItemsStore instance in our components


export function useItemsStore(): ItemsStoreInterface {

// our items store actions implementation:


const actions: ItemsStoreActionsInterface = {
// action that we invoke to load the items from an api:
loadItems: async () => {
// set loading to true and clear current data:
writableItemsStore.update((state) => {
state.loading = true
state.items = []
return state
})

// mock some data:


let mockData: ItemInterface[] = [{
id: 1,
name: 'Item 1',
selected: false
}, {
id: 2,
name: 'Item 2',
selected: false
}, {
id: 3,
name: 'Item 3',
selected: false
}]

// let's pretend we called some API end-point


// and it takes 1 second to return the data
// by using javascript setTimeout with 1000 for the milliseconds option
setTimeout(() => {
// set items data and loading to false
writableItemsStore.update((state) => {
state.items = mockData
state.loading = false
return state
})
console.log('itemsStore: loadItems: state updated')
}, 1000)
},
Chapter 6 - State Management 55

// action we invoke to toggle an item.selected property


toggleItemSelected: async (item: ItemInterface) => {
// update our state
writableItemsStore.update((state) => {
const itemIndex = (state.items || []).findIndex(a => a.id === item.id)
if (itemIndex < 0) {
console.warn('ItemsStore: action: toggleItemSelected: Could not find item \
in our state')
return
}
// toggle selected
state.items[itemIndex].selected = !state.items[itemIndex].selected
// keep current loading value
state.loading = state.loading
return state
})
}
}

// our items store getters implementation:


const loading = SvelteStore.derived(writableItemsStore, ($state) => $state.loading)
const items = SvelteStore.derived(writableItemsStore, ($state) => $state.items)

const getters: ItemsStoreGettersInterface = {


loading,
items
}

// return our store intance implementation


return {
getters,
actions
}
}

Here too add a barrel index.ts file to export the useItemsStore hook:

// file: src/store/items/index.ts

export * from './Items.store'


Chapter 6 - State Management 56

Root Store Instance


We will now implement our root store instance

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

// import our root store interface


import { RootStoreInterface } from './models'

// import our items slice and store


import { useItemsStore } from '../items/'

// 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

export * from './Root.store'

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

export * from './root'

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'

// import a reference to our ItemInterace


import type { ItemInterface } from '../models/items/Item.interface'
// import a reference to your ItemsList component:
import ItemsListComponent from '../components/items/ItemsList.component.svelte'
// import our useAppStore hook from our store
import { useAppStore } from '../store'

// get a reference to our itemsStore instanceusing our useAppStore() hook:


const {
itemsStore
} = useAppStore()

// get a reference to the items state data through our itemsStore getters:
const {
loading,
items
} = itemsStore.getters

// item select event handler


function onSelectItem (event: CustomEvent<{ item: ItemInterface }>) {
const item = event.detail.item
// invoke our store action to toggle the item.selected property
itemsStore.actions.toggleItemSelected(item)
}

// 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>

Save the file.

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">
...

// expose loading property:


export let loading = false // add this line
// expose a property called items with a default value of a blank array
export let items: ItemInterface[] = []
// expose a property to pass our selectItem event to the parent component
export let selectItem: (event: CustomEvent<{ item: ItemInterface }>) => void // ad\
d this line

...
</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>

Back to the Web Browser


Now, when we refresh the browser, we’ll first see a blank list, but in the header we’ll see the
text My items - loading: true:

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'

// expose loading property:


export let loading = false
// expose a property called items with a default value of a blank array
export let items: ItemInterface[] = []
// expose a property to pass our selectItem event to the parent component
export let selectItem: (event: CustomEvent<{ item: ItemInterface }>) => void
</script>

<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 Overview


Here is an overview of our API client architecture:
²⁸JsonPlaceHolder or miragejs for example
Chapter 7 - Api Client 67

API Client module will read the custom environment variable called VITE_API_CLIENT
and there are two possible outcomes:

• when VITE_API_CLIENT is mock: it will return the Mock API Client


• when VITE_API_CLIENT is live: it will return the Live API Client
Chapter 7 - Api Client 68

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.

The Main ApiClient


Create the directory src/api-client/models. Inside this directory, create the file
ApiClient.interface.ts with the following code:
Chapter 7 - Api Client 69

// file: src/api-client/models/ApiClient.interface.ts

import { ItemsApiClientInterface } from './items'

/**
* @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.

Items domain Api Client


Now we create the interfaces and model that defines a domain-specific API client.
Create the directory src/api-client/models/items. Inside thisd directory, create the follow-
ing files:

• index.ts
• ItemsApiClient.interface.ts
• ItemsApiClient.model.ts
• ItemsApiClientOptions.interface.ts

Your directory structure will look like this:


Chapter 7 - Api Client 70

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

import { ItemInterface } from '../../../models/items/Item.interface'

/**
* @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:

npm install axios --save

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

import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'

import { ItemsApiClientOptions, ItemsApiClientEndpoints } from './ItemsApiClientOpti\


ons.interface'
import { ItemsApiClientInterface } from './ItemsApiClient.interface'
import { ItemInterface } from '@/models'

...

And here is the class that implement our ItemsApiClientInterface:


Chapter 7 - Api Client 73

// 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

})
})
}
}

index.ts (barrel file)


This just exports all our interfaces and models under items/ so that we can more easily import
them later in other parts of the code:

// file: src/api-client/models/items/index.ts

export * from './ItemsApiClientOptions.interface'


export * from './ItemsApiClient.interface'
export * from './ItemsApiClient.model'

Mock and Live Api Clients


Now that we have defined our models for ApiClientInterface and ItemsApiClientInter-
face, let’s implement a mechanism that will allow us to either use a mock api-client that
returns static json data, or a live api-client that returns data from as real API.
Under the src/api-client directory, create two new sub-directories called:

• 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..

Mock Api Client

Items domain mock API instance

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

Inside the src/api-client/mock/items/index.ts file, paste the following code:

// file: src/api-client/mock/items/index.ts

import {
ItemsApiClientOptions,
ItemsApiClientInterface,
ItemsApiClientModel
} from '../../models/items'

const options: ItemsApiClientOptions = {


endpoints: {
fetchItems: '/static/mock-data/items/items.json'
},
mockDelay: 1000
}

// instantiate the ItemsApiClient pointing at the url that returns static json mock \
data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(options)

// export our instance


export {
itemsApiClient
}

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.

Mock API instance

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:

Inside the src/api-client/mock/index.ts file, paste the following code:

// file: src/api-client/mock/index.ts

import { ApiClientInterface } from '../models/ApiClient.interface'


import { itemsApiClient } from './items'

// create an instance of our main ApiClient that wraps the mock child clients
const apiMockClient: ApiClientInterface = {
items: itemsApiClient
}

// export our instance


Chapter 7 - Api Client 77

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.

Live Api Client

Items domain live API instance

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:

Inside the src/api-client/live/items/index.ts file, paste the following code:


Chapter 7 - Api Client 78

// file: src/api-client/live/items/index.ts

import {
ItemsApiClientOptions,
ItemsApiClientInterface,
ItemsApiClientModel
} from '../../models/items'

const options: ItemsApiClientOptions = {


endpoints: {
// this should be pointing to the live API end-point
fetchItems: '/path/to/your/real/api/end-point'
}
}

// instantiate the ItemsApiClient pointing at the url that returns live data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(options)

// export our instance


export {
itemsApiClient
}

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.

Live API instance

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

Inside the src/api-client/live/index.ts file, paste the following code:

// file: src/api-client/live/index.ts

import { ApiClientInterface } from '../models'


// import module instances
import { itemsApiClient } from './items'

// create an instance of our main ApiClient that wraps the live child clients
const apiLiveClient: ApiClientInterface = {
items: itemsApiClient
}

// export our instance


export {
apiLiveClient
}

This code is also almost identical to the related mock index.ts file. The only exceptions are:

1. We use the live ItemsApiClient from api-client/live-items


2. We name the instance apiLiveClient for more clarity

We then just export our apiLiveClient instance.


In a bit we’ll be adding one final index.ts at the root of src/api-client that will act as our
API client “provider”. This will return either the mock or the live instance based on an
environemnt variable. So let’s first setup some things to work with environment variables.
Chapter 7 - Api Client 80

Environment Variables
Since Vite uses dotenv²⁹ to load environment variables, we’ll have to create two .env files³⁰
at root of your src directory:

.env.dev # loaded when mode is dev for local development


.env.production # loaded when mode is production

Inside the .env.mock put the following:

# file src/.env.dev

VITE_API_CLIENT=mock

Inside the .env.production put the following:

# 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

/// <reference types="svelte" />


/// <reference types="vite/client" />

// types for Vite env variables:


// (reference: https://vitejs.dev/guide/env-and-mode.html#intellisense-for-typescrip\
t)
interface ImportMetaEnv {
readonly VITE_API_CLIENT: string
// more env variables...
}

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
...
},
...

Change the dev command to:

"dev": "vite --mode mock",

Change the build command to:

"build": "vite build --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:

"build-mock": "vite build --mode mock"

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" />

import { defineConfig } from 'vite'


import { svelte } from '@sveltejs/vite-plugin-svelte'

// 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.

Api Client Provider


Now we need one final index.ts that will server our main API client factory and return
either the mock or the live API client based on an environment variable (later you might
Chapter 7 - Api Client 83

find easier to drive this with different configuration files). Create an the file at the root of
src/api-client:

Inside the src/api-client/index.ts file, import a reference to our ApiClientInterface inter-


face, and both the instances for the mock and the live clients:

// file: src/api-client/index.ts

import { ApiClientInterface } from './models'


import { apiMockClient } from './mock'
import { apiLiveClient } from './live'
...

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

...

let env: string = 'mock'


// Note: Vite uses import.meta.env (reference: https://vitejs.dev/guide/env-and-mode\
.html)
// optional: you can console.log the content of import.meta.env to inspect its value\
s like this: console.log('import.meta.env', JSON.stringify(import.meta.env))
if (import.meta.env && import.meta.env.VITE_API_CLIENT) {
env = import.meta.env.VITE_API_CLIENT.trim()
}
// return either the live or the mock client
let apiClient: ApiClientInterface
if (env === 'live') {
apiClient = apiLiveClient
Chapter 7 - Api Client 84

} 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.

Store Instance updates


Back into our src/store/items/Items.store.ts code, we can now finally remove the reference
to the hard-coded data and use our new API client to retrieve these data. Start by adding an
import for our apiClient (note how we no longer have to worry about using the mock or
the live one, the system we’ll handle that automatically based on the VITE_API_CLIENT
environment variable we created earlier):

// 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
...

// our items store actions implementation:


const actions: ItemsStoreActionsInterface = {
// action that we invoke to load the items from an api:
loadItems: async () => {
// set loading to true and clear current data:
writableItemsStore.update((state) => {
state.loading = true
state.items = []
return state
})

// begin: remove code


// mock some data
const mockItems: ItemInterface[] = [{
id: 1,
name: 'Item 1',
selected: false
}, {
id: 2,
name: 'Item 2',
selected: false
}, {
id: 3,
name: 'Item 3',
selected: false
}]

// let's pretend we called some API end-point


// and it takes 1 second to return the data
// by using javascript setTimeout with 1000 for the milliseconds option
setTimeout(() => {
// set items data and loading to false
writableItemsStore.update((state) => {
state.items = mockData
state.loading = false
return state
})
}, 1000)
// end: remove code
Chapter 7 - Api Client 86

// begin: add code


// invoke our API cient fetchItems to load the data from an API end-point
const data = await apiClient.items.fetchItems()

// set items data and loading to false


writableItemsStore.update((state) => {
state.items = data
state.loading = false
return state
})
// end: add code
},
...

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.

Alternative: using json-server


Let’s start by install json-server:

npm install -D 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:

The content for vite.config.mock.ts will be:

/// <reference types="vite/client" />

import { defineConfig } from 'vite'


import { svelte } from '@sveltejs/vite-plugin-svelte'
import { fileURLToPath, URL } from 'url'

// 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'
]
}
})

The content for vite.config.production.ts will be:

/// <reference types="vite/client" />

import { defineConfig } from 'vite'


import { svelte } from '@sveltejs/vite-plugin-svelte'
import { fileURLToPath, URL } from 'url'

// 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

]
}
})

The content for vite.config.jsonserver.ts will be:

/// <reference types="vite/client" />

import { defineConfig } from 'vite'


import { svelte } from '@sveltejs/vite-plugin-svelte'
import { fileURLToPath, URL } from 'url'

// 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 tsconfig.json to reference also tsconfig.node.json:


Chapter 7 - Api Client 92

{
...

"references": [{ "path": "./tsconfig.node.json" }]


}

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",

...

Create json-server data under src/json-server/db.json with this:

// 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

...

const options: ItemsApiClientOptions = {


endpoints: {
//fetchItems: '/static/mock-data/items/items.json' // <-- comment this line out
fetchItems: '/jsonserver/items' // <-- add this line
},
mockDelay: 1000
}

...

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)

The browser should now display:

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.

HttpClient Interfaces and Models


Create the directory src/http-client/models. Within this directory, create the following files

• 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

Your directory structure will look like this:

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

...

// http content types


export const HttpContentTypes = Object.freeze({
applicationJson: 'application/json',
formUrlEncoded: 'application/x-www-form-urlencoded;charset=UTF-8'
})

// constant for http request methods names


export const HttpRequestMethods = Object.freeze({
get: 'GET',
post: 'POST',
put: 'PUT',
delete: 'DELETE',
patch: 'PATCH'
})

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

import { HttpRequestType } from './Constants'

/**
* @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
}

export const UrlUtils: UrlUtilsInterface = {


/**
* @name getFullUrlWithParams
* @description Returns the full formatted url for an API end-point
* by replacing parameters place holder with the actual values.
* @param baseUrl The base API end-point witht he params placeholders like {projec\
tId}
* @param params The request params object with the key/value entries for each par\
ameter
* @returns The fully formatted API end-point url with the actual parameter values
*/
getFullUrlWithParams: (baseUrl: string, params: { [key: string]: number | string }\
): string => {
const keys: string[] = Object.keys(params || {})
if ((baseUrl || '').indexOf('[') === -1 || keys.length === 0) {
return baseUrl
}
let fullUrl = baseUrl
keys.forEach((key) => {
fullUrl = fullUrl.replace(`[${key}]`, (params[key] || 'null').toString())
})
return fullUrl
}
}

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

import { HttpRequestParamsInterface } from './HttpRequestParams.interface'

/**
* @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'

import { HttpRequestParamsInterface } from './HttpRequestParams.interface'


import { HttpClientInterface, HttpClientConfigInterface } from './HttpClient.interfa\
ce'
import { HttpRequestType, HttpContentTypes } from './Constants'
import { UrlUtils } from './UrlUtils'

/**
* @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)

// set axios options


const options: AxiosRequestConfig = {
headers: {},
maxRedirects: 0
}

if (headers) {
options.headers = {
//...options.headers,
...headers
}
}

// set headers Authorization


if (requiresToken && options.headers) {
options.withCredentials = true
// optional: you could add coded here to set the Authorization header with a b\
earer token
// options.headers.Authorization = `bearer ${ JwtHelpers.getJwtToken() }`
}

let result!: R

try {
switch(requestType) {

// TODO: implement a case statement for each request type

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 new Promise<R>((resolve) => {


setTimeout(() => {
resolve(result)
}, mockDelay)
})
}

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

...

// executes a get request:


case HttpRequestType.get: {
const response = await axios.get(fullUrl, options)
result = response?.data as R
break
}

...

The post implementation:


Chapter 8 - Enhance the Api Client 105

// file: src/http-client/models/HttpClient.axios.ts

...

// executes a post request:


case HttpRequestType.post: {
const response = await axios.post(fullUrl, payload, options)
result = response?.data as R
break
}

...

The put implementation:


// file: src/http-client/models/HttpClient.axios.ts

...

// executes a put request:


case HttpRequestType.put: {
const response = await axios.put(fullUrl, payload, options)
result = response?.data as R
break
}

...

The delete implementation:


// file: src/http-client/models/HttpClient.axios.ts

...

// executes a delete request:


case HttpRequestType.delete: {
const response = await axios.delete(fullUrl, options)
result = response?.data as R
break
}

...

The patch implementation:


Chapter 8 - Enhance the Api Client 106

// file: src/http-client/models/HttpClient.axios.ts

...

// executes a patch request:


case HttpRequestType.patch: {
const response = await axios.patch(fullUrl, payload, options)
result = response?.data as R
break
}

...

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

* 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 = void>(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\
oad
const fullUrl = UrlUtils.getFullUrlWithParams(endpoint, payload as any)
console.log('HttpClientFetch: fullUrl: ', fullUrl, payload)

// set fetch options


const options: RequestInit = {
credentials: 'include',
redirect: 'follow',
headers: {}
}

if (headers) {
options.headers = {
...headers
}
}

if (!options.headers?.hasOwnProperty('Content-Type')) {
// default to content-type json
options.headers = {
...headers,
'Content-Type': HttpContentTypes.applicationJson
}
}

// set headers Authorization


if (requiresToken && options.headers) {
// optional: you could add coded here to set the Authorization header with a b\
earer token
// options.headers.Authorization = `bearer ${ JwtHelpers.getJwtToken() }`
}
Chapter 8 - Enhance the Api Client 108

let result!: R

// helper for checking if response is being redirected (302) in fetch


const checkRedirect = async (resp: any) => {
if (resp.redirected) {
// if so, redirect to response url
document.location = resp.url
return true
}
return false
}

try {
switch (requestType) {

// TODO: implement a case statement for each request type

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

...

// executes a get request:


case HttpRequestType.get: {
options.method = HttpRequestMethods.get
const response = (await fetch(fullUrl, options)) as any
const redirected = await checkRedirect(response)
if (!redirected) {
result = (await response.json()) as R
}
break
}

...

The post implementation:

// file: src/http-client/models/HttpClient.fetch.ts

...

// executes a post request:


case HttpRequestType.post: {
options.method = HttpRequestMethods.post
options.body = typeof payload === 'string' ? payload : JSON.stringify(payload)
const response = (await fetch(fullUrl, options)) as any
const redirected = await checkRedirect(response)
if (!redirected) {
result = (await response.json()) as R
}
break
}

...

The put implementation:


Chapter 8 - Enhance the Api Client 110

// file: src/http-client/models/HttpClient.fetch.ts

...

// executes a put request:


case HttpRequestType.put: {
options.method = HttpRequestMethods.put
options.body = typeof payload === 'string' ? payload : JSON.stringify(payload)
const response = (await fetch(fullUrl, options)) as any
const redirected = await checkRedirect(response)
if (!redirected) {
result = (await response.json()) as R
}
break
}

...

The delete implementation:

// file: src/http-client/models/HttpClient.fetch.ts

...

// executes a delete request:


case HttpRequestType.delete: {
options.method = HttpRequestMethods.delete
const response = (await fetch(fullUrl, options)) as any
const redirected = await checkRedirect(response)
if (!redirected) {
result = (await response.json()) as R
}
break
}

...

The patch implementation:


Chapter 8 - Enhance the Api Client 111

// file: src/http-client/models/HttpClient.fetch.ts

...

// executes a patch request:


case HttpRequestType.patch: {
options.method = HttpRequestMethods.patch
options.body = typeof payload === 'string' ? payload : JSON.stringify(payload)
const response = (await fetch(fullUrl, options)) as any
const redirected = await checkRedirect(response)
if (!redirected) {
result = (await response.json()) as R
}
break
}

...

http-client/models/index.ts (barrel file)


Inside the index file paste the following to export all the enums/ interfaces/models:

// file: src/http-client/models/index.ts

export * from './Constants'


export * from './HttpClient.axios'
export * from './HttpClient.fetch'
export * from './HttpClient.interface'
export * from './HttpRequestParams.interface'
export * from './UrlUtils'

http-client/index.ts (client factory)


Add another index file under src/http-client:
Chapter 8 - Enhance the Api Client 112

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

import { HttpClientInterface } from './models/HttpClient.interface'


// import { config } from '@/config'

import { HttpClientAxios } from './models/HttpClient.axios'


import { HttpClientFetch } from './models/HttpClient.fetch'

// export all our interfaces/models/enums


export * from './models'

let _httpClient: HttpClientInterface | undefined = undefined

// export out hook


export const useHttpClient = () => {
if (!_httpClient) {
// export instance of HttpClientInterface
const clientType = 'fetch'
// const clientType = config.httpClient.clientType // later will drive from conf\
ig

// 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

_httpClient = new HttpClientAxios()


}
}

return _httpClient as HttpClientInterface


}

UrlUtils Unit Tests


Create the directory tests/unit/http-client directory and add a new file called UrlU-
tils.getFullUrlWithParams.test.ts with the following:

// file: src/tests/unit/http-client/UrlUtils.getFullUrlWithParams.test.ts

import { UrlUtils } from '@/http-client'

describe('UrlUtils: getFullUrlWithParams', () => {


it('should return fullUrl formatted as expected with one param', () => {
const endpoint = 'https://unit-test-api/v1/domain/[catalogId]/[partId]'
const params = {
catalogId: 5346782,
partId: 'abcde23'
}
const result = UrlUtils.getFullUrlWithParams(endpoint, params)

expect('https://unit-test-api/v1/domain/5346782/abcde23').toEqual(result)
})

// test our component click event


it('should return fullUrl formatted as expected with multiple params', () => {
const endpoint = 'https://unit-test-api/v1/domain/[country]/[state]/[cityId]'
const params = {
country: 'USA',
state: 'NY',
cityId: 'gtref345ytr'
}
const result = UrlUtils.getFullUrlWithParams(endpoint, params)

expect('https://unit-test-api/v1/domain/USA/NY/gtref345ytr').toEqual(result)
})
})
Chapter 8 - Enhance the Api Client 114

HttpClient: Unit Tests


We need to add unit tests against HttpClientAxios and HttpClientFetch before we can
re-factor the ItemApiClient code to use it.

HttpClientAxios tests

Testing a successful ”get” response

Within the directory tests/unit/http-client directory create a sub-directory called axios-


client and here and add a new file called AxiosClient.request.get.test.ts. Within the file,
paste the following code:

// file: src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts

import axios from 'axios'


import { HttpClientAxios, HttpRequestType, HttpRequestParamsInterface } from '@/http\
-client'

let mockRequestParams: HttpRequestParamsInterface<any> = {


requestType: HttpRequestType.get,
endpoint: 'path/to/a/get/api/endpoint',
requiresToken: false
}

describe('HttpClient: axios-client: request: get', () => {


const httpClient = new HttpClientAxios()

it('should execute get request succesfully', () => {


vitest
.spyOn(axios, 'get')
.mockImplementation(async () => Promise.resolve({ data: `request completed: ${\
mockRequestParams.endpoint}` }))

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

console.info('AxiosClient.request.get.test.ts: error', error)


})
})

...

Testing an unsuccessful ”get” response

Within the same file, add the following code:

// file: src/tests/unit/http-client/axios-client/AxiosClient.request.get.test.ts

...

describe('HttpClient: axios-client: request: get', () => {

...

it('get should throw error on rejection', () => {


vitest
.spyOn(axios, 'get')
.mockImplementation(async () => Promise.reject({ data: `request completed: ${m\
ockRequestParams.endpoint}` }))

httpClient.request(mockRequestParams).catch((error) => {
expect(error).toBeDefined()
expect(error.toString()).toEqual('Error: HttpClientAxios: exception')
})
})
})

Testing a successful ”post” response

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

import axios from 'axios'


import { HttpClientAxios, HttpRequestType, HttpRequestParamsInterface } from '@/http\
-client'

let mockRequestParams: HttpRequestParamsInterface<any> = {


requestType: HttpRequestType.post,
endpoint: 'path/to/a/post/api/endpoint',
requiresToken: false,
payload: {}
}

type P = typeof mockRequestParams.payload

describe('HttpClient: axios-client: request: post', () => {


const httpClient = new HttpClientAxios()

it('should execute post request succesfully', () => {


vitest
.spyOn(axios, 'post')
.mockImplementation(async () => Promise.resolve({ data: `request completed: ${\
mockRequestParams.endpoint}` }))

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

Testing “get” responses

Within the directory tests/unit/http-client directory create a sub-directory called fetch-


client and here and add a new file called FetchClient.request.get.test.ts. Within the file,
paste the following code:

// file: src/tests/unit/http-client/fetch-client/FetchClient.request.get.test.ts

import { HttpClientFetch, HttpRequestType, HttpRequestParamsInterface, HttpRequestMe\


thods } from '@/http-client'

let mockRequestParams: HttpRequestParamsInterface<any> = {


requestType: HttpRequestType.get,
endpoint: 'path/to/a/get/api/endpoint',
requiresToken: false
}

describe('HttpClient: axios-client: request: get', (done) => {


const httpClient = new HttpClientFetch()

it('should execute get request succesfully', async () => {

// 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
})

it('get should throw error on rejection', () => {


// 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

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

import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios' // <-- \


remove this line
import { useHttpClient, HttpRequestType, HttpRequestParamsInterface } from '@/http-c\
lient' // <-- add this line

Then replace the fetchItems implementation with the following:

// 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.

vite-env.d.ts updates (or env.d.ts)


Let’s start by modifying the code within the Vite types declarations file. Rename the current
variable VITE_API_CLIENT to VITE_APP_CONFIG:

// file: src/vite-env.d.ts (or src/env.d.ts)

...

// types for Vite env variables:


// (reference: https://vitejs.dev/guide/env-and-mode.html#intellisense-for-typescrip\
t)
interface ImportMetaEnv {
readonly VITE_APP_CONFIG: string // rename this from VITE_API_CLIENT to VITE_APP_C\
ONFIG
// more env variables...
Chapter 9 - App Configuration 122

interface ImportMeta {
readonly env: ImportMetaEnv
}

.env files updates


Make sure to also update each ‘.env’ file by renaming VITE_API_CLIENT to VITE_APP_-
CONFIG:

// file: src/.env.mock

VITE_APP_CONFIG=mock

// file: src/.env.production

VITE_APP_CONFIG=production

Add also three additional files, .env.jsonserver, .env.localapis and .env.beta:

// 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:

• global: this will be for settings that span all domains


• httpClient: this is for things related to the HttpClient
• apiClient: this is for things related to the ApiClient
• items: this is for the Items domain settings (as we add more functionality/components
etc we will add more areas/domains similar to this)

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'

export interface HttpClientConfigInterface {


tokenKey: string
clientType: string
}

/**
* @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.

Config files map


Within the directory src/config/ add a file called config-files-map.ts.
Here we just import a reference to each of the configuration JSON files and create either a
strategy pattern or a JavaScript Map that contains a map to our files by environment key
(here we are showing this with Map):

// file: src/config/config-files-map.ts

// import a reference to our Config interface:


import { ConfigInterface } from './models/Config.interface'

// individual environments configs:


import configMock from './config-files/mock.json'
import configJsonServer from './config-files/jsonserver.json'
import configLocal from './config-files/localapis.json'
import configBeta from './config-files/beta.json'
import configProduction from './config-files/production.json'

// example with javascript Map()


export const configFilesMap: Map<string, ConfigInterface> = new Map<string, ConfigIn\
terface>([
['mock', configMock],
['jsonserver', configJsonServer],
['localapis', configLocal],
['beta', configBeta],
['production', configProduction]
])
Chapter 9 - App Configuration 130

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

// helper to read the value of SVELTE_APP_CONFIG (or VITE_APP_CONFIG if using vite)


export function getAppConfigKey() {
// if using create-react-ap (webpack):
// let env: string = 'mock'
// // @ts-ignore
// if (process.env && process.env.SVELTE_APP_CONFIG) {
// // @ts-ignore
// env = process.env.SVELTE_APP_CONFIG.trim()
// }
// return env

// Note: Vite uses import.meta.env (reference: https://vitejs.dev/guide/env-and-mo\


de.html)
// optional: you can console.log the content of import.meta.env to inspect its val\
ues like this: console.log('import.meta.env', JSON.stringify(import.meta.env))
// @ts-ignore
return (import.meta.env.VITE_APP_CONFIG || '').trim()
}

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 index.ts (config provider)


Still within src/config directory, add another file called index.ts. Here we’ll be consuming
the JSON file that matches the environment specified by the current VITE_APP_CONFIG
value.
Let’s start by importing a reference to ConfigInterface, configFilesMap, and our helper
getAppConfigKey:
Chapter 9 - App Configuration 131

// file: src/config/index.ts
// returns appropriate config based on env VITE_APP_CONFIG

// import a reference to our Config interface:


import { ConfigInterface } from './models/Config.interface'

// import reference to configFilesMap


import { configFilesMap } from './config-files-map'

// import reference to our getAppConfigKey helper function


import { getAppConfigKey } from './utils'

...

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() }\
"`)
}

...

Finally we export an instance of our ConfigInterface called config:

// file: src/config/index.ts

...

export const config = configFilesMap.get(getAppConfigKey()) as ConfigInterface

Here is the entire content of src/config/index.ts:


Chapter 9 - App Configuration 132

// file: src/config/index.ts
// returns appropriate config based on env VITE_APP_CONFIG

// import a reference to our Config interface:


import { ConfigInterface } from './models/Config.interface'

// import reference to configFilesMap


import { configFilesMap } from './config-files-map'

// import reference to our getAppConfigKey helper function


import { getAppConfigKey } from './utils'

// 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()}"`)
}

export const config = configFilesMap.get(getAppConfigKey()) as ConfigInterface

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.

Unit Tests against configsMap


Create directory tests/unit/config and add a new file called Config.configsMap.spec.ts.
Here we’ll be testing that our configsMap instance contains at least one entry for each
environment, as expected:
Chapter 9 - App Configuration 133

// file: src/tests/unit/config/config-files-map.test.ts

import { configFilesMap } from '@/config/config-files-map'

describe('configFilesMap', () => {

it('instance should have "mock" key', () => {


expect(configFilesMap.has('mock')).toBe(true)
})

it('instance should have "jsonserver" key', () => {


expect(configFilesMap.has('jsonserver')).toBe(true)
})

it('instance should have "localapis" key', () => {


expect(configFilesMap.has('localapis')).toBe(true)
})

it('instance should have "beta" key', () => {


expect(configFilesMap.has('beta')).toBe(true)
})

it('instance should have "production" key', () => {


expect(configFilesMap.has('production')).toBe(true)
})
}

Unit Tests against Config instances by environment


Note: if using Jest, we cannot just write unit tests against the config instance already created
in the src/config/index.ts file because Jest will throw the following error:
SyntaxError: Cannot use 'import.meta' outside a module

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

// import the Config interface


import { ConfigInterface } from '@/config/models/Config.interface'
// import a reference to the confiFilesMap
import { configFilesMap } from '@/config/config-files-map'

describe('config: mock', () => {


const config: ConfigInterface = configFilesMap.get('mock') as ConfigInterface

it('instance should have "global" section', () => {


expect(config).toHaveProperty('global')
})

it('instance should have "httpClient" section', () => {


expect(config).toHaveProperty('httpClient')
})

it('instance should have "items" section', () => {


expect(config).toHaveProperty('items')
})

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')
})

it('section should have "clientType" property', () => {


Chapter 9 - App Configuration 135

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

it('section should have "apiClientOptions" property', () => {


expect(section).toHaveProperty('apiClientOptions')
})

describe('apiClientOptions', () => {
const apiClientOptions = section.apiClientOptions

describe('endpoints', () => {
const endpoints = apiClientOptions.endpoints

it('section should have "fetchItems" property', () => {


expect(endpoints).toHaveProperty('fetchItems')
// verify that fetchItems url is a string and has a reasonable length
expect(typeof endpoints.fetchItems).toBe('string')
expect(endpoints.fetchItems.length).toBeGreaterThan(10)
})
})
})
})
})

Run the unit tests with npm run test: and verify all succeed:
Chapter 9 - App Configuration 136

// terminal output:

...

Test Files 8 passed (8)


Tests 25 passed (24)
Time 1.18s (in thread 77ms, 1537.16%)

...

Please keep adding additional unit tests for each environment (i.e. config.jsonserver.test.ts,
config.production.test.ts etc).

HttpClient code updates

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

import { HttpClientInterface } from './models/HttpClient.interface'


import { config } from '@/config' // <-- uncomment (or add) this line

import { HttpClientAxios } from './models/HttpClient.axios'


import { HttpClientFetch } from './models/HttpClient.fetch'

// export all our interfaces/models/enums


export * from './models'

let _httpClient: HttpClientInterface | undefined = undefined

// export out hook


export const useHttpClient = () => {
if (!_httpClient) {
// export instance of HttpClientInterface
const clientType = 'fetch' // <-- uncomment this line
const clientType: string = 'fetch' // <-- remove this line
Chapter 9 - App Configuration 137

// 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()
}
}

return _httpClient as HttpClientInterface


}

Api Client code updates

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

import { ApiClientInterface } from './models'


import { apiMockClient } from './mock'
import { apiLiveClient } from './live'

import { config } from '@/config'

// return either the live or the mock client


let apiClient: ApiClientInterface
if (config.apiClient.type === 'live') { // this time we just read our config.apiClie\
nt.type
apiClient = apiLiveClient
} else {
// default is always apiMockClient
apiClient = apiMockClient
}

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 { config } from '@/config' // <-- add this line

import {
ItemsApiClientInterface,
ItemsApiClientModel
} from '../../models/items'

// remove this block:


// const options: ItemsApiClientOptions = {
// mockDelay: 250,
// endpoints: {
// fetchItems: '/static/mock-data/items/items.json'
// }
// }

// 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

// export our instance


export {
itemsApiClient
}

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

// import a reference to the app config


import { config } from '@/config'

import { ItemsApiClientInterface, ItemsApiClientModel } from '../../models'

// instantiate the ItemsApiClient pointing at the url that returns static json live \
data
const itemsApiClient: ItemsApiClientInterface = new ItemsApiClientModel(config.items\
.apiClientOptions)

// export our instance


export { itemsApiClient }

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).

Plugins: i18next, svelte-i18n


There are many JavaScript libraries out there that simplify localization of a frontend app,
but the most widely used is the i18n library. The organization i18next³⁷ maintains several
plugins for React/Vue/etc but at the time of writing this book nothing was available for
Svelte.
Github user @keisermann developed svelte-i18n³⁸ which wraps some of the
reactive tools that Svelte already provides. This is is published on NPM here
https://www.npmjs.com/package/svelte-i18n
³⁶https://www.w3.org/International/questions/qa-i18n
³⁷https://github.com/i18next
³⁸https://www.npmjs.com/package/svelte-i18n
Chapter 10 - Localization and Internationalization - Language Localization 142

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:

npm install --save i18next velte-i18n

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

...

export interface ConfigInterface {


global: {
version: number // add this line
}

...

Let’s also add a new section called localization like this:


Chapter 10 - Localization and Internationalization - Language Localization 143

// file: src/config/models/Config.interface.ts

...

export interface ConfigInterface {


global: {
version: number
}

...
// 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
},

...

// begin: add the localization section


"localization": {
"apiClientOptions": {
"endpoints": {
Chapter 10 - Localization and Internationalization - Language Localization 144

"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.³⁹

Translation JSON data


Note that for the fetchTranslation end-point, we’ll use two parameters: [namespace] and
[key].
We’ll create the files under /public/static/mock-data/localization/.
For the [namespace] parameter we’ll always use ‘translation’ in our case, so go
ahead and create a sub-directory called translation at the path /public/static/mock-
data/localization/translation. Then, add 4 files:

• 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

Here is the content for the en-US.json one:

// 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",

"home.welcome": "Welcome: this message is localized in English",

"navigation.home": "Home",
"navigation.about": "About",

"items.list.header": "My Items"


}

Here is the content for the es-ES.json one:

// 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",

"home.welcome": "Bienvenido: this message is localized in Spanish",

"navigation.home": "Inicio",
"navigation.about": "Acerca de",

"items.list.header": "Mis cosas"


}

Here is the content for the fr-FR.json one:


Chapter 10 - Localization and Internationalization - Language Localization 146

// 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",

"home.welcome": "Bienvenue: this message is localized in French",

"navigation.home": "Accueil",
"navigation.about": "À propos de nous",

"items.list.header": "Mes articles"


}

Here is the content for the it-IT.json one:

// 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",

"home.welcome": "Benvenuti: this message is localized in Italian",

"navigation.home": "Home",
"navigation.about": "Chi Siamo",

"items.list.header": "I miei articoli"


}

We now have to add a new API client module for loading our localization data.

API Client updates


Create the directory src/api-client/models/localization.
Here we’ll create the interfaces and model for our localization API module.
Add the following 4 files:

• 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

export interface LocalizationApiClientEndpoints {


fetchTranslation: string
}

/**
* @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

import { useHttpClient, HttpRequestParamsInterface, HttpRequestType } from '@/http-c\


lient'

import { LocalizationApiClientOptions, LocalizationApiClientEndpoints } from './Loca\


lizationApiClientOptions.interface'
import { LocalizationApiClientInterface } from './LocalizationApiClient.interface'

/**
* @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
}
}

fetchTranslation(namespace: string, key: string): Promise<{ [key: string]: string \


}> {
const requestParameters: HttpRequestParamsInterface = {
requestType: HttpRequestType.get,
endpoint: this.endpoints.fetchTranslation,
requiresToken: false,
payload: {
namespace,
key
} as any,
mockDelay: this.mockDelay
}

return useHttpClient().request<{ [key: string]: string }>(requestParameters)


Chapter 10 - Localization and Internationalization - Language Localization 149

}
}

file src/api-client/models/localization/index.ts
Just a barrel file:

// file: src/api-client/models/localization/index.ts

export * from './LocalizationApiClientOptions.interface'


export * from './LocalizationApiClient.interface'
export * from './LocalizationApiClient.model'

Updates to ApiClient.interface.ts
Import and add our localization module:

// file: src/api-client/models/ApiClient.interface.ts

import { LocalizationApiClientInterface } from './localization'


import { ItemsApiClientInterface } from './items'

/**
* @Name ApiClientInterface
* @description
* Interface wraps all api client modules into one places for keeping code organized.
*/
export interface ApiClientInterface {
localization: LocalizationApiClientInterface
items: ItemsApiClientInterface
}

Updates to the main models barrel file


(api-client/models/index.ts)
Chapter 10 - Localization and Internationalization - Language Localization 150

// file: src/api-client/models/index.ts

export * from './ApiClient.interface'


export * from './localization'
export * from './items'

Updates to ApiClient instances

localization mock instance


(api-client/mock/localization/index.ts)
Create the file api-client/mock/localization/index.ts with this code:

// file: src/api-client/mock/localization/index.ts

// import a reference to the app config


import { config } from '@/config'

import { LocalizationApiClientInterface, LocalizationApiClientModel } from '../../mo\


dels'

// instantiate the LocalizationApiClient pointing at the url that returns static jso\
n mock data
const localizationApiClient: LocalizationApiClientInterface = new LocalizationApiCli\
entModel(config.localization.apiClientOptions)

// export our instance


export { localizationApiClient }

mock instance (api-client/mock/index.ts)


Update the code within the api-client/mock/index.ts file like this:
Chapter 10 - Localization and Internationalization - Language Localization 151

// file: src/api-client/mock/index.ts

import { ApiClientInterface } from '../models'


// import module instances
import { localizationApiClient } from './localization'
import { itemsApiClient } from './items'

// create an instance of our main ApiClient that wraps the mock child clients
const apiMockClient: ApiClientInterface = {
localization: localizationApiClient,
items: itemsApiClient
}

// export our instance


export { apiMockClient }

live instance (api-client/live/index.ts)


Update the code within the api-client/live/index.ts file like this :

// file: src/api-client/live/index.ts

// import a reference to the app config


import { config } from '@/config'

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

i18n initialization and useLocalization hook


Create the directory src/localization. Inside the localization folder we’ll create the following
files:

• utils.ts
• useLocalization.ts
• index.ts

file utils.ts
Here we’ll have three helper methods:

• getDefaultLocale used to retrieve the default locale


• getUserPreferredLocale used to retrieve the user preferred locale from local storage
• setUserPreferredLocale used to save the user preferred locale to local storage

// file: src/localization/utils.ts

import { config } from '@/config'

// key that will use to save the user preferred locale id


const userPreferredLocaleStorageKey = 'user-lcid'

// helper to returnt he default locael form config


export const getDefaultLocale = () => {
// get a reference from the available locales array from our config
const availableLocales = config.localization.locales
// return the one marked isDefault
return availableLocales.find((o) => o.isDefault) as {
key: string
isDefault: boolean
}
}

// helper method to retrieve the user preferred locale from localStorage


export const getUserPreferredLocale = () => {
// try to retrive from local storage if they have one saved
const preferredLocale = localStorage.getItem(userPreferredLocaleStorageKey)
Chapter 10 - Localization and Internationalization - Language Localization 153

if (!preferredLocale) {
// if not, use the default locale from config
const defaultLocale = getDefaultLocale().key
return defaultLocale
}
return preferredLocale
}

// helper to save the user preferred locale to localStorage


export const setUserPreferredLocale = (lcid: string) => {
localStorage.setItem(userPreferredLocaleStorageKey, lcid)
}

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

import * as SvelteI18N from 'svelte-i18n'


import { writable, derived } from 'svelte/store'

import { config } from '../config'


import { apiClient } from '../api-client'
import { getUserPreferredLocale, setUserPreferredLocale } from './utils'

...

Let’s get a reference to the localeStorageCache configuration:


Chapter 10 - Localization and Internationalization - Language Localization 154

// file: src/localization/useLocalization.ts
...

// get reference to out localization config


const localStorageConfig = config.localization.localStorageCache

...

Create a few reactive variables (code comment on each should be self-explanatory):

// file: src/localization/useLocalization.ts
...

// create a writable reactive flag called isLoadingLocale:


const isLoadingLocale = writable(false)
// create a reactive varaible called currentLocale that will return the svelte-i18n \
locale value:
const currentLocale = derived(SvelteI18N.locale, ($state) => $state)
// create a reactive flag called isLocaleLoaded that will return true once svelte-i1\
8n has loaded its state
const isLocaleLoaded = derived(SvelteI18N.locale, ($state) => typeof $state === 'str\
ing')

...

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
...

// helper to change the current 18n locale


const changeLocale = async (lcid: string) => {
// try to get it from locale storage
// dynamic key we use to cache the actual locale JSON data in the browser local st\
orage
const localeStorageKey = `lcid-data-${lcid}`
// retrieve JSON as string
const cacheEntryStr = localStorage.getItem(localeStorageKey) || '{}'
// a variable to hold the parsed JSON data:
Chapter 10 - Localization and Internationalization - Language Localization 155

let cacheEntry: { appVersion: number; expiresAt: number; json: string } = {


appVersion: -1,
expiresAt: 0,
json: ''
}

// 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)

// update our cache


const dt = new Date()
// calculate new expiration date
const expiresAt = dt.setMinutes(dt.getMinutes() + Number(localStorageConfig.expi\
rationInMinutes))
if (localStorageConfig.enabled) {
localStorage.setItem(
localeStorageKey,
JSON.stringify({
Chapter 10 - Localization and Internationalization - Language Localization 156

appVersion: config.global.version,
expiresAt: expiresAt,
json: translationData
})
)
}
// set our loading flag to false
isLoadingLocale.set(false)
}

// also save the user preference


setUserPreferredLocale(lcid)
}

...

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
...

// export all we need as a hook


export function useLocalization() {
const availableLocales = config.localization.locales

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">

...

// import a reference to useLocalization


import { useLocalization } from './localization/useLocalization'

// get what we need from useLocalization:


const {
locales,
currentLocale,
getUserPreferredLocale,
changeLocale,
isLoadingLocale,
isLocaleLoaded,
t
} = useLocalization()

// an event handler from changing the locale from our locale-selector


const onLocaleClick = (lcid: string) => {
changeLocale(lcid)
}
</script>

<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:

Test that our caching logic is working:

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

Also not how the selected locale is French:

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

We’ll create the 2 main hooks useDateTimeFormatters and useNumberFormatters and


then just export them together as useFormatters from the index.ts file.

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

export type DayNameFormatType = 'long' | 'short' | 'narrow' | undefined


export type MonthNameFormatType = 'long' | 'short' | 'narrow' | 'numeric' | '2-digit\
' | undefined

const defaultDateStyle = 'short' // 'full', 'long', 'medium', 'short'

// 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()

let cacheKey = `${lcid}-${dateStyle}`


if (timeStyle.length) {
cacheKey = `${cacheKey}-${timeStyle}`
}
return cacheKey.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

...

// hook to export the datetime, dayNames, monthNames utils


export const useDateTimeFormatters = (localeId: string) => {
const _lcid = localeId
const _cache = new Map<string, Intl.DateTimeFormat>()

return {
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 166

dateTime: (dateStyle?: string, timeStyle?: string) => {


dateStyle = (dateStyle || defaultDateStyle).trim().toLowerCase()
timeStyle = (timeStyle || '').trim().toLowerCase()

const cacheKey = getDateTimeFormattersCacheKey({


lcid: _lcid,
dateStyle,
timeStyle
})

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
},

// ... we'll be adding also dayNames and monthNames here shortly

}
}

Now let’s also return dayNames and monthNames:


Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 167

// file: src/formatters/useDateTimeFormatters.ts

...

// hook to export the datetime, dayNames, monthNames utils


export const useDateTimeFormatters = (localeId: string) => {
const _lcid = localeId
const _cache = new Map<string, Intl.DateTimeFormat>()

// add these two to cache also dayName and monthNames


const _cacheDayNames = new Map<string, { id: number; name: string }[]>()
const _cacheMonthNames = new Map<string, { id: number; name: string }[]>()

return {
dateTime: (dateStyle?: string, timeStyle?: string) => {
...
},

dayNames: (format: DayNameFormatType = 'long') => {


if (!_cacheDayNames.has(format)) {
// if not in our cache yet, create it and cache it
const items: { id: number; name: string }[] = []
for (let i = 0; i < 7; i++) {
// start from March 1st 1970 which is a Sunday
// calculate day and pad string start with zero
const strDay = (i + 1).toString().padStart(2, '0')
const date = new Date(`1970-03-${ strDay }T00:00:00.000Z`)
const name = date.toLocaleString(_lcid, { weekday: format, timeZone: 'UTC'\
})
items.push({ id: i, name })
}
_cacheDayNames.set(format, items)
}
// return cached items
return _cacheDayNames.get(format) as { id: number; name: string }[]
},

monthNames: (format: MonthNameFormatType = 'long') => {


if (!_cacheMonthNames.has(format)) {
// if not in our cache yet, create it and cache it
const items: { id: number; name: string }[] = []
for (let i = 0; i < 12; i++) {
// calculate month and pad string start with zero
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 168

const strMonth = (i + 1).toString().padStart(2, '0')


const date = new Date(`1970-${ strMonth }-01T00:00:00.000Z`)
const name = date.toLocaleString(_lcid, { month: format, timeZone: 'UTC' })
items.push({ id: i, name })
}
_cacheMonthNames.set(format, items)
}
// return cached items
return _cacheMonthNames.get(format) as { id: number; name: 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

const defaultcurrencyDisplay = 'symbol' // 'symbol', 'narrowSymbol', 'code', 'name'

// 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()

let cacheKey = `${lcid}-${style}`


if (currency.length > 0) {
cacheKey = `${cacheKey}-${currency}`
if (currencyDisplay.length > 0) {
cacheKey = `${cacheKey}-${currencyDisplay}`
}
}
cacheKey = `${cacheKey}-${minimumFractionDigits}-${maximumFractionDigits}`.trim().\
toLowerCase()
return cacheKey
}

...

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

...

// hook to export the various number formatters utils


export const useNumberFormatters = (localeId: string) => {
const _lcid = localeId
const _cache = new Map<string, Intl.NumberFormat>()

const _privateGetFormatter = (params: {


style?: string
currency?: string
currencyDisplay?: string
minimumFractionDigits: number
maximumFractionDigits: number
}) => {
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 170

let { style, currency, currencyDisplay, minimumFractionDigits, maximumFractionDi\


gits } = params

style = (style || 'decimal').trim().toLowerCase()


currency = (currency || '').trim()
currencyDisplay = (currencyDisplay || defaultcurrencyDisplay).trim()

let cacheKey = getNumberFormattersCacheKey({


lcid: _lcid,
style,
currency,
currencyDisplay,
minimumFractionDigits,
maximumFractionDigits
})

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

import { useDateTimeFormatters } from './useDateTimeFormatters'


import { useNumberFormatters } from './useNumberFormatters'

export const useFormatters = () => {


return {
useDateTimeFormatters,
useNumberFormatters
}
}

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'

// expose component properties


export let show = false

// get what we need from useLocalization:


const { currentLocale } = useLocalization()

$: dateTimeFormatter = (dateStyle: string = 'long', timeStyle: string = '') => {


return useDateTimeFormatters($currentLocale).dateTime(dateStyle, timeStyle)
}
$: dayNames = () => {
return useDateTimeFormatters($currentLocale).dayNames().map(o => o.name)
}
$: monthNames = () => {
return useDateTimeFormatters($currentLocale).monthNames().map(o => o.name)
}

$: 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

<div>percent: { percentNumberFormatter().format(1254.987654) }</div>

<div>currency (USD): { currencyNumberFormatter().format(123456789.321654) }</div>


<div>currency (CAD): { currencyNumberFormatter('CAD').format(123456789.321654) }\
</div>
<div>currency (EUR): { currencyNumberFormatter('EUR').format(123456789.321654) }\
</div>
<div>currency (CNY): { currencyNumberFormatter('CNY').format(123456789.321654) }\
</div>
<div>currency (JPY): { currencyNumberFormatter('JPY').format(123456789.321654) }\
</div>
<div>currency (INR): { currencyNumberFormatter('INR').format(123456789.321654) }\
</div>
<div>currency (CHF): { currencyNumberFormatter('CHF').format(123456789.321654) }\
</div>

<div>date-time (default): { dateTimeFormatter().format(new Date()) }</div>


<div>date-time (full): { dateTimeFormatter('full').format(new Date()) }</div>
<div>date-time (full + long time): { dateTimeFormatter('full', 'long').format(ne\
w Date()) }</div>

<div>day names: { dayNames() }</div>


<div>month names: { monthNames() }</div>
</div>
{/if}

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">

...

// import a reference to the DebugFormatters component


import DebugFormatters from '@/components/shared/DebugFormatters.component.svelte'

...

<main>
<div class="home">
Chapter 11 - Localization and Internationalization - Number and DateTime Formatters 175

{#if $isLocaleLoaded && !$isLoadingLocale}


<LocaleSelector
locales={locales}
currentLocale={$currentLocale}
onLocaleClick={onLocaleClick}
t={$t} />
<h1>{$t('home.welcome')}</h1>
<ItemsView />
<DebugFormatters show={true}/> <!-- add this here -->
{:else}
<p>Loading...</p>
{/if}
</div>
</main>

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:

npx svelte-add@latest tailwindcss

It might prompt you to install the package svelte-add@latest if you don’t have it already.
Choose y and press enter:

Need to install the following packages:


svelte-add@latest
Ok to proceed? (y) y

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

also verify that postcss.config.cjs has beenc reated with this:

const tailwindcss = require('tailwindcss')


const autoprefixer = require('autoprefixer')

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';

Within the main.ts file import a reference to the app.css file:


Chapter 12 - Adding Tailwind CSS 180

// file: src/main.ts
import App from './App.svelte'
// import tailwind main css file
import './tailwind/app.css'

const app = new App({


target: document.getElementById('app')
})

export default app

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:

...

// remove the following <style> block:


<style>
.home {
padding: 20px;
font-family: Verdana, Geneva, Tahoma, sans-serif;
font-size: 12px;
}
</style>

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.

Atomic Design and Similar Approaches


The way you can think of and organize your components might follow one or more
methodologies. One methodology that has seen an increase in adoption recently is Atomic
Design originally introduced by Brad Frost⁴³. The great Alba Silvente⁴⁴ has also a terrific
post about this that I strongly recommend you check out. You are free to follow this or other
methodologies either strictly or more losely, as well as chose to implement your own or even
use a mix of ideas from different ones.
In my personal and pragmatical way, I found over the years that all I really need is a
foundation of the most primitive elements liek buttons/textboxes/dropdowns/etc. These
primitives should be as simple as possible, even though in some cases the might contain
quite a bit of logic to determine how they render. In my world, primitives are more or less
the same as the Atoms in Atomic Design.
Then, you can build your higher level components by “composing” them from the primitives.
This is what I’ll be describing in this chapter. We’ll build a collection of primitives that are
simply Buttons, Inputs and similar and see what better strategies we can use there to reduce
the amount of code we have to write and maintain. We’ll then explore in the next chapters
how to build higher-level components from these.

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()

// alternativately you could use an array that is initialized with


// the default CSS, and if disabled is true, then add 'disabled'
// and return the result by joining the array with space as the separator
// (I usually feavor this approach especially when there
// is more than one check and additional logic)
Chapter 13 - Intro to Primitives 185

const cssClasses = ['p-6']


if (this.disabled) {
cssClasses.push('disabled')
}
return cssClasses.join(' ')
}
</script>

<button type="button" class={cssClass()}>


<span class="name">{ label }</span>
</button>

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 = ''

// a computed property that returns the css class value


$: cssClass = (): string => {
const cssClasses = ['p-1']
if ((addCss || '').trim().length > 0) {
cssClasses.push(addCss.trim())
}
return cssClasses.join(' ')
}

const render = () => {


Chapter 13 - Intro to Primitives 186

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:

<ElText tag="h2" addCss="text-red-500" text="Here ElText will render an &lth2&gt ele\


ment" />

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

The initial code for this file is the following:


⁴⁵https://svelte.dev/docs#template-syntax-html
Chapter 13 - Intro to Primitives 187

//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 &lth2&g\
t element"/>
<ElText tag="p" addCss="text-red-700" text="Here ElText will render a &ltp&gt \
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'

// expose a property called testid


export let testid: string = 'not-set'
// expose a property called id
export let id: string = 'not-set'
// expose a property called label
export let label: string = 'label-not-set'
// expose a property called disabled
export let disabled = false
// expose a property called addCss
export let addCss: string = ''

// create an instance of Svelte event dispatcher


const dispatch = createEventDispatcher()

// 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

result.push('bg-gray-500 text-gray-300 opacity-50 cursor-not-allowed')


} else {
// these are the button CSS classes when enabled
result.push('bg-blue-500 text-white hover:bg-blue-400')
}

// addCss will have additional CSS classes


// we want to apply from where we consume this component
if ((addCss || '').trim().length > 0) {
result.push(addCss.trim())
}
return result.join(' ')
}

// 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)

Here is an example on how we’ll consume our ElButton component:

<ElButton id="my-button" disabled={false} label="This is a button" on:clicked={onBut\


tonClicked} />

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.

Primitives View - update


Within the primitives view, let’s now consume the ElButton as in the example above so we
can visually prototype the different button states.
Modify the file code within src/views/Primitives.svelte:

// 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'

const onButtonClicked = (event: CustomEvent<{ id: string }>) => {


console.log('PrimitivesView: onButtonClicked', event.detail.id)
}
</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">
Chapter 14 - More Primitives 193

<ElText tag="h2" addCss="text-red-500" text="Here ElText will render a &lth2&g\


t element"/>
<ElText tag="p" addCss="text-red-700" text="Here ElText will render a &ltp&gt \
element"/>
</div>
<!-- begin: add code block: -->
<ElText tag="h2" addCss="text-gray-500" text="ElButton examples:"/>
<div class="p-6 border">
<ElButton id="my-button-1" disabled={false} label="This is a button" on:clicke\
d={onButtonClicked}/>
<ElButton id="my-button-2" disabled={true} label="This is a disabled button" a\
ddCss="ml-2" on:clicked={onButtonClicked}/>
</div>
<!-- end: add code block: -->
</div>
</div>

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'

// expose a property called testid


export let testid: string = 'not-set'
// expose a property called id
export let id: string = 'not-set'
// expose a property called checked
export let checked = false
// expose a property called disabled
export let disabled = false
// expose a property called addCss
export let addCss: string = ''

// create an instance of Svelte event dispatcher


const dispatch = createEventDispatcher()

// 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

return result.join(' ').trim()


}

$: innerCssClass = (): string => {


const result = ['bg-white shadow pointer-events-none inline-block h-6 w-6 rounde\
d-full transform ring-0 transition duration-200']
if (checked) {
result.push('translate-x-6')
} else {
result.push('translate-x-0')
}
return result.join(' ').trim()
}

// 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)

Here is an example on how we’ll consume our ElButton component:

<ElToggle id="toggle-a" checked={$state.checked} disabled={false} on:clicked={myTogg\


leClickHandler} />

Primitives View - one more update


Within the primitives view, let’s now consume the ElToggle as in the example above so we
can visually prototype the ElToggle.
Modify the file code within src/views/Primitives.svelte so that we add the ElToggle just
created, and some state to better track multiple instance and verify visually that is working.
Here is the full updated code:

// 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'

// add some state to test the toggle button


const writableStore = SvelteStore.writable({
toggleItemState: [
{
id: 'toggle-a',
checked: true
Chapter 14 - More Primitives 197

}, {
id: 'toggle-b',
checked: false
}, {
id: 'toggle-c',
checked: false
}
]
})
const state = SvelteStore.derived(writableStore, ($writableStore) => $writableStor\
e)

const onButtonClicked = (event: CustomEvent<{ id: string }>) => {


console.log('PrimitivesView: onButtonClicked', event.detail.id)
}

const onToggleClicked = (event: CustomEvent<{ id: string }>) => {


const id = event.detail.id
console.log(`You clicked the "${id}" toggle`)
writableStore.update((state) => {
const stateItem = state.toggleItemState.find(item => item.id === id)
if (stateItem) {
// toggle the value of the ElToggle that was clicked
stateItem.checked = !stateItem.checked
}
return state
})
}
</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 &lth2&g\
t element"/>
<ElText tag="p" addCss="text-red-700" text="Here ElText will render a &ltp&gt \
element"/>
</div>

<ElText tag="h2" addCss="text-gray-500" text="ElButton examples:"/>


<div class="p-6 border">
Chapter 14 - More Primitives 198

<ElButton id="my-button-1" disabled={false} label="This is a button" on:clicke\


d={onButtonClicked}/>
<ElButton id="my-button-2" disabled={true} label="This is a disabled button" a\
ddCss="ml-2" on:clicked={onButtonClicked}/>
</div>

<ElText tag="h2" addCss="text-gray-500" text="ElToggle examples:"/>


<div class="p-6 border">
<ElToggle id="toggle-a" checked={$state.toggleItemState.find(item => item.id =\
== 'toggle-a').checked} disabled={false} on:clicked={onToggleClicked} />
<ElToggle id="toggle-b" checked={$state.toggleItemState.find(item => item.id =\
== 'toggle-b').checked} disabled={true} addCss="ml-2" on:clicked={onToggleClicked} /\
>
<ElToggle id="toggle-c" checked={$state.toggleItemState.find(item => item.id =\
== 'toggle-c').checked} disabled={false} addCss="ml-2" on:clicked={onToggleClicked} \
/>
</div>
</div>
</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:

const modal = useModal({ cancelLabel: 'Cancel', confirmLabel: 'Ok' })

...

const result = await modal.prompt('Do you want to delete this record?')


// result will be true if the user has confirmed, otherwise false if they cancelled

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'

const getDefaultProps = () => {


return {
testid: 'testid-not-set',
cancelLabel: 'Cancel',
confirmLabel: 'Confirm?',
title: 'Do you confirm this action?',
longDesc: undefined,
primaryButtonType: 'primary',
icon: undefined,
iconAddCss: undefined
}
}
Chapter 15 - A Primitive Modal 205

// private properties (will set through setProps method)


let props: ModalProps = getDefaultProps()

// public setProps() method used to set the private props from our useModal hook
export const setProps = (updatedProps: ModalProps) => {
props = {
...getDefaultProps(),
...updatedProps
}
}

const fadeOptions = { duration: 125 }

// private flag that indicates whether the Modal is open or closed


let open: boolean

// a variable that will store a reference to a "resolve" from a Promise we created\


in the prompt() method
let privateResolve: (value: boolean | PromiseLike<boolean>) => void

// public prompt() method:


export const prompt = async (title?: string) => {
open = true
props.title = title || props.title
// return a new promise that will be waited by the consuming code
return new Promise<boolean>((resolve) => {
// here we store a reference to the resolve returned with the Promise to the c\
onsuming code
privateResolve = resolve
})
}

const close = () => {


open = false
}

// handle click from Cancel button


const onCancelClick = () => {
close()
privateResolve(false)
}

// handle click from Confirm button


Chapter 15 - A Primitive Modal 206

const onConfirmClick = () => {


close()
privateResolve(true)
}

$: 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>

... // html section code contiues below

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">&#8203;</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:

<svelte:component this={props.icon} addCss={props.iconAddCss}/>

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'

let instance!: ElModal


const domTargetId = 'modal'

/**
* @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

// set the Modal props


instance.setProps(props)

// return the instance


return instance
}

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)

// handle the new buttons with id "open-modal-x" (we'll be adding shortly)


if (event.detail.id === 'open-modal-1') {
// here we invoke our useModal with the custom labels for the buttons
const modal = useModal({
cancelLabel: 'Cancel',
confirmLabel: 'Ok',
primaryButtonType: 'danger'
})
// then we invoke modal.prompt() and await it
const result = await modal.prompt('Do you want to delete this record?')
// the result will be true if the user click on COnfirm, or false if click on \
Cancel
console.log('----- PrimitivesView: onButtonClicked: modal-1 prompt result', re\
sult)
} else if (event.detail.id === 'open-modal-2') {
// here we invoke our useModal with the custom labels for the buttons + icon a\
nd iconAddCss props
const modal = useModal({
cancelLabel: 'Cancel',
confirmLabel: 'Confirm',
longDesc: 'This has also a longer description and an icon',
icon: ElIconAlert, // here we use the icon component created earlier
iconAddCss: 'text-red-600'
})
// then we invoke modal.prompt() and await it
const result = await modal.prompt('Do you confirm this action?')
// the result will be true if the user click on COnfirm, or false if click on \
Chapter 15 - A Primitive Modal 211

Cancel
console.log('----- PrimitivesView: onButtonClicked: modal-2 prompt result', re\
sult)
}
}

...

<div class="p-6 border">


<ElButton id="my-button-1" disabled={false} label="This is a button" on:clicked=\
{onButtonClicked}/>
<ElButton id="my-button-2" disabled={true} label="This is a disabled button" add\
Css="ml-2" on:clicked={onButtonClicked}/>
<!-- add these two buttons: -->
<ElButton id="open-modal-1" disabled={false} label="Open modal 1" on:clicked={on\
OpenDialogClicked}/>
<ElButton id="open-modal-2" disabled={false} label="Open modal 2" on:clicked={on\
OpenDialogClicked}/>

...

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 instead you click on Cancel, it will log “result false”:

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)

Item Component - updates


Let’s start by opening the file src/components/items/children/Item.component.svelte and
observe the current HTML template:

// file: src/components/items/children/Item.component.svelte

...

<li role="button" data-testid={testid} class={cssClass()} on:click={() => handleClic\


k(item)}>
<div class="selected-indicator">*</div>
<div class="name">{item.name}</div>
</li>

...

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">

...

// add the following two lines:


import ElText from '@/components/primitives/text/ElText.svelte'
import ElToggle from '@/components/primitives/toggles/ElToggle.svelte'

...

Then start updating the HTML template as follows:

// file: src/components/items/children/Item.component.svelte

...

<li role="button" data-testid={testid} class={cssClass()} on:click={() => handleClic\


k(item)}>
<div class="selected-indicator">*</div>
<!-- remove this line: -->
<div class="name">{item.name}</div>
<!-- add this line: -->
<ElText testid={`${testid}-text`} tag="div" text={item.name} />
</li>

...

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

...

<li role="button" data-testid={testid} class={cssClass()} on:click={() => handleClic\


k(item)}>
<!-- remove this line: -->
<div class="selected-indicator">*</div>
<!-- add this line: -->
<ElToggle testid={`${testid}-toggle`} checked={item.selected} />
<ElText testid={`${testid}-text`} tag="div" text={item.name}/>
</li>

...

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):

Let’s move the toggle to the right side:


Chapter 16 - Higher-level components 219

// file: src/components/items/children/Item.component.svelte

...

<li role="button" data-testid={testid} class={cssClass()} on:click={() => handleClic\


k(item)}>
<ElText testid={`${testid}-text`} tag="div" text={item.name} />
<ElToggle testid={`${testid}-toggle`} checked={item.selected} />
</li>

...

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

...

// expose a property called testid


export let testid: string = 'not-set'
// expose a property called isLast // <-- add this line
export let isLast: boolean = false // <-- add this line

...

Modify the logic within the computed cssClass property:

...
$: 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.

// remove entire blocks of code like this


<style>
...
</style>

ItemsList Component - updates


We need to do a small update to the ItemsList.component.svelte code as well to pass a value
for the new isLast property of the Item component. We are going to use the index property
for this and comparing it against the total number of items. Modify the HTML template as
this:

// 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):

npm init vite@latest

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:

? Project name: › my-component-library

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

? Select a framework: › - Use arrow-keys. Return to submit.


vanilla
vue
react
preact
lit
� svelte

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:

? Select a variant: › - Use arrow-keys. Return to submit.


svelte
� svelte-ts

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:

Scaffolding project in /Volumes/projects/my-component-library...

Done. Now run:

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.

Remove obsolete files


Remove the following files as they are not needed in a component 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)

Remove also the public directory and all its contents.

Update vite.config.ts
Update the Vite’s config file as follows:

// file: vite.config.ts

import { defineConfig } from 'vite'


import { svelte } from '@sveltejs/vite-plugin-svelte'
import path from 'path' // <- add this line

// 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

}
})

A few things to notice in the config changes above:

• 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)

Updates to enable TypeScript types declaration for .svelte


files
At the time of this book writing, a library like vue-tsc does not yet exists and thus we cannot
easily generate TypeScript types declarations. In order to do so, we’ll leverage a package
called svelte2tsx (this was suggested by the SvelteKit developers, but as time goes by there
might better ways to do this so keep your eyes open for developments around this).
Let’s install svelte2tsx first:

npm install -D svelte2tsx

Let’s now add 2 files under src/:

• 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

// Whenever a ambient declaration changes, its number should be increased


// This way, we avoid the situation where multiple ambient versions of svelte2tsx
// are loaded and their declarations conflict each other
// See https://github.com/sveltejs/language-tools/issues/1059 for an example bug tha\
t stems from it

// -- start svelte-ls-remove --
declare module '*.svelte' {
export default _SvelteComponent
}
// -- end svelte-ls-remove --

declare class Svelte2TsxComponent<


Props extends {} = {},
Events extends {} = {},
Slots extends {} = {}
> {
// svelte2tsx-specific
/**
* @internal This is for type checking capabilities only
* and does not exist at runtime. Don't use this property.
*/
$$prop_def: Props;
/**
* @internal This is for type checking capabilities only
* and does not exist at runtime. Don't use this property.
*/
$$events_def: Events;
/**
* @internal This is for type checking capabilities only
* and does not exist at runtime. Don't use this property.
*/
$$slot_def: Slots;
// https://svelte.dev/docs#Client-side_component_API
constructor(options: Svelte2TsxComponentConstructorParameters<Props>);
/**
* Causes the callback function to be called whenever the component dispatches an \
event.
* A function is returned that will remove the event listener when called.
*/
$on<K extends keyof Events & string>(event: K, handler: (e: Events[K]) => any): ()\
Chapter 17 - Creating Component Libraries 228

=> 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 _SvelteComponent<Props=any,Events=any,Slots=any> = typeof import("svelte") exte\


nds {SvelteComponentTyped: any} ? import("svelte").SvelteComponentTyped<Props,Events\
,Slots> : Svelte2TsxComponent<Props,Events,Slots>;

interface Svelte2TsxComponentConstructorParameters<Props extends {}> {


/**
* An HTMLElement to render to. This option is required.
*/
target: Element | ShadowRoot;
/**
* A child of `target` to render the component immediately before.
*/
anchor?: Element;
/**
* An object of properties to supply to the component.
*/
props?: Props;
context?: Map<any, any>;
hydrate?: boolean;
intro?: boolean;
$$inline?: boolean;
}

type AConstructorTypeOf<T, U extends any[] = any[]> = new (...args: U) => T;


Chapter 17 - Creating Component Libraries 229

type SvelteComponentConstructor<T, U extends Svelte2TsxComponentConstructorParameter\


s<any>> = new (options: U) => T;

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 SvelteTransitionReturnType = SvelteTransitionConfig | (() => SvelteTransitionCo\


nfig)

type SvelteAnimationReturnType = {
delay?: number,
duration?: number,
easing?: (t: number) => number,
css?: (t: number, u: number) => string,
tick?: (t: number, u: number) => void
}

type SvelteWithOptionalProps<Props, Keys extends keyof Props> = Omit<Props, Keys> & \


Partial<Pick<Props, Keys>>;
type SvelteAllProps = { [index: string]: any }
type SveltePropsAnyFallback<Props> = {[K in keyof Props]: Props[K] extends undefined\
? any : Props[K]}
type SvelteSlotsAnyFallback<Slots> = {[K in keyof Slots]: {[S in keyof Slots[K]]: Sl\
ots[K][S] extends undefined ? any : Slots[K][S]}}
type SvelteRestProps = { [index: string]: any }
type SvelteSlots = { [index: string]: any }
type SvelteStore<T> = { subscribe: (run: (value: T) => any, invalidate?: any) => any\
}

// 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

// no ambient type references are left in the output


type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

type KeysMatching<Obj, V> = {[K in keyof Obj]-?: Obj[K] extends V ? K : never}[keyof\


Obj]
declare type __sveltets_1_CustomEvents<T> = {[K in KeysMatching<T, CustomEvent>]: T[\
K] extends CustomEvent ? T[K]['detail']: T[K]}

declare var process: NodeJS.Process & { browser: boolean }


declare var __sveltets_1_AnimationMove: { from: DOMRect, to: DOMRect }

declare function __sveltets_1_ensureAnimation(animationCall: SvelteAnimationReturnTy\


pe): {};
declare function __sveltets_1_ensureAction(actionCall: SvelteActionReturnType): {};
declare function __sveltets_1_ensureTransition(transitionCall: SvelteTransitionRetur\
nType): {};
declare function __sveltets_1_ensureFunction(expression: (e: Event & { detail?: any \
}) => unknown ): {};
// Includes undefined and null for all types as all usages also allow these
declare function __sveltets_1_ensureType<T>(type: AConstructorTypeOf<T>, el: T | und\
efined | null): {};
declare function __sveltets_1_ensureType<T1, T2>(type1: AConstructorTypeOf<T1>, type\
2: AConstructorTypeOf<T2>, el: T1 | T2 | undefined | null): {};

declare function __sveltets_1_createEnsureSlot<Slots = Record<string, Record<string,\


any>>>(): <K1 extends keyof Slots, K2 extends keyof Slots[K1]>(k1: K1, k2: K2, val:\
Slots[K1][K2]) => Slots[K1][K2];
declare function __sveltets_1_ensureRightProps<Props>(props: Props): {};
declare function __sveltets_1_cssProp(prop: Record<string, any>): {};
declare function __sveltets_1_ctorOf<T>(type: T): AConstructorTypeOf<T>;
declare function __sveltets_1_instanceOf<T = any>(type: AConstructorTypeOf<T>): T;
declare function __sveltets_1_allPropsType(): SvelteAllProps
declare function __sveltets_1_restPropsType(): SvelteRestProps
declare function __sveltets_1_slotsType<Slots, Key extends keyof Slots>(slots: Slots\
): Record<Key, boolean>;

// Overload of the following two functions is necessary.


// An empty array of optionalProps makes OptionalProps type any, which means we lose\
the prop typing.
// optionalProps need to be first or its type cannot be infered correctly.

declare function __sveltets_1_partial<Props = {}, Events = {}, Slots = {}>(


render: {props: Props, events: Events, slots: Slots }
Chapter 17 - Creating Component Libraries 231

): {props: Expand<SveltePropsAnyFallback<Props>>, events: Events, slots: Expand<Svel\


teSlotsAnyFallback<Slots>> }
declare function __sveltets_1_partial<Props = {}, Events = {}, Slots = {}, OptionalP\
rops extends keyof Props = any>(
optionalProps: OptionalProps[],
render: {props: Props, events: Events, slots: Slots }
): {props: Expand<SvelteWithOptionalProps<SveltePropsAnyFallback<Props>, OptionalPro\
ps>>, events: Events, slots: Expand<SvelteSlotsAnyFallback<Slots>> }

declare function __sveltets_1_partial_with_any<Props = {}, Events = {}, Slots = {}>(


render: {props: Props, events: Events, slots: Slots }
): {props: Expand<SveltePropsAnyFallback<Props> & SvelteAllProps>, events: Events, s\
lots: Expand<SvelteSlotsAnyFallback<Slots>> }
declare function __sveltets_1_partial_with_any<Props = {}, Events = {}, Slots = {}, \
OptionalProps extends keyof Props = any>(
optionalProps: OptionalProps[],
render: {props: Props, events: Events, slots: Slots }
): {props: Expand<SvelteWithOptionalProps<SveltePropsAnyFallback<Props>, OptionalPro\
ps> & SvelteAllProps>, events: Events, slots: Expand<SvelteSlotsAnyFallback<Slots>> \
}

declare function __sveltets_1_with_any<Props = {}, Events = {}, Slots = {}>(


render: {props: Props, events: Events, slots: Slots }
): {props: Expand<Props & SvelteAllProps>, events: Events, slots: Slots }

declare function __sveltets_1_with_any_event<Props = {}, Events = {}, Slots = {}>(


render: {props: Props, events: Events, slots: Slots }
): {props: Props, events: Events & {[evt: string]: CustomEvent<any>;}, slots: Slots }

declare function __sveltets_1_store_get<T = any>(store: SvelteStore<T>): T


declare function __sveltets_1_store_get<Store extends SvelteStore<any> | undefined |\
null>(store: Store): Store extends SvelteStore<infer T> ? T : Store;
declare function __sveltets_1_any(dummy: any): any;
declare function __sveltets_1_empty(...dummy: any[]): {};
declare function __sveltets_1_componentType(): AConstructorTypeOf<_SvelteComponent<a\
ny, any, any>>
declare function __sveltets_1_invalidate<T>(getValue: () => T): T

declare function __sveltets_1_mapWindowEvent<K extends keyof HTMLBodyElementEventMap\


>(
event: K
): HTMLBodyElementEventMap[K];
Chapter 17 - Creating Component Libraries 232

declare function __sveltets_1_mapBodyEvent<K extends keyof WindowEventMap>(


event: K
): WindowEventMap[K];
declare function __sveltets_1_mapElementEvent<K extends keyof HTMLElementEventMap>(
event: K
): HTMLElementEventMap[K];
declare function __sveltets_1_mapElementTag<K extends keyof ElementTagNameMap>(
tag: K
): ElementTagNameMap[K];
declare function __sveltets_1_mapElementTag<K extends keyof SVGElementTagNameMap>(
tag: K
): SVGElementTagNameMap[K];
declare function __sveltets_1_mapElementTag(
tag: any
): any; // needs to be any because used in context of <svelte:element>

declare function __sveltets_1_bubbleEventDef<Events, K extends keyof Events>(


events: Events, eventKey: K
): Events[K];
declare function __sveltets_1_bubbleEventDef(
events: any, eventKey: string
): any;

declare const __sveltets_1_customEvent: CustomEvent<any>;


declare function __sveltets_1_toEventTypings<Typings>(): {[Key in keyof Typings]: Cu\
stomEvent<Typings[Key]>};

declare function __sveltets_1_unionType<T1, T2>(t1: T1, t2: T2): T1 | T2;


declare function __sveltets_1_unionType<T1, T2, T3>(t1: T1, t2: T2, t3: T3): T1 | T2\
| T3;
declare function __sveltets_1_unionType<T1, T2, T3, T4>(t1: T1, t2: T2, t3: T3, t4: \
T4): T1 | T2 | T3 | T4;
declare function __sveltets_1_unionType(...types: any[]): any;

declare function __sveltets_1_awaitThen<T>(


promise: T,
onfulfilled: (value: T extends PromiseLike<infer U> ? U : T) => any,
onrejected?: (value: T extends PromiseLike<any> ? any : never) => any
): any;

declare function __sveltets_1_each<T extends ArrayLike<unknown>>(


array: T,
callbackfn: (value: T extends ArrayLike<infer U> ? U : any, index: number) => any
Chapter 17 - Creating Component Libraries 233

): any;

declare function __sveltets_1_createSvelte2TsxComponent<Props, Events, Slots>(


render: {props: Props, events: Events, slots: Slots }
): SvelteComponentConstructor<_SvelteComponent<Props, Events, Slots>,Svelte2TsxCompo\
nentConstructorParameters<Props>>;

declare function __sveltets_1_unwrapArr<T>(arr: ArrayLike<T>): T


declare function __sveltets_1_unwrapPromiseLike<T>(promise: PromiseLike<T> | T): T

// 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>;

declare function __sveltets_2_any(...dummy: any[]): any;


declare function __sveltets_2_empty(...dummy: any[]): {};
declare function __sveltets_2_union<T1,T2,T3,T4,T5>(t1:T1,t2?:T2,t3?:T3,t4?:T4,t5?:T\
5): T1 & T2 & T3 & T4 & T5;

declare function __sveltets_2_cssProp(prop: Record<string, 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

* @internal This is for type checking capabilities only


* and does not exist at runtime. Don't use this property.
*/
$$slot_def: any;

$on(event: string, handler: (e: any) => any): () => void;


}
/**
* Ambient type only used for intellisense, DO NOT USE IN YOUR PROJECT
*/
declare type ConstructorOfATypedSvelteComponent = new (args: {target: any, props?: a\
ny}) => ATypedSvelteComponent
declare function __sveltets_2_ensureComponent<T extends ConstructorOfATypedSvelteCom\
ponent>(type: T): T;

declare function __sveltets_2_ensureArray<T extends ArrayLike<unknown>>(array: T): T\


extends ArrayLike<infer U> ? U[] : any[];

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

import * as fs from 'fs'


import { join } from 'path'
import { emitDts } from 'svelte2tsx'

(async () => {

const cwd = process.cwd()


const svelteShimsPath = join(cwd, 'svelte2tsx-shims.d.ts')

const config = fs.existsSync(join(cwd, 'config.json'))


? JSON.parse(fs.readFileSync(join(cwd, 'config.json'), 'utf-8'))
: {
libRoot: 'src/'
};

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:

npm uninstall svelte


npm install -D [email protected]

Finally, let’s proceed updating the package.json commands so we can correctly build our
library.

Update package.json commands


Update the package.json file. First, make sure we update the following scripts commands so
that we can correctly build both JavaScript and the TypeScript types:

// 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"
},

...

Now let’s add a couple of simple components to our library.

Create Counter.svelte component


Create a new file at src/components/counter/Counter.svelte with the following code:

// 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>

Create SampleComp.svelte component


Create a new file at src/components/sample-component/SampleComp.svelte with the follow-
ing code
Chapter 17 - Creating Component Libraries 239

// file: src/components/sample-component/SampleComp.svelte

<script lang="ts">

// expose a property called testid


export let testid = 'not-set'
// expose a property called text
export let text = 'not-set'

// a computed property to return the css class


$: cssClass = (): string => {
return `p-2 border border-green-500`
}

</script>

<div data-testid={testid} class={cssClass()}>


<span>{ text }</span>
</div>

Add components/index.ts barrel file


Under components/, add a barrel index.ts file and just export all our components in an
organized way:

// file: src/components/index.ts

import Counter from './counter/Counter.svelte'


import SampleComp from './sample-component/SampleComp.svelte'

export {
Counter,
SampleComp
}

Build our library


Now finally run the “build” command (or you could run the “all” command) to compile and
build our library:
Chapter 17 - Creating Component Libraries 240

npm run build

Consuming our library


To consume our library locally, let’s switch now to our my-svelte-project and install a
reference to our library by running this command:

npm install -D file:../my-component-library

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'

...

In the template section, let’s consume our library components:

// 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

? Select a framework: › - Use arrow-keys. Return to submit.


� vanilla
vue
react
preact
lit
svelte

And then vanilla-ts for the framework “variant”:

? Select a variant: › - Use arrow-keys. Return to submit.


vanilla
� vanilla-ts

After you are done creating the project and have run “npm install”, let’s continue by
removing unecessary files (similarly to Chapter 17)

Remove obsolete files


Remove the following files as they are not needed in a NPM package:

• favicon.svg
• typescript.svg
• app.css (or style.css)
• index.html
• main.ts
• counter.ts

Remove also the public directory and all its contents.

Add main entry index.ts file


Add new new file under src/ called index.ts that export all the source code we want to
exposes from our NPM package. In our case, we’ll export everything from the sub-directory
called helpers (which will create in a bit):
Chapter 18 - Creating a JavaScript library 245

// 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

/// <reference types="vitest" />


/// <reference types="vite/client" />

import { defineConfig } from 'vite'


import path from 'path'

// 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

// Provide global variables to use in the UMD build


// Add external deps here
globals: {
},
},
},
}
})

Update package.json commands


Update the package.json file. First, make sure we update the following scripts commands so
that we can correctly build both JavaScript and the TypeScript types:

// 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"
},

...

Now let’s add a some JavaScript helpers to our NPM package.

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

export const randomid = (): string => {


let result: string = ''
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValue\
s) {
const array: Uint32Array = new Uint32Array(1)
window.crypto.getRandomValues(array)
result = array[0].toString()
} else {
// throw error
// throw Error('Browser does not support window.crypto.getRandomValues')
// if node, we could use crypto to do the same thing
Chapter 18 - Creating a JavaScript library 248

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.

random-id unit tests

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

import { randomid } from '../../helpers'

describe('id', () => {

it('should return value with expected length', () => {


const result = randomid()
expect(result.length).toEqual(11)
})

it('should return expected value', () => {


// testing 10,000 different ids
const attempts = 10000
const results = []
for (let i = 0; i < attempts; i++) {
const value = randomid()
Chapter 18 - Creating a JavaScript library 249

results.push(value)
}

const distinctResults = new Set([...results])


expect(results.length).toEqual(distinctResults.size)
})
})

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": {

...

"test": "vitest run",


"test-watch": "vitest watch",

...

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

iMacRetina:my-js-helpers damiano$ npm run test

> @largescaleapps/[email protected] test


> vitest run

RUN v0.19.0 /Volumes/code/large-scale-apps-my-svelte-project/my-js-helpers

✓ src/tests/random-id/randomid.test.ts (2)

Test Files 1 passed (1)


Tests 2 passed (2)
Time 3.29s (in thread 65ms, 5063.52%)

Build the library


To build the library, just run the command npm run all (note this will also pack the library
into a compressed file with .tgz extension and we could later consume from there or just by
referencing the local directory)

Consuming the my-js-helpers library


Now we have to open the my-svelte-project and consume our helpers library by referencing
it from a local path. The easiest way is to run the following command npm install -D
file:../my-js-helpers.

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:

<p>[randomid() result (from my-js-helpers): { randomid() }]</p>

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

<SampleComp text={`This is a sample component from my-component-library: ${ randomid\


() }`} />

The first one will output in the browser something like:

[randomid() result (from my-js-helpers): 03627536338]

And the second one should output something like this:

This is a sample component from my-component-library: 00244391593


Chapter 18 - Creating a JavaScript library 252

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.

Create an NPM user account


The first step will be for you to create an NPM user account, if you do not already have
one. You can do this on the NPM signup page at https://www.npmjs.com/signup. If you
need further help with that please see here https://docs.npmjs.com/creating-a-new-npm-
user-account

Create an Organization under your NPM profile


To publish a scoped public package, I would suggest to create a fictitious organization that
you can use to learn how to publish NPM packages. Once you have mastered this and are
more confident, you can better organize your packages under a real organization name or
publish them using your NPM username as the scope (which will have to be prefixed with
the @ char).
On NPM, once you are logged in, click on your avatar and select “Add Organization +”
(alternatively, you can click on Profile, then on the Organizations tab, then on the “+ Add
New Organization” button). Enter a name of your choice in the Organization field and click
on the Create button next to the “Unlimited public packages” option. In the next screen,
where it asks if you want to invite other developers, just click Skip. Your organization is now
created and will show under your Profile (Organizations tab).
Chapter 19 - Publish a library as a NPM package 254

Update my-js-helpers package.json


We need to scope our library name. In order to do this, you have to add a prefix to the
name property in the package.json field follow by a slash character. Here you could either
use your NPM username or organization name (note: you have to include the @ char at the
beginning):

// file: package.json

{
"name": "@your-org-name/my-js-helpers", // prefix is in the form @username/ or @or\
gname/
"version": "0.1.21",

...

Publishing the library


First, you’ll have to login to NPM with the command:

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):

npm notice Log in on https://registry.npmjs.org/


Username: yourusername
Password: yourpassword
Email: (this IS public) youremail

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

npm publish --access public

If everything goes well, your package will be published on NPM.

Consuming your NPM package


Let’s switch back to the my-svelte-project code.
Here, we’ll first uninstall the current local references to the my-js-helpers library:

npm uninstall my-js-helpers

Then we can install the one form the NPM registry with:

npm install -D @your-org-name/my-js-helpers

If you run the my-svelte-project everything should still work as before.


Chapter 19 - Publish a library as a NPM package 256

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

Avoid using any and rather always choose an interface/type/class


Interfaces
Interfaces are named with an Interface suffix. For example, an interface representing Item
will be named ItemInterface.
Each interface will be contained in its own file. The file naming convention will be
Item.interface.ts.

Directory/File Naming and Structure

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

In general, files will be named with a pascal-case convention, I.e. OrderHistory.ts


Barrel files will always be named index.ts (all lower case)
Files that export one instance of a class, or serve as a provider/factory will be also named
index.ts (as long as the folder in which they are contained specify the domain/rea name, i.e.
http-client/index.ts)

Interface File Names

Files containining interfaces will follow the convention [Name].interface.ts, i.e.


Item.interface.ts.

Components 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

Views/Pages File Names

Views files will be under src/views directory.


Their names follow the convention [ViewName].svelte (NOTE: in Svelte, everything is
really a component, including views. The separation is mostly for organization purposes. The
way we consume views and components differs and we talk more about this throughout the
book).

Unit Tests file names

For unit tests, we’ll follow the convention [ClassOrComponentBeingTested].test.ts. I.e.


ItemsList.test.ts (NOTE that test against models/classes will be stored under tests/unit
directory, while a test against a component will be located where each corresponding
component is)
Naming Conventions 260

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

Contains the Svelte source code

• src/assets: contains static assets like image files etc (organized in further sub-directories)

src/api: contains the API clients implementations

• src/api/mock: contains the API clients that return mock data


• src/api/live: contains the API clients that communicate with the live API end-points

src/components: contains the higher order components (while primitives are within a sub-
directory)

• src/components/[lowercase-component-name]: directory contains all the files that


make up a specific component. I.e. src/components/items
– src/components/[lowercase-component-name]/children: contains all the sub-
components, if any, consumed only by our main component. I.e. src/compo-
nents/items/children (NOTE: this is not a strict requirement. Might have multiple
sub-directory at the same level as children with more specific names for more
complex component that have many child components)

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/models: contains all the pure TypeScript interface/types/classes/models/etc (extension


.ts)

• src/models/[domain]: contains all the interfaces/classes/etc that are related to a


particular domain, I.e. items

src/store: contains the state manager implementation

• src/store/[domain]: contains the store module implementation for a specific domain,


I.e. items

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

You might also like