Altough, it might not look like much, but you will learn about:
onMount
callbackReplace your App.svelte with the code below. We are importing a fullscreen component that we haven't defined yet. No worries, let the compiler whine for a few minutes. It can wait.
<!-- App.svelte -->
<script>
import Fullscreen from "./Fullscreen.svelte";
</script>
<style>
h2 {
text-align: center;
font-size: 2rem;
}
</style>
<Fullscreen let:isFull>
<div>
<img
src="https://media.giphy.com/media/vCKC987OpQAco/giphy.gif"
alt="Yes you are!" />
<h2>
{#if isFull}I am now in fullscreen!{/if}
</h2>
</div>
</Fullscreen>
If you look at the code you can see that we passing a div as a child component to our fullscreen component. Our fullscreen component is also exposing a property called isFull
to the parent. We can use it to make decisions if we want to hide something when in fullscreen mode and other useful things. Let's keep things basic.
Here is our fullscreen component. Take a quick look and let's go through it below.
<!-- Fullscreen.svelte -->
<script>
import { onMount } from "svelte";
// define initial component state
let isFull = false;
let fsContainer = null;
// boring plain js fullscreen support stuff below
const noop = () => {};
const fullscreenSupport = !!(
document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
document.mozFullScreenEnabled ||
document.msFullscreenEnabled ||
false
);
const exitFullscreen = (
document.exitFullscreen ||
document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
document.msExitFullscreen ||
noop
).bind(document);
const requestFullscreen = () => {
const requestFS = (
fsContainer.requestFullscreen ||
fsContainer.mozRequestFullScreen ||
fsContainer.webkitRequestFullscreen ||
fsContainer.msRequestFullscreen ||
noop
).bind(fsContainer);
requestFS();
};
onMount(() => {
// Add the icon stylesheet dynamically
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://fonts.googleapis.com/icon?family=Material+Icons";
document.head.appendChild(link);
// remove the link when component is unmounted
return () => {
link.parentNode.removeChild(link);
};
});
// handler for the fullscreen button
const fsToggle = () => {
if (!fullscreenSupport) return;
if (isFull) {
exitFullscreen();
} else {
requestFullscreen(fsContainer);
}
isFull = !isFull;
};
// the icon name is computed automagically based
// on the state of the screen
$: icon = isFull ? "fullscreen_exit" : "fullscreen";
</script>
<style>
.isFull {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
button {
color: #000;
position: absolute;
right: 20px;
bottom: 20px;
}
</style>
<div class="fs" class:isFull bind:this={fsContainer}>
<slot {isFull} />
{#if fullscreenSupport}
<button on:click={fsToggle}>
<i class="material-icons md-36">{icon}</i>
</button>
{/if}
</div>
At the top we are defining two state variables. isFull
is a boolean that indicates if component is in fullscreen mode. This is also a variable that we'are exposing in the slot <slot {isFull} />
, so our parent component can use it if it needs. We are also binding the DOM node of the fullscreen div to the fsContainer
variable that we use in our code to trigger fullscreen state.
<div class="fs" class:isFull bind:this={fsContainer}>
In the code snippet above we are also using Svelte's nifty way of setting a CSS class on the component depending on the variable state. You can see that we defined a .isFull
class in the style tag. There is also a way to use a pseudo CSS class :fullscreen
when then component is in fullscreen mode, but for some reason I couldn't get it to work with Svelte's CSS scoping.
As you see, it's really easy to build custom components in Svelte because it's just vanilla JS with some sugar sprinkled on top. Sometimes it's easier to just copy and paste code and tweak it to your own desire, than to install a package from NPM.
The full code is at https://github.com/codechips/svelte-fullscreen-example
]]>Let's start by creating a simple new project with Yarn.
$ mkdir -p app/src && cd app
$ yarn init -y
Add Parcel, Svelte and required plugins.
$ yarn add -D parcel-bundler svelte parcel-plugin-svelte
We are almost ready to go, but before that we need to add some stuff to package.json
and some actual source files. Start by adding the following properties to your package.json
.
"scripts": {
"start": "parcel src/index.html --port 3000",
"build": "rm -rf dist && parcel build src/index.html --no-source-maps"
},
"browserslist": [
"last 1 chrome versions"
]
Our server will listen on port 3000 and when building production bundle we will skip source map generation.
Now, let's add some actual source files.
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>My App</title>
</head>
<body>
<script defer src="./main.js"></script>
<noscript>You need to enable JavaScript to run this app.</noscript>
</body>
</html>
src/main.js
import App from './App.svelte';
const app = new App({
target: document.body
});
export default app;
src/App.svelte
<script>
let name = "friend";
</script>
<h1>Hello {name}!</h1>
We are now ready to start our app.
$ yarn start
yarn run v1.21.1
$ parcel src/index.html --port 3000
Server running at http://localhost:3000
✨ Built in 923ms.
Wow! How awesome is that? And really fast too! Try changing some text in App.svelte
and see how fast everything recompiles.
We can also build a production bundle.
$ yarn build
yarn run v1.21.1
$ rm -rf dist && parcel build src/index.html --no-source-maps
✨ Built in 1.22s.
dist/main.a3795f1f.js 22.92 KB 895ms
dist/index.html 347 B 279ms
Done in 1.76s.
Just look at how fast the build is! Amazing!
Let's install Tailwind - a functional CSS framework and make it play nice with our current setup.
$ yarn add -D tailwindcss autoprefixer @fullhuman/postcss-purgecss
We have to add some additional files for Tailwind to work.
Create Tailwind config file.
$ yarn tailwind init
Create base styles file - src/global.pcss
with the following content.
@tailwind base;
@tailwind components;
@tailwind utilities;
Create a PostCSS config file - postcss.config.js
.
const plugins =
process.env.NODE_ENV === 'production'
? ['tailwindcss', 'autoprefixer', '@fullhuman/postcss-purgecss']
: ['tailwindcss'];
module.exports = { plugins };
The config purges unused CSS and adds browser prefixes only in production builds. Why? Because during development you want to have a full Tailwind CSS file so you can tinker with various classes in your browser's dev console.
Finally, let's add a PurgeCSS config so that PostCSS will know what unused CSS to purge during the production builds. Create a purgecss.config.js
file with the following content.
module.exports = {
content: [
'./src/index.html',
'./src/**/*.svelte'
],
whitelistPatterns: [/svelte-/],
defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
};
Here we are telling it to ignore svelte-
classes when purging. These are classes that Svelte generates whey you write scoped styles in you Svelte components.
We could have wired up our configuration only in postcss.config.js
, but I think it's nicer to have them in two different files for clear separation of concerns.
Finally, include the following line in the head tag in index.html
.
<link rel="stylesheet" href="./global.pcss" />
Add the following classes to the H1 tag in App.svelte
to see that everything works as expected.
<h1 class="text-5xl text-teal-700">Hello {name}!</h1>
Boom! If you now start the app you should see a styled heading. NOTE: If for some reason it doesn't work, delete Parcel's .cache
folder and restart the app.
So there you go. New fresh and slick Svelte setup. You can stop here and go build your next great thing or you can continue reading and maybe learn something new.
Inter font is pretty sweet if you are building UIs. Here is how to include it in our new setup with tailwindcss-font-inter plugin.
$ yarn add -D tailwindcss-font-inter
Replace tailwind.config.js
with following content.
module.exports = {
theme: {
interFontFeatures: {
default: ['calt', 'liga', 'kern'],
numeric: ['tnum', 'salt', 'ss02']
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '4rem',
'7xl': '6rem',
'8xl': '8rem',
'9xl': '9rem',
'10xl': '10rem'
},
extend: {}
},
variants: {},
plugins: [
require('tailwindcss-font-inter')({
importFontFace: true,
disableUnusedFeatures: true
})
]
};
Now add the following class to the body tag in index.html
.
<body class="font-inter">
Your app should now use the Inter font. There are many configuration options for how the font is rendered. Let that be a home assignment.
Linting and code formatting is important. I personally use Vim with coc.vim's Svelte extension when writing code. It's actually working mighty fine I must say! However, in order for that to work you have to install some additional plugins and add more configs.
$ yarn add -D prettier prettier-plugin-svelte eslint eslint-plugin-svelte3
Add .prettierrc.json
{
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"plugins": ["prettier-plugin-svelte"],
"svelteSortOrder": "styles-scripts-markup",
"svelteStrictMode": true,
"svelteBracketNewLine": true,
"svelteAllowShorthand": true
}
And .eslintrc.json
{
"env": {
"browser": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module"
},
"plugins": ["svelte3"],
"extends": ["eslint:recommended"],
"overrides": [
{
"files": ["**/*.svelte"],
"processor": "svelte3/svelte3"
}
],
"rules": {
"prettier/prettier": "error",
"svelte3/lint-template": 2
}
}
Voila! Linting and code formatting should now work.
As you see it's not hard to use Svelte with Parcel. The setup feels very fresh and snappy. Everything with Parcel works almost out-of-the-box. I really like it.
If you want to learn and understand how things are wired together you can repeat all the steps above to understand what's happening, but if you are lazy (like me) and just want to bang out some code, you can use my boilerplate and be done with it.
$ npx degit codechips/svelte-tailwind-parcel-starter facebook-killer
Thanks for reading and happy coding!
]]>$ scc src/auth.js
───────────────────────────────────────────────────────────────────────────────
Language Files Lines Blanks Comments Code
───────────────────────────────────────────────────────────────────────────────
JavaScript 1 107 19 22 66
───────────────────────────────────────────────────────────────────────────────
Total 1 107 19 22 66
───────────────────────────────────────────────────────────────────────────────
Estimated Cost to Develop $1,556
Estimated Schedule Effort 1.314907 months
Estimated People Required 0.140209
───────────────────────────────────────────────────────────────────────────────
Told ya! But I personally think other numbers are waaay more interesting. scc tells me that it takes around 1,5K USD to develop this in approximately 6 weeks by only 1/10 of me. It took me 20 minutes to write the code (I am very skilled at skimming the docs and copy-pasting). So, if I do the math correctly my hourly rate would be .... astronomic! Mind blown.
Note to self: raise the hourly rate pronto!
Ok, sorry for sidetracking. Back to the code. So Auth0 has a generous free tier in case you need to add authentication to your app. It also has all these cool starter packs for all these cool frameworks, but not for little Svelte. Turns out we don't really need one! Hah!
Let's boot up and add Auth0 dependency.
$ npx degit sveltejs/template svelte-auth0
$ cd svelte-auth0 && npm i
$ npm add -D @auth0/auth0-spa-js
Now, create an auth.js
file in src
dir. Here it is in all its glory with comments and all.
// src/auth.js
import {onMount, setContext, getContext} from 'svelte';
import {writable} from 'svelte/store';
import createAuth0Client from '@auth0/auth0-spa-js';
const isLoading = writable(true);
const isAuthenticated = writable(false);
const authToken = writable('');
const userInfo = writable({});
const authError = writable(null);
const AUTH_KEY = {};
// Default Auth0 expiration time is 10 hours or something like that.
// If you want to get fancy you can parse the JWT token and get
// token's actual expiration time.
const refreshRate = 10 * 60 * 60 * 1000;
function createAuth(config) {
let auth0 = null;
let intervalId = undefined;
// You can use Svelte's hooks in plain JS files. How nice!
onMount(async () => {
auth0 = await createAuth0Client(config);
// Not all browsers support this, please program defensively!
const params = new URLSearchParams(window.location.search);
// Check if something went wrong during login redirect
// and extract the error message
if (params.has('error')) {
authError.set(new Error(params.get('error_description')));
}
// if code then login success
if (params.has('code')) {
// Let the Auth0 SDK do it's stuff - save some state, etc.
await auth0.handleRedirectCallback();
// Can be smart here and redirect to original path instead of root
window.history.replaceState({}, document.title, '/');
authError.set(null);
}
const _isAuthenticated = await auth0.isAuthenticated();
isAuthenticated.set(_isAuthenticated);
if (_isAuthenticated) {
// while on it, fetch the user info
userInfo.set(await auth0.getUser());
// Get the access token. Make sure to supply audience property
// in Auth0 config, otherwise you will soon start throwing stuff!
const token = await auth0.getTokenSilently();
authToken.set(token);
// refresh token after specific period or things will stop
// working. Useful for long-lived apps like dashboards.
intervalId = setInterval(async () => {
authToken.set(await auth0.getTokenSilently());
}, refreshRate);
}
isLoading.set(false);
// clear token refresh interval on component unmount
return () => {
intervalId && clearInterval(intervalId);
};
});
// Provide a redirect page if you need.
// It must be whitelisted in Auth0. I think.
const login = async redirectPage => {
await auth0.loginWithRedirect({
redirect_uri: redirectPage || window.location.origin,
prompt: 'login' // Force login prompt. No silence auth for you!
});
};
const logout = () => {
auth0.logout({
returnTo: window.location.origin
});
};
const auth = {
isLoading,
isAuthenticated,
authToken,
authError,
login,
logout,
userInfo
};
// Put everything in context so that child
// components can access the state
setContext(AUTH_KEY, auth);
return auth;
}
// helper function for child components
// to access the auth context
function getAuth() {
return getContext(AUTH_KEY);
}
export {createAuth, getAuth};
In the onMount
hook we are setting up a timer to refresh the access token so it doesn't expire and things suddenly stop working. This is useful for long running apps like dashboards.
Now replace App.svelte
with the following contents.
<!-- App.svelte -->
<script>
import { createAuth } from './auth';
// Go to Auth0 to get the values and set everything up.
// Make sure all callback urls are set correctly.
const config = {
domain: 'your-auth0-tenant.auth0.com',
client_id: 'auth0-client-id',
audience: 'https://my-facebook-killer.io'
};
const {
isLoading,
isAuthenticated,
login,
logout,
authToken,
authError,
userInfo
} = createAuth(config);
$: state = {
isLoading: $isLoading,
isAuthenticated: $isAuthenticated,
authError: $authError,
userInfo: $userInfo ? $userInfo.name : null,
authToken: $authToken.slice(0, 20)
};
</script>
<style>
main {
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>
<div>
<div>
{#if $isAuthenticated}
<button on:click={() => logout()}>Logout</button>
{:else}
<button on:click={() => login()}>Login</button>
{/if}
</div>
<h2>State</h2>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
Done. Start the app (npm run dev
) and hopefully everything should work.
If you need auth, you probably need routing too. Let's add one.
Now, there are quite a few routing solutions available. I chose one with the shortest name - yrv. Also because the slogan spoke to me - Your routing bro! Nobody has called me "bro" in a long time. Hey, I live in Sweden!
Yrv is small, sweet and with great documentation. The author says that "v" stands for Svelte (you make the connection), but I think that it secretly stands for vato. If you look at the author's GH profile pic he looks really badass, a true OG.
Ok, let's throw Yrv in the mix (npm add -D yrv
) and add --single
argument in your package.json
in order to support SPA router.
"start": "sirv public --single"
Change your App.svelte
to this.
<script>
import { createAuth } from './auth';
import { Link, Router, Route } from 'yrv';
const config = {
domain: 'your-auth0-tenant.auth0.com',
client_id: 'auth0-client-id',
audience: 'https://my-facebook-killer.io'
};
const {
isLoading,
isAuthenticated,
login,
logout,
authError
} = createAuth(config);
$: disabled = !$isAuthenticated;
</script>
<style>
main {
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
:global(a[aria-current]) {
font-weight: 700;
}
</style>
<main>
<div>
{#if $isLoading}
<p>Loading ...</p>
{:else if $authError}
<p>Got error: {$authError.message}</p>
{:else if !$isAuthenticated}
<button on:click={() => login()}>Login</button>
{:else}
<button on:click={() => logout()}>Logout</button>
<div>
<Link href="/">Home</Link> |
<Link href="/settings">Settings</Link> |
<Link href="/hello/handsome">Hello!</Link>|
<Link href="/foobar">Not Found</Link>
<div>
<Router {disabled}>
<Route exact path="/">
<h2>Home</h2>
<p>This is the root page</p>
</Route>
<Route exact path="/settings">
<h2>Settings</h2>
<p>This is the settings page</p>
</Route>
<Route exact path="/hello/:name" let:router>
<h2>Hola {router.params.name}!</h2>
<p>Nice to see you</p>
</Route>
<Route fallback>
<h2>404 Not Found</h2>
<p>Sorry, page not found</p>
</Route>
</Router>
</div>
</div>
{/if}
</div>
</main>
Now, when you start the app you should see our routing in action. Boom!
This example is bare and sloppy and might not solve all your problems. As the saying goes - there are many ways to skin a cat or ... peel a banana, or something. View it as inspiration. You can use it as base and adjust to your needs. The main point is that sometimes you don't need an NPM package because it's easier to write the code ourselves. I feel that this is often the case with Svelte. What Svelte community needs is more examples and inspiration and not NPM packages. But on the other hand, there is the discoverability aspect that NPM brings to the table, but that's a story for another day!
Hope you learned something new or got some inspiration and as always, thanks for reading!
]]>I love Svelte's maintainer crew. They all seems very open-minded. They listen to smart people and understand the power of the community. "If it's a good idea, then let's do it" approach is very successful. Just look at the commit history yourself and you will understand what I mean. They are banging out features and bugfixes at incredible pace. Kudos to them and all the people involved!
Because they listen, and people asked for it, they have adopted the store contract to match the contract of the RxJS observable, which in its turn matches the ECMAScript Observable specification. That means we can almost use observables out of the box in Svelte, so let's test drive the combination.
Although I have used RxJS in production I am by no means an expert in it. I am still trying to wrap my head around thinking in streams so the examples in this article might not be the most efficient way of doing things in RxJS. Please point it out in the comments if you know of a better way of doing things!
Also, don't use RxJS because you can. It's pretty complex and many things can be solved by Promises and other simpler ways instead. Please, please don't view everything as a nail just because you have a hammer.
This article is not about RxJS but about the ways you can use RxJS in Svelte. However, I think it deserves a few words anyway. RxJS is a pretty cool declarative reactive framework that allows you to mangle and stream data in the ways you never imagined. Its declarative coding style is very concise and easy to read ... when you finally understand how streams work.
It's used heavily in Angular, so if you want to learn RxJS practically you might look into it. Last time I looked at Angular (version 1), I could only look for 10 minutes. Then I had to look away because I got a little nauseous. But, I heard things have greatly changed since then! Give it a try! For me personally, life is too short to try all the different frameworks, but there is one for everyone.
Alright, let's start by dipping our toes wet. Create a new Svelte app and install RxJs.
$ npx degit sveltejs/template svelte-rxjs && cd svelte-rxjs
$ npm i && npm i -D rxjs
Remember I said that Svelte's store contract fulfills the Observable spec? It's also the other way around. RxJS observable fulfills Svelte's store contract as well. A least partially.
What that means in practice is that we can prefix the RxJS observable with a dollar sign and Svelte compiler will treat it as as store and manage the subscribing/unsubscribing parts for us during the Svelte's component lifecycle.
Let's try it with a simple example - a counter that counts to 10 and then stops. Replace App.svelte with the code below.
<script>
import { interval } from "rxjs";
import { map, take, startWith } from "rxjs/operators";
const counter = interval(1000).pipe(
map(i => i + 1),
take(10)
);
</script>
<h2>Count to 10</h2>
{$counter}
Since the observable is prefixed with $
Svelte manages the subscription for us automatically. If you are observant you will see that the observable is undefined
first before the timer kicks in and start emitting values only after one second has passed. This is of course easily solved, but I wanted to show this as it is super important to know and understand why this is happening in order to save you the frustration and your hair.
Let me demonstrate why this is important. Try this code.
<script>
import { of } from "rxjs";
import { delay } from "rxjs/operators";
// emit an array with the initial delay of 2s
const values = of([1, 2, 3, 4, 5]).pipe(delay(2000));
</script>
<h2>Loop over array</h2>
<ul>
{#each $values as v}
<li>{v}</li>
{/each}
</ul>
And ..... BOOM!
Uncaught TypeError: Cannot read property 'length' of undefined
Whoops! It does't work? Why? That's because the initial value is undefined
and undefined is not something that you can loop over.
So we need to always make sure that our observable emits some initial value immediately when Svelte subscribes to it. Here is a quick fix. Later I will show you another way of handling this.
<script>
import { of } from "rxjs";
import { delay, startWith } from "rxjs/operators";
// emit an array with initial delay of 2s
const values = of([1, 2, 3, 4, 5]).pipe(
delay(2000),
startWith([])
);
</script>
<h2>Loop over array</h2>
<ul>
{#each $values as v}
<li>{v}</li>
{/each}
</ul>
Here is a simple counter example. You can see that I use BehaviorSubject
from RxJs. A subject in RxJS is an observer and observable at the same time, but this is not the focus of the article. You can simply view it as a store on steroids. By that I mean that you can do lots of fancy stuff with it and not just set values.
There are quite a few different subjects in RxJS. I chose BehaviorSubject because you can initialize it with a default value, thus escaping the undefined
problem upon subscription. You use next
method to push values into it.
<script>
import { BehaviorSubject } from "rxjs";
import { scan, tap } from "rxjs/operators";
const counter = new BehaviorSubject(0).pipe(
scan((acc, value) => {
return value.reset ? 0 : acc + value.delta;
}),
tap(console.log)
);
</script>
<h2>counter example</h2>
<h3>{$counter}</h3>
<div>
<button on:click={() => counter.next({ delta: -1 })}>sub</button>
<button on:click={() => counter.next({ delta: 1 })}>add</button>
<button on:click={() => counter.next({ reset: true })}>rst</button>
</div>
Even though the code is pretty simple in RxJS terms, and I totally stole it on Stack Overflow, I find it overly complex for such trivial task. Let's contrast it with Svelte's store solution.
<script>
import { writable } from "svelte/store";
let counter = writable(0);
</script>
<h2>counter example</h2>
<h3>{$counter}</h3>
<div>
<button on:click={() => ($counter = $counter - 1)}>sub</button>
<button on:click={() => ($counter = $counter + 1)}>add</button>
<button on:click={() => ($counter = 0)}>rst</button>
</div>
The code is much simpler if you ask me and does what it's suppose to do. This is what I mean that you should use the right tool for the job.
There is no set
method on the Rx Subject, but we can solve it in a multiple ways. Either by wrapping an observable in a custom object, by creating a subclass or by simply creating a method alias like counter.set = counter.next
. This will allow you to do fancy stuff like for example binding to it directly in your forms.
Alright, let's move on on how to handle click events with Svelte and RxJS, like when I click a button it should fetch something from a server and display it on a page. It's pretty easy to do if you use subjects. Here is a simple example.
<script>
import { BehaviorSubject } from "rxjs";
import { mergeAll, tap, pluck, take, toArray } from "rxjs/operators";
import { ajax } from "rxjs/ajax";
const news = new BehaviorSubject([]);
const fetchNews = () => {
ajax("https://api.hnpwa.com/v0/news/1.json")
.pipe(
pluck("response"),
mergeAll(),
take(10),
toArray(),
tap(console.log)
)
.subscribe(res => news.next(res));
};
</script>
<h2>on:click handler</h2>
<button on:click={fetchNews}>fetch news</button>
<ol>
{#each $news as item (item)}
<li>
<div>
<div>
<a href={item.url}>{item.title} ({item.domain})</a>
</div>
<div style="font-size: 13px">
{item.points} points by {item.user} {item.time_ago}
</div>
</div>
</li>
{/each}
</ol>
Here is another way to achieve the same thing using RxJS fromEvent
. I also threw in fromFetch
operator just to spice things up a bit.
<script>
import { onMount } from "svelte";
import { BehaviorSubject, fromEvent } from "rxjs";
import { mergeMap, switchMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
let btnFetch;
const news = new BehaviorSubject([]);
onMount(() => {
fromEvent(btnFetch, "click")
.pipe(
mergeMap(() =>
fromFetch("https://api.hnpwa.com/v0/news/1.json").pipe(
switchMap(res => res.json())
)
)
)
.subscribe(res => news.next(res));
});
</script>
<h2>fromEvent handler</h2>
<button bind:this={btnFetch}>fetch news</button>
<ol>
{#each $news as item (item)}
<li>
<div>
<div>
<a href={item.url}>{item.title} ({item.domain})</a>
</div>
<div style="font-size: 13px">
{item.points} points by {item.user} {item.time_ago}
</div>
</div>
</li>
{/each}
</ol>
It doesn't feel so "Sveltish" to me for some reason, like I am trying to cheat on Svelte by not using her click handler.
Here is a more complex example that shows the true power of RxJS and it's declarative reactivity. We will perform a simple weather search and render the results on a page.
<script>
import { BehaviorSubject, of, from } from "rxjs";
import { ajax } from "rxjs/ajax";
import {
debounceTime,
distinctUntilChanged,
filter,
map,
merge,
mergeMap,
pluck,
switchMap,
toArray
} from "rxjs/operators";
const fetchWeather = locs => {
if (!locs || !locs.length) return of([]);
return from(locs).pipe(
map(loc => loc.woeid),
mergeMap(id => {
return ajax(
`https://cors-anywhere.herokuapp.com/https://www.metaweather.com/api/location/${id}`
).pipe(pluck("response"));
}),
map(item => {
const today = item.consolidated_weather[0];
return {
id: item.woeid,
city: item.title,
desc: today.weather_state_name,
icon: `https://www.metaweather.com/static/img/weather/${today.weather_state_abbr}.svg`,
cel: Math.floor(today.the_temp),
far: Math.floor(today.the_temp * 1.8 + 32)
};
}),
toArray()
);
};
const fetchCities = query => {
return !query
? of([])
: ajax(
`https://cors-anywhere.herokuapp.com/https://www.metaweather.com/api/location/search/?query=${query}`
).pipe(
pluck("response"),
mergeMap(locs => fetchWeather(locs))
);
};
const search = new BehaviorSubject("").pipe(
filter(query => query.length > 2),
debounceTime(500),
distinctUntilChanged(),
switchMap(query => fetchCities(query))
);
const weather = new BehaviorSubject([]);
search.subscribe(weather);
</script>
<h2>Weather Search</h2>
<input
type="text"
on:input={e => search.next(e.target.value)}
placeholder="Enter city name" />
{#each $weather as loc (loc.id)}
<div>
<h3>
<img src={loc.icon} alt={loc.desc} style="width:24px;height:24px" />
{loc.city} {loc.cel}C ({loc.far}F)
</h3>
</div>
{/each}
What it does in terms of streams (or my intention at least) is:
Honestly, this is example took like 90% of my time to get working when writing this article. I also tried to implement a loading indicator with streams too, but gave up because my RxJS-fu is not that strong. I am also 100%, no 1000% sure that this code is not the true Rx way. It is also not working properly, but I can't figure out why. Please, please leave a comment or create a Gist if you know a better way or if you spotted the error, so I can learn!
The point of the article was to see how well Svelte plays with RxJS. Looks like Svelte and RxJS might be a decent match for each other, but I am afraid that RxJS is a little too smart for Svelte (and for me). If you have seen the movie "Good Will Hunting", you know what I mean. It's very easy to get lost in the RxJS land and I feel that most of the examples can be accomplished just as good with promises and regular Svelte stores, even if it means more code. But at least that's the code that you and the ones after you will be able to understand. It also felt a little clunky that you have to use subjects to get the default state, but maybe there is a better way. Please teach me then!
Nevertheless, I had fun playing around with both frameworks and I learned some new stuff on the way. Hope you did too.
]]>The code looks like this.
<style>
.input {
transition: border 0.2s ease-in-out;
min-width: 280px
}
.input:focus+.label,
.input:active+.label,
.input.filled+.label {
font-size: .75rem;
transition: all 0.2s ease-out;
top: -0.1rem;
color: #667eea;
}
.label {
transition: all 0.2s ease-out;
top: 0.4rem;
left: 0;
}
</style>
<script>
var toggleInputContainer = function (input) {
if (input.value != "") {
input.classList.add('filled');
} else {
input.classList.remove('filled');
}
}
var labels = document.querySelectorAll('.label');
for (var i = 0; i < labels.length; i++) {
labels[i].addEventListener('click', function () {
this.previousElementSibling.focus();
});
}
window.addEventListener("load", function () {
var inputs = document.getElementsByClassName("input");
for (var i = 0; i < inputs.length; i++) {
console.log('looped');
inputs[i].addEventListener('keyup', function () {
toggleInputContainer(this);
});
toggleInputContainer(inputs[i]);
}
});
</script>
<div class="max-w-xl p-10 bg-white rounded shadow-xl">
<h1 class="mb-4 text-4xl font-black">Login</h1>
<div class="relative mb-4">
<input class="removed-for-readability" id="email" type="text">
<label for="email" class="removed-for-readability">Email Address</label>
</div>
<div class="relative mb-4">
<input class="removed-for-readability" id="password" type="password">
<label for="password" class="removed-for-readability">Password</label>
</div>
<button class="removed-for-readability">Submit</button>
</div>
I didn't like the fact that you had to reach out to pure DOM functions to achieve this functionality. Turns out that Svelte's use
directive is a perfect fit for the job and also a good example of showing one of the things you can use it for. Let's refactor the code a bit.
<style>
.input {
transition: border 0.2s ease-in-out;
}
.input:focus + .label,
.input:active + .label,
.input.filled + .label {
font-size: 0.75rem;
transition: all 0.2s ease-out;
top: -0.1rem;
color: #667eea;
}
.label {
transition: all 0.2s ease-out;
top: 0.4rem;
left: 0;
}
</style>
<script>
const labelToggle = node => {
const handleKey = event => {
if (event.target.value) {
event.target.classList.add('filled');
} else {
event.target.classList.remove('filled');
}
};
node.addEventListener('keyup', handleKey);
return {
destroy() {
node.removeEventListener('keyup', handleKey);
}
};
};
const labelClick = node => {
const click = event => {
event.target.previousElementSibling.focus();
};
node.addEventListener('click', click);
return {
destroy() {
node.removeEventListener('click', click);
}
};
};
</script>
<div class="max-w-lg p-10 bg-white rounded shadow-md">
<h1 class="mb-4 text-3xl font-black">Login</h1>
<form>
<div class="relative mb-4">
<input use:labelToggle class="removed-for-readability" id="email" type="text" />
<label use:labelClick for="email" class="removed-for-readability">Email</label>
</div>
<div class="relative mb-4">
<input use:labelToggle class="removed-for-readability" />
<label use:labelClick for="password" class="removed-for-readability">Password</label>
</div>
<div class="text-center">
<button class="removed-for-readability">Continue</button>
</div>
</form>
</div>
Do you notice that our text inputs now have use:labelToggle
directives and our labels have use:labelClick
? Basically, we have defined the two "use" handlers, or actions as they are called in Svelte, in the script section of the file and then attached them to the appropriate html nodes. But how does it work?
The actions are custom code that will be run when the element is mounted on the DOM and will pass the element to that action as a raw DOM node. If the function returns an object with destroy
function on it, Svelte will run that function when the element is unmounted from the DOM. Very simple, but also incredibly powerful in case you want to do something outside of Svelte and use the full power of DOM.
Below is an annotated example of the toggle handler attached to our text inputs.
// Svelte passes in raw html DOM element when element is mounted on the DOM
const labelToggle = node => {
// Define a custom event handler for the text input element
const handleKey = event => {
// if element's value is not empty add class "filled"
if (event.target.value) {
event.target.classList.add('filled');
} else {
event.target.classList.remove('filled');
}
};
// bind custom event handler to element's keyup event
node.addEventListener('keyup', handleKey);
// when element is unmounted from the DOM remove the event listener
return {
destroy() {
node.removeEventListener('keyup', handleKey);
}
};
};
You can also pass in parameters to actions and run custom code if parameters change, but I wanted to keep the example simple here. Read the docs if you want to learn more.
Svelte's actions have many use cases,like drag-and-drop, tooltips, etc. Only your imagination is the limit.
]]>I wanted to take page.js out for a spin and see what's possible, so I spent an hour playing with it. Something pretty interesting came out as a result. Something that I want to share with you and also teach you a bit about how some of the stuff in Svelte works.
In this article you will learn about:
let
Let's skip the fluff. Just do the following.
$ npx degit sveltejs/template svelte-pagejs && cd svelte-pagejs
$ yarn add -D page
Create a few components and put some H2 tags in them so we have something to work with. Replace App.svelte
with the code below. Make sure to get your imports right for the components you created.
<script>
import page from 'page';
import Home from './pages/Home.svelte';
import About from './pages/About.svelte';
import Profile from './pages/Profile.svelte';
// set default component
let current = Home;
// Map routes to page. If a route is hit the current
// reference is set to the route's component
page('/', () => (current = Home));
page('/about', () => (current = About));
page('/profile', () => (current = Profile));
// activate router
page.start();
</script>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
margin: 0 auto;
}
h1 {
color: #ff3e00;
text-transform: uppercase;
font-size: 4em;
font-weight: 100;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
nav a {
padding-right: 3rem;
}
</style>
<main>
<nav>
<a href="/">home</a>
<a href="/about">about</a>
<a href="/profile">profile</a>
</nav>
<svelte:component this={current} />
</main>
In order to our SPA to work you have to add --single
flag to the start script in package.json
. Like this.
"start": "sirv public --single"
Start the app (yarn dev
) and be amazed that it works.
But HOW does it actually work? First, we wire up the router where each route when hit re-assigns the current
var to its matched component. Then our svelte:component
tag sees that the reference has changed. It then creates the new component and renders it.
<svelte:component>
This Svelte directive works like this:
this
is null
or undefined
it ignores it and does nothing.this
changes it will destroy old component and create and mount new one.Our simple solution works, but I wanted to have something better, something more declarative, something like this.
<Router>
<Route path="/" component="{Home}" />
<Route path="/about" component="{About}" />
<Route path="/profile" component="{Profile}" />
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
Can we make something like this? Yep. Sure we can. Totally achievable with the right level of abstraction. Read on to learn how.
Let's try to create our own router by somehow wrapping page.js to do the hard work for us. We can call it pager.js
. Start by creating a folder under src called pager
and create the following files in it.
$ tree src/pager
src/pager
├── NotFound.svelte
├── Router.svelte
└── Route.svelte
We will start with the router as it's the main file that will do the dirty work for us. Since we will do the routing in there we need to move the page.js to it.
We also need to declare the routes inside our router. For that we will use Svelte's slot. See slot as a placeholder into which you can put other components and html tags and stuff. Here is the file so far.
<script>
import page from 'page';
</script>
<slot />
Now create a Route.svelte
file and define the component and path properties in it.
<script>
export let path = '/';
export let component = null;
</script>
<slot />
Add NotFound.svelte
with just a <slot />
in it.
Import those files in the App.svelte
file and paste the declarative router code in the main area. The file should look like this (with style omitted).
<!-- App.svelte -->
<script>
import Router from './pager/Router.svelte';
import Route from './pager/Route.svelte';
import NotFound from './pager/NotFound.svelte';
import Home from './pages/Home.svelte';
import About from './pages/About.svelte';
import Profile from './pages/Profile.svelte';
</script>
<main>
<nav>
<a href="/">home</a>
<a href="/about">about</a>
<a href="/profile">profile</a>
<a href="/news">news</a>
</nav>
<Router>
<Route path="/" component="{Home}" />
<Route path="/about" component="{About}" />
<Route path="/profile" component="{Profile}" />
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
</main>
Start the app and now at least it should not give you compile errors. But it's not usable at all as we only got the structure, but not logic. Let's fill that part in. Back to our router.
Now, from our simple example in the beginning, we know that we have to use slots to render our components. How can we do that? We are passing path and components to individual routes, right? Add the following code line to the Route.svelte file right above the <slot />
tag and the components passed in will now be rendered.
<svelte:component this="{component}" />
Great! Well, actually not THAT great as all components are shown at once, but at least some progress!
We now need to get back to the main router file and add some logic to it. Somehow we need the routes register themselves with page.js which lives in the Router file. How do can we do that? We can use simple dictionary for that and export some kind of register
function from the Router file.
Before we start, we need to understand how Svelte components work. When you import a Svelte component somewhere in your app it has only a single default export and that is the component itself. This is important to understand.
// the standard way
import Router from './Router.svelte';
// same component but different name
import Foo from './Router.svelte';
// This will not work, unless ..
import { register } from './Router.svelte';
So the last import statement will not work unless you declare a module script in your component.
<script type="module">
export function register(route) {
console.log(route);
}
</script>
Add that module script to our Router.svelte file and now you can import register
function in the Route.svelte file.
When you define the module script in the component, all defined stuff in there (vars and functions) will be available to all the instances of that component. Thus they are "shared" variables. There are some more nuances to that and what you can and can't do. Please refer to the official docs to learn more.
Our route can now register itself with the Router.
<script>
import { register } from './Router.svelte';
export let path = '/';
export let component = null;
register({ path, component });
</script>
<svelte:component this="{component}" />
<slot />
In the Router we need a place to keep these route objects somewhere. We can use a simple dict for that and use path as a key.
<script context="module">
const routes = {};
export function register(route) {
routes[route.path] = route;
}
</script>
<script>
import { onMount } from "svelte";
import page from "page";
onMount(() => console.log(routes));
</script>
<slot />
If you have done everything correctly, you can now see the routes object printed in the browser's dev console. Progress!
Now we need to wire it up to the page.js somehow. We can create the following function that wires up page.
<script>
import { onMount, onDestroy } from "svelte";
import page from "page";
const setupPage = () => {
for (let [path, route] of Object.entries(routes)) {
page(path, () => console.log(route));
}
// start page.js
page.start();
};
// wire up page.js when component mounts on the dom
onMount(setupPage);
// remove page.js click handlers when component is destroyed
onDestroy(page.stop);
</script>
Now if you click around on the nav links you should see the mapped route printed in the dev tools console. We are slowly getting there!
Somehow we need to keep the state of the current component and for that we can use Svelte's reactive store. Add the following to Router.svelte
// on top of the module script
import { writable } from 'svelte/store';
export const activeRoute = writable({});
// and change the "page" line in the regular script to
page(path, () => ($activeRoute = route));
We now need our components to know which one is the active one, meaning which should be displayed. We can easily do that by importing our activeRoute
store. And since stores are reactive all components will know when it changes. Our Route.svelte
file looks like this now.
<script>
import { register, activeRoute } from './Router.svelte';
export let path = '/';
export let component = null;
register({ path, component });
</script>
{#if $activeRoute.path === path}
<svelte:component this="{component}" />
<slot />
{/if}
Now stuff should ... kind of work when you click around. Except we constantly see that "not found" route. Not good. Something we need to fix and something that is thankfully, pretty easy to fix.
<script>
import { register, activeRoute } from './Router.svelte';
// page.js catch all handler eg "not found" in this context
export let path = '*';
export let component = null;
register({ path, component });
</script>
{#if $activeRoute.path === path}
<svelte:component this="{component}" />
<slot />
{/if}
Phew! Everything finally works now and you can pat yourself on the shoulder for making it this far! But ... we are not quite done yet. I want more! I want to pass custom properties and page's params down to the components and also be able to protect the routes. Something like the code below.
<Router>
<Route path="/" component="{Home}" {data} {user} />
<Route path="/about" component="{About}" />
<Route path="/profile/:username" middleware="{[guard]}" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
Want to know how? Stay tuned for Part 2.
]]>This is how we want our final solution to look and work.
<Router>
<Route path="/" component="{Home}" {data} />
<Route path="/about" component="{About}" />
<Route path="/profile/:username" middleware="{[guard]}" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
We will start with the easiest part. Exposing params to the components and in routes. Page.js allows you to define params in the url path and will make them available to you in its context object. We first need to understand how page.js works
page('/profile/:name', (ctx, next) {
console.log('name is ', ctx.params.name);
});
Page.js takes a callback with context
and next
optional parameters. Context is the context object that will be passed to the next callback in the chain in this case. You can put stuff on the context object that will be available to the next callback. This is useful for building middlwares, for example pre-fetching user information, and also caching. Read more what's possible in the context docs.
Propagating params is actually pretty simple, we just have to put it in our activeRoute
store in the Router.svelte
file. Like this.
const setupPage = () => {
for (let [path, route] of Object.entries(routes)) {
page(path, (ctx) => ($activeRoute = { ...route, params: ctx.params }));
}
page.start();
};
And here is how our Route.svelte
file looks now.
<script>
import { register, activeRoute } from './Router.svelte';
export let path = '/';
export let component = null;
// Define empty params object
let params = {};
register({ path, component });
// if active route -> extract params
$: if ($activeRoute.path === path) {
params = $activeRoute.params;
}
</script>
{#if $activeRoute.path === path}
<!-- if component passed in ignore slot property -->
{#if $activeRoute.component}
<!-- passing custom properties and page.js extracted params -->
<svelte:component
this="{$activeRoute.component}"
{...$$restProps}
{...params}
/>
{:else}
<!-- expose params on the route via let:params -->
<slot {params} />
{/if}
{/if}
We use the spread operator to pass page.js params down to the component. That's just one way to do it. You might as well pass down the whole params
object if you want. The interesting part is the $$restProps
property that we also pass down to the underlying component. In Svelte, there are $$props
and $$restProps
properties. Props includes all props in component, the passed in ones and the defined ones, while restProps excludes the ones defined in the component and includes the only ones that are being passed in. This means that we also just solved passing custom properties down to components feature. Hooray!
Our main part of the App.svelte
looks like this now.
<main>
<nav>
<a href="/">home</a>
<a href="/about">about</a>
<a href="/profile/joe">profile</a>
<a href="/news">news</a>
</nav>
<Router>
<Route path="/" component="{Home}" />
<Route path="/about" component="{About}" />
<Route path="/profile/:username" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
</main>
Give the app a spin and see if our params feature works as expected. I left out custom data properties as an exercise.
The only missing part now is the protected routes part, which we can solve with the help of middleware. Let's implement this.
Page.js supports multiple callbacks for a route which will be executed in order they are defined. We will leverage this feature and build our middleware on top of it.
page('/profile', guard, loadUser, loadProfile, setActiveComponent);
It works something like this. Our "guard" callback will check for some pre-condition and decide whether to allow the next callback in the chain or not. Our last callback that sets the active route must be last in the chain, named setActiveComponent
in the example above. For that to work we need to refactor the main router file a bit.
// extract our active route callback to its own function
const last = (route) => {
return function (ctx) {
$activeRoute = { ...route, params: ctx.params };
};
};
const registerRoutes = () => {
Object.keys($routes).forEach((path) => {
const route = $routes[path];
// use the spread operator to pass supplied middleware (callbacks) to page.js
page(path, ...route.middleware, last(route));
});
page.start();
};
You might wonder where the route.middleware
comes from. That is something that we pass down to the individual routes.
<!-- Route.svelte -->
<script>
import { register, activeRoute } from './Router.svelte';
export let path = '/';
export let component = null;
// define new middleware property
export let middleware = [];
let params = {};
// pass in middlewares to Router.
register({ path, component, middleware });
$: if ($activeRoute.path === path) {
params = $activeRoute.params;
}
</script>
{#if $activeRoute.path === path}
{#if $activeRoute.component}
<svelte:component
this="{$activeRoute.component}"
{...$$restProps}
{...params}
/>
{:else}
<slot {params} />
{/if}
{/if}
If you try to run the app now you will get a reference error. That's because we have to add middleware property to NotFound.svelte
too.
<!-- NotFound.svelte -->
<script>
import { register, activeRoute } from './Router.svelte';
// page.js catch all handler
export let path = '*';
export let component = null;
register({ path, component, middleware: [] });
</script>
{#if $activeRoute.path === path}
<svelte:component this="{component}" />
<slot />
{/if}
And here what our App.svelte
looks now with style omitted.
<script>
import { Router, Route, NotFound, redirect } from './pager';
import Login from './pages/Login.svelte';
import Home from './pages/Home.svelte';
import About from './pages/About.svelte';
import Profile from './pages/Profile.svelte';
const data = { foo: 'bar', custom: true };
const guard = (ctx, next) => {
// check for example if user is authenticated
if (true) {
redirect('/login');
} else {
// go to the next callback in the chain
next();
}
};
</script>
<main>
<nav>
<a href="/">home</a>
<a href="/about">about</a>
<a href="/profile/joe">profile</a>
<a href="/news">news</a>
<a href="/login">login</a>
</nav>
<Router>
<Route path="/" component="{Home}" {data} />
<Route path="/about" component="{About}" />
<Route path="/login" component="{Login}" />
<Route path="/profile/:username" let:params>
<h2>Hello {params.username}!</h2>
<p>Here is your profile</p>
</Route>
<Route path="/news" middleware="{[guard]}">
<h2>Latest News</h2>
<p>Finally some good news!</p>
</Route>
<NotFound>
<h2>Sorry. Page not found.</h2>
</NotFound>
</Router>
</main>
The app file looks a little different now, but that's because I've added some bells and whistles to it. You can find the whole project here.
This wraps everything up. We've now created fully declarative router for Svelte based on page.js. It's not feature complete, but you can easily adjust it to your own requirements. It's hard to build libraries that cover every possible corner case, kudos to those who try!
I hope that I showed you that it's actually not that hard to build something in Svelte that fits just your requirements, while also keeping control of the code. I also hope that you picked up some knowledge on the way of how Svelte works.
]]>For this example we will use my Svelte template which has Tailwind baked into it.
$ npx degit iljoo/svelte-tailwind-parcel-starter svelte-switch
The hardest part of this is to get the CSS right. Fortunately Tailwind makes it very easy. Start of by creating a Switch.svelte
file.
<!-- Switch.svelte -->
<style>
.switch {
@apply relative inline-block align-middle cursor-pointer select-none bg-transparent;
}
.track {
@apply w-12 h-6 bg-gray-600 rounded-full shadow-inner;
}
.thumb {
@apply transition-all duration-300 ease-in-out absolute top-0 left-0 w-6 h-6 bg-white border-2 border-gray-600 rounded-full;
}
input[type='checkbox']:checked ~ .thumb {
@apply transform translate-x-full border-green-500;
}
input[type='checkbox']:checked ~ .track {
@apply transform transition-colors bg-green-500;
}
input[type='checkbox']:disabled ~ .track {
@apply bg-gray-500;
}
input[type='checkbox']:disabled ~ .thumb {
@apply bg-gray-100 border-gray-500;
}
input[type='checkbox']:focus + .track,
input[type='checkbox']:active + .track {
@apply shadow-outline;
}
</style>
<script>
export let id = '';
export let text = '';
export let checked = false;
export let disabled = false;
</script>
<label for="{id}">
<div class="switch">
<input {id} name="{id}" type="checkbox" class="sr-only" {disabled} bind:checked />
<div class="track"></div>
<div class="thumb"></div>
</div>
<span class="ml-2 cursor-pointer">{text}</span>
</label>
Now, import Switch into the App.svelte
.
<!-- App.svelte -->
<script>
import Switch from './Switch.svelte';
let uno = false;
let dos = true;
let tres = false;
let quatro = true;
$: values = { uno, dos, tres, quatro };
</script>
<div class="flex items-center justify-center flex-grow h-screen">
<div class="max-w-xl">
<h1 class="text-2xl font-semibold text-gray-700">
Accessible switch toggle with svelte.js + tailwind.css
</h1>
<div class="mt-5">
<input
type="text"
class="w-full p-2 border border-gray-200"
placeholder="Tab from here to go to the next switch button"
id="text"
/>
</div>
<div class="flex justify-between mt-5">
<Switch bind:checked="{uno}" id="uno" text="uno" />
<Switch bind:checked="{dos}" id="dos" text="dos" />
<Switch bind:checked="{tres}" id="tres" text="tres" />
<Switch bind:checked="{quatro}" disabled="{true}" id="quatro" text="quatro"/>
</div>
<div class="mt-5">
<pre class="p-4 font-mono bg-teal-100">{JSON.stringify(values)}</pre>
</div>
</div>
</div>
That's it. We can now use tab for navigation, to switch between different buttons, and use space to toggle their state. The secret sauce to why it works is Tailwind's sr-only class where checkbox will be hidden, but still be accessible to screen readers. Accessibility is hard to get right, but that doesn't mean we should ignore it.
You can find the code here. Adjust it to fit your needs and as usual, hope that you learned something new. Thanks for reading!
]]>All of the bundlers are capable of compiling Svelte, but which one should you use?
Here is the list of Svelte plugins for all bundlers.
Rollup and Webpack ones are officially supported by Svelte.
For this experiment I've used a simple Svelte app and some CSS and compared different bundlers in terms of the bundle size they produce and runtime debugging capabilities. Here is the App file.
<!-- App.svelte -->
<script>
let name = "stranger";
</script>
<style>
main {
text-align: center;
padding: 1em;
max-width: 240px;
margin: 0 auto;
color: #1616b9;
}
@media (min-width: 640px) {
main {
max-width: none;
}
}
</style>
<main>
<h2>Hello {name}!</h2>
</main>
Total bundle size: 36K
4.0K index.html
4.0K main.a0838bf2.css
28K main.ceed3f71.js
Total bundle size: 12K
4.0K bundle.css
4.0K bundle.js
4.0K index.html
Total bundle size: 16K
4.0K bundle.css
8.0K bundle.js
4.0K index.html
Rollup is the clear winner here, tightly followed by Webpack. Parcel's JS bundle size is three times as big as Rollup's.
Here I wanted to see how well different bundlers can point to the runtime errors in the console. For this I introduced a small error in the App file.
<script>
let name = "stranger";
// this will blow up
let foo = undefined;
console.log(foo.bar());
</script>
Parcel's stacktrace is useless as it points you to the transpiled JS file.
Parcel console
Parcel stacktrace
Rollup actually points you to correct line in Svelte file.
Rollup console
Rollup stacktrace
Webpack does also a good job of pointing you to correct file.
Webpack console
Webpack stacktrace
To me, personally, Rollup is a clear winner here. It produces small bundles and does a good job of pointing you to the right place in case of runtime error. Only critique I have is that configuration can be somewhat daunting.
Webpack is good too. It's fast, mature and has tons of plugins for your other needs. Both Rollup and Webpack have support for HMR (Hot Module Reload).
Parcel? Parcel is a disappointment. It's fast while developing, because of caching, but the bundle size and development experience is not optimal.
But the good thing is that you can use all three in your project if you like! Check out my example here https://github.com/codechips/svelte-parcel-vs-rollup-vs-webpack
If I've missed any other worth mentioning bundler that works well with Svelte, please let me know in the comments.
]]>This article has been updated since the official Typescript support for Svelte was released. The official Svelte blog post explains how to get started with Svelte and Typescript quickly, but this article takes a little different approach with an opinionated (improved if you ask me) setup, plus some extra bonus materials.
Also, Rollup, which I use in the article as a bundler is pretty slow. Why? Because it needs to re-compile the whole shebang every time you change a file. It produces very small and efficient bundles though.
Things in tech move almost at the speed of light. Since this article was written a few other alternative and faster bundlers popped up. Bundlers that help you shorten the feedback loop. You have Snowpack and Svite for example.
If you are curious about Snowpack I have written an article on it - Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise.
One of the show stoppers for people who want to start using Svelte was the lack of first class Typescript integration. And it's not so much about the type safety as about tooling. Great supporting tools for any framework are important for its future growth and popularity. Svelte is still a young framework, but without proper tooling ecosystem surrounding it, I am afraid it might die. It would be such shame.
I've done some experimenting with Rollup, Webpack and Parcel trying to get Svelte to play nice with Typescript. While I achieved decent results with all of them, I will use Rollup here as it has a pretty straight forward setup plus the re-compilation step was the fastest of them all.
Let's start with a standard vanilla JS Svelte setup and adjust our project there to enable Typescript support. The best way to learn is by doing.
$ npx degit sveltejs/template svelte-and-typescript
$ cd svelte-and-typescript && npm i
We now have a simple Svelte app, with all the greatest and latest dependencies, that we can run with npm run dev
.
I prefer a slightly different Rollup configuration, so we will adjust it a bit. It requires us to bring in a few new utilities first. We will start with them.
$ npm add -D rollup-plugin-serve rollup-plugin-html2 rim-raf npm-run-all
Time to refactor our rollup.config.js
to something more readable.
// rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
import html from 'rollup-plugin-html2';
import livereload from 'rollup-plugin-livereload';
import resolve from '@rollup/plugin-node-resolve';
import serve from 'rollup-plugin-serve';
import svelte from 'rollup-plugin-svelte';
import { terser } from 'rollup-plugin-terser';
const isDev = process.env.NODE_ENV === 'development';
const buildDir = 'dist';
const port = 3000;
// define all our plugins
const plugins = [
svelte({
dev: isDev,
// extract all styles to an external file
css: css => {
css.write(`${buildDir}/bundle.css`);
},
extensions: ['.svelte'],
}),
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
// injects your bundles into index page
html({
template: 'src/index.html',
fileName: 'index.html',
}),
];
if (isDev) {
plugins.push(
// like a webpack-dev-server
serve({
contentBase: buildDir,
historyApiFallback: true, // for SPAs
port,
}),
livereload({ watch: buildDir })
);
} else {
plugins.push(terser({ sourcemap: isDev }));
}
module.exports = {
input: 'src/main.js',
output: {
name: 'bundle',
file: `${buildDir}/bundle.js`,
sourcemap: isDev,
format: 'iife',
},
plugins,
};
Alright, Rollup config done. Now we need to fix our package.json
too. Replace your scripts
property with the following content.
{
"start": "rimraf dist && run-p watch:*",
"clean": "rimraf dist",
"build": "run-s clean compile",
"compile": "cross-env NODE_ENV=production rollup --config",
"watch:build": "cross-env NODE_ENV=development rollup --config --watch"
}
There are a few things going on in scripts. We use run-s
(run serial) and run-p
(run parallel) from the npm-run-all utility package to run NPM scripts in serially and in parallel. We also use rimraf
and cross-env
utilities to achieve cross-platform functionality.
We now need to create a minimal index.html
file in the src
directory with the following content.
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Svelte App</title>
<link rel='stylesheet' href='/bundle.css'>
</head>
<body></body>
</html>
We can now run and build our app with npm start
and npm build
respectively. Try it!
It's time to bring out the big guns - Typescript. For this to work we need to add a few NPM modules - @rollup/plugin-typescript, @tsconfig/svelte, typescript and tslib, which is a dependency for Rollup's typescript plugin.
$ npm add -D typescript tslib @rollup/plugin-typescript @tsconfig/svelte
Done? Good! Now we have to create a minimal tsconfig.json
using @tsconfig/svelte as our base.
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "dist/*"]
}
We also need to add Typescript support to our Rollup config, so we can process regular Typescript files.
// add typescript plugin to imports
import typescript from '@rollup/plugin-typescript';
// and replace plugins section with this
const plugins = [
svelte({
dev: isDev,
extensions: ['.svelte']
}),
typescript({ sourceMap: isDev }),
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
html({
template: 'src/index.html',
fileName: 'index.html'
}),
];
We will now test drive this with an actual Typescript file. Create a timer.ts
file in your src
folder.
// timer.ts
import { readable } from 'svelte/store';
const timer = readable(0, (set) => {
let current: number = 0;
const id = setInterval(() => {
current += 1;
set(current);
}, 1000);
return () => clearInterval(id);
});
export { timer };
Let's require it in our App.svelte
and see if it works.
<!-- App.svelte -->
<script>
import { timer } from './timer';
</script>
<main>
<h2>Count is { $timer }</h2>
</main>
Fire up the app and see if it works. Hint: It should!
Just your main.js
to main.ts
and don't forget to change the input
property in the Rollup config.
When we now start the app it should also work as expected. Achievement unlocked. We now can work with Typescript files in our Svelte project!
Ah! Of course! Glad you asked. The most important thing! For that you have to use the awesome svelte-preprocess plugin. It's a plugin that can help you pre-process many different kinds of languages in Svelte files such as SASS, Less, Pug and, of course, Typescript.
$ npm add -D svelte-preprocess
Tell Svelte plugin to use preprocess in our rollup.config.js
// import preprocess
import preprocess from `svelte-preprocess`;
// add preprocess to Svelte config
// ...
svelte({
dev: isDev,
extensions: [".svelte"],
preprocess: preprocess()
})
// ...
In our App.svelte
we can now change the script tag from default JS to Typescript. To test that it actually works we will add a variable with a type.
<script lang="typescript">
import { timer } from './timer';
let a: number = 42;
</script>
<main>
<h2>Count is {$timer}</h2>
<p>What's the meaning of life? {a}</p>
</main>
You can annotate your scripts in different ways. All examples below are valid.
<script lang="ts">
And now we can write code like this.
<script lang="typescript">
import { timer } from './timer';
let a: number = 42;
$: sum = a + $timer;
</script>
Our goal is accomplished. We can write parts of our codebase in Typescript and we can also use Typescript in our Svelte files.
Svelte language-tools repo includes a great little CLI utility called svelte-check.
It helps you check your files for unused CSS, Svelte a11y hints and JavaScript/TypeScript compiler errors.
Let's try to integrate it in out setup. First we need to install it.
$ npm add -D svelte-check npm-run-all
Next we need to change our scripts
section in package.json
to this.
// package.json scripts section
{
"start": "rimraf dist && run-p watch:*",
"clean": "rimraf dist",
"check": "svelte-check",
"build": "run-s clean check compile",
"compile": "cross-env NODE_ENV=production rollup --config",
"watch:check": "svelte-check --watch",
"watch:build": "cross-env NODE_ENV=development rollup --config --watch"
}
When we start our dev server (npm start), svelte-check
runs in watch mode and continuously checks our Svelte files for errors. When we bundle the application (npm run build) we run svelte-check
before the bundling step.
Rollup is great for building production bundles, but TS compiler is really slow. If you are feeling adventurous and want faster compilation times you can try the SWC compiler to transpile your Svelte TS scripts. It's written in Rust and is still young, but looks very promising.
First, we have to install the SWC npm package.
$ npm add -D @swc/core
Now we have to change the Typescript configuration in our Rollup config.
// rollup.config.js
import { transformSync } from '@swc/core';
// change the preprocess to this
preprocess: preprocess({
typescript({ content }) {
// use SWC to transpile TS scripts in Svelte files
const { code } = transformSync(content, {
jsc: {
parser: { syntax: 'typescript' }
}
});
return { code };
}
})
I've also tried to replace the regular Rollup Typescript plugin with rollup-plugin-swc, but for some reason it cannot resolve external Typescript files in Svelte files.
You are most likely using VS Code, Neovim or Vim when coding. There are extensions for all of them - Svelte extension for VSCode and coc-svelte for Neovim/Vim.
Before the official TS support you had to create a special svelte.config.js
file so the editor could understand Typescript in Svelte files. This has now changed and you no longer need to do it.
But if you see that your editor is not recognizing Typescript you can create that file with the following contents.
// svelte.config.js
const { preprocess } = require('svelte-preprocess');
module.exports = {
preprocess: preprocess(),
};
There might be Svelte extensions for other editors too. You can find more info at https://github.com/sveltejs/language-tools.
We can also install the "love-it-or-hate-it" Prettier plugin to help us out with code formatting.
$ npm add -D prettier prettier-plugin-svelte
Create a Prettier config file and adjust it to your needs.
// prettier.config.js
module.exports = {
tabWidth: 2,
semi: true,
singleQuote: true,
printWidth: 100,
plugins: ['prettier-plugin-svelte'],
svelteSortOrder: 'styles-scripts-markup',
svelteStrictMode: false,
svelteBracketNewLine: true,
};
If you have a Prettier plugin in your editor it will now read these settings.
And if you want to run Prettier from the command line, with npm run format
for example, add this line to your scripts
section in package.json
.
{
"scripts": {
// ...
"format": "prettier --write src"
}
}
You can probably add other useful code linting plugins, but that's out of the scope for the article, so I will stop here.
There you go. We just learned how to use Typescript in Svelte together with Rollup bundler. But no matter what bundler you use svelte-preprocess always plays the central role when it comes to Typescript integration in Svelte. Actually, without it we wouldn't be able to use any other languages in our Svelte files.
Even though it's now possible to use Typescript in Svelte, I would recommend to keep your Svelte files thin and write the heavy logic in separate files. By utilizing libraries such as XState, RxJS and Rambda you can write concise and testable code. The bar is high, but it's totally worth it! Give them a fair chance!
You can find the code here https://github.com/codechips/svelte-and-typescript
If you want to use this setup in your next Svelte project, you can use my template.
$ npx degit codechips/svelte-starter-template#with-typescript my-app
Hope you learned something new with me today and if something is not right or can be improved, please ping me on Twitter or shoot me an email.
]]>In your parent component you have three different child components that you want to toggle visibility for. In every child component there is a "close" button. When it's clicked the component should hide itself. Since the "close" button is within each child component, you as a parent can't really control it. What are your options?
Turns out it's quite easy to do in Svelte. I can think of three different ways. Let's go through them all.
Let's dive straight in. The best way to learn is to actually write the code. That way your brain will have time to process the information because it's faster then your typing. We will start by creating a new project.
$ npx degit codechips/svelte-starter-template cross-component-communication
$ cd cross-component-communication && yarn install
$ yarn start
Done? Great! We should have a local server running. Now, create tree simple Svelte components in src/components
folder. Create the folder first.
<!-- One.svelte -->
<div class="component">
<h2>Component One</h2>
<img src="https://media.giphy.com/media/1Ju5mGZlWAqek/giphy.gif" alt="happy" />
<button>close</button>
</div>
<!-- Two.svelte -->
<div class="component">
<h2>Component Two</h2>
<img src="https://media.giphy.com/media/lcySndwSDLxC4eOU86/giphy.gif" alt="confused" />
<button>close</button>
</div>
<!-- Three.svelte -->
<div class="component">
<h2>Component Three</h2>
<img src="https://media.giphy.com/media/EdRgVzb2X3iJW/giphy.gif" alt="amazed" />
<button>close</button>
</div>
Nothing fancy yet. We will extend them later. Now change App.svelte
to the code below.
<!-- App.svelte -->
<style>
:global(button) {
display: block;
margin-top: 1rem;
}
:global(body) {
font-family: sans-serif;
font-size: 1.25em;
background-color: #262626;
color: #cbd5e0;
}
:global(h1){
margin-bottom: 1em;
margin-block-end: 0;
}
:global(.component) {
padding: 1rem 2rem;
background-color: #67ecd4;
margin: 0 2rem 0 0;
display: inline-block;
min-height: 28rem;
color: #262626;
}
.menu {
display: flex;
margin-bottom: 1rem;
}
.menu button {
margin-right: 1rem;
}
.container {
display: flex;
}
</style>
<script>
// import slide animation for some visual FX
import { slide } from 'svelte/transition';
import One from './components/One.svelte';
import Two from './components/Two.svelte';
import Three from './components/Three.svelte';
// define booleans for our component visibility status
let showOne = false;
let showTwo = false;
let showThree = false;
</script>
<h1>Svelte Component Communication Examples</h1>
<div class="menu">
<!-- show component actions -->
<button on:click={() => (showOne = !showOne)}>show one</button>
<button on:click={() => (showTwo = !showTwo)}>show two</button>
<button on:click={() => (showThree = !showThree)}>show three</button>
</div>
<div class="container">
{#if showOne}
<div transition:slide|local>
<One />
</div>
{/if}
{#if showTwo}
<div transition:slide|local>
<Two />
</div>
{/if}
{#if showThree}
<div transition:slide|local>
<Three />
</div>
{/if}
</div>
Nothing fancy yet here either. All our components are hidden by default and you can make then visible by clicking on the show buttons. But how can we hide the children if the visibility state is defined in the parent? All of our children has close buttons, but they have no logic bound to them. Let's fill that part in.
This is the most straight forward option. You encapsulate parent's state in a closure and pass it down to the child component that triggers in when needed. In this case we bind it to the button's on:click
event. Change One.svelte
component to the following code.
<script>
// a noop function that will be overridden
// by passed-in handler
export let closeHandler = () => {};
</script>
<div class="component">
<h2>Component One</h2>
<img src="https://media.giphy.com/media/1Ju5mGZlWAqek/giphy.gif" alt="happy" />
<button on:click={closeHandler}>close</button>
</div>
What's going on here? We are exposing a closeHandler
function from the component. This means that we can pass a property closeHandler
to the component from our parent. We initially assign it to a dummy noop function so it doesn't blow up when we click the button and no handler was passed. Let's add this.
// add this function to the App.svelte
const closeOne = () => (showOne = false);
We also need to pass the closure to our One.component
. Change to this in App.svelte
.
<!-- we pass in our local handler to the component -->
<One closeHandler={closeOne} />
Now it should be possible to close the component from the component itself. First option completed!
If you export a variable in the child component you can bind it in the parent by using bind:var={local}
property where var
is the variable name in the child component and local
is the parent's local variable.
<!-- Two.svelte -->
<script>
// since the variable is exported we can bind
// to it in our App.svelte
export let show = false;
</script>
<div class="component">
<h2>Component Two</h2>
<img src="https://media.giphy.com/media/lcySndwSDLxC4eOU86/giphy.gif" alt="confused" />
<button on:click={() => (show = false)}>close</button>
</div>
The only thing we need to do in App.svelte
is to bind it to local variable.
<!-- we bind Two.show variable to our local showTwo variable -->
<Two bind:show={showTwo} />
Tip: if the variable name is the same both in parent and child then you can use a shortcut bind:foo
where foo
is the shared variable name.
It should now be possible to close the component. Second option is now complete!
The last option you have is to use Svelte's dispatcher to send messages between components. This is the most flexible option, but also the most complex one of the three. Personally, I don't use it that much as I don't like that you have to destructure the received event. It feels noisy for some reason.
Change the code in our Three.svelte
component to following.
<!-- Three.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
// when the handler is triggered in on:click
// can subscribe to the on:close message in our App.svelte
// and do what's needed
const closeHandler = () => dispatch('close');
</script>
<div class="component">
<h2>Component Three</h2>
<img src="https://media.giphy.com/media/EdRgVzb2X3iJW/giphy.gif" alt="amazed" />
<button on:click={closeHandler}>close</button>
</div>
We also need to subscribe to the message in App.svelte
. Change to following.
<!-- we listen for close message triggered in the component -->
<!-- we don't care about data in this case as it's not needed -->
<Three on:close={() => (showThree = false)} />
What's going on here? We initialize an event dispatcher in our component and then in the on:click
handler we dispatch a close
message that parent listens to. Since we only care about the message name we don't need to send any data.
Note: dispatcher can only be used in parent/child relationships. Meaning, you cannot use dispatcher to send messages from parent to children. Only the other way around.
You should now be able to close the third component as well. Third option is also done.
There you go. By now you should know three different ways of how to share data between components. Which one is the best depends on your use case of course. Start simple and adjust to your needs if it doesn't work. If you have more complex needs, where multiple components need to "listen" for data, then Svelte's stores might be a better option.
You can find the example code here https://github.com/codechips/svelte-component-communication
]]>Technology moves at the speed of light and since this article was written Snowpack has released a stable version. This means that some of the things explained here (like Typescript support) are baked in and Snowpack also has support for pluggable bundlers now. I have updated example Github repo. You can find the link at the end of the article.
I've been keeping an eye on Snowpack for some time now - an (un)bundler that leverages the power of the ES modules. Personally, I like the ESM idea and hope that we all soon will align and come to a consensus. One promising thing in Snowpack's favour is that almost every modern browser already supports ES modules.
Ok, ok, but what does it actually mean? This is Snowpack's elevator pitch taken straight from its website.
Snowpack helps you build web apps faster by replacing your traditional app bundler during development. No bundling means less unnecessary tooling, instant dev startup time and faster updates on every save.
When you're ready to deploy your site, Snowpack automatically optimizes and bundles your assets for production.
Now, how can one NOT like that?! However, it all sounds too good to be true and thus requires some actual proof. Let's try to climb the mountain.
Snowpack recently re-caught my attention on Twitter, where its author, @FredKSchott, announced the pre-release of Snowpack v2 and also posted a link to a bunch of starter templates - Create Snowpack App (CSA). One of those was a template for Svelte. It was too tempting not to try it.
So I did.
The setup experience was very pleasant. You can tell CSA to yarn
instead of default npm
with the --use-yarn
option when bootstrapping, if Yarn is your thing.
$ npx create-snowpack-app snow-drive --template @snowpack/app-template-svelte
$ cd snow-drive && npm start
My first impression is that it starts really fast! Or more precisely in 9ms. That's crazy fast!
The browser opens up automatically with a nice clean page that has an animated Svelte logo and clear instructions on how to proceed.
I changed some text and deleted some elements to see how fast it would discover the changes and reload. I tried not to blink, but couldn't notice the actual reload. That fast! Had to try it a few times more to make sure that Snowpack wasn't fooling me. Nope, same thing. Blazing fast reloads!
I don't think Snowpack has HMR (Hot Module Reload) support yet for Svelte as Svelte doesn't have support for it itself yet, so Snowpack reloads the whole app when you change something.
NOTE: There are some hacky and experimental ways you can get HMR support with Rollup and Webpack, I believe.
I like to write most of the logic in separate files and just use Svelte as a thin view layer that glues all the pieces together. For that I usually use Typescript, mostly because it gives me nice autocomplete in my editor. Some of you are probably rolling your eyes right now, but hey, tooling and DX is important!
I had some trouble understanding what's required to get Typescript support, so I asked the question on Twitter hoping to get some pointers in the right direction.
To get Typescript support working in Showpack's Svelte template you have to tweak a few things.
First, you need to install Snowpack's Babel plugin, Babel's Typescript preset and the actual Typescript compiler.
$ npm add -D @snowpack/plugin-babel @babel/preset-typescript typescript
Second, we need to tell Babel that we want to transpile Typescript to Javascript. Add the babel.config.json
in the project root folder with the contents below. Snowpack's Babel plugin will pick it up automatically if the file exists.
{
"presets": [
"@babel/preset-typescript"
]
}
Third, you have to tell Snowpack that you want to transpile and lint Typescript. Change your snowpack.config.json
to the code below.
{
"extends": "@snowpack/app-scripts-svelte",
"scripts": {
"run:tsc": "tsc --noEmit",
"run:tsc::watch": "$1 --watch"
},
"devOptions": {},
"installOptions": {}
}
Snowpack uses Babel for transpiling TS to JS, because Babel is much faster than TS compiler. The reason for that is that Babel only strips out the types and does not do any type checking. For TS linting Snowpack uses the actual Typescript compiler. However, before we can lint we need to create a tsconfig.json
, so that Typescript will know what to do.
{
"include": ["src"],
"exclude": ["node_modules/*"],
"compilerOptions": {
"target": "es2019",
"types": ["svelte"],
"moduleResolution": "node"
}
}
Just for the kicks, let's install RxJS and use that too. I am curious to see how this will play out with Snowpack's production builds later.
$ npm add -D rxjs
When you add another dependency Snowpack will automatically convert it to an ES module and install it in web_modules
folder. How convenient!
Now we will actually add some Typescript files and see how it flies. Create a src/timer.ts
file with the following contents.
// timer.ts
import { readable } from 'svelte/store';
const timer = readable(0, set => {
let current: number = 0;
const id = setInterval(() => {
current += 1;
set(current);
}, 1000);
return () => clearInterval(id);
});
export { timer };
Now, change the App.svelte
to the code below.
<!-- App.svelte -->
<style>
:global(body) {
font-family: Arial, Helvetica, sans-serif;
font-size: 5rem;
height: 100vh;
display: flex;
background: azure;
}
.app {
display: grid;
align-items: center;
justify-content: center;
width: 100%;
}
.box {
margin: 0 auto;
}
</style>
<script>
import { timer } from './timer';
import { interval } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
const rxTimer = interval(1000).pipe(
startWith(0),
map(v => v * 2)
);
</script>
<div class="app">
<div class="box">
<div>TS Timer {$timer}</div>
<div>Rx Timer {$rxTimer}</div>
</div>
</div>
If you start the app now everything should work as expected.
As I wrote earlier Snowpack converts your dependencies and puts them into the web_modules
folder. Delete the web_modules
folder and see everything blow up. To re-create it just run npm prepare
. That script runs snowpack install
command, which will scan the source code for all import
statements to find every NPM package used by your application and package it as a ESM in the web modules
folder.
$ npm prepare # or npx snowpack
We should now have Typescript support. How nice!
Let's climb higher and try to enable PostCSS together with TailwindCSS.
Tailwind is a popular atomic CSS framework that I often use and since Snowpack promise us PostCSS support we should be able to use Tailwind.
# install tailwind, autoprefixer, cssnano for css minification
$ npm add -D tailwindcss autoprefixer cssnano
# create tailwind config
$ npx tailwindcss init
# we also need to install postcss
$ npm add -D postcss-cli
Next we need to create a postcss.config.js
.
// postcss.config.js
const tailwind = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');
const plugins = process.env.NODE_ENV === 'production'
? [tailwind, autoprefixer, cssnano]
: [tailwind, autoprefixer];
module.exports = { plugins };
Latest version of Tailwind has PurgeCSS built-in, a library that removes unused CSS. We need to tweak our tailwind.config.js
file a bit to use that.
// tailwind.config.js
module.exports = {
purge: ['./src/**/*.svelte', './public/*.html'],
theme: {
extend: {}
},
variants: {},
plugins: []
};
We now have to create a base Tailwind style sheet and then import it into our app.
/* src/main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Import it at the top of the <script>
tag in App.svelte
import `./main.css`;
Add a few Tailwind classes to the "box" div in our App.svelte
.
<div class="app">
<div class="px-5 bg-green-300 box">
<div>TS Timer {$timer}</div>
<div>Rx Timer {$rxTimer}</div>
</div>
</div>
Before we can use PostCSS we need to tell Snowpack that we want to use it by adding it to the "scripts"
property in snowpack.config.json
.
{
"extends": "@snowpack/app-scripts-svelte",
"scripts": {
"run:tsc": "tsc --noEmit",
"run:tsc::watch": "$1 --watch",
"build:css": "postcss"
},
"devOptions": {},
"installOptions": {}
}
Fire up the app and we should now have Tailwind support. Yay!
The development flow is very fast, code reloads are blazing, but you are running on the local computer. Real applications will need to download all assets through a network. Let's see how Snowpack deals with bundling stuff for production by running npm run build
.
What's in the build
folder now?
$ tree -h build
build
├── [4.0K] _dist_
│ ├── [2.3K] App.js
│ ├── [ 99] index.js
│ ├── [3.5K] main.css
│ ├── [3.7K] main.css.proxy.js
│ └── [ 247] timer.js
├── [1.1K] favicon.ico
├── [ 882] index.html
├── [1.1K] logo.svg
├── [ 67] robots.txt
└── [4.0K] web_modules
├── [4.0K] common
│ └── [ 97K] zip-9224ffb3.js
├── [ 179] import-map.json
├── [4.0K] rxjs
│ └── [154K] operators.js
├── [ 30K] rxjs.js
└── [4.0K] svelte
├── [ 50K] internal.js
└── [3.2K] store.js
5 directories, 15 files
I can see that the CSS file is very small. It means that PurgeCSS is working as expected. We haven't bundle the app so everything will be served as modules. In a large app that might be a problem, many files. But they will be cached on subsequent requests.
Snowpack recommends that we add Parcel as a bundler, so we will follow the recommendation and add it to the mix.
$ npm add -D parcel-bundler && rm -rf build && npm run build
Tailwind is spitting out some warnings, but we can safely ignore them. Let's inspect the build
directory again.
$ tree -h build
build
├── [4.0K] _dist_
│ ├── [2.3K] App.js
│ ├── [ 99] index.js
│ ├── [3.5K] main.css
│ └── [ 247] timer.js
├── [142K] _dist_.337b8775.js
├── [543K] _dist_.337b8775.js.map
├── [3.4K] _dist_.5fe4eb89.css
├── [5.1K] _dist_.5fe4eb89.css.map
├── [1.1K] favicon.fadd3cc5.ico
├── [1.1K] favicon.ico
├── [ 470] index.html
├── [1.1K] logo.svg
├── [ 67] robots.txt
└── [4.0K] web_modules
├── [4.0K] common
│ └── [ 97K] zip-9224ffb3.js
├── [ 179] import-map.json
├── [4.0K] rxjs
│ └── [154K] operators.js
├── [ 30K] rxjs.js
└── [4.0K] svelte
├── [ 50K] internal.js
└── [3.2K] store.js
5 directories, 19 files
The total size of the minified JS bundle in 142 kb. It seems unnecessary large for such small and simple app. I suspect that it's due that Snowpack uses Parcel as bundler. I've written a benchmarking post about most popular bundlers that you can read here.
Another thing that puzzles me is why Snowpack spits out the modules to build directory when we are not using them in production?
You can try to run the production bundle yourself by installing a simple web server like serve.
$ npx add -D serve && npx serve build
Snowpack v2 is currently in beta. It's a moving target with almost daily changes, so expect API and things to break until it reaches stability. Overall, it gives developers a great DX with lots of tips and clever easy to understand stats. However, there are some things that I hope can be improved in the future.
Some people seem skeptical of ES modules. Svelvet was a promising experimental Svelte bundler built on top of Snowpack, but the author decided to stop working on it after running some networking benchmarks. The browsers and network are becoming faster and faster, and with parallel requests, HTTP/2/3 and Brotli compression this should not be such a big issue as it seems now.
Personally, I am very exited about the project and Pika ecosystem in general. Hopefully all the wrinkles will be ironed out soon, and when they are, this might become my new favorite setup for Svelte development.
As usual, you can find the example code in Github https://github.com/codechips/svelte-snowpack-testdrive
]]>I've decided to recreate a classic functional reactive programming tutorial from 2014 with the most recent version of RxJS (currently at 6.5) and tiny reactive web framework called Svelte.js.
This is the tutorial in question that we are going to recreate and then also extend.
The introduction to Reactive Programming you've been missing.
Honestly, the frontend framework doesn't really matter for these examples, but I chose Svelte, because I really like it. The main logic (business domain) lives outside Svelte and I often use Svelte just as a thin view layer.
Actually, I realized that I just lied. Another, and very important factor, I chose Svelte for is for its reactive stores. RxJS is a perfect fit for Svelte because you can use RxJS observables as Svelte stores right out of the box. Well, almost. You can use them as Svelte's readable stores. To use RxJS as writable stores you have to do a pinch of monkey patching.
I wrote up an introduction about Svelte and RxJS a while ago - If Svelte and RxJS had a baby. You should read it before continuing, so you understand how they two fit together.
You should have basic knowledge of RxJS to get something out of this article. If you want to learn RxJS, google it. Internet is littered with thousands of tutorials that will teach you how to build basic countdown timers and use document click events. All the same, literally just rewrites of the examples from the main RxJS documentation site.
Simple. It's a big box of full of Lego pieces with no instructions that will keep you occupied for days, and not in a happy way. But when you finally come out, you come out stronger and smarter. But not necessary better looking. Sorry.
Jokes aside, if you ask me, it's a paradigm shift, a mind rape. If you really want to dive in, make sure to stock up on pain killers, because your head will hurt. Mine did.
Ok ok, on a serious note, RxJS will allow you to solve problems in a declarative way, instead of imperative (go and read this), with the help of functional reactive (read async, pull vs push) programming.
Everything is a stream and almost everything can be solved with streams.
And since most things in Javascript land are async, RxJS is a good fit for most things. But not all, of course.
With the air now cleared, why should you learn RxJS?
Are you ready? Have you decided to take the plunge? Cool. Let's do this!
The author of the original tutorial, André Staltz, does a really great job of explaining how to think in RxJS streams. The code examples are written in an old version of RxJS (the article is from 2014) and it would be very easy and not a real challenge to just port the code to the most recent version of RxJS.
I wanted to do it a bit differently. I wanted to recreate the full functionality using my own interpretation of the problem, and instead using the code examples only as my guide slash cheat sheet in case I got stuck on the way.
We want to build a small widget that displays a list of Github users. We should be able to refresh the whole list and also be able to refresh each individual user suggestion.
Seems doable.
Looks like we have a rough implementation plan. A loose one, but still a plan. Let's get cranking.
We will use Snowpack bundler for this project. Why? Because it's awesome and fast!
$ npx create-snowpack-app svelte-frp-tutorial --template @snowpack/app-template-svelte
$ cd svelte-ftp-tutorial
$ npm add -D rxjs && npm start
When started, Snowpack should automatically open our project's main page in the browser.
Now, create new users.ts
file in the src
directory.
This is the file where we will be working in mostly and yes, it's in Typescript. Snowpack has built-in support for Typescript out of the box.
Let's think about the first part of our implementation plan, fetching users from Github, and how we can solve it with the help of RxJS.
For API calls RxJS has both fetch and ajax support. We will use ajax and start small.
// users.ts
import { ajax } from 'rxjs/ajax';
const usersEndpoint = 'https://api.github.com/users';
const users = ajax.getJSON(usersEndpoint);
export { users };
Replace App.svelte
file with this code.
<!-- App.svelte -->
<script>
// import our RxJS stream from users.ts
import { users } from './users';
</script>
<!-- Remember that I told that we can use streams as Svelte stores? -->
<!-- We just have to prefix a stream with $. Yes, it's that simple -->
<pre>{JSON.stringify($users, null, 2)}</pre>
When you start the app you should see a long JSON array with Github users.
[
{
"login": "mojombo",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/mojombo",
"html_url": "https://github.com/mojombo",
"followers_url": "https://api.github.com/users/mojombo/followers",
"following_url": "https://api.github.com/users/mojombo/following{/other_user}",
"gists_url": "https://api.github.com/users/mojombo/gists{/gist_id}",
"starred_url": "https://api.github.com/users/mojombo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/mojombo/subscriptions",
"organizations_url": "https://api.github.com/users/mojombo/orgs",
"repos_url": "https://api.github.com/users/mojombo/repos",
"events_url": "https://api.github.com/users/mojombo/events{/privacy}",
"received_events_url": "https://api.github.com/users/mojombo/received_events",
"type": "User",
"site_admin": false
},
{
"login": "defunkt",
"id": 2,
"node_id": "MDQ6VXNlcjI=",
"avatar_url": "https://avatars0.githubusercontent.com/u/2?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/defunkt",
"html_url": "https://github.com/defunkt",
"followers_url": "https://api.github.com/users/defunkt/followers",
"following_url": "https://api.github.com/users/defunkt/following{/other_user}",
"gists_url": "https://api.github.com/users/defunkt/gists{/gist_id}",
"starred_url": "https://api.github.com/users/defunkt/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/defunkt/subscriptions",
"organizations_url": "https://api.github.com/users/defunkt/orgs",
"repos_url": "https://api.github.com/users/defunkt/repos",
"events_url": "https://api.github.com/users/defunkt/events{/privacy}",
"received_events_url": "https://api.github.com/users/defunkt/received_events",
"type": "User",
"site_admin": false
},
...
]
Sweet! It works!
The problem is that the users are returned to us sequentially with Github founders being first ones in the list. We need to introduce randomness. It's possible to achieve by appending a since
query parameter to the URL. That will return users starting at time when they created their Github account.
// users.ts
import { ajax } from 'rxjs/ajax';
const usersEndpoint = 'https://api.github.com/users';
const randomOffset = () => Math.floor(Math.random() * 100000);
const users = ajax.getJSON(`${usersEndpoint}?since=${randomOffset()}`);
export { users };
Much better.
If you call Github's API too often they will throttle you. What you can do instead is to create a Github auth token and use that in your API calls. We are jumping a head of time a bit, but this is how you can solve it.
// users.ts
import { map } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
const usersEndpoint = 'https://api.github.com/users';
const randomOffset = () => Math.floor(Math.random() * 100000);
const token = 'your-github-auth-token';
const users = ajax({
url: `${usersEndpoint}?since=${randomOffset()}`,
headers: {
authorization: `token ${token}`
}
}).pipe(map(res => res.response));
export { users };
I suggest you do that because there will be a lot of reloading involved soon.
I've already mentioned it, but building software with RxJS is like building things with Lego bricks. You have all these small pieces that you assemble into larger pieces, that you then assemble into walls, that you then use to build the whole house. Some pieces don't fit together so you try a different one.
And looks like we just built our first Lego part. We now have a pool of Github users to choose from. Let's add the 'refresh' button.
In the original tutorial the author is using fromEvent operator to capture the button click, but we will use Subject instead. While it's possible to do it exactly like in the original tutorial by using Svelte's bind
directive, I think that RxSbjects are actually a better fit here.
Shortly explained, Rx Subjects are both observers and observables, meaning you can both push, via next
method and pull data from them by subscribing. Don't worry if this doesn't make sense right now.
Back to our problem. We will use plain Subject
as a signal to trigger our users
stream and force it to reload.
// users.ts
import { map } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
import { Subject } from 'rxjs';
const usersEndpoint = 'https://api.github.com/users';
const randomOffset = () => Math.floor(Math.random() * 100000);
const token = 'your-github-auth-token';
// Rx Subject that we use only to trigger reloads
const reload = new Subject();
// instead of exposing subject directly we wrap it in a function
const refresh = () => reload.next();
const users = ajax({
url: `${usersEndpoint}?since=${randomOffset()}`,
headers: {
authorization: `token ${token}`
}
}).pipe(map(res => res.response));
export { users, refresh };
Let's add a button to the main file and bind out refresh
function to it.
<!-- App.svelte -->
<script>
import { users, refresh } from './users';
</script>
<button on:click={refresh}>refresh</button>
<pre>{JSON.stringify($users, null, 2)}</pre>
The button does nothing yet. We need to wire it up to re-trigger users
stream when it's clicked.
// users.ts
import { map, mergeMap } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
import { Subject } from 'rxjs';
const usersEndpoint = 'https://api.github.com/users';
const randomOffset = () => Math.floor(Math.random() * 100000);
const token = 'your-github-auth-token';
// Rx Subject that we use only to trigger reloads
const reload = new Subject();
// instead of exposing subject directly we wrap it in a function
const refresh = () => reload.next();
const url = `${usersEndpoint}?since=${randomOffset()}`;
// let's change the users name to response instead
const response = ajax({
url: `${usersEndpoint}?since=${randomOffset()}`,
headers: {
authorization: `token ${token}`
}
}).pipe(map(res => res.response));
// when clicking refresh button we call `reload.next()`
// which triggers the observable chain.
const users = reload.pipe(mergeMap(() => response));
export { users, refresh };
You can see that we used mergeMap operator. In short, this operator will replace the outer observable with the inner observable (response
in our case).
With our changes we have introduced a couple of problems. First, when you load the page, no users are present until you click the 'refresh' button.
This is easily fixed. We can emulate the first click, and thus trigger the chain, by using the startWith operator. What we send in doesn't really matter here as we will not use it.
// users.ts
// add startWith to imports
import { map, mergeMap, startWith } from 'rxjs/operators';
// add `startWith` operator as first in the chain
const users = reload.pipe(
startWith(null),
mergeMap(() => response)
);
The second problem is that when we click the button, the data is re-fetched, but the results are the same. That is because response
stream is not re-triggered, but reused. We need to solve that. The easiest solution that comes to mind is to extract it to a function and create a new stream on every call.
Our whole users.ts
file should look like this now and it should work as expected too.
// users.ts
import { map, mergeMap, startWith } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
import { Subject } from 'rxjs';
const usersEndpoint = 'https://api.github.com/users';
const randomOffset = () => Math.floor(Math.random() * 100000);
const token = 'your-github-auth-token';
// Rx Subject that we use only to trigger reloads
const reload = new Subject();
// instead of exposing subject directly we wrap it in a function
const refresh = () => reload.next();
// replace users with a function instead so that we return
// a new stream on every 'refresh' click
const fetchUsers = () => {
return ajax({
url: `${usersEndpoint}?since=${randomOffset()}`,
headers: {
authorization: `token ${token}`
}
}).pipe(map(res => res.response));
};
// call fetchUsers function when we click the refresh button
const users = reload.pipe(startWith(null), mergeMap(fetchUsers));
export { users, refresh };
Awesome! Second Lego part is now in place and we only had to refactor our logic to achieve this, not touching the view aka App.svelte
. Are you starting to see how we are building our app piece by piece? Nice, isn't it?
We will now continue to the next step, creating a list with three user suggestions and this will require some view changes.
First, we need to think on how to get a random user from the users
stream (the list of users we got back from Github API). We can achieve that with the following code that we can add at the end.
// users.ts
// helper function to get a random user from
// the user list we got from Github
const randomUser = (users: any[]) => users[Math.floor(Math.random() * users.length)];
// random user suggestion stream
const suggestionOne = users.pipe(map(users => randomUser(users)));
export { users, refresh, suggestionOne };
So what does this code do? I will explain.
map
operatorSo far so good. We could just make three suggestions and then export/import them. But, let's hold on to that thought for now and try to implement the 'suggest' button instead. When clicked, it will give us a new user suggestion.
What's needed here? Well, we need to have a button to click. Obviously.
Maybe it's a good idea to replicate the functionality of the 'refresh' button with the Subject?
Maybe. Let's try.
const refreshOne = new Subject();
Now what? Something needs to happen when it's clicked, right? It should kick off (trigger) a chain of something that will get us a new user.
How about this? How about I write the code and then explain it? Deal? OK.
const refreshOne = new Subject();
const suggestionOne = refreshOne.pipe(
startWith(null),
combineLatest(users, (click, users) => randomUser(users))
);
export { users, refresh, suggestionOne, refreshOne };
Done.
What is this "combineLatest" thing, you say? I say, it's pretty awesome.
combineLatest is an RxJS operator that takes provided streams and emits them when any of the provided streams emit (or gets triggered). If you provide a function at the end, so called project function, it will pass all the streams into it. In there you can do what's needed (get a random user in our case) and pass that down the line.
The important thing with combineLatest
is that it will not emit anything until all provided streams has emitted at least once. Something to keep in mind for the future, when you are pushing buttons like mad scientist but nothing is happening.
The rest, startWith
operator, should be familiar to us by now.
Let me explain exactly what happens in our case:
pipe
operator to build up a chain of other operators that will transform the data as it passes through that chain.startWith
operatorcombineLatest
operator.combineLatest
takes the null
click stream and users
stream and executes our provided project function passing our current stream and dependant streams in order they are specifiedusers
stream and execute our randomUser
function with the list of users, the value of the users
stream.It's important to understand what's happening. I suggest you play around a bit, before continuing, by commenting stuff out and using good ol' console.log
to print out values.
tap
operatorWhen you need to debug (log) something in the stream pipeline tap operator is really handy. Just import it and plug tap(console.log)
inside the pipe
operator.
OK. Business logic is done. Let's import the suggestion in our App.svelte
file and try it out.
<!-- App.svelte -->
<script>
import { users, refresh, suggestionOne, refreshOne } from './users';
</script>
<button on:click={refresh}>refresh</button>
<!-- notice that I didn't wrap our subject in an exported function -->
<!-- I did it only for demo purposes here. You should always wrap -->
<button on:click={() => refreshOne.next()}>refresh one</button>
<pre>{JSON.stringify($suggestionOne, null, 2)}</pre>
<pre>{JSON.stringify($users, null, 2)}</pre>
Click the 'refresh one' button and see that we get a new user suggestion.
Click the 'refresh' button and notice that our suggestionOne
also reloads. It's because in RxJS every stream can be connected to each other and when triggering the top stream it can trigger other streams that depends on it. Kind of hard to understand in the beginning and very easy to get lost in, but totally awesome when you finally get it.
We are almost done. Let's add two more suggestions and add some layout to our view file.
We could copy and paste code for the rest of our suggestions, but we will create a helper function instead.
Like this.
const refreshOne = new Subject();
const refreshTwo = new Subject();
const refreshThree = new Subject();
// make sure to import `Observable` from 'rxjs'
const createSuggestion = (refresh: Observable<any>) =>
refresh.pipe(
startWith(null),
// name variable as _ to show that we don't care about it
combineLatest(users, (_, users) => randomUser(users))
);
const suggestionOne = createSuggestion(refreshOne);
const suggestionTwo = createSuggestion(refreshTwo);
const suggestionThree = createSuggestion(refreshThree);
export {
refresh, refreshOne, refreshTwo, refreshThree,
suggestionOne, suggestionTwo, suggestionThree
};
Next, let's adjust the App.svelte
a bit. Add some layout and styling.
<!-- App.svelte -->
<style>
:global(*, *:before, *:after) {
box-sizing: border-box;
}
:global(body) {
font-family: sans-serif;
background-color: #f4fffc;
}
.container {
display: flex;
min-height: 98vh;
justify-content: center;
align-items: center;
}
ul {
list-style: none;
max-width: 30em;
padding: 0;
}
.user {
display: flex;
align-items: center;
padding: 12px 18px 12px 12px;
background-color: #d0ece7;
}
li {
margin-top: 0.5em;
min-width: 30em;
}
.user a {
font-size: 1.65em;
text-decoration: none;
color: #2e2e2e;
font-weight: 600;
}
.user a:hover {
text-decoration: underline;
}
.user img {
width: 48px;
height: 48px;
margin-right: 12px;
border-radius: 9999px;
}
.user button {
margin-left: auto;
border: none;
padding: 6px 12px;
background-color: #666;
font-weight: 600;
color: #fefefe;
}
.refresh {
border: none;
border-radius: 4px;
padding: 5px 10px;
background-color: #eee;
font-size: 1.2em;
}
</style>
<script>
import {
refresh,
refreshOne,
refreshTwo,
refreshThree,
suggestionOne,
suggestionTwo,
suggestionThree
} from './users';
</script>
<div class="container">
<div>
<button on:click={refresh}>refresh</button>
<ul>
{#if $suggestionOne}
<li>
<div class="user">
<img src={$suggestionOne.avatar_url} alt={$suggestionOne.login} />
<a href={$suggestionOne.html_url}>{$suggestionOne.login}</a>
<button on:click={() => refreshOne.next()}>x</button>
</div>
</li>
{/if}
{#if $suggestionTwo}
<li>
<div class="user">
<img src={$suggestionTwo.avatar_url} alt={$suggestionTwo.login} />
<a href={$suggestionTwo.html_url}>{$suggestionTwo.login}</a>
<button on:click={() => refreshTwo.next()}>x</button>
</div>
</li>
{/if}
{#if $suggestionThree}
<li>
<div class="user">
<img src={$suggestionThree.avatar_url} alt={$suggestionThree.login} />
<a href={$suggestionThree.html_url}>{$suggestionThree.login}</a>
<button on:click={() => refreshThree.next()}>x</button>
</div>
</li>
{/if}
</ul>
</div>
</div>
We can say that we are done, but I say, we are not quite done.
If you open the network tab in the dev tools console and play around, you will notice that we do way too many requests than necessary. How come, you may ask?
This has to do with hot and cold RxJS observables. A topic that would require it's own article. But, briefly, and very simplified, cold (unicast) observables are started (created) each time they get a new subscriber, while hot (multicast) observables "keep" their existing state when getting new subscribers eg they don't start from the beginning again.
Thankfully, it easily fixed with the help of the share operator. Import it and add it to the end our users
stream.
// call fetchUsers function when we click the refresh button
const users = reload.pipe(startWith(null), mergeMap(fetchUsers), share());
Watch the network tab now.
You can also add a few more operators if you want to enhance the UI interaction, but we will stop here. I recommend that you go to the original article instead and look for more inspiration there if you are interested.
With the final solution now done, there was one thing left, that was still bothering me. A think that I couldn't stop thinking about. The way suggestions are created. The DRY principle. The current solution is ugly. Code is repetitive.
André, the author, also effectively lures you in by saying:
This is not DRY, but it will keep our example simple for this tutorial, plus I think it's a good exercise to think how to avoid repetition in this case.
Of course I had to give it a try. I mean, how hard can it be?
And this is where I should have stopped. Should have gone on with my life. Done something fun instead. But the stubborn donkey in me took over. "I mean, how hard can it be," I thought.
Well. If you look at all the hundreds of comments in the original article, everyone is saying how awesome the article is, but NOT A SINGLE ONE offered a solution to the problem.
Why is that, you may ask? Because it's hard.
In RxJs land you can't just take the next step, because that step is too steep.
Before you are able to do that you have to stare at the screen for hours, trying to understand the code and the flow, and why that small change you introduced borked everything you just got working.
You have to read all the articles on the Internet and desperately google for information until your fingers hurt.
You have to curse yourself with very strong words for being stupid and cry yourself to sleep.
But suddenly, it just clicks and it's like a messiah coming down from the clear blue sky while the angel choir sings. You become almost euphoric and people might ask you what pill you just popped.
Don't worry, that high won't last long. It will actually wear off pretty quickly when you realize that there is another step you have to climb and it's just as steep as the previous one.
That's exactly the process I went through. Every. Single. Thing. What soothed the pain a bit was that I worked in bursts. You know, HDD, Hammock Driven Development.
I learned it years ago and it really works!
Onward.
My only goal was to get rid of static suggestion list and make it dynamic, so we can easily change how many users we would like to display.
The task seemed easy - instead of returning separate suggestion, we want to return a list of user suggestions that we can iterate over. When clicking 'x' (suggest) button, we should replace that user in our list.
We can reuse many of our existing Lego parts we have already built. To start off we can delete all individual suggestions and individual refresh streams. Both from code and exports.
Next, we can rename our users
stream to userPool
, because it will be a pool of users that we want to choose three random ones from.
// call fetchUsers function when we click the refresh button
const userPool = reload.pipe(startWith(null), mergeMap(fetchUsers), share());
Now we can select a well, semi-random, first three top users from our userPool
stream. This will be our starting point.
const users = userPool.pipe(mergeAll(), take(3), toArray());
I will briefly explain what's going on here.
userPool
stream eg all users returned from Github APIWe could have done it slightly differently, for example flattening the array where we fetch the result from Github, but this will do for now.
I encourage you to try and do this as an exercise! If you succeed, then you know that you are starting to think in streams!
If you like you can try our new solution by changing App.svelte
to this.
<!-- App.svelte -->
<script>
import { refresh, users } from './users';
</script>
<div class="container">
<div>
<button class="refresh" on:click={refresh}>refresh</button>
{#if $users}
<ul>
{#each $users as user}
<li>
<div class="user">
<img src={user.avatar_url} alt={user.login} />
<a href={user.html_url}>{user.login}</a>
<button on:click={() => console.log(user.login)}>x</button>
</div>
</li>
{/each}
</ul>
{/if}
</div>
</div>
The 'refresh' button will not work, but this is something we will fix soon.
Like this. Create a new stream called suggestions
.
const suggestions = reload.pipe(
// emulate the first click to trigger the chain
startWith(null),
// replace `null` with `users` stream
mergeMap(() => users),
// log to console. useful for debugging
tap(console.log),
// start with an empty array to keep Svelte store happy
startWith([])
);
The flow explained:
suggestions
stream from reload
button streammergeMap
I think that suggestions
is actually a better word in this context than users
.
Import suggestions
in our App.svelte
and replace all references to $users
with $suggestions
.
Everything should work just as it did before except we can now refresh our suggestions list. Yay! Winning!
In our App.svelte
we have a null
check were we don't show the list if it's null
. The thing is that reloads are happening so fast that we don't even notice. To make it visible we can replace mergeMap
operator line with this one. I will leave it up to you to figure out what it does.
mergeMap(() => users.pipe(delay(1000), startWith(null)))
This is the thing with RxJS. By entering right things at the right places you can get lots of things done quickly. I didn't say it was easy though.
Alright, we finally have come to the part that was the most painful for me. Generating new suggestions for the individual user.
Because no matter how I tried I kept coming to the same fact, that I needed to keep state somewhere somehow, but I really didn't want to use external state variables. Hey, that would be cheating!
After lots of experimenting and googling I finally got it to work. Turns out we don't need any state at all.
First, let's think what we need to do. We already have a suggestions stream so we need to:
Doesn't sound to hard, right?
For triggering individual reloads we will again use RxJS subjects, but this time we will use BehaivorSubject instead of the plain one.
BehaviorSubject is special. It keeps track of the latest state and upon every new subscriber it returns the latest state to them when they subscribe. We can also initialize it with as starting state and that's exactly what we need.
Why do we need that? Glad you asked! It's because we will need to use combineLatest
operator again and this operator will not emit anything until all of the streams it depends on emit. Remember?
If we decide to use the plain Subject
we end up in a limbo state. We would need to click the 'x' button it so it emits, but we cannot access it because it's not visible since the combineLatest
operator is waiting for our subject to first emit something.
We will start simple, and refactor the code as we go.
// users.ts
// the suggest 'x' button stream
// we initialize it with an empty string as starting state
const suggest = new BehaviorSubject('');
// wrap the suggest stream in the helper function that we export
const replace = (username: string) => suggest.next(username);
// our main suggestions stream
const suggestions = reload.pipe(
// emulate the first click of reload button to trigger the chain
startWith(null),
// replace `null` with `users` stream
mergeMap(() => users),
// log to console. useful for debugging
tap(console.log),
// this is where we handle new user suggestions
combineLatest(userPool, suggest, (users, pool, suggest) => {
console.log(users, pool, suggest);
return users;
}),
// start with an empty array to keep Svelte store happy
startWith([])
);
export { users, replace, refresh, suggestions };
Import replace
in the App.svelte
and wire it up as an action in the 'x' button.
<script>
import { refresh, suggestions, replace } from './users';
</script>
<div class="container">
<div>
<button class="refresh" on:click={refresh}>refresh</button>
{#if $suggestions}
<ul>
{#each $suggestions as user}
<li>
<div class="user">
<img src={user.avatar_url} alt={user.login} />
<a href={user.html_url}>{user.login}</a>
<button on:click={() => replace(user.login)}>x</button>
</div>
</li>
{/each}
</ul>
{/if}
</div>
</div>
If you click the 'x' button now and open dev tools you will see the console.log
output in our "project" function we supplied last in our combineLatest
statement. This is where we will implement the actual suggestion replacement.
I recommend you to click the button a few times, inspect its output and think on how we can replace the individual user in our user list before continuing.
Did you do it? No? OK then, I will show you how I solved it.
First, let's extract our "project" function so we can concentrate on it.
// our project function which contains the suggestion logic
const replaceUser = (users: any[], pool: any[], login: string) => {
console.log(users, pool, login);
return users;
};
// replace `combineLatest` in `suggestions` stream to this
// combineLatest(userPool, suggest, replaceUser)
Cool. In that function we get the users (our suggestions) list, a pool of all users and the login (username) of the user that needs to be replaced in the list.
Here is how we can solve it.
And here is the actual code. I hope I got it right!
const replaceUser = (users: any[], pool: any[], login: string) => {
const getIndex = (username: string) =>
users.findIndex(user => user.login === username);
const idx = getIndex(login);
while (true || idx !== -1) {
let newUser = randomUser(pool);
if (getIndex(newUser.login) === -1) {
users.splice(idx, 1, newUser);
break;
}
}
return users;
};
Add this code, save your files and you should see everything work.
Try changing the number of displayed suggestions to five and see that it still works.
Our final goal is now complete. We have a truly dynamic suggestions list that it also totally DRY!
Remember that almost everything in Javascript is passed by reference and therefore we are actually mutating the array returned from the users
stream.
You can validate this if you want by importing users
stream in App.svelte
and doing cheap man's debugging aka pre + JSON.stringify
.
We can say that our suggestions stream sits and listens at two points in code:
combineLatest
operator changes only it gets executedDid you notice how we used Svelte just as a thin view layer? Our script statement in App.svelte
contains only imports. All of the logic is encapsulated in separate files.
One nice side effect of doing it this way is that it makes our code very TDD-friendly.
With that said, here are the main takeaways:
Mind bending stuff. Learning RxJS is super hard, but also super rewarding. Declarative programming makes your life easier, but it also requires more of you.
Less writing, more thinking. I don't like writing imperative code, because I often already know what I need to write, and it's so damn boring to type. Writing FRP code actually involves less writing and more thinking. Thinking about how to solve the problem with streams.
RxJS is not for beginners. You need some functional programming experience and a solid knowledge of Javascript. Add Typescript to the mix and you will start crying. Personally, I think that RxJS is even harder to learn than Vim!
One-to-many. Every problem can be solved in many different ways in RxJS. There is no right or wrong. Only more and less efficient, feels like.
RxJS not a hammer. RxJS is super cool, but don't use it for everything just because you can. Most of the small problems can be solved in much more simple (boring) way instead.
Svelte + RxJS = WINNING. In my opinion, Svelte is a perfect fit for RxJS. Both are truly reactive and Svelte has almost native RxJS support with its auto subscriptions. It's a beautiful marriage.
Better developer. Even if you are not planning on using RxJS it's still a very good exercise to learn it. It will level up your game. Guaranteed.
This was a very long article. Thanks for sticking all the way to the end with me!
As always, I hope that you learned something, even if you head might hurt slightly right now.
Here is the link to code https://github.com/codechips/svelte-rxjs-intro-frp
If you know of a different way of solving this please make a PR so I can learn. It would make me really really happy!
]]>In this article you will learn how to:
You will also learn about how Svelte compiler wires up everything together.
If you want to follow along create a basic Svelte app using Snowpack CSA.
Most of the bind examples have to do with Svelte forms. Why don't we start there with simple text input bindings that binds to a local variable?
<!-- App.svelte -->
<script>
let username = 'jane';
</script>
<div>
<h2>Simple two-way bind</h2>
<form>
<div>
<input type="text" name="username" bind:value={username} />
</div>
<div>
<input type="text" name="repeat" bind:value={username} />
</div>
<p>
username: <code>{username}</code>
</p>
<button on:click|preventDefault={() => (username = '')}>reset</button>
</form>
</div>
In the code about we bind one variable to two text input fields. Nothing spectacular. It's very straight-forward and just works.
If you change one field, the value in another changes too.
If you need to bind a textarea
element to a variable, it works exactly the same was as with text inputs.
When working with forms it's pretty tedious to bind different input fields to standalone variables. Instead, we should bind to objects.
<!-- App.svelte -->
<style>
:global(body) {
font-family: Arial, Helvetica, sans-serif;
background-color: #f3f3f3;
}
:global(*, *:before, *:after) {
box-sizing: border-box;
}
:global(input[type='text'], input[type='select'],
input[type='password'], input[type='number'],
select) {
padding: 5px;
min-width: 30em;
}
.fields {
margin: 2em 0;
}
.fields label {
display: block;
margin-top: 1em;
}
</style>
<script>
let values = {};
const reset = () => Object.keys(values).forEach(key => (values[key] = ''));
const submit = () => alert(JSON.stringify(values, null, 2));
</script>
<div>
<h2>Complex Form</h2>
<form on:submit|preventDefault={submit}>
<div class="fields">
<label for="username">Username</label>
<input type="text" name="username" bind:value={values.username} />
<label for="password">Password</label>
<input type="password" name="password" bind:value={values.password} />
<label for="company">Company name</label>
<input type="text" name="company" bind:value={values.company} />
<label for="slug">Slug</label>
<input type="text" name="slug" bind:value={values.slug} />
<label for="startYear">Start year</label>
<input type="number" name="startYear" bind:value={values.startYear} />
</div>
<div style="margin-top:3em">
<button on:click|preventDefault={reset}>reset</button>
<button type="submit">submit</button>
</div>
</form>
</div>
<pre>{JSON.stringify(values, null, 2)}</pre>
This makes our code a bit cleaner. One important thing to know is that Svelte is smart enough to coerce the type for us. It the example above it will parse the year as type number
.
We can easily manipulate text dependant text fields with Svelte's reactive statements. Let's calculate the slug from our company name.
First, install Slugify by running npm add -D slugify
.
Next, we add it to our code.
<script>
import slugify from 'slugify';
...
$: values.slug = slugify(values.company, { lower: true });
</script>
Every time the value of values.company
changes we will dynamically calculate the slug field. Try it using a company name with multiple words, like "Ali Baba Inc."
This is a prime example of Svelte's reactivity. Accomplishing a lot with very little code!
Unfortunately, this is a one-way binding. Nothing stops you from changing the slug to regular text later on. It's doable, but involves a little more code. I will save it for a future article.
Checkbox bindings are straight forward. Add this code to our form.
<label>
<input type="checkbox" bind:checked={values.agree} />
I agree to sell my personal information to evil corporations
</label>
If you set the initial value to true
in our values.agree
property, the checkbox will be checked when the form is rendered.
This is where things get interesting. If you a building forms they will most likely involve dropdown lists.
In Svelte you can bind to pure values or to objects. The simplest way is to bind to a list of static values.
<div>
<h3>Simple select bind with static values</h3>
<select bind:value={values.conferencing}>
<option value="" />
<option value="zoom">Zoom.us</option>
<option value="hangouts">Google Hangouts</option>
<option value="skype">Microsoft Skype</option>
</select>
</div>
If you set the initial value in values.conferencing
to something that exists in the list, it will be selected when the form is first rendered.
You can also make our select list dynamic, if you have a list of objects that you need to bind.
<script>
let conferencing = [
{ id: '1', label: 'Zoom' },
{ id: '2', label: 'Hangouts' },
{ id: '3', label: 'Skype' }
];
</script>
<div>
<h3>Dynamic select bind with objects</h3>
<select bind:value={values.dynamic}>
{#each conferencing as conference}
<option value={conference}>{conference.label}</option>
{/each}
</select>
</div>
You can also bind the list directly as objects. That way you will get the whole object as a value in the values.dynamic
property.
<script>
let conferencing = [
{ id: '1', label: 'Zoom' },
{ id: '2', label: 'Hangouts' },
{ id: '3', label: 'Skype' }
];
</script>
<div>
<h3>Dynamic select bind with objects</h3>
<select bind:value={values.dynamic}>
{#each conferencing as conference}
<option value={conference}>{conference.label}</option>
{/each}
</select>
</div>
Same thing here. You can preselect the dropdown selection by setting the initial value. For example values.dynamic = conferencing[2]
. Pretty neat!
Multi selection works just like you think it does.
<script>
let conferencing = [
{ id: '1', label: 'Zoom' },
{ id: '2', label: 'Hangouts' },
{ id: '3', label: 'Skype' }
];
</script>
<div>
<h3>Dynamic multiple select bind with objects</h3>
<select multiple bind:value={values.tools}>
{#each conferencing as conference}
<option value={conference}>{conference.label}</option>
{/each}
</select>
</div>
This is the thing with Svelte. When you write the code the way you think it should work, the chances are big that it does work like that.
For radio buttons and checkboxes you should bind to a group by using bind:group
directive.
<script>
const frameworks = [
{ id: 1, name: 'React.js' },
{ id: 2, name: 'Svelte.js' },
{ id: 3, name: 'Vue.js' },
{ id: 4, name: 'Marko.js' }
];
</script>
<div>
<h3>Static radio button bind</h3>
<label>
<input type="radio" bind:group={values.framework} value="React" />
React
</label>
<label>
<input type="radio" bind:group={values.framework} value="Svelte" />
Svelte
</label>
<label>
<input type="radio" bind:group={values.framework} value="Vue" />
Vue
</label>
</div>
<div>
<h3>Dynamic radio button bind</h3>
{#each frameworks as framework}
<label>
<input type="radio" bind:group={values.reactiveFrameworks} value={framework} />
{framework.name}
</label>
{/each}
</div>
Just as with select
you can preselect the initial value by assigning a value from the list to a property we bind to.
You can bind DOM elements to local variables and manipulate them directly if you like.
In my previous article, Recreating a classic FRP tutorial with Svelte and RxJS, we chose not to use RxJS's fromEvent
observable.
This is how we could have done it instead.
<script>
import { onMount } from 'svelte';
import { fromEvent } from 'rxjs';
let clickStream = null;
onMount(() => {
clickStream = fromEvent(button, 'click');
});
</script>
<button bind:this={button}>click me</button>
Note that you have to wire up your logic in the onMount
hook, otherwise you will get a null reference exception.
Here is another example of what you can do with DOM elements.
<style>
.box {
width: 100px;
height: 100px;
background-color: #ff4;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<script>
import { onMount } from 'svelte';
import { fromEvent } from 'rxjs';
let box = null;
onMount(() => {
var colors = ['#F99', '#9F9', '#99F', '#FF9', '#F9F'];
setInterval(() => {
let random = Math.floor(Math.random() * colors.length);
let color = colors[random];
box.style.background = color;
box.innerHTML = color;
}, 1000);
});
</script>
<div class="box" bind:this={box}>color</div>
If you are working with animations and SVG manipulation this binding is a must.
There are also a few more useful bind
directives available in Svelte, like block-level element bindings and media level bindings, but they are not as frequently used.
Last thing I want to talk about is how to bind Svelte components to local variables. Or more precisely, how to bind child component properties to local variables.
If you have read some of my earlier articles such as 3 different ways of cross-component communication in Svelte or Accessible switch toggle button with svelte.js and tailwind.css, you have seen this type of bind
in action.
As the saying goes, "Repetition is the mother of learning," so let's repeat this with another example.
Start by creating a component named Hotdog.svelte
in the src
directory, because you know, who doesn't like hot dogs?
<!-- Hotdog.svelte -->
<style>
.addons {
margin-top: 1em;
}
select,
input {
min-width: 0;
}
</style>
<script>
export let order = {};
export let amount = 0;
</script>
<h4>Order your hot dog!</h4>
<label for="kind">Choose kind</label>
<select name="kind" class="kind" bind:value={order.kind}>
<option value="" />
<option value="bratwurst">Bratwürst</option>
<option value="kielbasa">Kielbasa</option>
<option value="frakfurter">Frakfurter</option>
<option value="merguez">Merguez</option>
<option value="chorizo">Chorizo</option>
</select>
<label for="amount">How many?</label>
<input name="amount" type="number" bind:value={amount} />
<div class="addons">
<label>
<input type="checkbox" bind:checked={order.sauerkraut} />
Sauerkraut
</label>
<label>
<input type="checkbox" bind:checked={order.mustard} />
Mustard
</label>
<label>
<input type="checkbox" bind:checked={order.ketchup} />
Ketchup
</label>
</div>
Notice that we are exposing two variables in our component - order
and numberOfOrders
.
Import the newly created component in the App.svelte
and wire it up.
<script>
import Hotdog from './Hotdog.svelte';
...
</script>
<div>
<h3>Component bind</h3>
<Hotdog bind:order={values.hotdog} bind:amount={values.numberOfHotdogs} />
</div>
Since we have exposed our variables we can bind to them in out App.svelte
.
Good question. Let's look at the generated source code and try to understand how this is all wired up.
We can take a look at the internals of Hotdog.svelte
component, because it's rather small and the principles are the same across all components.
There is no reason to go over the whole source file so we will concentrate only on the part which are related to the bind
directive.
// `kind` select event listener
function select_change_handler() {
order.kind = select_value(this);
$$invalidate(0, order);
}
// `amount` number input event listener
function input0_input_handler() {
amount = to_number(this.value);
$$invalidate(1, amount);
}
// the rest of the event handlers should be clear
function input1_change_handler() {
order.sauerkraut = this.checked;
$$invalidate(0, order);
}
function input2_change_handler() {
order.mustard = this.checked;
$$invalidate(0, order);
}
function input3_change_handler() {
order.ketchup = this.checked;
$$invalidate(0, order);
}
What Svelte compiler does behind the scenes is that it attaches correct event handlers to our inputs. That's all there is to it.
If you look at the amount
event handler you will also notice that it knows it has type of number and Svelte compiler converts it for us. How kind of it!
That's all there is to it! Svelte bindings works just like you expect them too, or want them to. With very little code we can build pretty sophisticated forms in Svelte.
Form handling is hard in any framework and I will explore this topic more in-depth in the near future. Stay tuned and subscribe on the main page when I publish a new article!
As usual, here is the code https://github.com/codechips/svelte-bind-directive-examples
]]>This type of text is actually much easier for me to write than a tech article, because I don't need to do any research. I know this stuff by heart. I see it almost daily. And what I see makes me frustrated.
I've been on both sides of the table. I have interviewed lots of developers. I know what works and what doesn't and I am here to teach you.
It's not as hard to get the interview as you think.
Replace all I's with You's in your application letter. Seriously. Try to write an email without a single I in it.
Companies don't care what you know or where you worked. They have problems to solve, and the only thing that matters is how you can help them.
Once I received an application where I counted 13 I's and not a single You. The person had the tech skills, but she didn't put them in context. I wrote back short feedback email where I explained the problem, because I am nice, but most often this kind of email goes straight to trash in other companies.
Also, please never send mass emails. They are so see-through. Never write "your company" in an email. Always use a company name and also include the name of a person you are writing to. People love hearing their own names.
Do your research. Learn the company and it's products and tell them why you think their company is cool, how you can help and why you would love to join the journey.
People are busy. They have problems to solve. How can you help them?
A very low hanging fruit here. People often want to see the face behind the words.
No mugshots. No fake smiles. No professional shots.
Just a simple photo of you where you smile and look friendly. You don't even have to look into the camera.
Find a pic of you were you look happy and where you look like you are enjoying life. It can even be from a party, but you shouldn't look drunk of course.
Always include a photo of you. Period.
Many developers are worried that they are not skilled enough to apply for a specific job. I used to be one of them.
If you are still green it's a valid concern, but here is the thing.
The technology wheel is spinning so fast today that your skills quickly become outdated.
The only thing that matters is how fast you can learn new stuff. You have to prove that you are a fast learner. That you can learn news things quickly.
When a company hires you, they hire you mostly for your talent and your ability to learn. Company invests in you and people don't expect you to be productive from day one.
At first the company loses money on you. They pay more than they get. After some time they expect you to become productive. They expect to get a return on their investment plus some more, so to say.
When I say more, I mean they expect you to learn new skills and also their tech stack. It's a win/win situation.
How to prove people that you are a fast learner is a topic on it's own, but if I could give one tip it would be this.
Start your own blog and include a link to it on your resume.
This is often a neglected part of the resume and unfortunately the most underappreciated too.
What makes you unique? What makes you stand out from the rest?
Here are a few of my hobbies for example.
Now, did that catch your interest? Wouldn't you like to know more?
This is the thing. You want to get people interested in you, get them curious. You want to make them say "I want to meet this person!"
And here is the best part. It doesn't even have to be work related.
What makes you you?
This might be the most important tip of all. You have to bypass the job boards, application emails, recruiters and try to find your way into the company. Find the right people to email or call.
If you send your application the "recommended" way it will end up with the rest of the applications. You will be just one of many.
Even if you got the skills, people who will have to go through all applications will probably not give much attention to yours. Especially if the job offering is a popular one where you compete with hundreds of people.
You get the point, right?
You want to stand out. You need to find your way in. Do your research, find the people that you think are good contact points. Ask around with people you know. There is always someone that knows someone.
If the company is using external recruiters you will also save company money by bypassing the process. Always go straight to the source.
For example, when someone emails me directly, I often feel obliged to reply or to forward the email to the right people.
Find your way in.
Of course there is a lot more to it than the tips above. Things like email copy, phone calls, resume format and pitch, your online presence, etc.
Remember, people often read between the lines.
But, if you do any of the things above, I guarantee that you will increase your chances of getting the interview.
In the end, it's all about psychology, timing, right message, right tone and a little luck.
Maybe I will write a book on the subject one day.
]]>Svelte is no exception. There are a few form handling frameworks in the market, but most of them look abandoned. However, there is one specific library that comes to mind that is being actively maintained - svelte-forms-lib. It's pretty good and I've used it myself. Check it out!
I work a lot with forms and nowadays I don't use any library. Instead, I've developed a set of abstractions on top of Svelte that work well for me and my needs.
Today I am going to teach you how to do a simple form validation using the awesome Yup library, because it's a pure Joi to use. Pun intended.
We will build a simple registration form where we will validate user's name and email, if passwords match and also check if the username is available.
Onward.
Yup is a library that validates your objects using a validation schema that you provide. You validate the shapes of your objects and their values. Let me illustrate with an example.
If you want to follow along here is how you can quickly create a new Svelte app.
# scaffold a new Svelte app first
$ npx create-snowpack-app svelte-yup-form-validation --template @snowpack/app-template-svelte
# add yup as a dependency
$ npm add -D yup
We will be validating fields in the registration form which consists of the following fields:
To start off gently we will only validate that field values are not empty. We will also validate that email address has correct format.
Create an new file in src
directory called schema.js
.
// schema.js
import * as yup from 'yup';
const regSchema = yup.object().shape({
name: yup.string().required(),
email: yup.string().required().email(),
username: yup.string().required(),
password: yup.string().required(),
passwordConfirm: yup.string().required()
});
export { regSchema };
As you can see we defined a schema to validate an object's shape. The properties of the object match the names of the fields and it's not hard to read the validation schema thanks to Yup's expressive DSL. It should pretty much be self-explanatory.
There are a lot of different validators available in Yup that you can mix and match to create very advanced and extremely expressive validation rules.
Yup itself is heavily inspired by Joi and if you ever used Hapi.js you probably used Joi too.
Let's do the actual validation of an object by using our schema. Replace App.svelte
with the following code.
<script>
import { regSchema } from './schema';
let values = {
name: 'Ilia',
email: 'ilia@example', // wrong email format
username: 'ilia',
password: 'qwerty'
};
const result = regSchema.validate(values);
</script>
<div>
{#await result}
{:then value}
<h2>Validation Result</h2>
<pre>{JSON.stringify(value, null, 2)}</pre>
{:catch value}
<h2>Validation Error</h2>
<pre>{JSON.stringify(value, null, 2)}</pre>
{/await}
</div>
The validate
method returns a promise and we can use Svelte's await to render it on the page.
When you start the app you will the following validation error exception.
{
"name": "ValidationError",
"value": {
"name": "Ilia",
"email": "ilia@example",
"username": "ilia",
"password": "qwerty"
},
"path": "passwordConfirm",
"type": "required",
"errors": [
"passwordConfirm is a required field"
],
"inner": [],
"message": "passwordConfirm is a required field",
"params": {
"path": "passwordConfirm"
}
}
Although we provided a wrong email address our schema doesn't catch that and only tells us that we didn't provide the required passwordConfirm
property.
How come? It's because Yup has a default setting abortEarly
set to true
, which means it will abort on the first error and required
validator comes before the email format validation.
Try providing the passwordConfirm
property and you will see that now Yup will give back "email must be a valid email" error.
If we want to validate the whole object we can pass a config to the validate
call.
const result = regSchema.validate(values, { abortEarly: false });
I recommend that you play around by passing in different values to get a feel for what errors are returns before continuing.
Next, we need to build a simple registration form. Replace App.svelte
with the following code.
<!-- App.svelte -->
<style>
form * + * {
margin-top: 1em;
}
</style>
<script>
import { regSchema } from './schema';
</script>
<div>
<h1>Please register</h1>
<form>
<div>
<input type="text" name="name" placeholder="Your name" />
</div>
<div>
<input type="text" name="email" placeholder="Your email" />
</div>
<div>
<input type="text" name="username" placeholder="Choose username" />
</div>
<div>
<input type="password" name="password" placeholder="Password" />
</div>
<div>
<input type="password" name="passwordConfirm" placeholder="Confirm password" />
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</div>
I omitted the labels and styling because they don't provide any value in this context right now.
Now we need to bind the form fields to an object that we will later validate.
If you want to know more about how Svelte bind
works, check out my article - Svelte bind directive explained in-depth.
<!-- App.svelte -->
<style>
form * + * {
margin-top: 1em;
}
</style>
<script>
import { regSchema } from './schema';
let values = {};
const submitHandler = () => {
alert(JSON.stringify(values, null, 2));
};
</script>
<div>
<h1>Please register</h1>
<form on:submit|preventDefault={submitHandler}>
<div>
<input
type="text"
name="name"
bind:value={values.name}
placeholder="Your name"
/>
</div>
<div>
<input
type="text"
name="email"
bind:value={values.email}
placeholder="Your email"
/>
</div>
<div>
<input
type="text"
name="username"
bind:value={values.username}
placeholder="Choose username"
/>
</div>
<div>
<input
type="password"
name="password"
bind:value={values.password}
placeholder="Password"
/>
</div>
<div>
<input
type="password"
name="passwordConfirm"
bind:value={values.passwordConfirm}
placeholder="Confirm password"
/>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</div>
Nothing fancy yet. We can fill out the form and submit it. Next, we will add validation and then gradually improve it.
Now we will try to add our Yup validation schema in the mix. The one we created in the beginning. We can do that in our submitHandler
so that when the user clicks the form we will first validate the values before submitting the form.
The only thing we need to do is to change our submitHandler
to this.
const submitHandler = () => {
regSchema
.validate(values, { abortEarly: false })
.then(() => {
alert(JSON.stringify(values, null, 2));
})
.catch(console.log);
};
If the form is valid you will get an alert popup with the form values, otherwise we just log the errors to the console.
Wouldn't it be nice if we could show the errors to the user? Yes, it would!
To achieve that we first need to extract our errors to an object that we can use to display the errors.
For that we will create a helper function.
const extractErrors = ({ inner }) => {
return inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message };
}, {});
};
It might look like a pretty advanced function, but what it basically does is to loop over the Yup's validation error.inner
array and return a new object consisting of fields and their error messages.
We can now add it to our validation chain. Like this.
const submitHandler = () => {
regSchema
.validate(values, { abortEarly: false })
.then(() => {
alert(JSON.stringify(values, null, 2));
})
.catch(err => console.log(extractErrors(err)));
};
If you look at the console output now you will see our custom errors object being logged.
Are you with me so far?
Now we need somehow display those errors in correct place. Next to invalid form field.
This is how our new code in script
tag looks now.
<script>
import { regSchema } from './schema';
let values = {};
let errors = {};
const extractErrors = err => {
return err.inner.reduce((acc, err) => {
return { ...acc, [err.path]: err.message };
}, {});
};
const submitHandler = () => {
regSchema
.validate(values, { abortEarly: false })
.then(() => {
// submit a form to the server here, etc
alert(JSON.stringify(values, null, 2));
// clear the errors
errors = {};
})
.catch(err => (errors = extractErrors(err)));
};
</script>
We have introduced errors
object that we assign when we submit the form. Now we also need to add individual errors next to our input fields.
<div>
<h1>Please register</h1>
<form on:submit|preventDefault={submitHandler}>
<div>
<input
type="text"
name="name"
bind:value={values.name}
placeholder="Your name"
/>
{#if errors.name}{errors.name}{/if}
</div>
<div>
<input
type="text"
name="email"
bind:value={values.email}
placeholder="Your email"
/>
{#if errors.email}{errors.email}{/if}
</div>
<div>
<input
type="text"
name="username"
bind:value={values.username}
placeholder="Choose username"
/>
{#if errors.username}{errors.username}{/if}
</div>
<div>
<input
type="password"
name="password"
bind:value={values.password}
placeholder="Password"
/>
{#if errors.password}{errors.password}{/if}
</div>
<div>
<input
type="password"
name="passwordConfirm"
bind:value={values.passwordConfirm}
placeholder="Confirm password"
/>
{#if errors.passwordConfirm}{errors.passwordConfirm}{/if}
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</div>
If add that code and try to submit the form you will see the validation errors. It doesn't look pretty, but it works!
We now need to check if the passwords match and therefore we need to go back to our validation schema.
As I wrote in the beginning you can do some advanced validation gymnastics in Yup. To compare if our two passwords match we will use Yup's oneOf validator.
import * as yup from 'yup';
const regSchema = yup.object().shape({
name: yup.string().required(),
email: yup.string().required().email(),
username: yup.string().required(),
password: yup.string().required(),
passwordConfirm: yup
.string()
.required()
.oneOf([yup.ref('password'), null], 'Passwords do not match')
});
export { regSchema };
Now if the passwords don't match Yup will show us the error "Passwords do not match".
Not many people know this, but you can also do custom validation in Yup by using the test method. We will now simulate a call to the server to check if the username is available.
import * as yup from 'yup';
// simulate a network or database call
const checkUsername = username =>
new Promise(resolve => {
const takenUsernames = ['jane', 'john', 'elon', 'foo'];
const available = !takenUsernames.includes(username);
// if we return `true` then validation has passed
setTimeout(() => resolve(available), 500);
});
const regSchema = yup.object().shape({
name: yup.string().required(),
email: yup.string().required().email(),
username: yup
.string()
.required()
.test('usernameTaken', 'Please choose another username', checkUsername),
password: yup.string().required(),
passwordConfirm: yup
.string()
.required()
.oneOf([yup.ref('password'), null], 'Passwords do not match')
});
export { regSchema };
The test function needs to return a boolean. If false
is returned then the validation will not pass and error will be displayed.
Notice that we introduced 500ms timeout to username check and since we validate the whole form it will take 500ms for our form to validate itself. The slowest wins.
The case would be different if we validated individual fields instead.
The message "passwordConfirm is a required field" is not very user friendly. You can provide your own error messages to Yup.
import * as yup from 'yup';
// simulate a network or database call
const checkUsername = username =>
new Promise(resolve => {
const takenUsernames = ['jane', 'john', 'elon', 'foo'];
const available = !takenUsernames.includes(username);
// if we return `true` then validation has passed
setTimeout(() => resolve(available), 500);
});
const regSchema = yup.object().shape({
name: yup.string().required('Please enter your name'),
email: yup
.string()
.required('Please provide your email')
.email("Email doesn't look right"),
username: yup
.string()
.required('Username is a manadatory field')
.test('usernameTaken', 'Please choose another username', checkUsername),
password: yup.string().required('Password is required'),
passwordConfirm: yup
.string()
.required('Please confirm your password')
.oneOf([yup.ref('password'), null], 'Passwords do not match')
});
export { regSchema };
Ah! Much better!
If you fancy async/await over promise chains this is how you can rewrite the submitHandler
.
const submitHandler = async () => {
try {
await regSchema.validate(values, { abortEarly: false });
alert(JSON.stringify(values, null, 2));
errors = {};
} catch (err) {
errors = extractErrors(err);
}
};
This was a very basic example of how you can do custom form validation in Svelte with the help of external and specialized validation library - Yup. Hope that you got the idea.
Form validation is a big area to explore and everything would not fit into a single article. I've not included onfocus
and onblur
field validations for example. Not error CSS classes and nested forms either.
Here is the full code https://github.com/codechips/svelte-yup-form-validation
If you liked the article you will love my book - Svelte Forms: From WTF to FTW!. By reading it you will learn everything about working with forms in Svelte effectively, while learning deeper Svelte concepts at the same time.
]]>If you are thinking of doing a small POC or if you are on a tight budget it's hard to beat Firebase. You get everything you need out of the box. Storage, database, serverless functions, hosting, messaging plus a ton other stuff. And the best thing is that it won't break your bank.
Plus, you get a generous free quota and also the full power of Google Cloud Platform in case you need it.
I am using Snowpack with Svelte as examples, but the concepts of this setup can be applied to any web framework or bundler.
If you want to know more about Snowpack you can read my article - Snowpack with Svelte, Typescript and Tailwind CSS is a very pleasant surprise.
Let's start by setting up new Snowpack project and later we will add Firebase to the mix.
$ npx create-snowpack-app svelte-firebase --template @snowpack/app-template-svelte
$ cd svelte-firebase && npm start
You should now see our app's start page in the browser with the local dev server running on port 8080
.
Next step we need to do is add Firebase to the mix.
Note: before continuing make sure you have a functioning local Java runtime environment since the Firebase emulators are built on top of Java.
To get a required firebase
CLI command we need to install firebase-tools. The easiest way is to install it globally with npm
.
$ npm i -g firebase-tools
There are other methods of installing Firebase CLI, here is more information.
Now we need to add Firebase to our project. For that we need to do two things.
Before we can use Firebase CLI we need to login to Firebase console. We can do that from the command line.
$ firebase login
Firebase will open a webpage for us in the browser where you can authenticate yourself.
Before continuing we need to create a new Firebase project in the Firebase console, if you don't already have an existing one. There is also an option to create a new project straight from the Firebase CLI, but I found it a little glitchy. That's why I recommend to do it in the Firebase console instead.
Did you do it? Nice! We are now ready to add Firebase integration to our project.
$ firebase init
You will be presented with a few options.
Select Firestore and Emulators options by pressing Space key. Press Enter when done.
Next select Use existing project option and select our new Firebase project we created earlier in the Firebase console.
Accept the defaults for the rest of the options. Just say "Yes" to everything. We can always change it later.
If everything went smoothly, you will end up with the following new files in out directory.
# main firebase config
firebase.json
# firestore compound indexes config
firestore.indexes.json
# firestore seurity rules definitions
firestore.rules
# firebase project linking file
.firebaserc
The most important file is firebase.json
. It's a main config file which tells Firebase where to find stuff, what is enabled in the project and what local ports emulators should use.
{
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"ui": {
"enabled": true
}
}
}
From the file above we can see that we will have two local emulators running - functions and Firestore. The Firestore emulator's port is a little problematic as it listens on the same port as Snowpack (8080).
Note: If you want to see what default ports are used for Firebase emulators see this page.
Let's add the Firebase start script to our package.json
so that we can start the Firebase emulators with npm CLI.
Add this row to the scripts
part in the package.json
.
"start:firebase": "firebase emulators:start"
Now we can start Firebase emulators with npm run start:firebase
command. Neat!
The output also tells that we have an emulator UI running on http://localhost:4000
.
If you visit that page you will see this page.
Each emulator has its own status card and the only active is the Firebase emulator which is running on port 8080
.
If you would like to know more about how Firebase emulator can be setup and used here is a link to the official documentation.
We could have added Firebase functions support from the start, but I didn't do it on purpose just so I could show how you can add it later.
If you look at the terminal screenshot above, you saw that Firebase emulator complained that it couldn't find any functions.
Let's fix it.
$ firebase init functions
Choose Typescript and say no to the tslint part. We don't need it, because Typescript compiler will catch most of the errors for us. Plus tslint has been deprecated anyway.
Note: Firebase functions aka Google Cloud Functions support only Node.js v10. Well, Node.js v8 too, but my guess is that you don't want to use it. A more resent LTS Node version should work fine for local development, but that's something to keep in mind if you get any weird behaviour when deploying to live environment later.
As you can see Firebase initialized our Firebase functions project in the new functions
directory. It's actually a separate subproject with it's own package.json
and all.
If you look at our firebase.json
file, you will see the new section in it.
{
"functions": {
"predeploy": "npm --prefix \"$RESOURCE_DIR\" run build"
}
}
What is this you may ask and what is the $RESOURCE_DIR
environment variable? That's actually a Firebase functions predeploy hook and that variable is just an alias for word functions
, or more precise, it defaults to the functions
word and allows you to change the default location and name of your Firebase functions directory.
We might as well have written this.
{
"functions": {
"predeploy": "npm --prefix functions run build"
}
}
The predeploy hook's job is to build your functions the last time before deploying them to live environment.
Unfortunately it does not fire in the dev environment when we use our emulators. Let's try to start the Firebase emulator again.
That's because we haven't built our functions yet. The emulator expect to find the transpiled Javascript code in the functions/lib
directory and right now it's empty. We need to build our functions by executing the same command as in the predeploy hook - npm --prefix functions run build
, but before you do please edit the functions/src/index.ts
and uncomment the function body.
If you start the emulator now and go to the Firebase Emulator UI you will see that our functions emulator is now running too. If you click on the logs button you will see the function url.
If you visit the function URL you will get back a friendly "Hello from Firebase!" greeting back.
Nice! But not quite. We still have a small problem. Every time we change the function code we need to rebuild it. Lucky us that Typescript compiler has a --watch
option!
To take advantage of it we can add the following row to our functions/package.json
scripts section.
"watch": "tsc --watch"
We can now run npm start watch
in our functions
project and Typescript will watch for file changes and recompile them every time they change.
Note: you can also run the command from our main project with npm run watch --prefix functions
.
Alright, we can now run our main app, start the Firebase emulator and do automatic Firebase function recompilation. That alone requires three different terminals. It's there a better way?
Good news! There is. You see, there is a small NPM package called npm-run-all that will solve all our problems.
It's like a Swiss Army knife. One of the tools it has it the ability to run multiple npm scripts in parallel with its run-p
command. That's exactly what we need to start our Snowpack app, Firebase emulator and Typescript compiler at once.
No time to waste. Let's get straight to it.
First, add the package as a dependency to our project npm add -D npm-run-all
. Next, we need to remix our scripts
section in package.json
a bit.
{
"scripts": {
"start": "run-p dev start:*",
"build": "snowpack build",
"test": "jest",
"dev": "snowpack dev",
"start:firebase": "firebase emulators:start",
"start:functions": "npm run watch --prefix functions"
}
}
You can see that we replaced the start
property with the magic run-p dev start:*
command.
What it does is to execute all script passed as arguments in parallel. The star after the start:
is a pretty neat way to tell that all scripts prefixed with start:
should be run. Think of it as a glob function.
However, there is still a small problem with our setup. Both Snowpack and Firestore emulator use port 8080
. We need to change one of them to use a different port.
Let's change Snowpack's. We will run Snowpack on port 8000
instead. Open snowpack.config.json
and add a new devOptions section.
{
"extends": "@snowpack/app-scripts-svelte",
"devOptions": {
"port": 8000
},
"scripts": {},
"plugins": []
}
Now everything should start normally with only one command npm start
.
Isn't life wonderful?!
Alright, we now have this new fancy setup, but how do we use Firestore in code? Not to worry! There are many ways to skin a cat. Here is a naïve one.
Add firebase.ts
to the src
directory with the following code.
// firebase.ts
import firebase from 'firebase/app';
import 'firebase/firebase-firestore';
import 'firebase/firebase-functions';
let firestore: firebase.firestore.Firestore | null = null;
let functions: firebase.functions.Functions | null = null;
// Naive implementation of Firebase init.
// For education purposes. Never store your config in source control!
const config = {
apiKey: 'your-firebase-key',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(config);
const db = (): firebase.firestore.Firestore => {
if (firestore === null) {
firestore = firebase.firestore();
// Snowpack's env variables. Does now work in Svelte files
if (import.meta.env.MODE === 'development') {
// firebase.firestore.setLogLevel('debug');
firestore.settings({
host: 'localhost:8080',
ssl: false
});
}
}
return firestore;
};
const funcs = (): firebase.functions.Functions => {
if (functions === null) {
functions = firebase.app().functions();
if (import.meta.env.MODE === 'development') {
// tell Firebase where to find the Firebase functions emulator
functions.useFunctionsEmulator('http://localhost:5001');
}
}
return functions;
};
export { db, funcs };
Boom! We now have a basic Firebase setup that we can use in our code.
Let's use the new setup in our Svelte app. For the sake of examples to prove that everything works we will make a call to our helloWorld
Firebase function and create a simple TODO list backed by local Firestore.
Replace App.svelte
with the code below.
<!-- App.svelte -->
<script>
import { onMount } from 'svelte';
import { db, funcs } from './firebase';
import firebase from 'firebase/app';
import 'firebase/firebase-firestore';
let message = '';
let todo = '';
let todos = [];
// Firestore collection reference
let todoCollection = null;
// Firestore server timestamp function
const timestamp = firebase.firestore.FieldValue.serverTimestamp;
onMount(async () => {
// reference to our cloud function
const helloFn = funcs().httpsCallable('helloWorld');
const response = await helloFn();
// assign result to message variable
message = response.data.message;
// assign collection to a variable
todoCollection = db().collection('todos');
// create a firestore listener that listens to collection changes
const unsubscribe = todoCollection.orderBy('createdAt', 'desc').onSnapshot(ss => {
let docs = [];
// snapshot has only a forEach method
ss.forEach(doc => {
docs = [...docs, { id: doc.id, ...doc.data() }];
});
// replace todo variable with firebase snapshot changes
todos = docs;
});
// unsubscribe to Firestore collection listener when unmounting
return unsubscribe;
});
const submitHandler = async () => {
if (!todo) return;
// create new todo document
await todoCollection.add({ action: todo, createdAt: timestamp() });
todo = '';
};
</script>
<h2>Functions Emulator</h2>
<!-- result from the helloWorld Firebase function call -->
<p>{message}</p>
<h2>Firestore Emulator</h2>
<form on:submit|preventDefault={submitHandler}>
<input type="text" bind:value={todo} placeholder="Add new todo" />
<button type="submit">add</button>
</form>
{#if todos.length}
<ul>
{#each todos as todo (todo.id)}
<li>{todo.action}</li>
{/each}
</ul>
{:else}
<p>No todos. Please add one.</p>
{/if}
If you start the app now, you will get a CORS error from our Firebase HTTP function. That's expected because Firebase HTTP functions don't have CORS support built-in. We could add it to our cloud function, but there is a better way - Firebase Callable functions.
The fix is easy. We just need to change the type of our Firebase cloud function to callable. Firebase will then call it differently and we don't have to worry about CORS at all.
Change the code of our helloWorld
function to this.
// functions/src/index.ts
import * as functions from 'firebase-functions';
export const helloWorld = functions.https.onCall((data, context) => {
return { message: 'Hello from Firebase!' };
});
The object is returned as response.data
. That's pretty nice as we don't have to worry about the HTTP response/request at all. We just return a plain object and Firebase will take care of serialization for us.
As of time of writing Firebase Authentication is not yet supported in the Firebase emulator, but it's hopefully coming very soon.
But not to worry, you can effectively mock it in your tests if you need to. There are a few different ways you can do it, but it's a little too long to explain here. Maybe in another article.
I won't touch the subject of testing now, but 'firebase emulators:exec' is your friend here. With its help it possible to start the local Firebase emulators, run your tests, and then shut down the emulators.
We should now have a pretty nice setup where we can fire up all the emulators and code with just one command. Mission accomplished!
Firebase have really matured during the last couple of years and if you want to do a quick prototype or have to build an internal app with storage needs please take another look at Firebase.
Here is the full code https://github.com/codechips/svelte-local-firebase-emulator-setup
Thanks for reading and I hope that you learned something new!
]]>But as I sit down and write this I realize that there is no blueprint, no script. It's not something you can rehearse as every situation is different.
Same for the interviews. Every company has their own interview process, their own set of questions.
But one thing is always universal - you are invited to the interview because they believe you might be the right person for the job.
If you read my other article, 5 essential job application tips from a CTO, hopefully you have been invited to the interview already.
So congrats for making it this far! The battle is almost half won. You just have to convince the person across the table that you are the right one.
I've been in quite many interviews in my career myself, but now I mostly sit on the other side of the table interviewing developers.
What follows are some tips on top of my head. Things that I noticed people do wrong or could do better at. Maybe "patterns" is a better word than tips.
Note: I am not talking about technical interviews or screening calls, but "soft" interviews. Technical interviews are different, but many tips below will apply to them as well.
Remember I said that first impression lasts? This is the moment. The critical part. This is the moment that sets the tone for the whole interview. The moment the person across the table decides if she likes you or not. It's the first time they meet you.
Don't screw this up.
This is also the simplest part of the interview. Just say "Hello! I am [name]. Nice to meet you!" with a genuine smile. That's it!
You have to appear self-confident and not nervous. People can sense that. It's normal to get nervous, especially if you haven't been to many interviews before.
My advice would be not to try and paint a picture of this in your head before hand. Try to go into the interview with a blank state of mind.
Imagining that you are meeting your friend's parents for the first time or your friend's friend might help as well.
If it's a physical interview (not a video call), PLEASE do a firm handshake. Nobody likes to hold a dead fish in their hand.
Before going on the interview, find as much information about the company as you can, so you can ask the right questions. Companies love candidates that come prepared and ask smart questions.
Search around on the Internet, on the company's website, LinkedIn, Twitter and write down a set questions. Search for news, announcements, tech blogs. Do some research on the space the company is in.
Write down all your questions, but don't bring them to the interview of course. The writing part is just for your mind to process and internalize them.
Most of the interviews starts with this question. It's a tough one.
Make sure you can give a short sales pitch about yourself. It should be no more than five sentences and provide a clear picture of what you are about. A summary, if you please.
Try writing one down and then ask a friend for feedback by reading it out loud to them. When satisfied with results, try to memorize it.
The best interview is a dialog. Try to listen actively and ask follow-up questions. Always save a few questions to the "Do you have any more questions?" part at the end of the interview.
I cannot stress this highly enough. Questions such as:
You will get them sooner or later. I've met way too many people that stumble on this one.
My best tip is to do a Myers-Briggs personality test. Seriously. There are plenty of free ones on the Internet. They will tell you in great detail about your strengths and weaknesses.
Use the results and try to weave them into your story.
When asked a question, start answering before you know the answer. The best thing to do that is to paraphrase the question. It will give you some extra time to figure the answer on the way.
Why? You will appear more self-confident and avoid the sometimes awkward silence.
This is the question I always ask. Free lunches and beer pong is not the right answer.
Know what's important to you and what you value.
Never lie about your skills. Honesty lasts a long way. If there is some technology you haven't worked with before try to find an angle. Maybe you have worked with similar technology before. The principles are, after all, universal.
Often it pays off to be honest, because you appear vulnerable. You admit that you don't know things, and that's OK. Remember that I told that people often are not looking for skills, but for fast learners? If you convinced the people across the table that you are one, saying no should not be a problem.
Most of the people will use you resume as a base for the interview. One question I like to ask is to draw a timeline of your professional carrier. Workplaces you have been to and what you have done there.
This is not a bragging contest. Keep it short and to the point. If you accomplished something special, sure, tell that.
People like statistics. If you have any interesting numbers to share feel free to do so.
If you don't have any professional experience yet, try to prove that you are a fast learner. Maybe you have done some school projects or some gigs on the side. Try to weave in time constraint factors here.
People often want to know how you helped other companies. Don't talk so much about yourself. Concentrate on your work projects and tell interesting details about them.
Never complain or say bad things about your previous or current employers or bosses. You don't want to be that person. Trust me on this one.
Always save a few questions till the end. Try to make mental notes during the interview and come back to them later.
Never ask direct questions about bicycle parking possibilities and similar. If it's important to you, try to do it indirectly.
If you are meeting with other developers or a team lead you have to make them feel that you will be a valuable addition to the team.
There are countless times where I've heard: He is a little junior, but we think he's got potential!
You want them to say "I really liked the candidate" after the interview.
If they are not sure, they are sure. Make them sure.
If you want to speed up the whole process and create a sense of urgency, tell the interviewer that you are currently involved in another recruitment process too.
Just don't tell them I told you!
Interviews are hard. There is not right or wrong. It's a lot about personal chemistry and psychology.
Interviews are all about finding the right candidate with right skills and culture fit, as they are for you to find the place where you can grow.
Remember, companies are looking for help just as much as you are looking for a job.
Don't get discouraged if you don't pass the interview. Maybe it wasn't the place for you anyway.
Mistakes are your best teacher.
Reflect, adjust, move on.
]]>We will use Snowpack and pnpm to create out project, because I find them pretty nice to work with, but you can of course use any bundler and package manager you want.
# create project
$ pnpx create-snowpack-app svelte-notification-center \
--template @snowpack/app-template-svelte
$ cd svelte-notification-center
# install tailwind css and friends
$ pnpm add -D postcss-cli tailwindcss autoprefixer
$ pnpx tailwindcss init
Next, we need to tell Snowpack to process our CSS files. Create postcss.config.js
in the root folder of the project.
// postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer')
]
};
Create main.css
in the src
directory with the following contents.
/* main.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Last thing we need to do is to add postcss
command to snowpack.config.json
{
"extends": "@snowpack/app-scripts-svelte",
"scripts": {
"build:css": "postcss"
},
"plugins": []
}
All setup and ready to go!
When prototyping an idea I like to work outside in. It helps me to identify the needed data and then write my logic on the shape and flows defined by that data.
<!-- App.svelte -->
<style>
/* unread message count */
.badge {
display: inline-block;
position: absolute;
top: 0;
background-color: #4285f4;
color: #d7e6fd;
right: 0;
border-radius: 9999px;
font-size: 12px;
min-width: 18px;
line-height: 18px;
min-height: 18px;
text-align: center;
}
/* custom width for message popup */
.messages {
min-width: 400px;
}
</style>
<script>
// Main stylesheet. Snowpack will inject it in the head tag
import './main.css';
</script>
<div class="container flex flex-col max-w-4xl min-h-screen mx-auto">
<!-- top menu -->
<header
class="flex items-center justify-between px-5 py-3 text-gray-100 bg-gray-800"
>
<h1 class="text-2xl">Hooli</h1>
<nav class="relative">
<!-- notification center -->
<button class="relative p-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-6 h-6"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
<span class="badge">2</span>
</button>
<!-- this button will be used for closing the popup later -->
<button
tabindex="-1"
class="fixed inset-0 w-full h-full cursor-default focus:outline-none"
/>
<div
class="absolute right-0 p-3 mt-1 text-gray-600 bg-white bg-gray-100 rounded shadow-md messages"
>
<ul class="space-y-3">
<li class="p-3 border rounded">
<p>Message One</p>
<div class="mt-1">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
dismiss
</button>
</div>
</li>
<li class="p-3 border rounded">
<p>Message Two</p>
<div class="mt-1">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
dismiss
</button>
</div>
</li>
</ul>
<div class="flex justify-end mt-3">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
clear all
</button>
</div>
</div>
</nav>
</header>
<div class="flex-grow px-5 py-3 bg-gray-200">
<h2>Content</h2>
</div>
</div>
If you've done everything right you should see something like this.
This is the static version. As we go alone we will add dynamic functionality and slowly refactor it.
First thing we will do is to extract our notification center to own file.
Create a src/Messages.svelte
file and move the messages part of the file to it.
<!-- Messages.svelte -->
<style>
/* unread message count */
.badge {
display: inline-block;
position: absolute;
top: 0;
background-color: #4285f4;
color: #d7e6fd;
right: 0;
border-radius: 9999px;
font-size: 12px;
min-width: 18px;
line-height: 18px;
min-height: 18px;
text-align: center;
}
/* custom width for message popup */
.messages {
min-width: 400px;
}
</style>
<!-- notification center -->
<button class="relative p-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-6 h-6"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
<span class="badge">2</span>
</button>
<!-- this button will be used for closing the popup later -->
<button
tabindex="-1"
class="fixed inset-0 w-full h-full cursor-default focus:outline-none"
/>
<div class="absolute right-0 p-3 mt-1 text-gray-600 bg-white bg-gray-100 rounded shadow-md messages">
<ul class="space-y-3">
<li class="p-3 border rounded">
<p>Message One</p>
<div class="mt-1">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
dismiss
</button>
</div>
</li>
<li class="p-3 border rounded">
<p>Message Two</p>
<div class="mt-1">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
dismiss
</button>
</div>
</li>
</ul>
<div class="flex justify-end mt-3">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
clear all
</button>
</div>
</div>
We can now import it into the App.svelte
.
<!-- App.svelte -->
<script>
// Main stylesheet. Snowpack will inject it in the head tag
import './main.css';
import Messages from './Messages.svelte';
</script>
<div class="container flex flex-col max-w-4xl min-h-screen mx-auto">
<!-- top menu -->
<header
class="flex items-center justify-between px-5 py-3 text-gray-100 bg-gray-800"
>
<h1 class="text-2xl">Hooli</h1>
<nav class="relative">
<Messages />
</nav>
</header>
<div class="flex-grow px-5 py-3 bg-gray-200">
<h1>Content</h1>
</div>
</div>
Everything should work as before and we can now concentrate on one file only.
We will now make our messages popup interactive. It will start in closed state and when you click on the bell button it will appear.
In order to achieve this we need to add a state variable.
<!-- Messages.svelte with style tag omitted -->
<script>
// open-close state
let show = false;
</script>
<button class="relative p-1" on:click={() => (show = !show)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
STROke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-6 h-6"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
<span class="badge">2</span>
</button>
<!-- show only if true -->
{#if show}
<!-- clicking anywhere on the page will close the popup -->
<button
tabindex="-1"
class="fixed inset-0 w-full h-full cursor-default focus:outline-none"
on:click|preventDefault={() => (show = false)}
/>
<div
class="absolute right-0 p-3 mt-1 text-gray-600 bg-white bg-gray-100 rounded shadow-md messages"
>
<ul class="space-y-3">
<li class="p-3 border rounded">
<p>Message One</p>
<div class="mt-1">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
dismiss
</button>
</div>
</li>
<li class="p-3 border rounded">
<p>Message Two</p>
<div class="mt-1">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
dismiss
</button>
</div>
</li>
</ul>
<div class="flex justify-end mt-3">
<button class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm">
clear all
</button>
</div>
</div>
{/if}
We added a show
variable and two event handlers to our buttons that will toggle the visibility state.
If you now click on the bell button the messages should popup on the screen and toggle every time you click on the bell button.
When the message popup is visible you can also click anywhere on screen to close it. That's the trick with the invisible fullscreen button.
We now have everything in place to start building the actual notification center logic.
When people talk about State management in Svelte they often talk about Svelte stores. There are three store types in Svelte.
The whole topic on stores is quite big, but the concept itself is quite simple. You can maybe compare them to React's state, but I don't think it would be quite accurate.
I like to view Svelte's stores as global reactive variables.
Below is Svelte's implementation of writable
store. As you can see it's written in Typescript and returns an object with three methods: set
, update
and subscribe
.
When you subscribe to a store in your Svelte component, your callback function is added to the list of subscribers. When you set or update a value, it will loop through the list of subscribers and notify each one of them.
export function writable<T>(value: T, start: StartStopNotifier<T> = noop): Writable<T> {
let stop: Unsubscriber;
const subscribers: Array<SubscribeInvalidateTuple<T>> = [];
function set(new_value: T): void {
if (safe_not_equal(value, new_value)) {
value = new_value;
if (stop) { // store is ready
const run_queue = !subscriber_queue.length;
for (let i = 0; i < subscribers.length; i += 1) {
const s = subscribers[i];
s[1]();
subscriber_queue.push(s, value);
}
if (run_queue) {
for (let i = 0; i < subscriber_queue.length; i += 2) {
subscriber_queue[i][0](subscriber_queue[i + 1]);
}
subscriber_queue.length = 0;
}
}
}
}
function update(fn: Updater<T>): void {
set(fn(value));
}
function subscribe(run: Subscriber<T>, invalidate: Invalidator<T> = noop): Unsubscriber {
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
subscribers.push(subscriber);
if (subscribers.length === 1) {
stop = start(set) || noop;
}
run(value);
return () => {
const index = subscribers.indexOf(subscriber);
if (index !== -1) {
subscribers.splice(index, 1);
}
if (subscribers.length === 0) {
stop();
stop = null;
}
};
}
return { set, update, subscribe };
}
There are, of course, some other internal mechanisms, but the rough idea of managing, updating and notifying subscribers should be straight forward.
Now that we know how what Svelte store is and how it works, let's use one to build our notification store.
We will create a custom store, built on top of Svelte's writable store. That is because we need to expose a few custom methods on it and also hide the direct access to writable's set
and update
methods.
Let's also write it in Typescript just for the sake of it. I like writing my non-Svelte components in Typescript as it gives me nice autocomplete in the editor and also some type safety.
If you want to learn more how to use Typescript together with Svelte I wrote an extensive tutorial on the subject - How to use Typescript with Svelte.
Before we start writing code we need to think what methods our store needs. Here are some that come to mind.
Now that we have a rough plan let's implement our store. We will use plain strings as messages to keep it simple.
Create an new file called notifications.ts
in the src
directory and add the following code.
import { writable } from 'svelte/store';
// constructor function
const createStore = () => {
// initialize internal writable store with empty list
const { subscribe, set, update } = writable<string[]>([]);
// mark message as read by removing it from the list
const dismiss = (message: string) =>
update(messages => messages.filter(mess => mess !== message));
// add new message to the top of the list
const add = (message: string) => update(messages => [message, ...messages]);
return {
subscribe,
add,
init: set, // alias set method to init
dismiss,
clear: () => set([])
};
};
// initialize the store
const center = createStore();
export { center };
We now have a notification store with five methods. Do you see what we did here? We've hidden the internal writable store inside our own custom store.
The only thing Svelte cares about is that you return an object with a subscribe
function, because that function is required for auto-subscriptions to work.
This leads us straight into the next section where we will leverage the feature in our own code.
You can remove a lot of boilerplate code by prefixing your store variable name with a dollar sign. The compiler will then generate the subscription code for you on the fly. I personally love this feature.
Here is the code needed to wire up our store in the Messages.svelte
component with explaining comments in the right places.
<!-- Messsages.svelte -->
<!-- style tag is omitted -->
<script>
// import the custom store
import { center } from './notifications';
// open-close state
let show = false;
const handleDismiss = message => {
center.dismiss(message);
// guard to close popup when there are no more messages
if ($center.length === 0) {
show = false;
}
};
const handlePopup = () => {
// don't show popup when no messages
if ($center.length === 0) {
show = false;
} else {
show = !show;
}
};
const clearAll = () => {
center.clear();
// close popup
show = false;
};
</script>
<button class="relative p-1" on:click={handlePopup}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="w-6 h-6"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
<!-- show number of messages or hide if zero -->
{#if $center.length}
<span class="badge">{$center.length}</span>
{/if}
</button>
<!-- show only if there are messages -->
{#if show && $center.length}
<!-- clicking anywhere on the page will close the popup -->
<button
tabindex="-1"
class="fixed inset-0 w-full h-full cursor-default focus:outline-none"
on:click|preventDefault={() => (show = false)}
/>
<div
class="absolute right-0 p-3 mt-1 text-gray-600 bg-white bg-gray-100 rounded shadow-md messages"
>
<ul class="space-y-3">
{#each $center as message}
<li class="p-3 border rounded">
<p>{message}</p>
<div class="mt-1">
<!-- add dismiss handler -->
<button
class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm"
on:click={() => handleDismiss(message)}
>
dismiss
</button>
</div>
</li>
{/each}
</ul>
<div class="flex justify-end mt-3">
<!-- add clear all handler -->
<button
class="px-2 text-sm text-blue-200 bg-blue-700 rounded-sm"
on:click={clearAll}
>
clear all
</button>
</div>
</div>
{/if}
If you view the app you should not see any badge and when you click the bell button no popup will not be shown, because we haven't added any messages yet.
How does Svelte's autosubscription work? If peek under the hood and look at the generated code we will see this.
function instance($$self, $$props, $$invalidate) {
let $center;
validate_store(center, "center");
component_subscribe($$self, center, $$value => $$invalidate(1, $center = $$value));
// ...
}
Svelte compiler searches for all variables prefixed with a dollar sign, validates that it's a store by checking for subscribe
method and then subscribes to it.
You will also find that Svelte compiler creates custom code blocks where we refer to $center
in the code such as if
statements and each
loops.
Alright, we have the functionality in place. Let's test-drive our store by adding some messages on app start.
Let's do it straight in App.svelte
.
<script>
import './main.css';
import Messages from './Messages.svelte';
import { onMount } from 'svelte';
import { center } from './notifications';
// seed out notification store on component mount
onMount(() => {
center.init([
'Suspicious login on your server less then a minute ago',
'Successful login attempt by @johndoe',
'Successful login attempt by @amy',
'Suspicious login on your server 7 min',
'Suspicious login on your server 11 min ago',
'Successful login attempt by @horace',
'Suspicious login on your server 14 min ago',
'Successful login attempt by @jack'
]);
});
</script>
<div class="container flex flex-col max-w-4xl min-h-screen mx-auto">
<!-- top menu -->
<header
class="flex items-center justify-between px-5 py-3 text-gray-100 bg-gray-800"
>
<h1 class="text-2xl">Hooli</h1>
<nav class="relative">
<Messages />
</nav>
</header>
<div class="flex-grow px-5 py-3 bg-gray-200">
<h1>Content</h1>
</div>
</div>
Now you can see the message count and our messages in the popup. We can also dismiss individual messages and clear them all.
We had to create custom handlers for our actions with guards in them in order to prevent the UI from ending up in the weird state.
But what about if we have many messages to show? Our popup list will be very long in that case.
It's possible to limit the number of messages by using another reactive feature of Svelte - computed variables.
<!-- Messages.svelte -->
<script>
// import our custom store
import { center } from './notifications';
// ...
// limit the number of displayed messages to 5
export let count = 5;
// create a list of messages to display
$: messages = $center.slice(0, count);
</script>
Change the variable reference in the each loop to messages instead of $center
{#each messages as message}
...
{/each}
This way we will always display five latest messages, plus we can override the number of displayed messages if we want to by passing a count variable to Messages component like this <Messages count="3" />
.
Pretty neat, right?
Just for the sake of it let's add a form that allows us to enter a new message.
Create a new AddMessage.svelte
file in the src
directory with the following code.
<!-- AddMessage.svelte -->
<script>
import { center } from './notifications';
let value = '';
const addMessage = () => {
if (!value) return;
center.add(value);
value = '';
};
</script>
<h2 class="text-2xl font-medium">Add new message</h2>
<form class="mt-3" on:submit|preventDefault={addMessage}>
<input
type="text"
class="w-1/2 p-2"
bind:value
class="w-1/2 p-2"
/>
class="w-1/2 p-2"
</form>
Include it in the App.svelte
file.
<script>
// Main stylesheet. Snowpack will inject it in the head tag
import './main.css';
import Messages from './Messages.svelte';
import { onMount } from 'svelte';
import { center } from './notifications';
import AddMessage from './AddMessage.svelte';
// seed out notification store on component mount
onMount(() => {
center.init([
'Suspicious login on your server less then a minute ago',
'Successful login attempt by @johndoe',
'Successful login attempt by @amy',
'Suspicious login on your server 7 min',
'Suspicious login on your server 11 min ago',
'Successful login attempt by @horace',
'Suspicious login on your server 14 min ago',
'Successful login attempt by @jack'
]);
});
</script>
<div class="container flex flex-col max-w-4xl min-h-screen mx-auto">
<!-- top menu -->
<header
class="flex items-center justify-between px-5 py-3 text-gray-100 bg-gray-800"
>
<h1 class="text-2xl">Hooli</h1>
<nav class="relative">
<Messages count="3" />
</nav>
</header>
<div class="flex-grow px-5 py-3 bg-gray-200">
<AddMessage />
</div>
</div>
The final result should look like this.
Svelte's stores is a beautiful concept and they are lightweight too. You can accomplish a lot with little code.
In this tutorial we learned how to create a simple notification center by building our own custom store.
If you want to dive deeper check out this extensive article on how to build your own custom stores.
You can find the full code here https://github.com/codechips/svelte-notification-center
Thank you for reading and hope you found this useful!
]]>Also, we will use RxJS, and its Firebase bindings - RxFire, to showcase how you can use RxJS together with Firebase's Firestore and why it's such an awesome combination.
What you will learn:
As a base for business logic and UX we will take the classic TodoMVC app as example. To get a feel for what we will build take a look at Firebase + AngularJS example.
Here is the final final result of what we will build.
We won't build it exactly according to the TodoMVC spec, but rather use it as an inspiration. We will however reuse its stylesheets and HTML layout. Why reinvent the wheel, right?
To get the local Firebase environment working we will reuse the base project from my last Firebase article - Smooth local Firebase development setup with Firebase emulator and Snowpack.
$ git clone git@github.com:codechips/svelte-local-firebase-emulator-setup.git \
svelte-firebase-rxjs-todo-app
$ cd svelte-firebase-rxjs-todo-app && npm i
Note: make sure you have Java Runtime and firebase-tools installed before continuing. Use my previous article as a reference on how to install them.
Also, we won't be using Firebase functions in our project so we can remove all references to them.
functions
directory"start:functions"
script from package.json
firebase.json
Done? Good. Let's continue.
First, we need to download two CSS files from TodoMVC repos.
$ curl -o src/base.css https://raw.githubusercontent.com/tastejs/todomvc-common/master/base.css
$ curl -o src/index.css https://raw.githubusercontent.com/tastejs/todomvc-app-css/master/index.css
Next, we need to set the base layout for our app. Edit the src/App.svelte
file and replace it with the following code.
<!-- App.svelte -->
<script>
// import TodoMVC stylesheets
import './base.css';
import './index.css';
</script>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus />
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>Taste JavaScript</label>
<button class="destroy" />
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button class="destroy" />
</div>
<input class="edit" value="Rule the web" />
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>0</strong>
item left
</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
</section>
It's pretty neat that Snowpack allows us to import CSS files in our code.
If you start the app now (npm start) you will see a static version of the app that we will build.
We want to keep things clean so we will extract the header and footer to separate Svelte components.
First, we will extract the header.
<!-- Header.svelte -->
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus />
</header>
Now, we will extract the footer.
<!-- Footer.svelte -->
<footer class="footer">
<span class="todo-count">
<strong>0</strong>
item left
</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
Our App.svelte
file should look like this now.
<!-- App.svelte -->
<script>
// import TodoMVC stylesheets
import './base.css';
import './index.css';
// import components
import Header from './Header.svelte';
import Footer from './Footer.svelte';
</script>
<section class="todoapp">
<Header />
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>Taste JavaScript</label>
<button class="destroy" />
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button class="destroy" />
</div>
<input class="edit" value="Rule the web" />
</li>
</ul>
</section>
<Footer />
</section>
We are now ready to add Firebase + RxFire into the mix.
Let's start writing the business logic. First, we need to install RxJS and RxFire.
$ npm add -D rxjs rxfire
To keep things simple we will keep all todos in a todos
Firestore collection.
Next, we need to think about the data structure of a todo item.
interface Todo {
id: string;
action: string;
completed: boolean;
createdAt: firebase.firestore.Timestamp;
}
That will do. The timestamp is needed for sorting todos by time.
Create a file src/todos.ts
with the following code.
// todos.ts
import { db } from './firebase';
import firebase from 'firebase/app';
import 'firebase/firebase-firestore';
import { collectionData } from 'rxfire/firestore';
import { startWith } from 'rxjs/operators';
const timestamp = firebase.firestore.FieldValue.serverTimestamp;
interface Todo {
id: string;
action: string;
completed: boolean;
createdAt: firebase.firestore.Timestamp;
}
// todos Firebase collection reference
const todoCollection = db().collection('todos');
// rxfire wrapped collection
export const todos = collectionData(
todoCollection.orderBy('createdAt', 'desc'),
'id'
).pipe(startWith([]));
That's the base. You see that we have a startWith
operator. It's required for Svelte's each
template directive to work. We provide it with an initial empty list on start. The empty list will be replaced by the actual Firebase data right after.
Next step is to create a function to add new item to the list.
// todos.ts
export const createTodo = async (action: string) => {
// guard function to check if todo is not empty
if (!action) return;
await todoCollection.add({ action, completed: false, createdAt: timestamp() });
};
We now have all the business logic in place to create at new todo item. Let's put it to use.
Open Header.svelte
and replace it with the following code.
<!-- Header.svelte -->
<script>
import { onMount } from 'svelte';
import { createTodo } from './todos';
let action = '';
// input field element reference
let ref = null;
onMount(() => {
// focus the input field on app load
ref.focus();
});
const createHandler = async () => {
if (!action) return;
await createTodo(action);
action = '';
};
</script>
<header class="header">
<h1>todos</h1>
<form on:submit|preventDefault={createHandler}>
<input
class="new-todo"
type="text"
bind:value={action}
bind:this={ref}
placeholder="What needs to be done?"
/>
</form>
</header>
Nothing complicated. We bind the input's value to the action
variable and when the form is submitted we call our createTodo
function and reset the action
variable.
According to the TodoMVC specification the input field must have focus when the app is loaded. We achieved that by using Svelte's onMount
lifecycle event.
One trick I like to use when developing is to JSON stringify output on page. I know, poor man's debuging, but it's nice to see data visually when developing.
<!-- App.svelte -->
<script>
// ...
import { todos } from './todos';
</script>
Add this line at the end of the file
<pre>{JSON.stringify($todos, null, 2)}</pre>
It should now be possible to create new todo items and see them serialized as JSON on the page.
When we add a new item to Firebase collection, RxFire notices it and refreshes the list for us.
You might have noticed that we prefixed our todos
RxFire observable with a dollar sign - $todos
. The reason it works is that RxJS observables have a subscribe
method on them and Svelte treats them as plain readable Svelte stores.
Now we need to extract the individual item component. Create a Todo.svelte
in the src
directory with the following code.
<!-- Todo.svelte -->
<script>
export let todo;
$: completed = todo && todo.completed;
</script>
<li class:completed>
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>{todo.action}</label>
<button class="destroy" />
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
You can see that we will pass a todo
item to the component and we also created a computed property called completed
that we use in CSS logic - class:completed
. It might look weird, but that's a Svelte shortcut for working with CSS classes.
The reason we extracted component together with an li
HTML element is that we need to be able to set CSS classes on it.
We now need to adjust our App.svelte
component, by introducing an each
loop. Also, according to TodoMVC spec, the main section should be hidden when there are no todo items.
<!-- App.svelte -->
<script>
// import TodoMVC stylesheets
import './base.css';
import './index.css';
// import components
import Header from './Header.svelte';
import Footer from './Footer.svelte';
// import Todo component
import Todo from './Todo.svelte';
import { todos } from './todos';
</script>
<section class="todoapp">
<Header />
<!-- display only if there are todo items -->
{#if $todos.length}
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- iterate over todos stream -->
{#each $todos as todo (todo.id)}
<Todo {todo} />
{/each}
</ul>
</section>
{/if}
<Footer />
</section>
<pre>{JSON.stringify($todos, null, 2)}</pre>
Cool! We can now see todo items when we add them.
Let's implement the toggling of the individual item.
For that we need to add a toggle function to our todos.ts
.
// todos.ts
export const toggleTodo = async (todo: Todo) => {
await todoCollection.doc(todo.id).update({ completed: !todo.completed });
};
And also add the following code to our Todo.svelte
component.
<!-- Todo.svelte -->
<script>
import { toggleTodo } from './todos';
export let todo;
$: completed = todo && todo.completed;
</script>
<li class:completed>
<div class="view">
<input
class="toggle"
type="checkbox"
checked={todo.completed}
on:change={() => toggleTodo(todo)}
/>
<label>{todo.action}</label>
<button class="destroy" />
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
We imported our toggleTodo
function and wired it up to Svelte's on:change
event.
Now you can toggle your heart out if you want and everything is synced to Firestore.
Deleting an item is almost the same.
// todos.ts
export const deleteTodo = async (todo: Todo) => {
await todoCollection.doc(todo.id).delete();
};
The deleteTodo
function should be self-explanatory.
<!-- Todo.svelte -->
<script>
import { deleteTodo, toggleTodo } from './todos';
export let todo;
$: completed = todo && todo.completed;
</script>
<li class:completed>
<div class="view">
<input
class="toggle"
type="checkbox"
checked={todo.completed}
on:change={() => toggleTodo(todo)}
/>
<label>{todo.action}</label>
<button class="destroy" on:click={() => deleteTodo(todo)} />
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
And we can delete the items too. Half way now.
We have now come to the most complex part of the application. Editing a todo item.
The editing business logic is straight forward.
// todos.ts
export const updateTodo = async (id: string, action: string) => {
if (action.trim()) await todoCollection.doc(id).update({ action });
};
The UI part, not so much. The spec says when you double click an item it should toggle the editing mode on and focus the input field. We should also revert the changes if the user clicks outside the form or hits the Escape key.
Double clicking is not a problem, Svelte has on:dblclick
event handler, nor are the other requirements that can be satisfied with on:blur
and on:keyup
events.
What took the most time was to figure out how to focus the input field. For that we need a reference to the input element and keep track if the component is in "editing" mode. Only then we should focus the input element.
I tried to use Svelte's action directive, but failed, so I ended up using Svelte's afterUpdate
lifecycle event.
Sounds complicated, let's put words into code. This is our updated Todo.svelte
component. I've added explaining comments in the right places.
<!-- Todo.svelte -->
<script>
import { afterUpdate } from 'svelte';
import { deleteTodo, toggleTodo, updateTodo } from './todos';
export let todo = null;
// keep action in own variable. needed for reverting editing changes
let action = todo.action;
// editing state
let editing = false;
// reference to the input field
let ref = null;
const updateHandler = async () => {
await updateTodo(todo.id, action);
// turn off editing state
editing = false;
};
const editHandler = () => {
// turn on editing state
editing = true;
// copy todo item's action
action = todo.action;
};
// handler for blur and keyup events
const doneEditing = event => {
// if match, turn off editing state
if (event.type === 'blur' || event.key === 'Escape') editing = false;
};
afterUpdate(() => {
// focus input field if component is in edit mode
if (editing) {
ref.focus();
}
});
$: completed = todo.completed;
</script>
<li class:completed class:editing>
{#if editing}
<form on:submit|preventDefault={updateHandler}>
<input
bind:this={ref}
class="edit"
type="text"
on:blur={doneEditing}
on:keyup={doneEditing}
bind:value={action}
/>
</form>
{:else}
<div class="view">
<input
class="toggle"
type="checkbox"
checked={todo.completed}
on:change={() => toggleTodo(todo)}
/>
<label on:dblclick={editHandler}>{todo.action}</label>
<button class="destroy" on:click={() => deleteTodo(todo)} />
</div>
{/if}
</li>
This was the hardest part to implement and I am not sure I've done it in the most efficient way possible, because we re-focus the input field on every keypress when component is in edit mode. But hey, it works!
This is the fun part of the app and the part where RxJS really shines.
First, we need to create a filter observable. Its job will be to keep track of the current view.
RxJS BehaviourSubject is perfect for the job. We can initialize it with a default value and it will always return its latest state to us.
// todos.ts
import { BehaviorSubject } from 'rxjs';
// default value is 'all' todos
const todoFilter = new BehaviorSubject('all');
// Export the Subject's next function as setFilter
export const setFilter = todoFilter.next.bind(todoFilter);
For the filter to be useful we need to create a few filter functions. We will keep them in a dictionary.
// todos.ts
const filters = {
completed: (todo: Todo) => todo.completed,
active: (todo: Todo) => !todo.completed,
all: (todo: Todo) => todo
};
We need to be able to show completed, active and all todo items. For that we can create two RxJS sub-streams. Think of them as of Svelte's derived stores.
We can use the filter functions we defined earlier. Don't forget to import map
operator from rxjs/operators
.
// todos.ts
export const completedTodos = todos.pipe(
map(todos => todos.filter(filters.completed))
);
// active todos stream
export const activeTodos = todos.pipe(
map(todos => todos.filter(filters.active))
);
Next, we will concentrate on our Svelte filter component. The TodoMVC spec dictates that filter footer should only be visible if there are any todo items and the "items left" indicator should respect singular and plural.
Here we will deviate from the TodoMVC spec a bit, because we are not using any routing library, so we will use good old radio buttons instead of links.
For the filtering and "items left" indicator we will use Svelte's computed properties as they are a perfect candidate for the job.
Enough talking. Here is the annotated code for the Footer.svelte
component.
<!-- Footer.svelte -->
<script>
import { todos, activeTodos, setFilter } from './todos';
let filterValue = 'all';
// every time the filterValue changes we call our setFilter function
$: setFilter(filterValue);
// auto-computed pluralization
$: itemsLabel = $activeTodos.length === 1 ? 'item' : 'items';
</script>
{#if $todos.length}
<footer class="footer">
<span class="todo-count">
<strong>{$activeTodos.length}</strong>
{itemsLabel} left
</span>
<ul class="filters">
<li>
<label>
<input name="filter" type="radio" bind:group={filterValue} value="all" />
all
</label>
</li>
<li>
<label>
<input name="filter" type="radio" bind:group={filterValue} value="active" />
active
</label>
</li>
<li>
<label>
<input name="filter" type="radio" bind:group={filterValue} value="completed" />
completed
</label>
</li>
</ul>
<button class="clear-completed">Clear completed</button>
</footer>
{/if}
You should now be able to see the working remaining items indicator, but when we change filter options nothing is happening. This is expected as we need to create another RxJS stream for that.
When the filter changes we need to filter our items on their completed
status. We already have filter functions and a filter. Let's implement the actual stream.
For that we need to use a combineLatest observable. It's pretty straight forward. You give it a number of streams and it will emit them all to a function we specify when any of the supplied stream emits. Here is how it looks.
// todos.ts
import { BehaviorSubject, combineLatest } from 'rxjs';
export const filteredTodos = combineLatest(todos, todoFilter).pipe(
map(([todos, filter]) => {
// filter todos on current filter, default to all if not found
return todos.filter(filters[filter] || filters.all);
})
);
Now, we need to use our new filteredTodo
stream in our App.svelte
. Import it form todos.ts
first and replace $todos
with the $filteredTodos
stream.
<ul class="todo-list">
<!-- iterate over filteredTodos stream -->
{#each $filteredTodos as todo (todo.id)}
<Todo {todo} />
{/each}
</ul>
Voila! We have now implemented a filtered view! Change the filters and admire your work.
We are almost done. Only two features left to implement. One of them is clearing all completed items. No need to involve RxJS. Raw Firebase library will do.
// todos.ts
export const clearCompleted = async () => {
// query reference to all completed todo items
const completed = await todoCollection.where('completed', '==', true).get();
// loop over items and delete each one
completed.forEach(async doc => {
await todoCollection.doc(doc.id).delete();
});
};
And now we need to import the function in our Footer.svelte
component and wire it to the "Clear completed" button.
<!-- Footer.svelte -->
<button class="clear-completed" on:click={clearCompleted}>Clear completed</button>
That was easy, right?
Last thing we need to do according to the TodoMVC spec is to toggle all items as complete when we click the arrow to the left of the create action field.
It's actually a hidden checkbox and we can use its checked
value as an indicator and pass it into our status modifier function that looks like this.
// todos.ts
export const checkAll = async (completed: boolean) => {
// get reference to all todo items
const allTodos = await todoCollection.get();
allTodos.forEach(async doc => {
// change the item status if not the same as supplied
if (doc.data().completed !== completed) {
await todoCollection.doc(doc.id).update({ completed });
}
});
};
The checkbox should also become checked if all items are complete. Again, we can use the power of Svelte's computed variables.
Here is the final version of App.svelte
component in all its glory.
<!-- App.svelte -->
<script>
import './base.css';
import './index.css';
import Header from './Header.svelte';
import Footer from './Footer.svelte';
import Todo from './Todo.svelte';
import { todos, filteredTodos, completedTodos, markAll } from './todos';
// on:change handler
const markAll = event => {
checkAll(event.target.checked);
};
// used for checkbox auto status
$: allCompleted = $todos.length === $completedTodos.length;
</script>
<section class="todoapp">
<Header />
<!-- display only if there are todo items -->
{#if $todos.length}
<section class="main">
<input
on:change={markAll}
id="toggle-all"
class="toggle-all"
checked={allCompleted}
type="checkbox"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- iterate over filteredTodos stream -->
{#each $filteredTodos as todo (todo.id)}
<Todo {todo} />
{/each}
</ul>
</section>
{/if}
<Footer />
</section>
<pre>{JSON.stringify($todos, null, 2)}</pre>
All our requirements are now implemented, which means we are done!
Test drive the app and pat yourself on the shoulder for a job well done!
That was a fun exercise. If you followed along you should have learned:
The most painful feature to implement was the edit form autofocus, when the item enters the edit state. I am still not sure if I've done it correctly. But it works and that's the most important.
Remember: your users don't care if your code is linted, tested or uses the right abstractions. They just want to get things done and move on with their life.
I am, of course, joking, but there is some truth in every joke. Linting and testing helps you gain the confidence of shipping things that work faster, but it's also dangerous. I've been burned by it so many times. It's very easy to get ignorant and over-confident and start relying on your automated tests too much.
As always, here is the full code (for those of you too lazy to type):
https://github.com/codechips/svelte-firebase-rxjs-todo-app
This was a long article. If you made it this far, and actually wrote the code on the way, like typed it out on your keyboard, you hopefully learned a lot.
Thank you for reading!
]]>In this article we will explore Firebase Authentication. If you want to know how to use Auth0 I've written an article on the topic - Svelte Auth0 integration in 66 LOC.
Some of the things you will learn:
No time to waste. Let's get started!
For this experiment I decided to try a new Svelte bundler - Svite. I will also use Tailwind CSS because I want to see how good Svite works with PostCSS, plus Tailwind CSS blog-friendly. It lets you concentrate on the actual problem and not writing CSS.
Here are the commands needed to bootstrap our project.
$ npx svite create svelte-firebase-auth-example
$ cd svelte-firebase-auth-example
$ npm add -D firebase tailwindcss postcss svelte-preprocess postcss-preset-env
$ npx tailwindcss init
We also need to create a PostCSS config in the root directory.
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss')(),
require('postcss-preset-env')({ stage: 1 }),
],
};
And tell Tailwind to purge unused CSS classes on production builds.
// tailwind.config.js
module.exports = {
purge: ['./src/**/*.svelte'],
theme: {
extend: {},
},
variants: {},
plugins: [],
}
Next, we need to create a Tailwind CSS file in src
directory.
/* index.css */
@import 'tailwindcss/base.css';
@import 'tailwindcss/components.css';
@import 'tailwindcss/utilities.css';
And import it into our app.
// index.js
import App from './App.svelte';
import './index.css';
const app = new App({
target: document.body,
});
export default app;
We also need to tell our editor on how to process Svelte files with PostCSS (Tailwind) in them.
For that we need to create a svelte.config.js
file.
// svelte.config.js
const { postcss } = require('svelte-preprocess');
module.exports = {
preprocess: [postcss()]
};
We are finally ready to build our login form. For this example I chose to borrow most of the markup from Tailwind's own login form example.
<!-- App.svelte -->
<style lang="postcss">
label {
@apply block mb-2 text-sm font-bold text-gray-700;
}
.input-field {
@apply border w-full py-2 px-3 text-gray-700 mb-3;
}
.input-field:focus {
@apply shadow-outline outline-none;
}
button {
@apply w-full px-4 py-2 font-bold text-white bg-blue-500 rounded-sm;
}
button:hover {
@apply bg-blue-700;
}
button:focus {
@apply outline-none shadow-outline;
}
.wrapper {
@apply flex flex-grow h-screen justify-center items-center bg-blue-100;
}
</style>
<div class="wrapper">
<div class="w-full max-w-xs">
<form class="px-8 pt-6 pb-8 bg-white shadow-md">
<div class="mb-4">
<label for="email">Email</label>
<input
class="input-field"
id="email"
type="text"
placeholder="name@acme.com"
/>
</div>
<div class="mb-6">
<label for="password">Password</label>
<input
class="input-field"
id="password"
type="password"
placeholder="******************"
/>
</div>
<div class="">
<button type="submit">Sign In</button>
</div>
<div class="mt-3">
<button type="button">Sign In with Google</button>
</div>
</form>
</div>
</div>
Notice that I added a lang="postcss"
attribute to the style tag. That is required for the code editor to understand that it's dealing with PostCSS and not pure CSS. I think that you can also use type="postcss"
to achieve the same result.
If you did everything correctly the result should look like this.
We are now ready to write some authentication logic, but before we start we need talk a little about Firebase Authentication.
Before we start, we need to create an app in the Firebase Console if you haven't done that already.
Assuming you already created a project, go to Firebase project and you should see an "Add app" button right below the project title. Create a new app, choose web, complete the process and the result will be a Firebase config code snippet.
Save it, because we are going to need it soon.
Next, we need to enable Firebase authentication. There are quite a few different options available, but we will only enable email/password and Google, because they work out of the box.
We also need to create a Firebase user that we will use to test our email and password authentication. You can find it in the Authentication section.
Done? Good! Because we need to talk about how Firebase authentication works.
I often find it easier to look at the code than reading about how the code works. Below is the annotated code that explains how Firebase authentication works and what methods we will use.
// import firebase app (required)
import firebase from 'firebase/app';
// firebase auth mixin (required for every firebase feature)
import 'firebase/auth';
// firebase config with non-auth properties skipped
const firebaseConfig = {
apiKey: 'firebase-api-key',
authDomain: 'testing-firebase-emulators.firebaseapp.com',
projectId: 'testing-firebase-emulators',
};
// initialize firebase app. required as first step
firebase.initializeApp(firebaseConfig);
// get the firebase auth object
const auth = firebase.auth();
We are now ready to use Firebase authentication.
Since this article is only about Firebase authentication we will skip the registration, password reset and other features and concentrate only on login and logout logic.
Below are the Firebase authentication methods we are going to use.
// a promise that returns an error in case of error
// or nothing in case of success
auth.signInWithEmailAndPassword('email@example.com', 'qwerty');
// sign-in with Google provider
// same concept for Github, Twitter, etc
const google = new firebase.auth.GoogleAuthProvider();
// redirect to Googles login page
auth.signInWithRedirect(google);
// show a login popup without leaving the app
auth.signInWithPopup(google);
// logout promise. clear firebase auth cookies, etc
auth.signOut();
// Firebase listener that fires when auth state changes.
// Will be fired on login, logout and also check and fire
// when you load or reload the page
auth.onAuthStateChanged(auth => {
// if user is not logged in the auth will be null
if (auth) {
console.log('logged in');
} else {
console.log('not logged in');
}
});
The most important concept to understand is that Firebase authentication is decoupled from the login/logut actions. When you login, Firebase onAuthStateChanged
listener will be fired and it's in that code block that you must perform your specific app logic. Such as loading user data, redirecting to another page, etc.
I've noticed that people in Svelte community often like to write code The Svelte Way. What do I mean by that? They tend to keep the logic in Svelte components instead of external JS or TS files. Let's try that and see how it turns out.
We will start by creating an Auth.svelte
component that we will use in our main file. Svelte slot seems like a good solution to our problem. Let's use it.
<!-- Auth.svelte -->
<script>
import firebase from 'firebase/app';
import 'firebase/auth';
const auth = firebase.auth();
// Firebase user
let user = null;
// expose property on the component that we can use
// to choose if we want use popup or redirect
export let useRedirect = false;
// small mapper function
const userMapper = claims => ({
id: claims.user_id,
name: claims.name,
email: claims.email,
picture: claims.picture
});
export const loginWithEmailPassword = (email, password) =>
auth.signInWithEmailAndPassword(email, password);
export const loginWithGoogle = () => {
const provider = new firebase.auth.GoogleAuthProvider();
if (useRedirect) {
return auth.signInWithRedirect(provider);
} else {
return auth.signInWithPopup(provider);
}
};
export const logout = () => auth.signOut();
// will be fired every time auth state changes
auth.onAuthStateChanged(async fireUser => {
if (fireUser) {
// in here you might want to do some further actions
// such as loading more data, etc.
// if you want to set custom claims such as roles on a user
// this is how to get them because they will be present
// on the token.claims object
const token = await fireUser.getIdTokenResult();
user = userMapper(token.claims);
} else {
user = null;
}
});
// reactive helper variable
$: loggedIn = user !== null;
</script>
<!-- we will expose all required methods and properties on our slot -->
<div>
<slot {user} {loggedIn} {loginWithGoogle} {loginWithEmailPassword} {logout} />
</div>
Now, let's turn our attention to our main file App.svelte
.
<!-- App.svelte with styles omitted -->
<script>
import firebase from 'firebase/app';
import Auth from './Auth.svelte';
const firebaseConfig = {
apiKey: 'firebase-api-key',
authDomain: 'testing-firebase-emulators.firebaseapp.com',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(firebaseConfig);
</script>
<div class="wrapper">
<Auth
useRedirect={true}
let:user
let:loggedIn
let:loginWithGoogle
let:loginWithEmailPassword
let:logout
>
{#if loggedIn}
<div class="w-full max-w-xs">
<div class="text-center">
<h2>{user.email}</h2>
<button type="button" class="mt-3" on:click={logout}>Logout</button>
</div>
</div>
{:else}
<div class="w-full max-w-xs">
<form
class="px-8 pt-6 pb-8 bg-white shadow-md"
>
<div class="mb-4">
<label for="email">Email</label>
<input
class="input-field"
id="email"
type="email"
placeholder="name@acme.com"
/>
</div>
<div class="mb-6">
<label for="password">Password</label>
<input
class="input-field"
id="password"
type="password"
placeholder="******************"
/>
</div>
<div>
<button type="submit">Sign In</button>
</div>
<div class="mt-3">
<button type="button" on:click|preventDefault={loginWithGoogle}>
Sign In with Google
</button>
</div>
</form>
</div>
{/if}
</Auth>
</div>
This is how our main file looks now. Svelte slots let us use their exposed properties by the let:property
directive.
All good, but there is a small problem. We need to access loginWithEmailPassword
function outside slot's scope.
We need to use it in our login form handler and also check for potential login errors.
Slot's properties are only available inside its scope, but we can change the let:loginWithEmailPassword
to a bind:loginWithEmailPassword
and in that way bind it to a local variable.
It works because we prefixed that function with export
statement in our Auth.svelte
component.
While we are on it we will also create our login form handler and add an error message. We will also use Svelte's fade
transition just for fun.
Here is the full code with style still omitted.
<!-- App.svelte -->
<script>
import firebase from 'firebase/app';
import Auth from './Auth.svelte';
import { fade } from 'svelte/transition';
const firebaseConfig = {
apiKey: 'firebase-api-key',
authDomain: 'testing-firebase-emulators.firebaseapp.com',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(firebaseConfig);
let loginWithEmailPassword;
let error = null;
const loginHandler = async event => {
const { email, password } = event.target.elements;
try {
error = null;
await loginWithEmailPassword(email.value, password.value);
} catch (err) {
error = err;
}
};
</script>
<div class="wrapper">
<Auth
useRedirect={true}
let:user
let:loggedIn
let:loginWithGoogle
bind:loginWithEmailPassword
let:logout
>
{#if loggedIn}
<div class="w-full max-w-xs">
<div class="text-center">
<h2>{user.email}</h2>
<button type="button" class="mt-3" on:click={logout}>Logout</button>
</div>
</div>
{:else}
<div class="w-full max-w-xs">
<form
on:submit|preventDefault={loginHandler}
class="px-8 pt-6 pb-8 bg-white shadow-md"
>
<div class="mb-4">
<label for="email">Email</label>
<input
class="input-field"
id="email"
type="email"
placeholder="name@acme.com"
/>
</div>
<div class="mb-6">
<label for="password">Password</label>
<input
class="input-field"
id="password"
type="password"
placeholder="******************"
/>
</div>
{#if error}
<div transition:fade class="p-2 mb-6 bg-red-300">{error.message}</div>
{/if}
<div>
<button type="submit">Sign In</button>
</div>
<div class="mt-3">
<button type="button" on:click|preventDefault={loginWithGoogle}>
Sign In with Google
</button>
</div>
</form>
</div>
{/if}
</Auth>
</div>
Try it out and see that it works. Hint: it should.
I don't know about you, but even if it works, the code feels a bit awkward to me.
Personally, I would not write authentication code directly in a Svelte file, but write it in a separate Javascript file.
Why don't we do that and then compare our solutions?
As I said earlier, I prefer to keep as little logic as possible in Svelte files and instead try to use them as a thin view layer that binds everything together.
It helps me separate the business logic from the view and makes it easily testable. Here is how I would roughly write the authentication logic.
Create an auth
directory in src
directory and create an index.js
file with the following code.
// auth/index.js
import firebase from 'firebase/app';
import 'firebase/auth';
import { readable } from 'svelte/store';
const userMapper = claims => ({
id: claims.user_id,
name: claims.name,
email: claims.email,
picture: claims.picture
});
// construction function. need to call it after we
// initialize our firebase app
export const initAuth = (useRedirect = false) => {
const auth = firebase.auth();
const loginWithEmailPassword = (email, password) =>
auth.signInWithEmailAndPassword(email, password);
const loginWithGoogle = () => {
const provider = new firebase.auth.GoogleAuthProvider();
if (useRedirect) {
return auth.signInWithRedirect(provider);
} else {
return auth.signInWithPopup(provider);
}
};
const logout = () => auth.signOut();
// wrap Firebase user in a Svelte readable store
const user = readable(null, set => {
const unsub = auth.onAuthStateChanged(async fireUser => {
if (fireUser) {
const token = await fireUser.getIdTokenResult();
const user = userMapper(token.claims);
set(user);
} else {
set(null);
}
});
return unsub;
});
return {
user,
loginWithGoogle,
loginWithEmailPassword,
logout
};
};
We also need to adjust our App.svelte
a bit.
<!-- App.svelte with style ommited -->
<script>
import firebase from 'firebase/app';
import { initAuth } from './auth';
import { fade } from 'svelte/transition';
const firebaseConfig = {
apiKey: 'firebase-api-key',
authDomain: 'testing-firebase-emulators.firebaseapp.com',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(firebaseConfig);
const { loginWithEmailPassword, loginWithGoogle, logout, user } = initAuth();
let error = null;
const loginHandler = async event => {
const { email, password } = event.target.elements;
try {
error = null;
await loginWithEmailPassword(email.value, password.value);
} catch (err) {
error = err;
}
};
</script>
<div class="wrapper">
{#if $user}
<div class="w-full max-w-xs">
<div class="text-center">
<h2>{$user.email}</h2>
<button type="button" class="mt-3" on:click={logout}>Logout</button>
</div>
</div>
{:else}
<div class="w-full max-w-xs">
<form
on:submit|preventDefault={loginHandler}
class="px-8 pt-6 pb-8 bg-white shadow-md"
>
<div class="mb-4">
<label for="email">Email</label>
<input
class="input-field"
id="email"
type="email"
placeholder="name@acme.com"
/>
</div>
<div class="mb-6">
<label for="password">Password</label>
<input
class="input-field"
id="password"
type="password"
placeholder="******************"
/>
</div>
{#if error}
<div transition:fade class="p-2 mb-6 bg-red-300">{error.message}</div>
{/if}
<div>
<button type="submit">Sign In</button>
</div>
<div class="mt-3">
<button type="button" on:click|preventDefault={loginWithGoogle}>
Sign In with Google
</button>
</div>
</form>
</div>
{/if}
</div>
We've now switched to the new JS-only solution and everything should work just as before.
By now you should know how to use Firebase authentication in Svelte in two different ways. The Svelte way and the JS way. Only you can answer which one feels more intuitive to you.
As for me, I find it more convenient to keep most of my logic out of Svelte. Call me old school.
But, there is one thing bothering me. The whole authentication process feels binary. You are either logged in or you are not.
It also looks weird when you first load the page or reload it. You start with the login form, but when the authentication listener kicks in and sees that you are logged in, the login form disappears. Not a good user experience.
An authentication flow in real app has many states like authenticating, signed in, signed out, signing in, signing out, loading user data, etc.
Wouldn't it be nice if we could somehow show the user the current authentication state she is in?
The good news is that we can. We can do it with the help of a small (and awesome) state library called XState.
That will be the topic for my next article. We will pick up where we left off and gently explore XState's capabilities to see if it can help us enhance our current authentication flow and make it more robust.
]]>It's hard to understand XState just with light bulb and red light examples. I need something concrete, something to chew on. A practical use case. That's why I asked myself if I could use XState for Firebase authentication.
XState is hard to learn. In order to understand the concepts you must actually sit down and type out the code.
Take 20 minutes of your busy schedule. It will be time well spent. I promise!
Below is a list of the things you will learn:
This is what the final result will look like.
Onward.
Since this is a follow-up article to Firebase authentication with Svelte we will base all the code on it. Read it first if you want to understand how Firebase authentication works.
XState is a small (and lightweight) library that helps you manage state in your application. The gist of XState is that you application can only be in one state at a time and from that state you can only move to other predefined states by the rules you define. Because of that, it makes working with state very predictable, but notice I didn't say easy.
Even though, XState has one of the best documentations I have seen, it's not easy to understand all its concepts. You have to start with a blank state of mind and take your time to absorb all the new ideas it offers.
Many people think that XState can only be used in UI when working with forms, but that not true. You can use it anywhere you need predicable state.
I won't explain the XState in-depth, because XState's documentation does it 1000 times better that I ever could, but I want to briefly explain state machines.
State machines are the core of XState and you will be writing many when working with the XState library.
Here is a very basic and oversimplified example of an auth state machine just to get an idea on how to work with XState.
import { createMachine, interpret } from 'xstate';
// create a state machine config
const config = {
id: 'auth',
initial: 'signedOut',
states: {
authenticating: {
on: {
'SUCCESS': 'signedIn',
'FAILURE': 'signedOut'
}
},
signedIn: {
on: { 'LOGOUT': 'signedOut' }
},
signedOut: {
on: { 'LOGIN': 'authenticating' }
}
}
};
// create a machine instance from config
const machine = createMachine(config);
// create an instance of an interpreted machine,
// also known as a service.
const service = interpret(machine)
// start our service
service.start();
service.state.value === 'signedOut';
// true
// send an event that initiates a state transition
// to the 'authenticating' state
service.send('LOGIN');
service.state.matches('authenticating');
// true
service.send('SUCCESS');
service.state.matches('signedIn');
// true
service.stop();
You can do many fancy things with state machines, as you will learn. The basics of the state machines - you make decisions on the current state by sending events to the instance of the state machine.
With our newly gained basic state machine knowledge, let's try to identify the states we have when it comes to our Firebase authentication.
We have three obvious states:
But if we think hard about it we have two more states:
You might think they are actions, but they are actually states. At least in my eyes.
When I am trying to identify states I often start with an action, such as "log in", and try to convert action to state - "logging in". If it makes sense, then it's probably a state.
We can also have additional states that are related to authentication such as loading, where we enrich the user with additional data like roles and permissions.
With our naïve assumptions let's try and build an initial version of our state machine.
Let's get cranking! Create an authMachine.js
file in the src/auth
directory.
const config = {
id: 'auth',
// we want to start by checking if
// user is logged in when page loads
initial: 'authenticating',
// context is where you keep your app state
context: {
auth: null,
error: null
},
// all possible authentication states
states: {
authenticating: {},
// we will enrich the user profile
// with additional data
loading: {},
signedIn: {
// when receiving 'LOGOUT' event
// transition to singingOut state
on: { LOGOUT: { target: 'signingOut' } }
},
signedOut: {
// when receiving 'LOGIN' event
// transition to signingIn state
on: { LOGIN: { target: 'signingIn' } }
},
signingIn: {},
signingOut: {}
}
};
That will do for now. But how do we check if the user is authenticated or not? That's where XState's effects come in.
In XState you can invoke different actions when you transition to a state. You can invoke callbacks, promises, observables and even other state machines.
The options are many, but in our example we will use promises to keep things simple. Because everyone knows how promises work, right?
When we want transition to a different state (by sending an event for example) we can invoke a XState service, a function that return promise in our case, with the invoke
property.
When invoking the service XState will pass two arguments to it - machine's context and event that triggered the state transition.
Here is a pseudo-example of it could look for our authenticating
state.
states: {
authenticating: {
// when entering a state invoke
// the service
invoke: {
id: 'checkAuth',
// can be inline or we can refer to it
// by an identifier as we will see later
src: (ctx, event) => {
console.log({ctx, event});
return new Promise((resolve, reject) => {
// simulate a slow auth method
setTimeout(resolve, 1000);
});
},
// called on Promise.resolve()
onDone: {
target: 'signedIn'
},
// called on Promise.reject()
onError: {
target: 'signedOut',
}
}
},
// ...
signedIn: {},
signedOut: {}
}
Are you with me so far? In the example above when our auth machine starts, it will start in the authenticating
state and immediately invoke the promise with the checkAuth
id.
The promise will in this case resolve and therefore call the onDone
hook, which will transition from authenticating
to signedIn
state. In the onDone
hook it will pass in the current app context and event as arguments.
One important concept to understand is that XState uses events to transition from one state to another. They are external and also internal stimulus that we can act on.
If you ever worked with Redux you can think of events as Redux actions. Every event always has a type, its name, and along side with that you can pass in data.
As an analogy we can think of (context, event)
arguments as Redux (state, action)
concept.
Don't know if its a fair comparison, but that's how I see it. More on that later.
To make things more clear let's play around a bit our auth machine in its current state and wire it up to few buttons that we can push.
First, we need to wrap it in a Svelte readable store. Create a useMachine.js
file in src/auth
directory.
// useMachine.js
import { readable } from 'svelte/store';
import { interpret } from 'xstate';
export const useMachine = (config, options) => {
// interpret the machine config
const service = interpret(config, options);
// wrap machine in a svelte readable store with
// initial state (defined in config) as its starting state
const store = readable(service.initialState, set => {
// every time we change state onTransition
// hook is triggered
service.onTransition(state => {
set(state);
});
// start the machine
service.start();
return () => {
service.stop();
};
});
// return a custom Svelte store
return {
state: store,
send: service.send
};
};
Before we can use machine we need to create it. Adjust the authMachine.js
to following.
// authMachine.js
import { createMachine } from 'xstate';
const config = {
// ...
}
export const initAuthMachine = () => createMachine(config);
Now we are ready to use it in the our auth/index.js
file. Let's change it to this for now.
import firebase from 'firebase/app';
import 'firebase/auth';
import { useMachine } from './useMachine';
import { initAuthMachine } from './authMachine';
const userMapper = claims => ({
id: claims.user_id,
name: claims.name,
email: claims.email,
picture: claims.picture
});
export const initAuth = (useRedirect = false) => {
const auth = firebase.auth();
const loginWithEmailPassword = (email, password) =>
auth.signInWithEmailAndPassword(email, password);
const loginWithGoogle = () => {
const provider = new firebase.auth.GoogleAuthProvider();
return useRedirect
? auth.signInWithRedirect(provider)
: auth.signInWithPopup(provider);
};
const authMachine = initAuthMachine();
return useMachine(authMachine);
};
There are a few unused functions, but don't worry, we will put them to use later.
We can finally wire up the machine in our App.svelte
file. You can find the complete setup code for the app and styles for the main file in my previous tutorial on Firebase authentication.
<!-- App.svelte with styles omitted -->
<script>
import firebase from 'firebase/app';
import { initAuth } from './auth';
import { fade } from 'svelte/transition';
const firebaseConfig = {
// vite reads env variables from .env file in root dir
apiKey: `${import.meta.env.VITE_FIREBASE_KEY}`,
authDomain: 'testing-firebase-emulators.firebaseapp.com',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(firebaseConfig);
// user custom auth machine store
const { state, send } = initAuth();
const loginHandler = async event => {
const { email, password } = event.target.elements;
// send login event
send('LOGIN');
};
</script>
<div class="wrapper">
<div class="w-full max-w-xs">
{#if $state.matches('authenticating')}
<h2 class="text-2xl text-center">Authenticating ...</h2>
{/if}
{#if $state.matches('signedIn')}
<div class="text-center">
<button type="button" class="mt-3" on:click={() => send('LOGOUT')}>
Logout
</button>
</div>
{/if}
{#if $state.matches('signedOut')}
<form
on:submit|preventDefault={loginHandler}
class="px-8 pt-6 pb-8 bg-white shadow-md"
>
<div class="mb-4">
<label for="email">Email</label>
<input
class="input-field"
id="email"
type="email"
placeholder="name@acme.com"
/>
</div>
<div class="mb-6">
<label for="password">Password</label>
<input
class="input-field"
id="password"
type="password"
placeholder="******************"
/>
</div>
<div>
<button type="submit">Sign In</button>
</div>
<div class="mt-3">
<button type="button" on:click|preventDefault={() => send('LOGIN')}>
Sign In with Google
</button>
</div>
</form>
{/if}
<!-- debugging: show current state -->
<div class="p-3 mt-5 bg-blue-200">
<pre>{JSON.stringify($state.value, null, 2)}</pre>
</div>
</div>
</div>
You might have noticed that we only have "if" statements in our code. By using XState you can only be in one state at a time. Pretty neat!
We've come far, but without app state our app is useless. Let's do something about it.
Remember that we defined a context
property in our auth machine config? That's where we are going to keep our app state.
For manipulating it, XState has the concept of actions, fire-and-forget side effects.
XState actions are functions that take context and event as arguments. Actions can do many useful things, but you will most likely use actions to manipulate the XState context.
To understand the concept we will continue with a pseudo-example of transitioning from authenticating
state, but instead we will reject the promise and put that error on the context.error
property.
import { assign } from 'xstate';
// ...
states: {
authenticating: {
invoke: {
id: 'checkAuth',
src: (ctx, event) => {
console.log({ ctx, event });
return new Promise((resolve, reject) => {
// simulate a slow auth method
setTimeout(() => reject('wrong password'), 1000);
});
},
// called on Promise.resolve()
onDone: {
target: 'signedIn'
},
// called on Promise.reject()
onError: {
target: 'signedOut',
// assign the error to the `context.error`
// that comes in event.data from the Promise.reject.
// assign is a helper function that you import from XState
// that we will use later
actions: assign({
error: (_, event) => event.data
})
}
}
},
// ...
signedIn: {},
signedOut: {}
}
XState is flexible when it comes to defining its config. You can supply a list of actions and also pass them in a separate external dictionary and refer to them by key name instead of writing them inline.
Actions are kind of hard to understand, but stick with me. Everything will clear as we progress further.
Now that we know how to perform side effects in XState we need to wrap all Firebase authentication methods in XState services and also add actions to manipulate our app state.
Remember that I told that we can supply both actions and services as external objects and refer to them by name in our config? Let's do that.
We will start with XState services. Open the auth/index.js
file and replace initAuth
function with this code.
// auth/index.js
// constructor function. need to call it after we
// initialize our firebase app
export const initAuth = (useRedirect = false) => {
const auth = firebase.auth();
const loginWithEmailPassword = (email, password) =>
auth.signInWithEmailAndPassword(email, password);
const loginWithGoogle = () => {
const provider = new firebase.auth.GoogleAuthProvider();
return useRedirect
? auth.signInWithRedirect(provider)
: auth.signInWithPopup(provider);
};
// define XState services
const services = {
authChecker: () =>
// wrap the onAuthStateChanged hook in a promise and
// immediately unsubscribe when triggered
new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged(auth => {
unsubscribe();
return auth ? resolve(auth) : reject();
});
}),
authenticator: (_, event) => {
if (event.provider === 'email') {
return loginWithEmailPassword(event.email, event.password);
} else if (event.provider === 'google') {
return loginWithGoogle();
}
},
loader: (ctx, _) => {
return new Promise(resolve => {
setTimeout(() => {
ctx.auth
.getIdTokenResult()
.then(({ claims }) => userMapper(claims))
.then(resolve);
}, 1500);
});
},
logout: () => auth.signOut()
};
// pass the services as argument
const authMachine = initAuthMachine(services);
return useMachine(authMachine);
};
It's time to fix our auth machine config.
I can talk a lot about how to adjust our config, but code speaks louder than words, even if it's made up of words.
Here is updated authMachine.js
in all its glory, with comments and all.
// authMachine.js
import { createMachine, assign } from 'xstate';
const config = {
id: 'auth',
// we want to start by checking if
// user is logged in when page loads
initial: 'authenticating',
// context is where you keep state
context: {
auth: null,
user: null,
error: null
},
// all possible authentication states
states: {
authenticating: {
// when entering a state invoke
// the authChecker service
invoke: {
id: 'authChecker',
src: 'authChecker',
onDone: { target: 'loading', actions: 'setAuth' },
onError: { target: 'signedOut' }
}
},
// we will enrich the user profile
// with additional data
loading: {
invoke: {
id: 'loader',
src: 'loader',
onDone: { target: 'signedIn', actions: 'setUser' },
onError: {
target: 'signedOut.failure',
actions: ['setError', 'clearAuth']
}
}
},
signedIn: {
// when receiving 'LOGOUT' event
// transition to singingOut state
on: { LOGOUT: { target: 'signingOut' } }
},
// signedOut has two sub-states
// we will transition to failure in
// case of wrong password, username
// or network error
signedOut: {
initial: 'ok',
states: {
ok: { type: 'final' },
failure: {}
},
on: {
LOGIN: { target: 'signingIn' }
}
},
signingIn: {
invoke: {
id: 'authenticator',
src: 'authenticator',
onDone: {
target: 'authenticating',
// clear error if successful login
actions: 'clearError'
},
onError: {
// transition to failure state
// and set an error
target: 'signedOut.failure',
actions: 'setError'
}
}
},
signingOut: {
invoke: {
id: 'logout',
src: 'logout',
onDone: {
target: 'signedOut',
actions: ['clearAuth', 'clearError']
},
onError: {
target: 'signedOut.failure',
actions: ['clearAuth', 'setError']
}
}
}
}
};
export const initAuthMachine = services => {
// define XState actions so that we can
// refer to them by name in machine config
const actions = {
// clear user info on logout
clearAuth: assign({ user: null, auth: null }),
clearError: assign({ error: null }),
// put Firebase auth object on context
setAuth: assign({ auth: (_, event) => event.data }),
// put user on context in loading service
setUser: assign({ user: (_, event) => event.data }),
setError: assign({
error: (_, event) => event.data
})
};
// create an options object containing
// actions and services
const options = {
actions,
services
};
return createMachine(config, options);
};
And this is what it looks like in the XState's visualizer.
If you start at the authenticating
state, and follow a path, it will all make sense. Notice that in configuration we refer to our services and actions by their names (keys) instead of writing them inline.
One thing to know is that if you have a transition with no action you can omit the target
keyword and just use a string. XState is flexible, but in my opinion, it's good to be consistent and not take any shortcuts.
You can also see that we supplied an options
object with our actions and services to createMachine
functions. XState has plenty of different options to create your machines.
Just like in Redux, you define an action, but it's called event in this case.
In the event you can also supply event data. Here is the link to the XState documentation that explains it in great detail.
Below are a few examples of XState events.
// simplest case possible
send('LOGIN');
// same as above, this is what XState tranforms to internally
send({ type: 'LOGIN'});
// provide an object payload
send('LOGIN', { provider: 'google'});
// or
send({type:'LOGIN', provider: 'google'});
qend('LOGIN', { provider: 'email', email: 'john@example.com', password: 'qwerty' });
It's crucial that you understand the concept of XState events, because they are the base of everything in XState.
Empowered by our new knowledge, let's do a final adjustment to our UI in App.svelte
. We already logic for state management in place. We only need to adjust our handlers with correct events and also take care of error handling.
Here is the final version of App.svelte
sprinkled with comments.
<!-- App.svelte -->
<style lang="postcss">
label {
@apply block mb-2 text-sm font-bold text-gray-700;
}
.input-field {
@apply border w-full py-2 px-3 text-gray-700 mb-3;
}
.input-field:focus {
@apply shadow-outline outline-none;
}
button {
@apply w-full px-4 py-2 font-bold text-white bg-blue-500 rounded-sm;
}
button:hover {
@apply bg-blue-700;
}
button:focus {
@apply outline-none shadow-outline;
}
.wrapper {
@apply flex flex-grow h-screen justify-center items-center bg-blue-100;
}
</style>
<script>
import firebase from 'firebase/app';
import { initAuth } from './auth';
import { fade } from 'svelte/transition';
const firebaseConfig = {
// vite reads env variables from .env file in root dir
apiKey: `${import.meta.env.VITE_FIREBASE_KEY}`,
authDomain: 'testing-firebase-emulators.firebaseapp.com',
projectId: 'testing-firebase-emulators'
};
firebase.initializeApp(firebaseConfig);
// use custom auth machine store
const { state, send } = initAuth();
const loginHandler = async event => {
const { email, password } = event.target.elements;
// send login event
send('LOGIN', {
provider: 'email',
email: email.value,
password: password.value
});
};
// we don't want to be explicit about signingIn state
$: displayLoginForm = ['signingIn', 'signedOut'].some($state.matches);
</script>
<div class="wrapper">
<div class="w-full max-w-xs">
{#if $state.matches('authenticating')}
<h2 class="text-2xl text-center">Authenticating ...</h2>
{/if}
{#if $state.matches('loading')}
<h2 class="text-2xl text-center">Loading ...</h2>
{/if}
<!-- uncomment if you want to be explicit about signingIn state -->
<!--
{#if $state.matches('signingIn')}
<h2 class="text-2xl text-center">Signing in ...</h2>
{/if}
-->
{#if $state.matches('signedIn')}
<div class="text-center">
<h2 class="text-2xl">{$state.context.user.email}</h2>
<button type="button" class="mt-3" on:click={() => send('LOGOUT')}>
Logout
</button>
</div>
{/if}
{#if displayLoginForm}
<form
on:submit|preventDefault={loginHandler}
class="px-8 pt-6 pb-8 bg-white shadow-md"
>
<div class="mb-4">
<label for="email">Email</label>
<input
class="input-field"
id="email"
type="email"
placeholder="name@acme.com"
/>
</div>
<div class="mb-6">
<label for="password">Password</label>
<input
class="input-field"
id="password"
type="password"
placeholder="******************"
/>
</div>
<!-- show auth errors -->
{#if $state.context.error}
<div class="bg-red-500 p-3 mb-6" transition:fade>
{$state.context.error}
</div>
{/if}
<div>
<button type="submit">Sign In</button>
</div>
<div class="mt-3">
<!-- tell auth machine that this is a Google login -->
<button
type="button"
on:click|preventDefault={() => send('LOGIN', {
provider: 'google'
})}
>
Sign In with Google
</button>
</div>
</form>
{/if}
</div>
</div>
If you look carefully you will see that we added data to our events and we also show an error to the user in case of authentication failure.
Notice that we communicate with our machine by sending events. The logic is encapsulated in the machine itself.
I don't know about you, but I actually find it quite beautiful.
While our states are now explicit, I am struggling to show user correct information (or state transitions).
But this is not a code problem, only my lack of UX skills.
To compensate for it I created a reactive variable displayLoginForm
. That way login form is visible even in the signingIn
state.
This was a fun and educating experience. We achieved our goal and have a fully functioning Firebase authentication solution.
There are many ways to structure your application state with XState and if you are not careful you will end up with an unmanageable config that is hard to understand. The key is to write small, single purpose machines.
XState is also flexible in how you define your state machines. Its flexibility is also one of its weaknesses, because it's easy to get lost in all the options available and what to use where. It's easy to get into an analysis paralysis state, especially when you are just starting out learning the library.
With that said, I am happy with results and look forward to exploring XState further. Hope you do too!
Here is the code to the final solution on Github.
https://github.com/codechips/svelte-firebase-auth-xstate-example
Thanks for reading!
]]>Yes, even promises are new. Javascript use to be a verbose language with lots of quirks and it still is. For example, are you sure you understand how this
works? Do you know why you sometimes need to use bind
method? What about scoping and hoisting?
I started programming in Javascript professionally when jQuery was cool, Node.js was just announced on HackerNews and all the cool devs were working with Backbone.js.
It was not all roses and sunshine. Trust me. Callback Hell and Pyramid of Doom were real. Sometimes I dreamed about double-callbacks at night. It was very hard following the code at times.
Thankfully, things have greatly improved since, but we as developers always take things for granted without giving them any thought or appreciation.
That's why I decided to show what Javascript used to look like back in the days, (old school) and how much it has evolved in just a few years (new school).
Without further ado, here are nine of the ES features I am glad exist.
There features save my fingers from hurting and make my code more concise and easier to understand.
Read on to learn them all!
This is probably the ES feature I use and love the most. It makes code so much easier to read in my opinion.
// old school
function fullname(user) {
return `${user.firstName} + ${user.lastName}`;
}
// or
const fullname = function () {
return `${user.firstName} + ${user.lastName}`;
}
document.addEventListener('click', function (event) {
alert('old school');
});
// new school
const fullname = user => `${user.firstName} + ${user.lastName}`;
document.addEventListener('click', event => {
alert('new school');
});
// or just this
document.addEventListener('click', () => alert('new school'));
Note: Something to keep in the back of the head, when using arrow functions, is that this
works just as you expect it to inside them. Or maybe better to say: arrow functions don't have their own this
. It's taken from the outer scope instead. Sometimes it's actually not what you want.
Pretty neat and much easier to read. Don't you agree?
Default parameters is a real life-saver sometimes. A simple code example is the best explanation.
// old school
const createUser = function (firstName, lastName, role) {
return {
firstName: firstName,
lastName: lastName,
role: role || 'user'
};
}
// new school
const createUser = (firstName, lastName, role = 'user') => ({
firstName,
lastName,
role,
});
Less code, less logic and reads much better, doesn't it?
This is a really beautiful ES feature. It works with objects, arrays and function parameters.
Desctructuring lets you extract one or more properties from arrays and objects into own variables.
const user = { firstName: 'Jane', lastName: 'Doe', role: 'admin' };
// extract role property to a variable
const { role } = user;
console.log(role);
// admin
You have probably used it many times in your import
statements without giving it much thought.
You can use destructuring with arrays too. If you worked with React hooks, then you have definitely used it.
// Naïve state hook implementation
// Don't use in production!
const state = {};
const useState = (name, initial) => {
state[name] = initial;
// return an array
return [
state[name],
value => (state[name] = value)
];
};
// returns an array
const loggedInState = useState('loggedIn', false);
// old school
const isLoggedIn = loggedInState[0];
const setLoginStatus = loggedInState[1];
// new school
// assign array[0] and array[1] to variables
const [isLoggedIn, setLoginStatus] = useState('loggedIn', false);
if (isLoggedIn) {
setLoginStatus(false);
}
It works with function parameters too. Very nice!
// instead of this
let isAdmin = user => user.role === 'admin';
// you can do this
isAdmin = ({ role }) => role === 'admin';
// and also combine it with default parameters
isAdmin = ({ role = 'user' }) => role === 'admin';
const user = { uid: 'jdoe', role: 'admin' };
console.log(isAdmin(user));
// true
There are other useful things you can do with desctructuring, such as rest parameters. I encourage you to explore it further on your own. You will find a list of resources at the end of the article.
The name "object literal" says nada. That's why I like to call it object property initializer shorthand. Maybe that's even its official name. Not sure.
Anyhow, it's one of the features that can save you a lot of repetitive typing. If the variable name and property name are the same you only have to specify variable name when creating an object's property.
// old school
function createUser(firstName, lastName, dob) {
const id = generateId();
return {
id: id,
firstName: firstName,
lastName, lastName,
dob: dob
};
}
// new school
const createUser = (firstName, lastName, dob) =>
({ id: generateId(), firstName, lastName, dob });
Just look at how many potential typos and untyped characters we just saved!
You can also use the shorthand operator with methods on the object.
// old school
function createUser(firstName, lastName, dob) {
return {
firstName: firstName,
lastName, lastName,
dob: dob,
fullName: function () {
return firstName + ' ' + lastName;
}
};
}
// new school
const createUser = (firstName, lastName, dob) => ({
firstName,
lastName,
dob,
fullName() {
return `${this.firstName} ${this.lastName}`;
},
});
I don't know about you, but I find this feature very useful.
I've already sneaked in default returns in some of the examples above, but it's not something that existed in Javascript before. Believe it or not.
const arr = [1, 2, 3, 4, 5];
// old school
const doubled = arr.map(function (val) {
return val * 2;
});
// new school with default implicit return
const tripled = arr.map(val => val * 3);
Default returns also lets you return objects directly.
// old school
function createUser(firstName, lastName) {
return {
firstName: firstName,
lastName: lastName,
role: 'user'
};
}
// new school
const createUser = (firstName, lastName) => ({ firstName, lastName });
Don't take simple things like this for granted. Be thankful for all the typing they save you in the long run.
I actually not sure if it's an operator or a syntax change, but I use it all the time.
It's a very useful feature and if you have worked with Redux or any other modern Javascript library you have probably used the pattern, or operator, or syntax.
It often used to create shallow copies of objects and arrays, or to merge objects or arrays.
Let's start with the arrays, since A comes first in the English alphabet.
const one = ['a', 'b', 'c'];
const two = ['d', 'e', 'f'];
// old school
// copy array
const copy = one.slice(0);
// combine arrays
const combined = one.concat(two);
// new school
// copy array
const copy = [...one];
// combine arrays
const combined = [...one, ...two];
Now, here is what we can do with spread operator and objects.
let user = { uid: 'jdoe', name: 'Jane Doe' };
let status = { loggedIn: true };
// create a new object by combining other objects
// properties with the same name will be overwritten
// by the last object in argument chain
let state = Object.assign({}, user, status, { createdAt: Date.now() });
// or by simply using the spread operator
state = { ...user, ...status, createdAt: Date.now() };
Spread operator (or syntax) is very versatile. I only scratched the surface of what it can do.
If you learn the inside and outs of it, you will have a powerful tool in your toolbox.
How could I even work effectively before it existed? I actually don't remember right now, but I suspect it was painful.
Promises are also new to Javascript and they might be the most significant new addition to the language. Before promises we had callbacks, which could lead to Callback Hell and the dreaded Pyramid of Doom.
Promises improved that, but I still found them a little hard to read. Today, I often find myself reaching for the async/await pattern, which makes my code easy to read and reason about.
Plus, async/await pattern can make the code easier to structure.
// fake function to return a user by id
const fetchUser = id => new Promise((resolve, reject) => {
if (id === 1) {
resolve({ id: 1, username: 'jdoe' });
} else {
reject(new Error('no such user'));
}
});
// the promise way
const getUserInfo = id =>
fetchUser(id)
.then(user => fetchInfomation(user))
.then(userInfo => {
if (userInfo.address) {
// how do we get user? we need to pass it down somehow
enrichProfile(user, userInfo);
}
if (userInfo.age && userInfo.age < 18) {
// hmm ... how do we access user here?
addAgeRestrictions(user);
}
})
.catch(err => console.log(err));
// the async/await way
const getUserInfo = async id => {
// error handling with try/catch blocks
try {
const user = await fetchUser(id);
const userInfo = await fetchInformation(user);
// both user and userInfo are available in the function scope
if (userInfo.address) {
enrichProfile(user, userInfo);
}
if (userInfo.age && userInfo.age < 18) {
addAgeRestrictions(user);
}
} catch (err) {
console.log(err.message);
throw err;
}
}
As you can see above, prefixing your function with async
keyword and using await
to get the promise result can make your code easier to structure, read and reason about.
This new feature is my recent favorite as it saves me a lot of typing. It actually comes from Typescript, but has now been accepted into ECMAScript standard spec.
You will not get runtime errors if some deeply-nested property does not exist on the object.
Errors like the one below, that you have probably seen many times before.
const customer = { name: 'Jane' };
console.log(customer.address.doorCode);
// Uncaught TypeError: Cannot read property 'doorCode' of undefined
Here is a better example of what this operator brings to the table (or screen).
// old school
const sendInstructions = function (customer) {
// old, cumbersome way to check for a property
const hasDoorCode =
customer && customer.address && customer.address.doorCode;
if (hasDoorCode) {
messageToCourier(carryIn());
}
}
// new school
const sendInstructions = customer => {
// optional chaining operator at work
const hasDoorCode = customer?.address?.doorCode;
if (hasDoorCode) {
messageToCourier(carryIn());
}
}
Simply explained, the optional chaining operator short circuits and returns undefined
for the first non-existent property.
This operator with a weird name is new, so thread carefully and use a bundler setup that supports it.
What it does it to strictly check if the value is either null
or undefined
. Why is that important? Because one of Javascript's quirks is that many values can be falsy or truthy. Empty string, zero and boolean false
are all falsy values, so is undefined
and null
.
Sometimes we need to strictly check if value is null
or undefined
and not only falsy.
Let me demonstrate with a few examples to make it more clear.
const user = {
name: 'John Doe',
settings: { showWelcomeScreen: false, animation: 0 },
};
// old school
// the actual value is false, but it incorrectly defaults to true,
// which can lead to hard to find bugs
const showWelcomeScreen =
(user && user.settings && user.settings.showWelcomeScreen) || true;
// the animation value is actually 0, but we incorrectly set it to 100
// since 0 is a falsy value in Javascript
const duration = (user && user.settings && user.settings.animation) || 100;
// new school
// this behavior is correct. We now only set a value if
// the property is null or undefined
const showWelcomeScreen = user?.settings?.showWelcomeScreen ?? true;
const animation = user?.settings?.animation ?? 100;
Even though this operator is very new, I find myself using it more often. It's really useful when dealing with user-controlled settings in user interfaces.
There you go. 9 Javascript features I am glad exist.
Of course, there are many more new cool and useful ES features, but these are the ones I use the most.
I strongly recommend you to get familiar with them all. They will make you level-up your game and help you impress your colleagues.
If you want to dig deeper, here are some great learning resources:
An ECMAScript explainer - ECMAScript spec, that Javascript is based on, is constantly evolving. If you are curious about how Javascript versioning works and what feature requests get accepted, here is a great article explaining the process.
MDN - Many great in-depth tutorials from the trusty Mozilla Developer Network.
javascripttutorial.net - another great resource. More like a reference with good examples than a tutorial.
ES6+ Cheatsheet - sweet, short and good looking reference from devhints.io.
Learn ES2015 (ES6) - dense, but good tutorial from Babel.js that covers many modern JS features.
Even though these new syntax features and operators make your code more concise, always remember the trade-off between conciseness and readability. It's you who might be reading the code in the future.
]]>First up is Vite. A young bundler from the creator of the popular Vue.js framework, Evan You. I've heard many good things about Vite and decided to try it out.
I am on the quest to find the best bundler setup for Svelte development. My requirements are simple.
Let's proceed with this list as our benchmark for a good Svelte bundler setup.
For the purpose of testing I created a simple Svelte app. Its functionality is simple. You press a button and it fetches a random Kanye West tweet from Kanye as a Service.
The app might be simple, maybe even naïve, but it has a few interesting parts.
With that checklist in place, let's proceed and see if Vite can handle all our requirements.
Although, I built the app specifically for testing different Svelte bundlers, I will walk you though how to set up Vite from scratch using a simpler Svelte app as an example.
As I write this Vite haven't had an official release yet, but it's nearing one. Currently it's on 1.0.0-rc.4
. There are probably still a few wrinkles to iron out.
Vite is not a traditional bundler, like Webpack or Rollup, but an ESM bundler.
What does it mean? It means that it serves all your files and dependencies via native ES modules imports that most modern browsers support. This means superfast reloads during development as only file that was changes needs to be recomplied.
It comes with "batteries included", meaning it has sensible defaults and supports many features that you might need during development.
Vite, just like Snowpack, is using ESBuild as its Typescript compiler.
If you want to know more details about please read the How and Why section in Vite's README.
This can be a little confusing to many. Why should you use an ESM bundler instead of a traditional one line Webpack or Rollup?
There is an option to create Vite backed apps with create-vite-app command, but as of now there is no Svelte template, so we will setup everything manually for now. I will try to find some time to create a Svelte template based on my findings.
For my examples I will use pnpm, a fast and disk space efficient package manager, but all the commands apply to npm
as well.
Let's get cranking!
First, we need to initialize our project and add Vite. Here are the steps.
$ mkdir vite-svelte-typescript
$ cd vite-svelte-typescript
$ pnpm init -y
$ pnpm add -D vite
Now we need to add an index.html
file and a src
directory where we will be keeping our app's source files.
Create a src
directory and add an index file in the root directory with the following contents.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>
This file will be used by Vite as entry or template to our app. You can add anything you want there. Just make sure to point the entry JS/TS file to your app's main file.
You configure Vite by creating a vite.config.js
in the root directory. It's in there that you can change Vite's dev server port and set many other options.
The configuration documentation is lacking behind at the moment. The best place to see what options are available is to look at Vite's config source.
We don't have anything to configure yet, so we will postpone this task for now.
We are building a Svelte app, so we need to tell Vite how to deal with Svelte files. Luckily, there is a great Vite Svelte plugin we can use - vite-plugin-svelte. Install the plugin and also the Svelte framework.
$ pnpm add -D vite-plugin-svelte svelte
The time has come to write some Vite configuration. We will just follow recommendation from the plugin's README file. Edit your vite.config.js
and add the following.
// vite.config.js
import svelte from 'vite-plugin-svelte';
export default {
plugins: [svelte()],
rollupDedupe: ['svelte']
};
Let's test drive it by creating the simplest Svelte app possible.
First, create an App.svelte
file in the src
directory with the following contents.
<!-- App.svelte -->
<h1>Hello Svelte!</h1>
Now, create the main app entry file index.js
, also in the src
directory, with the following contents.
// main.js
import App from './App.svelte';
// import './index.css';
const app = new App({
target: document.getElementById('app')
});
export default app;
Start the app by running pnpx vite
and open the browser on localhost:3000
.
Bam! Now Vite knows what Svelte is. While we are on it, let's tackle the Typescript and Svelte part next.
Vite has Typescript support out of the box for normal Typescript files. As many other modern ESM bundlers it uses esbuild which is written in Golang and is very fast. It's fast because it performs only transpilation of .ts files and does NOT perform type checking. If you need it, you must to run tsc --noEmit
in the build script. More on that later.
If you ask me, a better choice would have been SWC compiler. It's written in Rust, is just as fast and handles things a little better than ESBuild.
Let's add a simple timer written in Typescript and use it in our file.
// timer.ts
import { readable } from 'svelte/store';
export const enum Intervals {
OneSec = 1,
FiveSec = 5,
TenSec = 10
}
export const init = (intervals: Intervals = Intervals.OneSec) => {
return readable(0, set => {
let current = 0;
const timerId = setInterval(() => {
current++;
set(current);
}, intervals * 1000);
return () => clearTimeout(timerId);
});
};
We are using enums, a Typescript feature, in order to not get any false positives.
Let's add it to our App.svelte
file.
<!-- App.svelte -->
<script>
import { init } from './timer';
const counter = init();
</script>
<h1>Hello Svelte {$counter}!</h1>
Yep. Seems to work. Vite transpiles Typescript files to Javascript using ESBuild. It just works!
When it comes to various template and languages support in Svelte files, svelte-preprocess is king. Without this plugin, Typescript support in Svelte would not be possible.
Simply explained, svelte-preprocess works by injecting itself as a first-in-line preprocessor in the Svelte compilation chain. It parses your Svelte files and depending on the type delegates the parsing to a sub-processor, like Typescript, PostCSS, Less or Pug. The results are then passed on to the Svelte compiler.
Let's install it and add it to out setup.
$ pnpm add -D svelte-preprocess typescript
We need to change out vite.config.js
and add the svelte-preprocess
library.
// vite.config.js
import svelte from 'vite-plugin-svelte';
import preprocess from 'svelte-preprocess';
export default {
plugins: [svelte({ preprocess: preprocess() })],
rollupdedupe: ['svelte']
};
And change our App.svelte
to use Typescript.
<!-- App.svelte -->
<script lang="ts">
import { init, Intervals } from './timer';
const counter = init(Intervals.FiveSec);
</script>
<h1>Hello Svelte {$counter}!</h1>
We initialized our counter with 5s interval and everything works as advertised. svelte-preprocess
for president!
Notice how little configuration we have written so far. If you ever worked with Rollup, you will definitely notice!
If your editor shows syntax errors it's most likely you forgot to add svelte.config.js
.
const preprocess = require('svelte-preprocess');
module.exports = { preprocess: preprocess() };
This configuration file is still a mystery to me, but I know that it's used by the Svelte Language Server which is used in VSCode Svelte extension and at least one other bundler - Snowpack.
There are actually two parts of working with PostCSS in Vite and Svelte. Fist one is the Vite part. Vite has out-of-the-box support for PostCSS. You just need to install your PostCSS plugins and setup postcss.config.js
file.
Let's do that. Let's add PostCSS and Tailwind CSS support.
$ pnpm add -D tailwindcss && pnpx tailwindcss init
Create a PostCSS config with the following contents.
module.exports = {
plugins: [require("tailwindcss")]
};
And add base Tailwind styles by creating an index.css
in the src
directory.
/* index.css */
@tailwind base;
body {
@apply font-sans bg-indigo-200;
}
@tailwind components;
@tailwind utilities;
Last thing we need to do is to import index.css
in our main index.js
file.
// index.js
import App from './App.svelte';
import './index.css';
const app = new App({
target: document.getElementById('app')
});
export default app;
If you did everything right the page background should have a light indigo background.
When it comes to Svelte and PostCSS, as usual, svelte-preprocess
is your friend here. As Vite, it has support for PostCSS.
However, we need to tweak the settings a bit as it doesn't work out of the box.
According to the svelte-preprocess documentation you can do it in two ways. Specify PostCSS plugins and pass them to the svelte-preprocess as arguments or install a postcss-load-conf
plugin that will look for an existing postcss.config.js
file.
This seems like the best option. I mean, we already have an existing PostCSS configuration. Why not (re)use it?
Let's install the postcss-load-conf library.
$ pnpm add -D postcss-load-conf
We also need to tweak our vite.config.js
again.
import svelte from 'vite-plugin-svelte';
import preprocess from 'svelte-preprocess';
export default {
plugins: [svelte({ preprocess: preprocess({ postcss: true }) })],
rollupdedupe: ['svelte']
};
Let's test it by adding some Tailwind directives to the style tag in App.svelte
.
<!-- App.svelte -->
<script lang="ts">
import { init, Intervals } from './timer';
import logo from './assets/logo.svg';
const counter = init(Intervals.FiveSec);
</script>
<style lang="postcss">
h1 {
@apply text-5xl font-semibold;
}
</style>
<h1>Hello Svelte {$counter}!</h1>
Yep. Works fine. Notice that I added lang="postcss"
to the style tag in order to make the editor happy.
Vite has built-in support for importing JSON and CSS files, but what about other assets like images and SVGs? It's possible too.
If you import an image or an SVG into your code it will be copied to the destination directory when bundling for production. Also, image assets smaller than 4kb will be base64 inlined.
Let's add an SVG logo to our App.svelte
.
<!-- App.svelte -->
<script lang="ts">
import { init, Intervals } from './timer';
import logo from './assets/logo.svg';
const counter = init(Intervals.FiveSec);
</script>
<style lang="postcss">
h1 {
@apply text-5xl font-semibold;
}
</style>
<h1>Hello Svelte {$counter}!</h1>
<img class="w-64 h-64" src={logo} alt="Svelte Logo" />
However, in our case, since we are using Typescript in Svelte we will get a type error. That's because Typescript doesn't know what an SVG is. The code will still work, but it's annoying to see this kind of error in the editor.
We can fix this by adding a Typescript type declaration file for most common asset types. While we are on it we can create a Typescript config file. It's actually not needed by Vite, because it does not do any typechecking, only transpiling, but it's needed for the editor and also for our TS type checker that we will setup later.
First, install the common Svelte Typescript config.
$ pnpm add -D @tsconfig/svelte
Next, create a tsconfig.json
in the root directory of the project.
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "dist"]
}
Last thing we need to do is to add a Typescript declaration file in the src
directory. The name is not important, but it should have a .d.ts
extention. More of a convention than a requirement.
I named mine types.d.ts
.
// types.d.ts - "borrowed" from Snowpack
declare module '*.css';
declare module '*.svg' {
const ref: string;
export default ref;
}
declare module '*.bmp' {
const ref: string;
export default ref;
}
declare module '*.gif' {
const ref: string;
export default ref;
}
declare module '*.jpg' {
const ref: string;
export default ref;
}
declare module '*.jpeg' {
const ref: string;
export default ref;
}
declare module '*.png' {
const ref: string;
export default ref;
}
declare module '*.webp' {
const ref: string;
export default ref;
}
If you did everything correctly you should not see any errors in your editor.
It pretty common to make use of the environment variables in your code. While developing locally you might want to use a development API instance for your, while in production you need to hit the real API.
Vite supports environment variables. They must however be prefixed with VITE_
. Vite support many ways to import your environment variables through different .env
file. You can read more about it here.
For the sake of demonstration, let's setup and require and use an environment variable in our code.
Create an .env.local
file with the following contents.
VITE_KANYE_API=https://api.kanye.rest
We now need to import it in our app. The way you do it is through import.meta.env
object.
<!-- App.svelte -->
<script lang="ts">
// import meta.env types from vite
import type {} from 'vite';
import { init, Intervals } from './timer';
import logo from './assets/logo.svg';
const counter = init(Intervals.FiveSec);
const KANYE_API = import.meta.env.VITE_KANYE_API;
console.log(KANYE_API);
</script>
<style lang="postcss">
h1 {
@apply text-5xl font-semibold;
}
</style>
<h1>Hello Svelte {$counter}!</h1>
<img class="w-64 h-64" src={logo} alt="Svelte Logo" />
If you open you dev tools you should see it printed in console.
Getting everything to compile and start is one thing. Getting your development environment to run smoothly is another.
Let's spend a few minutes to set it up.
We already have everything we need to typecheck our Typescript files. This should be done outside Vite by running tsc --noEmit
.
Svelte has this cool CLI app called svelte-check. It's very good at catching all types of errors and warnings in your Svelte files.
Last step is to put everything together. For that purpose we will use npm-run-all package. It will help us run npm scripts in parallel.
First, let's install the missing tools. While we are on it we will install a few other helpful utilities too that we will use.
$ pnpm add -D npm-run-all svelte-check cross-env sirv-cli
Replace the scripts
property in package.json
with the following object.
{
"dev": "vite",
"compile": "cross-env NODE_ENV=production vite build",
"check": "svelte-check --human && tsc --noEmit",
"watch:svelte": "svelte-check --human --watch",
"watch:ts": "tsc --noEmit --watch",
"start": "run-p watch:* dev",
"build": "run-s check compile",
"serve": "sirv dist"
}
Now you can simply run pnpm start
and it will start local development server and also continuously lint our Svelte and Typescript files.
When you are done just run pnpm run build
. Your app will be linted before it's compiled.
If you want to compile and serve the app in production mode just issue pnpm run build serve
.
For production bundling Vite is using Rollup, which is known for creating very efficient bundles, so you are in save hands.
When it comes to code you don't have to configure anything special. It just works.
But we need to tell Tailwind to purge our unused styles. You do it in tailwind.config.js
file.
// tailwind.config.js
module.exports = {
purge: ['./src/**/*.svelte', 'index.html'],
theme: {
extend: {}
},
variants: {},
plugins: []
};
Now both our app and styles will be mean and lean. Here are some stats from my test app.
[write] dist/_assets/index.03af5881.js 32.03kb, brotli: 9.50kb
[write] dist/_assets/style.89655988.css 6.37kb, brotli: 1.67kb
[write] dist/_assets/usa.29970740.svg 0.88kb
[write] dist/index.html 0.41kb, brotli: 0.17kb
Build completed in 5.17s.
When bundling for production Vite injects CSS and JS tags into index.html
automatically. However, it leaves the script tag as type="module
. Thread carefully if you need to support old browsers.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" href="/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<link rel="stylesheet" href="/_assets/style.89655988.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/_assets/index.03af5881.js"></script>
</body>
</html>
Right. Svite is a Svelte specific bundler that's built on top of Vite. You should definitely check it out. It's great!
Let's re-visit our list of requirements.
My goal was to see how good Vite is for Svelte development and also to show you how to setup an efficient local development environment.
I must say that I am happy with results. That happy, that I even dare to ask whether not Vite is currently the best bundler for Svelte.
If you have made it so far, you should not only learned about Vite, but also about how to effectively setup your development environment. Many of the things we went through apply to many different bundlers, not only Vite.
Vite is built by the creator of Vue.js. Although, it's a framework agnostic bundler, you can tell that it's probably has a tighter connection to Vue. You can find Vue specific things sprinkled here and there.
What I like most about Vite is its speed and flexibility. It has sensible default configuration options that are easy to change. I was also surprised how little configuration I had to write!
Probably the best thing is that Vite uses Rollup for creating production bundles. I've learned to trust Rollup by now after testing many different module bundlers.
You can find the full app setup on Github. Watch this repo as I test more bundlers for Svelte development.
https://github.com/codechips/svelte-typescript-setups
Thanks for reading and happy coding!
]]>Hi Ilia,
I found your blog about Svelte and Js development really well written.
It seems that you’re liking Svelte a lot. My company uses VueJS for now but was trying to use Nuxt because of the SSR aspects.
However I found the SSR part to be brittle, like if it was an add on not well thought out.
Would you recommend Svelte and what Router do you like? We’re a small team of 3 so we do need some structure and some “best practices “.
Laurent
Hello Laurent,
And thank you for the kind words! I will try to answer your question in detail. These are, of course, my personal thoughts.
Correct, I like Svelte because it "fits" in my head. Its simplicity is what attracted me in the first place. I've used both React and Vue at work. While both are fine, we never clicked. Call me old school, but while I understand the concept and the need behind JSX, I find it weird. It's not for me. The Vue model fits "my style" better than React, but I find some of its design and architecture decisions weird. But let us not deviate from your question!
Second reason I like Svelte is that it doesn't use virtual DOM, which I always thought is pure overhead. Svelte is not a framework, but a compiler, right? You can do so much more with a compiler, than with a virtual machine, which React actually is. For example, one obvious thing is that a compiler catches compile time errors. Add Typescript to the mix and you just halved your potential runtime errors. Also, with compiler you end up with smaller bundles, as all the code is optimized at compile time and you don't have to bake in a Virtual DOM machine.
Now, back to the first part of your question - Would you recommend Svelte?
Definitely! It's stable and is used successfully in production by many companies. Now when it finally has official Typescript support hopefully more developers will start taking it seriously.
One big concern I often hear is that Svelte has a small ecosystem. By that I mean supporting tools and libraries. That's a valid point, Svelte is still young, but I found that I actually don't need and use many external Svelte libraries in my projects. Why? Because there's often no need. Svelte has a lot of common features built-in, its state management is simple, and you can easily integrate any external Javascript library you need. In Svelte, you are often not constrained by a framework specific code or syntax, it's just plain Javascript.
And for the second part of your question - What router do you like?
I am a big fan of DYI, but up to a certain degree. If something takes me a few hours to implement, I will often do it instead of using some library. For my projects I use Page.js as my router. It's a battle-tested library that I've used before and that I trust. I've created a simple Svelte wrapper on top of it that fits my needs. If you are interested, here is the link to the article - Svelte routing with Page.js, Part 1.
Routify seems to be a popular option, but I haven't had the chance to try it yet. Spontaneously, I don't like a library that forces you to use it in a certain way. With Routify you have to start your app from Routify, so to say. I understand the reason behind. It's a file-based router, it needs to use the file system in order to build up its router tree. Routify also supports SSR if that's important to you.
With that said, one Svelte router I've been digging lately is svelte-navigator. It's actively maintained, has an intuitive API and seems to be feature complete. Check it out!
Just like Vue has Nuxt and React has Next, Svelte also has an official SSR solution - Sapper. But Sapper's development haven't been very active for the past year. It's currently on version 0.28, but looks like the development is picking up again. I don't know when and if it will reach version 1.0, but expect a few breaking API changes on the way. With that said, many companies are already using it successfully in production today.
Let's talk a bit about SSR vs SPA debate. Why and when should you use SSR instead of a SPA?
In my opinion, if you are building an app that needs to be crawlable by search bots, SSR is the way to go. Blogs, forums and news apps come to mind. The rest is a grey area.
There are pros and cons when it comes to SSR apps. Sapper in Svelte's case.
Some of the pros:
Some of the cons:
What about SPA's pros and cons?
Some of the pros:
Some of the cons:
I just realized that this was a very long answer to your email, so I will stop here, even though I love talking about these topics!
In summary:
Good luck! I am sure you will have lots of fun and "no, it can't be that easy" moments on the way if you choose Svelte!
All the best, Ilia
]]>I needed to integrate PostCSS together with Sapper. Meanwhile it's not so hard to integrate plain Tailwind CSS into Sapper, turns out integrating PostCSS together with TailwindCSS requires a little more work. After trying a few different approaches I finally landed on something that works for me.
Plain CSS can take you far, but I often prefer to use Tailwind CSS. I find it really nice to work with declarative CSS instead of writing everything from scratch. I like Tailwind as it is, but I often use a few other PostCSS plugins too that help me work with Tailwind CSS more efficiently. Maybe a better word would be "augment" and not "help."
Sapper has an internal router built-in. Which is helpful. The router intercepts all link clicks and fetches each page individually when you visit it. When you click on the link that leads to another page in your app, Sapper will fetch the page in the background and replace the content in your Sapper app.
It will actually put the content into the slot
in the src/routes/_layout.svelte
page. That's how it's setup in the official boilerplate at least.
Sapper injects the styles for different components and pages when you navigate between the pages. When you visit a page, Sapper will fetch that page and also inject the style for that page and for the components it uses into the head tag of the document.
Sapper and Svelte scope CSS classes defined in the components to the components themselves, reducing the risk of overriding the CSS.
To understand more read the blog post The zen of Just Writing CSS.
It's actually a really nice feature that you get out of the box in Svelte! You can see that by inspecting elements in the dev tools console. Each styled element will have a svelte-[hash]
class defined on it.
After wrestling with rollup-plugin-postcss for some time, I gave up and went with the simplest setup possible.
Instead of trying to integrate PostCSS into Rollup itself, I moved PostCSS processing outside of Rollup's pipeline. It's fast too, because the processing is done outside Rollup.
Here is how I did it.
In order to fully understand what's needed, we will start from scratch by creating a standard Sapper project.
$ npx degit sveltejs/sapper-template#rollup sapper-with-postcss
$ cd sapper-with-postcss && npm i
You can now start the app by running npm run dev
.
Let's add Tailwind and Tailwind's typography plugin that we will use to style the blog posts.
$ npm add -D tailwindcss @tailwindcss/typography
$ npx tailwindcss init
We now need to replace Tailwind's configuration file with this.
// tailwind.config.js
module.exports = {
future: {
removeDeprecatedGapUtilities: true,
},
experimental: {
uniformColorPalette: true,
extendedFontSizeScale: true,
applyComplexClasses: true,
},
purge: {
// needs to be set if we want to purge all unused
// @tailwind/typography styles
mode: 'all',
content: ['./src/**/*.svelte', './src/**/*.html'],
},
theme: {
container: {
center: true,
},
extend: {},
},
variants: {},
plugins: [require('@tailwindcss/typography')],
};
Next thing we need to do is to create Tailwind's base file. We will put it in src/assets
folder, that you need to create first, and we will name it global.pcss
.
We are using .pcss
extension just to distinguish that it's a PostCSS file. It's not something you have to do. Plain .css
extension works just a good. I like to distinguish PostCSS files from plain CSS.
/* global.pcss */
@tailwind base;
body {
@apply bg-indigo-100;
}
@tailwind components;
@tailwind utilities;
Alright. Now that we are done with Tailwind configuration, let's wire it up into our PostCSS pipeline.
First things first. We need to install PostCSS cli and a few PostCSS plugins that we will use.
$ npm add -D postcss-cli
$ npm add -D cssnano postcss-load-config postcss-preset-env
Next, we need to create PostCSS configuration file in the project's root folder.
// postcss.config.js
const tailwind = require('tailwindcss');
const cssnano = require('cssnano');
const presetEnv = require('postcss-preset-env')({
features: {
// enable nesting
'nesting-rules': true,
},
});
const plugins =
process.env.NODE_ENV === 'production'
? [tailwind, presetEnv, cssnano]
: [tailwind, presetEnv];
module.exports = { plugins };
Cool! We are almost there. Theoretically, we have everything we need. We just need to wire everything up.
Actually, I forgot something. We want to style our Svelte components with Tailwind and PostCSS too. In order for that to work we need to use the good ol' svelte-preprocess
plugin.
$ npm add -D svelte-preprocess
Let's cheat a bit. We will create a svelte.config.js
and setup the preprocessor there. Svelte config is needed for the editors to be able to work correctly. Syntax highlighting, intellisense and all those things.
We will later re-use the exported preprocessor in our Rollup config to keep things DRY.
// svelte.config.js
const autoProcess = require('svelte-preprocess');
module.exports = {
preprocess: autoProcess({ postcss: true }),
};
There are a few different ways to setup Svelte prepocessor, but I found this the most minimal. The reason it works is that we installed postcss-load-config plugin earlier. It will automatically load postcss.config.js
file if it exists. No need to require it in our code!
Now that we have finished setting up the preprocessor, we need to import it in our Rollup config.
// rollup.config.js
// svelte.config is a CommonJS module
// it needs to be imported this way
const { preprocess } = require('./svelte.config');
// add preprocess to Svelte plugin in client section
svelte({
dev,
hydratable: true,
emitCss: true,
preprocess, // <-- add this
}),
// add preprocess to Svelte plugin in server section
svelte({
generate: 'ssr',
hydratable: true,
dev,
preprocess, // <-- add this
})
Phew! Everything is now configured correctly. Hopefully.
Last thing we need to do is to wire everything together. We will do it by changing the scripts
section in our package.json
.
We have to install npm-run-all cli utility for this to work - npm add -D npm-run-all
.
"scripts": {
"dev": "run-p watch:*",
"watch:css": "postcss src/assets/global.pcss -o static/global.css -w",
"watch:dev": "sapper dev",
"build": "run-s build:css build:sapper",
"build:css": "NODE_ENV=production postcss src/assets/global.pcss -o static/global.css",
"build:sapper": "sapper build --legacy",
"build:export": "sapper export --legacy",
"export": "run-s build:css build:export",
"start": "node __sapper__/build",
"serve": "serve ___sapper__/export",
"cy:run": "cypress run",
"cy:open": "cypress open",
"test": "run-p --race dev cy:run"
}
This requires some explanation. You can see that we have a watch:css
script. What it does is replaces Sappers static/global.css
with our Tailwind base file. We also need to explicitly set NODE_ENV
to "production" in build:css
since we are doing our PostCSS processing outside Sapper. It's needed by Tailwind in order to purge unused CSS styles from its base file.
Be careful not to set NODE_ENV
to production in the Sapper build and export scripts. If you do, and you set any :global
styles in your components, they will be purged leading to missing styles.
Oh, just another tip. If you what to use a background image in you CSS put it in the static
folder. You can then use it your CSS like this.
.hero {
background-image(image.png);
}
To check that Tailwind and PostCSS works in Svelte components, replace your src/routes/index.svelte
with this code.
<!-- index.svelte -->
<style lang="postcss">
.btn {
@apply bg-red-500 text-red-100 uppercase tracking-wide font-semibold
text-4xl px-4 py-3 shadow-lg rounded;
}
</style>
<svelte:head>
<title>Sapper project template</title>
</svelte:head>
<div class="space-y-10 text-center">
<h1 class="text-7xl uppercase font-bold">Great success!</h1>
<button class="btn">DO NOT PRESS THIS BUTTON!</button>
</div>
You can see that we set lang="postcss"
in the style tag. That's not something that is required, PostCSS will still be processed. It's only so that editor understands it's dealing with PostCSS.
To see Tailwind's typography plugin in action change src/routes/blog/[slug].svelte
to the code below.
<script context="module">
export async function preload({ params, query }) {
const res = await this.fetch(`blog/${params.slug}.json`);
const data = await res.json();
if (res.status === 200) {
return { post: data };
} else {
this.error(res.status, data.message);
}
}
</script>
<script>
export let post;
</script>
<svelte:head>
<title>{post.title}</title>
</svelte:head>
<!-- prose is a class from Tailwind typography plugin -->
<div class='prose prose-lg'>
<h1>{post.title}</h1>
{@html post.html}
</div>
And ... we are finally done!
Below you can see the setup in action running on Vercel. Make sure to check out individual blog posts to see Tailwind's typography plugin in action.
Oh, and please don't press that button. Don't say I didn't warn you!
Live demo: https://sapper-with-postcss-and-tailwind.vercel.app
Implementing PostCSS in Sapper becomes clear when you understand how Sapper deals with CSS files.
We set up two separate PostCSS pipelines in our example app. First is processing Sapper's global CSS file. Second is replacing Sapper's component styling with PostCSS. We didn't actually change the way Sapper handles and serves CSS files, we only replaced it with PostCSS. Maybe "augmented" is a better word.
You can find the full code here github.com/codechips/sapper-with-postcss-and-tailwind.
Now go and create some beautifully styled apps!
]]>I am on the quest to find the easiest, fastest and most flexible bundler for Svelte development. I think I just did.
Testing different Svelte bundler setups is a weird hobby of mine. I get a kick out of connecting different plugins together, while trying to setup a smooth development workflow.
The requirements are pretty straight forward.
For my tests I created a simple Svelte app. Its functionality is simple. You press a button and it fetches a random Kanye West tweet from Kanye as a Service.
While simple, the application has some interesting parts.
Can Svite handle the pressure? Let's see!
Svite is a pure Svelte bundler built by dominikg on top of Vite. I've already tested Vite with very impressive results, and since Svite is almost Vite, I expect the results to be equal or better.
What's great about Svite is that it has many different templates you can use when creating your app. Currently there are four:
It has also a few other useful options you can use too.
Let's create an app by using postcss-tailwind template and adding TypeScript support.
$ npx svite create -pm pnpm -ts -t postcss-tailwind -sc svelte-svite-typescript
$ cd svelte-svite-typescript
$ npm run dev
That was easy! Let's look at the files it produced.
$ ls -1
index.html
node_modules
package.json
pnpm-lock.yaml
postcss.config.js
public
src
svelte.config.js
tailwind.config.js
tsconfig.json
And in the src
directory you have these files.
$ ls src
App.svelte index.css index.ts
Nice! Almost the same files I had to write by hand when testing Vite.
Why almost? If you worked with Vite, you might notice that there is no vite.config.js
. Because Svite has build in support for Svelte, there is no need to have one.
The file is small and with helpful comments in the right places.
<script lang="ts">
const world = 'postcss'; // edit world and save to see hmr update
</script>
<style>
h1 {
color: orangered; /* change color an save to see hmr update */
/* you can also use css nesting
& .world {
font-size: 2rem;
}
*/
}
.world {
@apply text-teal-500 italic; /* here's some tailwind apply */
}
</style>
<h1 class="border border-current rounded p-4 m-4">
<!-- tailwind classes in svelte template -->
Hello
<span class="world">{world}</span>
</h1>
The main file is clean and imports our base Tailwind file.
import App from './App.svelte';
import './index.css';
const app = new App({
target: document.body,
});
export default app;
The package.json
is clean and small.
{
"name": "svelte-svite-typescript",
"version": "0.0.0",
"scripts": {
"validate": "svelte-check && tsc --noEmit",
"dev": "svite -ts",
"build": "svite build -ts"
},
"dependencies": {
"svelte": "3.24.1",
"svelte-hmr": "0.10.2"
},
"devDependencies": {
"@tsconfig/svelte": "^1.0.10",
"postcss": "^7.0.32",
"postcss-import": "^12.0.1",
"postcss-load-config": "^2.1.0",
"postcss-preset-env": "^6.7.0",
"svelte-check": "^1.0.21",
"svelte-preprocess": "4.1.1",
"svite": "^0.6.0",
"tailwindcss": "^1.7.3",
"tslib": "^2.0.1",
"typescript": "^3.9.7"
}
}
A few dependencies are outdated, but we can update them with ncu -u && pnpm install
command.
When starting everything works as advertised. Impressive!
I copied the files in src
directory from my Vite test setup and the app worked out of the box! I am not surprised since Svite is Vite abstracted.
Let's compare Svite builds to Vite's.
# Svite
[write] dist/_assets/index.ab6f11f2.js 32.04kb, brotli: 9.50kb
[write] dist/_assets/style.45ed9e6c.css 8.10kb, brotli: 1.81kb
[write] dist/_assets/usa.fcca7834.svg 0.88kb
[write] dist/index.html 0.40kb, brotli: 0.16kb
Build completed in 8.37s.
# Vite
[write] dist/_assets/index.0729edbe.js 32.04kb, brotli: 9.50kb
[write] dist/_assets/style.393fa0c4.css 6.39kb, brotli: 1.67kb
[write] dist/_assets/usa.6aae1c82.svg 0.88kb
[write] dist/index.html 0.41kb, brotli: 0.16kb
Build completed in 5.16s.
The results are almost identical in terms of output and speed.
Let's re-visit our list of requirements.
One annoying thing I found is that when you kill the dev server the whole app starts reloading itself.
Svite is a nice little abstraction of Vitejs specifically tailored for Svelte development. It's fast, small and doesn't get in your way. Everything just works.
It feels thought though. For example, one thing I like is that Svite allows you to choose which package manager you want to use. It might be a small thing, but it's really nice that the author has thought of this.
If you feel you need to use some other Vite plugins that Svite doesn't offer, you can always create vite.config.js
and wire them up there.
I didn't bother to setting up other useful scripts this time, like continuous linting, but if you are interested check out package.json
in my Vite setup in the Github repo.
Svite might just be the easiest way to get your new Svelte project off the ground. The documentation is excellent and you have many different boilerplate templates to choose from. Just make sure to read the docs for all the possible options!
You can find the code here https://github.com/codechips/svelte-typescript-setups
I will definitely use Svite in the future!
]]>Actually, creating a dropdown menu is not as simple as it sounds. First, you have to handle mouse clicks outside of it and close the menu if it's currently open. Second, you should support pressing Escape
key and close the menu if it's currently open. Third, you should add nice animation to the menu so it feels more alive.
Implementing the menu in React wasn't quite as straight forward as I hoped for. The Tailwind styling itself is not a problem, but it took me some time to figure out how to handle "click away" or "click outside" functionality and handling the escape key. On top of that I had to research how to do CSS transitions in React. Turns out that creators of Tailwind created a helpful transition library as React does not have the functionality built-in.
Doing a Google search for "react click away listener" didn't really help. A search on NPM for "react click outside" and "react click away" returned way too many more results than I needed. Sure, there are plenty of React libraries, but I felt that there should be a much simpler way of handling that.
Here is the Next.js (React + TypeScript) code I ended up with.
import Link from 'next/link';
import React, { useState, useRef, useEffect } from 'react';
import { Transition } from '@tailwindui/react';
const Menu = ({ user }) => {
const [show, setShow] = useState(false);
const container = useRef(null);
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (!container.current.contains(event.target)) {
if (!show) return;
setShow(false);
}
};
window.addEventListener('click', handleOutsideClick);
return () => window.removeEventListener('click', handleOutsideClick);
}, [show, container]);
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (!show) return;
if (event.key === 'Escape') {
setShow(false);
}
};
document.addEventListener('keyup', handleEscape);
return () => document.removeEventListener('keyup', handleEscape);
}, [show]);
return (
<div ref={container} className="relative">
<button
className="menu focus:outline-none focus:shadow-solid "
onClick={() => setShow(!show)}
>
<img
className="w-10 h-10 rounded-full"
src={user.picture}
alt={user.name}
/>
</button>
<Transition
show={show}
enter="transition ease-out duration-100 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75 transform"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="origin-top-right absolute right-0 w-48 py-2 mt-1 bg-gray-800 rounded shadow-md">
<Link href="/profile">
<a className="block px-4 py-2 hover:bg-green-500 hover:text-green-100">
Profile
</a>
</Link>
<Link href="/api/logout">
<a className="block px-4 py-2 hover:bg-green-500 hover:text-green-100">
Logout
</a>
</Link>
</div>
</Transition>
</div>
);
};
export default Menu;
When I was done with React implementation, I thought to myself of how I would implement the same menu in Svelte. So I took some time to port it to Svelte.
One of the many niceties about Svelte is that it has CSS transitions and animations built in. Here is my take on it.
<script>
import { onMount } from 'svelte';
import { scale } from 'svelte/transition';
export let user;
let show = false; // menu state
let menu = null; // menu wrapper DOM reference
onMount(() => {
const handleOutsideClick = (event) => {
if (show && !menu.contains(event.target)) {
show = false;
}
};
const handleEscape = (event) => {
if (show && event.key === 'Escape') {
show = false;
}
};
// add events when element is added to the DOM
document.addEventListener('click', handleOutsideClick, false);
document.addEventListener('keyup', handleEscape, false);
// remove events when element is removed from the DOM
return () => {
document.removeEventListener('click', handleOutsideClick, false);
document.removeEventListener('keyup', handleEscape, false);
};
});
</script>
<div class="relative" bind:this={menu}>
<div>
<button
on:click={() => (show = !show)}
class="menu focus:outline-none focus:shadow-solid"
>
<img class="w-10 h-10 rounded-full" src={user.picture} alt={user.name} />
</button>
{#if show}
<div
in:scale={{ duration: 100, start: 0.95 }}
out:scale={{ duration: 75, start: 0.95 }}
class="origin-top-right absolute right-0 w-48 py-2 mt-1 bg-gray-800
rounded shadow-md"
>
<a
href="/profile"
class="block px-4 py-2 hover:bg-green-500 hover:text-green-100"
>Profile</a>
<a
href="/api/logout"
class="block px-4 py-2 hover:bg-green-500 hover:text-green-100"
>Logout</a>
</div>
{/if}
</div>
</div>
Sure, the amount of code is little less in Svelte than in React, but what about the cognitive load? Which one is easier to read and understand? You be the judge.
]]>The world around us is spinning faster and faster. It's not only stressful enough just to keep up to date with the information, but also constantly learn something new.
The Internet is a blessing, but also a curse. There is so much information out there! It's hard to find and process it all. My children don't see me reading many books at home nowadays, but only if they knew how much text I consume on a normal day! Blogs, Slack, Twitter, emails, manuals, forums, work documents. These things add up!
Today everyone competing on the same terms, but you can gain competitive advantage over others by being a fast learner. This brings me to the core question of this essay: What is the fastest way to learn?
Everyone is different, therefore there is no ultimate learning process. You have to find what works best just for you. Are you a visual, auditory, reading/writing or kinetic learner? You are often not only one type, but a mix of different combinations.
Of course, you can learn by using any style - doing, watching, reading, listening, but in order to maximize your learning you have to know which one of those suits you best.
I personally is definitely a reading/writing type, but I like to watch videos too in order to get a better understanding of the topic I am learning.
Podcasts are not really my thing. I can't listen to podcasts because I need to concentrate while listening. I tried to do it in bed before sleeping, but that only made me fall asleep faster.
How about watching videos? Coding videos are also not high on my list. I hate to scrub and pause in the video player, plus I want to be able to copy and try out the code while I am watching.
Note: I know there are platforms that show video and code side-by-side, but they are rare.
Reading, on the other hand, is different. It's fast. You can easily skim to the juicy parts, plus copying and pasting text is much faster than typing it.
We've talked about the learning styles, but what about the process? Which process should you follow in order to learn fast?
There are a few different approaches. Some people like to dive heads down and figure things on the way. Some like to understand what they are dealing with first.
I am definitely the second type. I like to understand before I start.
Not sure how and when I finally understood this, but I've now used it for many years with great success.
Below are the steps I normally follow in order to learn a topic as fast and as efficiently as possible. It's not rocket science, but works great for me. Maybe it will work for you too?
Before learning something it's important to know what you want, or need, to learn. What's the minimum you need to know in order to reach your goal? What questions need answers?
Often you already might have some idea or some kind of goal in your mind. Make sure you know it beforehand. It will make judging sources of information easier.
If you are not sure, go for a walk and think about it. Somehow things tend to clear up in motion.
The first thing I do is to find as much useful slash interesting information on the topic as possible. It can be blog posts, Youtube videos, official documentation, code examples, Github repositories, Github issues, Stack Overflow threads. Anything goes here. As long as you think it can help you reach your goal.
The important part in this step is to find as much information you think might be useful as possible. Don't judge it at this stage. Just skim through it quickly, if something catches your interest, keep it and continue with your quest. Go with your gut feeling. If something seems interesting or useful, save it, otherwise scrap it.
I also often search for bad or negative parts. Parts that people struggle with or complain about. Review posts are super useful here, even if they are only someone's opinions and not necessary the truth. I try to get a sense if it worth it and if there are any known problems or pitfalls.
Following links I find in the articles is also something I do often. If some link catches my attention I open it in a new browser tab.
When I feel I have enough potential sources of information I stop. At the end of this session I usually have 20-30 browser tabs open, 10-15 bookmarks and 5-10 Youtube videos added to my "Watch Later" list.
This step usually takes me 30-60 minutes.
During this phase I start to process the found information one by one. I don't go deep here. My goal is to find interesting bits of information and extract the core concepts. I usually read only a paragraph or two, maybe look at some diagrams or glance briefly at some code examples.
If I had to describe the level of my concentration between 1 being glancing over text and 10 being fully emerged, I would say it's around 3. I am fully concentrated, but not on the whole text. My goal at this stage is to only understand the concepts and not the details. I want to get a rough idea of what this is all about.
When it comes to videos I usually play them at 1.5X speed and often forward to the parts I find interesting.
I branch out to other related things I find on the way that I don't yet understand.
I do this to the point where I am feeling completely lost and nothing makes sense any more.
This phase usually takes me between 1-5 hours depending on the size of the subject in question.
When my brain is completely fried with information I step away from the research phase and go and do something else. Something that doesn't require brain power.
This is a very important step, don't skip it! It's called Background Processing. You have overwhelmed your brain, bombed it with information. Now you have to give your unconscious mind a chance to process and sort it all. Go to bed early and try to get 7-8 hours of good sleep.
Now it's time to take some notes. To try to recap what you learned and make your own understanding of the subject. I usually grab a pen and paper and try to write down core concepts using lists and keywords. Sometimes I draw a sloppy mind map. Remember, this is only for you! Nobody will read this!
Here, I am using pen and paper. It allows me to draw doodles and other stuff and it's fast too. Plus, physically writing it down on paper has a totally different effect than typing it out on a computer. It feels more free form and your brain processes this differently. At least that's what I've noticed.
You mind has already background-processed all the information. You helped it structure it further by your review. Things and concepts suddenly start to make sense. Look at you notes and rewrite them once again, but this time only picking out the core parts and concepts.
Now that you have an idea of the concepts and all the dots are connected, think through what is important. What concepts, terms, patterns have you learned? Try to identify them.
It's time to have some fun. Try to do something very simple. Baby steps. There is no pressure at this stage. You are in exploration mode.
Once you get something going, try to expand it. Do some more, play around. View it as a POC. You are only trying to do something here. There are no "musts."
While playing around and getting stuck you will notice that somehow you already know where to look for the solution to your problem. You will either remember it from the "overwhelm" session, or you will somehow know how and where to search for the solution.
Again, thanks to the power of your mind and its background-processing ability. It helped you distill and internalize the core concepts and connect the dots.
It's now time to do the real work. Here I often use the outcome of the "play" phase as my base.
All these steps can take differently long time, it depends on the size and complexity of the topic. For example, I had to learn a lot of new things in the past few weeks. One of them was Next.js.
By following my learning process I went from knowing nothing about Next.js to building a fully functional app with authentication and other bells and whistles running in production in just 3 days.
If I can do it so can you!
Developing an effective learning process is a must if you want to stay ahead of competition, but it's not only about learning fast, but also about learning smart.
Experiment with different approaches. Evaluate. There is no universal approach to learning. Find out what works best just for you.
The more I learn, the less I know, but also the less stressed I get. Because in the end, it's not about knowing, but about understanding. Even if you don't know, somehow you get a feeling that you have seen it before. You brain is good at making connections. By constantly learning you are building your brain capital.
By having developed my own learning process I always know that I can learn fast and effectively and that keeps me calm.
Stay curious and always be learning!
]]>If you are building semi-complex UIs Finite State Machines can help you deal with state. Learn what FSMs are and how you can use them with a simple example.
Shortly, Finite State Machine or FSM is an old, but recently rediscovered pattern, to help us deal with complex state.
The gist of FSM is that your application can only be in one state at a time. If you think about it, it actually makes sense. Just like in real life, you can only be in one state at at time. Unless you are a quark.
I find Finite State Machines a great fit when working with complex UIs. Applications are getting more and more interactive and keeping track of UI state can quickly get challenging.
There are currently two popular FSM libraries in JavaScript that I know of. XState and Robot.
I would say that XState is the most popular. It has a lot of features and really great documentation. However, FSM can be intimidating to learn and XState especially, because there you can achieve the same result by different ways.
If you are interested in XState, I've written an article where I used XState as a state machine for Firebase Authentication - Firebase authentication with XState and Svelte.
For this example I decided to use Robot FSM library. First, it's much smaller than XState - 1kb vs 13kb. Second, it's also easier to learn, because it has less bells and whistles.
Robot doesn't have as good documentation as XState, so you have to read between the lines and experiment a bit to understand it's concepts and how to work with it.
But that's why I am here! Hopefully by the end of this article everything will click, you will know what an FSM is and understand where you can use them.
I promise to keep things simple. Let's code!
First, we need to create a new Svelte project. We will use Svite with pnpm package manager to save some disk space. I find this combination the fastest way to get going.
$ npx svite@beta create -pm pnpm svelte-robot-fsm-example
We will be building a panel menu with different sections. Think tabs component, only prettier.
Next step is to create a few Svelte components.
We will create a simple panel component called Section.svelte
. It will take a title
and a close
function handler as parameters.
Close handler will be wired up to the Close button in the component. When clicked, it should hide the menu. We will also add Svelte's slide transition for some neat visual FX.
<!-- src/components/Section.svelte -->
<script>
import { slide } from 'svelte/transition';
export let title = '';
export let close = () => {};
</script>
<style>
.section {
border-bottom: 1px solid #65b6ca;
}
.inner {
position: relative;
padding: 1em;
margin: 0 auto;
max-width: 100ch;
min-height: 200px;
}
h2 {
font-weight: 700;
font-size: 3em;
}
.close {
position: absolute;
bottom: 2em;
}
</style>
<div class="section" transition:slide>
<div class="inner">
<h2>{title}</h2>
<button on:click={close} class="close">Close</button>
</div>
</div>
Let's setup three sections in our App.svelte
. Replace the file with the code below.
<!-- App.svelte -->
<script>
import Section from './components/Section.svelte';
</script>
<style>
.wrapper {
font-family: sans-serif;
}
.container {
margin: 1em auto;
max-width: 100ch;
}
.panel {
background-color: #b9f0fe;
}
.buttons > button {
padding: 0.5em 1em;
}
</style>
<div class="wrapper">
<div class="heading container">
<h1>Svelte with Robot FSM Example</h1>
</div>
<div class="panel">
<Section title="One" />
<Section title="Two" />
<Section title="Three" />
</div>
<div class="buttons container">
<button>One</button>
<button>Two</button>
<button>Three</button>
</div>
</div>
If you start the app (pnpm run dev), all of the three sections should be visible. That's not something we want. Let's add some interactivity logic next.
Adding interactivity to these panel sections is straight forward. For that we need to add three boolean variables, one for each section, and then manipulate their state.
Here is the updated App.svelte
file with state variables and on:click
handlers.
<!-- App.svelte -->
<script>
import Section from './components/Section.svelte';
// open/close state variables. one for each section.
let showOne = false;
let showTwo = false;
let showThree = false;
</script>
<style>
.wrapper {
font-family: sans-serif;
}
.container {
margin: 1em auto;
max-width: 100ch;
}
.panel {
background-color: #b9f0fe;
}
.buttons > button {
padding: 0.5em 1em;
}
</style>
<div class="wrapper">
<div class="heading container">
<h1>Svelte with Robot FSM Example</h1>
</div>
<div class="panel">
{#if showOne}
<Section title="One" close={() => (showOne = false)} />
{/if}
{#if showTwo}
<Section title="Two" close={() => (showTwo = false)} />
{/if}
{#if showThree}
<Section title="Three" close={() => (showThree = false)} />
{/if}
</div>
<div class="buttons container">
<button on:click={() => (showOne = true)}>One</button>
<button on:click={() => (showTwo = true)}>Two</button>
<button on:click={() => (showThree = true)}>Three</button>
</div>
</div>
You can see that we added basic on:click
handlers to buttons and we also pass in close
handlers that set state variable to false
.
If you've done everything right you should now be able to open and close each panel. Good times!
But what about the scenario that only one section should be visible at one time? Meaning if you open a new one, the currently visible section should close.
It's doable, but things can get pretty hairy, because you have to track individual state of every section. That's something Finite State Machines can help us with.
Let's add Robot FSM library to our project.
$ pnpm add robot3
Now we need to create our Finite State Machine. We will name it panelMachine
as it will be responsible for keeping track of our panel's state.
Add this code to App.svelte
and I will explain it in a second.
<!-- App.svelte -->
<script>
// ...
import { createMachine, state, transition } from 'robot3';
const panelMachine = createMachine({
closed: state(
transition('showOne', 'one'),
transition('showTwo', 'two'),
transition('showThree', 'three')
),
one: state(transition('close', 'closed')),
two: state(transition('close', 'closed')),
three: state(transition('close', 'closed')),
});
</script>
As you can see we used Robot's createMachine
function to define a state machine. The machine consists of total four states - closed, one, two and three. Our first state is closed and that will be the state machine will start with. If you look carefully you can see that our state machine is just a simple dictionary.
Each dictionary value is then defined by a state
function. The state function is used to define state in Robot FSM library. But what going on inside the state functions?
State function is the meat of the Robot library. It's in there that you define your state and transitions to other states. Transitions? States? What?!
Yes, I understand if you are confused and I promised to keep things simple for that reason. Let me explain.
As I wrote earlier, an state machine can only be in one state at at time. When our panel FSM is started it starts in the closed
state, because it's first in the dictionary.
If you imagine that you are in the closed
state you can see that inside it we have defined three transition
functions. Transition function dictates the states you can go to from the state FSM that is currently in. They are used to move from one state to another.
Every transition function takes a trigger event as first argument and the destination state as second argument. You can name events however you like, but I like to prefix mine with verbs as I like to think of them not as events, but rather commands or actions. By sending an event to the state machine you command it to go to another state than it's currently in.
One important concept to understand is that you can only move those states that you have setup transitions for. So, from our closed
state we can only go to states one
, two
or three
. If you send in some other event name than showOne
, showTwo
or showThree
our FSM will simply ignore them and just stay in the current state it is in.
Similarly, you can only go to closed
state from other states by sending a close
event to our state machine. Are you with me so far?
Alright, we have defined our state machine. Now we have to activate it by using interpret
function from the Robot library.
The interpret
function takes a machine and wraps it into a service that can send events into the machine to change its states. A service does not mutate a machine. Instead it creates derived machines with the current state set.
You can find the current state of the machine by the machine.current
property. If you wrap it in the service it will be located under service.machine.current
.
Here is the code that demonstrates how a machine service works.
// Wrap machine into a service. Every time state changes it's printed.
// A service has a `send` command that we can use to send it events.
const service = interpret(panelMachine, service => {
console.log('current state: ', service.machine.current);
});
console.log('current state: ', s.machine.current)
// current state: closed
// Transition from `closed` state to `one` [legal]
service.send('showOne');
// current state: one
// Transition from `one` to `two` [illegal]
service.send('showTwo');
// current state: one
// Send non-existent event
service.send('foo');
// current state: one
// Transition from `one` to `closed` [legal]
service.send('close');
// current state: closed
As you can see from the example above, if we send in a valid event, that we defined in our state transition in the current state we are in, we can switch to a different state in our state machine.
Robot does not have Svelte support per default, but it's not hard to wire it up. For that we will can write a custom Svelte store.
UPDATE: Check out svelte-robot-factory library
import { createMachine, state, transition, interpret} from 'robot3';
import { writable } from 'svelte/store';
export const useMachine = (machine, event = {}) => {
// every time the state changes we will update our Svelte store
const service = interpret(
machine,
(service) => {
set({ state: service.machine.current, context: service.context });
},
event
);
const { subscribe, set } = writable({
state: machine.current,
// we want the service context and not machine context
context: service.context,
});
return [{ subscribe }, service.send];
};
As you can see we defined a useMachine
constructor function. It takes a Robot FSM as its argument and returns an array. First value is our custom Svelte store with our machine's context
and state
as two properties. Second value is the service send
method. It will allow us to send events to our machine.
Let's use it in our code.
const [panelState, send] = useMachine(panelMachine);
We have everything we need. Next step we need to do is to replace our on:click
and close
handlers. We also need to use our new panelService
store for state checking instead of individual state variables.
I won't bore you with all the code changes needed. Instead, here is the updated App.svelte
file in all its glory.
<!-- App.svelte -->
<script>
import Section from './components/Section.svelte';
import { createMachine, state, transition, interpret } from 'robot3';
import { writable } from 'svelte/store';
// constructor function to interpret the machine and wrap
// the FSM service into a custom svelte store.
// `event` will be used as initial context if passed
export const useMachine = (machine, event = {}) => {
// every time the state changes we will update our Svelte store
const service = interpret(
machine,
(service) => {
set({ state: service.machine.current, context: service.context });
},
event
);
const { subscribe, set } = writable({
state: machine.current,
// we want the service context and not machine context
context: service.context,
});
return [{ subscribe }, service.send];
};
// create our Robot FSM definition
const panelMachine = createMachine({
closed: state(
transition('showOne', 'one'),
transition('showTwo', 'two'),
transition('showThree', 'three')
),
one: state(transition('close', 'closed')),
two: state(transition('close', 'closed')),
three: state(transition('close', 'closed')),
});
const [panelState, send] = useMachine(panelMachine);
// close handler
const close = () => send('close');
// send in event to our FSM
const toggle = (action) => send(action);
</script>
<style>
.wrapper {
font-family: sans-serif;
}
.container {
margin: 1em auto;
max-width: 100ch;
}
.panel {
background-color: #b9f0fe;
}
.buttons > button {
padding: 0.5em 1em;
}
</style>
<div class="wrapper">
<div class="heading container">
<h1>Svelte with Robot FSM Example</h1>
</div>
<div class="panel">
{#if $panelState.state === 'one'}
<Section title="One" {close} />
{/if}
{#if $panelState.state === 'two'}
<Section title="Two" {close} />
{/if}
{#if $panelState.state === 'three'}
<Section title="Three" {close} />
{/if}
</div>
<div class="buttons container">
<button on:click={() => toggle('showOne')}>One</button>
<button on:click={() => toggle('showTwo')}>Two</button>
<button on:click={() => toggle('showThree')}>Three</button>
</div>
</div>
You can see that we have replaced our individual state variables with our state machine. We have also create two handlers - close
and toggle
.
If you start the app now, you will notice that we can only open one panel at a time. But I must say, it's not a correct behaviour. We cannot open other panel before we close the currently open one.
Why? If you look at our machine definition you can see that the only valid transition from a panel is to the closed
state. This means the only valid event we can sent to the open panel is close
event.
We can actually use this knowledge to our advantage by changing our toggle
handler to the following.
const toggle = (action) => {
send('close');
send(action);
};
Before sending our show event, we first send the close
event so that our machine enters closed
state.
I don't know about you, but I find it pretty elegant.
But what if we want our button not only to open, but also toggle a panel's state. Meaning if the panel is currently open clicking the same button should close it.
For that we need add a guard that checks if the current state our machine matches the desired state. If it does, it just closes the panel.
const toggle = (action, state) => {
// if current state matches desired panel state, close it
if ($panelState.state === state) {
send('close');
return;
}
// else first close, then open desired state
send('close');
send(action);
};
We also need to provide the desired state in our buttons' on:click
handlers.
<div class="buttons container">
<button on:click={() => toggle('showOne', 'one')}>One</button>
<button on:click={() => toggle('showTwo', 'two')}>Two</button>
<button on:click={() => toggle('showThree', 'three')}>Three</button>
</div>
Let's take a look if the toggle works.
Yep. Works dandy fine! Mission accomplished.
Finite State Machines are hard to understand if you haven't been exposed to them before. They also offer so much more than I showed in my example.
In Robot there is also context that lets you work with state, guards that allows you to enter a state only if condition is met, invoke that helps us work with promises and sub-machines, and immediate transitions.
XState has even more to offer. It's easy to get lost if you don't have a solid understanding of the basics first.
I chose the simplest example I could think of on purpose that only shows how to setup state and transition from one state to another by sending events.
If you want to test you skills try building a tabs component. The concepts are exactly the same and you can re-use a lot of the code we have written.
When you understand the concepts and how to work with state, all other FSM concepts become easy. In the end, it's all about states, sending events and transitions.
Start simple, experiment and I can guarantee that you will quickly start seeing places in your codebase where a state machine makes sense to use. Plus, as a bonus, you will get one more powerful tool in your toolbox.
You can find the full code at https://github.com/codechips/svelte-robot-fsm-example
Thanks for reading! Now go and replace something in your codebase with an FSM FTW.
]]>Building a good bundler is not easy. Building a fast bundler is almost impossible. Mad props to those who dare.
I discovered Snowpack before version 2 was officially released and was very excited about it. Today I decided to revisit it to see how and if things have changed since.
My goal is to compare different bundlers for Svelte development in order to find the best one. I've already tested Vite, Svite and now it's Snowpack's turn.
For the purpose of evaluation I created a simple web app. Its functionality is simple. You press a button and it fetches a random Kanye West tweet from Kanye as a Service.
While simple, the application has interesting parts.
I also have a list of my own requirements when it comes to bundlers.
Let's see if Snowpack can satisfy all of them.
Snowpack comes with many official framework templates that you can use as base for your apps. Since the app is written in TypeScript we will use @snowpack/app-template-svelte-typescript. It's not listed on the website, but in Github.
$ npx create-snowpack-app svelte-snowpack-typescript \
--template @snowpack/app-template-svelte-typescript --use-pnpm
$ cd svelte-snowpack-typescript
# add test app's dependencies
$ pnpm add -D date-fns svelte-inline-svg
$ pnpm start
I like how Snowpack lets me specify the package manager. pnpm, the package manger I often use, is not in the main documentation, but I found the option somewhere in Github.
Dev server starts instantly and Snowpack is nice enough to open the browser for me. I appreciate it, but I don't like it or need it.
Another thing I like about Snowpack is that it uses npm start
script and not dev
to start everything up. The npm run dev
command is so 2015, right?
I also noticed that app template's config file has changed from JSON to JS. I like it because it makes it easy to comment and do other advanced JS gymnastics.
module.exports = {
mount: {
public: '/',
src: '/_dist_',
},
plugins: [
'@snowpack/plugin-svelte',
'@snowpack/plugin-dotenv',
'@snowpack/plugin-typescript',
[
'@snowpack/plugin-run-script',
{cmd: 'svelte-check --output human', watch: '$1 --watch', output: 'stream'},
],
],
install: [
/* ... */
],
installOptions: {
/* ... */
},
devOptions: {
// don't open browser
open: 'none',
// don't clear the output
output: 'stream'
},
buildOptions: {
/* ... */
},
proxy: {
/* ... */
},
alias: {
/* ... */
},
};
So far so good.
Let's see what files Svelte template generates for us.
$ ls -1
babel.config.json
jest.config.js
jest.setup.js
LICENSE
node_modules
package.json
pnpm-lock.yaml
public
README.md
snowpack.config.js
src
svelte.config.js
tsconfig.json
types
Not bad. I don't know why Babel config has to be explicit and I am usually not writing any unit tests, so jest has no use for me. But other that that it looks nice and clean.
The app entry file comes with an explicit HMR section. I like that it's explicit and how it tells you where to find more information. Good!
import App from "./App.svelte";
var app = new App({
target: document.body,
});
export default app;
// Hot Module Replacement (HMR) - Remove this snippet to remove HMR.
// Learn more: https://www.snowpack.dev/#hot-module-replacement
if (import.meta.hot) {
import.meta.hot.accept();
import.meta.hot.dispose(() => {
app.$destroy();
});
}
By now I've copied the code from my test web app, added missing dependencies and tried to start the dev server. Things don't quite work yet.
This is where I hit the first road block. Looks like Snowpack uses pkg.module
file if it exists. The svelte-inline-svg library I am using had the module
property defined, but the file was missing in the NPM package. Showpack borked on this. I wish that it could have fallbacks in place and try with something else if the file is missing. The author of the SVG library has fixed it after I filed a bug report.
Snowpack has no PostCSS support out-of-the box, because in Snowpack it's all about plugins and build scripts, but it offers instructions on the main documentation page on how to enable PostCSS. But turns out there is some ambiguity. The main documentation webpage tells one thing while Github tells another. Which one to trust?
Alright, let's go with the second one and use Snowpack's plugin-postcss. Not sure I understand why I have to explicitly install postcss
and postcss-cli
. Wouldn't it be better if they were baked into the Snowpack's PostCSS plugin?
$ pnpm add @snowpack/plugin-postcss postcss postcss-cli
Next, we need to add the plugin to snowpack.config.js
.
...
plugins: [
...
'@snowpack/plugin-postcss',
],
...
Talking about plugins. Snowpack has quite strange plugin configuration. You can either supply plugin as string, or as an array if plugin needs some arguments, where first item is the name of the plugin and second is an object of the arguments that will be passed to the plugin.
[
'@snowpack/plugin-run-script',
{ cmd: 'svelte-check --output human', watch: '$1 --watch', output: 'stream' },
],
Let's install and setup Tailwind and friends.
$ pnpm add -D tailwindcss postcss-preset-env
$ pnpx tailwind init
Replace Tailwind configuration file with the following contents.
// tailwind.config.js
module.exports = {
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
experimental: {
uniformColorPalette: true,
extendedFontSizeScale: true,
},
purge: {
content: ['./src/**/*.svelte', './public/*.html'],
whitelistPatterns: [/svelte-/],
},
theme: {
extend: {},
},
variants: {},
plugins: [],
};
And postcss.config.js
with this contents.
// postcss.config.js
const tailwind = require('tailwindcss');
const purge = require('@fullhuman/postcss-purgecss');
const cssnano = require('cssnano');
const presetEnv = require('postcss-preset-env')({ stage: 1 });
const plugins =
process.env.NODE_ENV === 'production'
? [tailwind, presetEnv, cssnano]
: [tailwind, presetEnv];
module.exports = { plugins };
Svelte template comes with dotenv
pre-packaged as Snowpack plugin. This is nice as it doesn't require a working dotenv
environment in the shell.
The ES2020 import.meta.env
works out of the box, but not in my Vim editor.
VS Code works fine and you get autocompletion too. I figured that coc-svelte
plugin I am using is way behind VS Codes Svelte extension. As soon as I forked and updated dependencies, Vim stopped complaining.
NOTE: If you are using Vim, coc.nvim and coc-svelte extension, you can use mine. It's more up-to-date than the original one @codechips/coc-svelte.
You have to prefix all env variables with SNOWPACK_
if you want them to be accessible in the app. Most bundlers today follow this convention in order to avoid environment variable clashes.
For my app I had to create an .env.local
file so that dotnenv
could pick it up, but any dotenv
convention works.
# .env.local
SNOWPACK_PUBLIC_KANYE_API=https://api.kanye.rest
As I understand svelte.config.js
file is mostly only used by code editors for autocompletion. Not with Snowpack. Snowpack is one of the few bundlers that relies on that file to exist. It's used by its Svelte plugin. Good thing that it comes with the template.
TypeScript is nice, but getting TypeScript configuration right is usually a game of trial and error for me. Snowpack's Svelte template has a dependency on the @tsconfig/svelte
NPM package, but from what I can see it's not used anywhere?
The svelte-inline-svg
module does not come with any TypeScript declaration. We need to fake it locally.
Create svelte-inline-svg.d.ts
in the types
directory with the following contents.
declare module 'svelte-inline-svg';
By the way, I really like how the svelte-check
tool, which comes already injected into the build pipeline, warns me about it and provides instruction on what to do.
I also like that Snowpack has a dedicated types
directory for your own TypeScript definitions. Somehow, I always struggle to configure type paths. Snowpack is the first bundler that makes this clear for me.
The test app is now finally working! Lets build it for production.
$ pnpm build
> snowpack build
...
[@snowpack/plugin-typescript] src/index.ts(2,17): error TS2307: Cannot find module './App.svelte' or its corresponding type declarations.
[@snowpack/plugin-typescript] Error: Command failed with exit code 2: tsc --noEmit
src/index.ts(2,17): error TS2307: Cannot find module './App.svelte' or its corresponding type declarations.
ERROR Command failed with exit code 1.
Oops. Something is wrong, but what? I actually never figured out what, so I recreated the whole project from scratch, which helped. Who told you that this was going to be easy, right?
When building Snowpack shows you a nice stats table of all the build artifacts.
But what's in the build?
tree -h build
build
├── [4.0K] _dist_
│ ├── [1.4K] App.js
│ ├── [4.0K] assets
│ │ ├── [ 899] usa.svg
│ │ └── [ 40] usa.svg.proxy.js
│ ├── [ 14K] index.css
│ ├── [ 15K] index.css.proxy.js
│ ├── [ 334] index.js
│ ├── [1.5K] Time.js
│ ├── [ 539] timer.js
│ ├── [ 721] Wisdom.css
│ ├── [1.0K] Wisdom.css.proxy.js
│ └── [5.9K] Wisdom.js
├── [1.1K] favicon.ico
├── [ 882] index.html
├── [1.1K] logo.svg
├── [ 67] robots.txt
├── [4.0K] __snowpack__
│ └── [ 126] env.js
└── [4.0K] web_modules
├── [4.0K] common
│ └── [ 16K] index-2f302f93.js
├── [ 82K] date-fns.js
├── [ 273] import-map.json
├── [4.0K] svelte
│ ├── [ 448] internal.js
│ ├── [2.0K] store.js
│ └── [ 308] transition.js
├── [4.9K] svelte-inline-svg.js
└── [ 59] svelte.js
6 directories, 24 files
You have __dist__
folder with all your application files, __snowpack__
folder with the env variables, and web_modules
folder with all the dependencies.
Snowpack bundles your assets (CSS, SVG) files as JS files, the .proxy.js
ones. I wonder why it also puts raw SVG and CSS file in the build directory if they are not used in production.
The dependencies are not bundled, but server to browser as ES modules. After all, Snowpack is an ESM bundler.
This has its advantages and drawbacks. The development is super-fast as only the files changes need to be recompiled and reloaded. The files and dependencies are not optimized and served as is, which leads to many more requests if you have many dependencies and also to larger downloads as you need to download the whole module even if you only use one function in it.
You might need to only do it once on the first load, then the files might be cached, but I am not the expert on this.
Ah! The ultimate test for a bundler. How efficient bundles a bundler can produce. Snowpack doesn't have bundler support built-in, but there are some plugins that we can use.
Some time while ago Snowpack released an optimization plugin. It's not a bundler plugin, but more of a minifier. It helps you minify CSS, HTML and JS. From what I have read, this plugin uses ESBuild's minifying functionality and from what I understand it's almost on par with Rollup's.
Enough talking, let's test!
$ pnpm add -D @snowpack/plugin-optimize
We need to add it to the list of plugins in snowpack.config.js
and do a new build.
Strange. I see no difference in the stats table output.
The terminal output is saying that file size didn't change, but if I look at the actual file on disk it's actually 21K and not 83K as Snowpack tells me.
$ tree -h build
build
├── [4.0K] _dist_
│ ├── [ 768] App.js
│ ├── [4.0K] assets
│ │ ├── [ 899] usa.svg
│ │ └── [ 40] usa.svg.proxy.js
│ ├── [5.0K] index.css
│ ├── [ 14K] index.css.proxy.js
│ ├── [ 251] index.js
│ ├── [ 860] Time.js
│ ├── [ 305] timer.js
│ ├── [ 554] Wisdom.css
│ ├── [ 893] Wisdom.css.proxy.js
│ └── [2.7K] Wisdom.js
├── [1.1K] favicon.ico
├── [ 416] index.html
├── [1.1K] logo.svg
├── [ 67] robots.txt
├── [4.0K] __snowpack__
│ └── [ 126] env.js
└── [4.0K] web_modules
├── [4.0K] common
│ └── [6.1K] index-2f302f93.js
├── [ 21K] date-fns.js
├── [ 273] import-map.json
├── [4.0K] svelte
│ ├── [ 421] internal.js
│ ├── [ 564] store.js
│ └── [ 217] transition.js
├── [4.8K] svelte-inline-svg.js
└── [ 54] svelte.js
6 directories, 24 files
Weird. I don't know if it's cached somewhere or if the numbers are showing something else.
Looks like Snowpack's plugin-optimize does some light optimization, but Showpack also has a real official bundler plugin - plugin-snowpack. It actually use to have an official Parcel plugin, but looks like it has been removed.
Last time I tried Snowpack's Parcel and Snowpack plugin none of them worked. Has anything changed? Let's try Webpack plugin.
Nope. Still getting the same "'babel-loader' not found" error from it.
Entry module not found: Error: Can't resolve 'babel-loader'
It installed it, but it didn't help. I give up. Don't feel like I want do go down the rabbit hole.
As of Rollup, no official Rollup plugin exists (yet), but Svelte crew promised that it's coming soon.
Snowpack is all about plugins and they have great documentation on how to build your own. They actually encourage people to build them.
I decided to test and build an SWC compiler plugin for TypeScript files to see how hard it would be. Just for fun.
Let's get dirty. Install the SWC compiler and create a plugins
directory.
$ pnpm add -D @swc/core && mkdir plugins
In the new plugins directory create a file named plugin-swc.js
with the following contents.
// plugins/plugin-swc.js
const swc = require('@swc/core');
const fs = require('fs');
module.exports = function (snowpackConfig) {
// read options from the main Snowpack config file
const useSourceMaps = snowpackConfig.buildOptions.sourceMaps;
return {
name: 'snowpack-swc',
resolve: {
input: ['.ts'],
output: ['.js'],
},
async load({ filePath }) {
// read the TypeScript file
const contents = await fs.promises.readFile(filePath, 'utf-8');
// transform it with SWC compiler
const output = await swc.transform(contents, {
filename: filePath,
sourceMaps: useSourceMaps,
isModule: true,
jsc: {
parser: {
syntax: 'typescript',
},
target: 'esnext',
},
});
return {
'.js': {
code: output.code,
map: output.map,
},
};
},
};
};
Now, add it to the Snowpack config.
...
plugins: [
'@snowpack/plugin-svelte',
'@snowpack/plugin-dotenv',
'@snowpack/plugin-typescript',
'@snowpack/plugin-optimize',
'@snowpack/plugin-postcss',
'./plugins/snowpack-swc.js',
[
'@snowpack/plugin-run-script',
{ cmd: 'svelte-check --output human', watch: '$1 --watch', output: 'stream' },
],
],
...
Start the dev server and be amazed. Your TypeScript files are now being transpiled by the SWC compiler.
Well, I am not actually 100% sure about that, because Snowpack comes with ESBuild plugin baked-in and I don't know if my SWC plugin overrides it or not.
Anyhow, this was for demonstration purpose only to show how easy it is to build a plugin in Snowpack with only a few lines of code.
If you look at Snowpack's official plugins, most of them are really simple to understand and only a few hundred lines long.
I really like this plugin approach. It makes it very easy to build your own and developers have already started building all sorts of plugins.
Sorry for getting side-tracked. The test web app now actually works. Let us revisit the list of initial requirements and see how well Snowpack did.
Snowpack's underlying build pipeline design is very good. It feels thought through.
There are a few parts that I like. I like that it uses start
script as the entry point to start the dev server. It always felt more intuitive to me than npm run dev
.
When the dev server is disconnected the web app does not spam Dev Tools console log trying to reconnect to the dev server.
SvelteKit, the newly announced Svelte framework that will replace Sapper, is betting on Snowpack. According to Svelte's crew they evaluated Vite.js as an alternative, but found it not to be framework agnostic enough.
Snowpack does not have any official bundler plugin that works with Svelte, but as I understand the Svelte gang will work on making a Rollup plugin. Don't know if it will be only as a part of SvelteKit's build pipeline or an official framework-agnostic Snowpack Rollup plugin.
But maybe Rollup plugin is not needed at all? From what I've read esbuild has tree shaking functionality that is almost on par with Rollup's. I don't think that it's used in Snowpack yet and don't know either if there are any plans to implement it.
As I wrote earlier, the official Webpack plugin didn't work for me, but since it's easy to build plugins in Snowpack and the documentation is good, I bet that we can expect a lot of new and interesting plugins from the developer community in the near future. Like this one for example, Svelte with Snowpack with Google Closure Compiler.
Overall, Snowpack is an interesting and future-glancing project with a helpful community and a fierce development speed. But somehow it feels like it's rushing forward a little too fast for its own good. A lot of features feel half-finished and documentation, although great, often lags behind or is outdated.
Sorry Snowpack, I like you, but I will use another bundler for my Svelte projects for now. But don't be sad! I will come and visit in a few months. I am sure it will be a nice visit.
You can find the final code here https://github.com/codechips/svelte-typescript-setups
]]>Let's say that we have a nested JavaScript object that looks like this. If you work with Firebase Firestore, or some of Google Cloud Platform's or AWS API, you might have stumbled on structures like these.
const obj = {
id: 'one',
services: {
'service-foo': {
slug: 'foo',
public: true,
readonly: true,
},
'service-bar': {
slug: 'bar',
public: false,
readonly: true,
users: ['john', 'jane'],
},
'service-baz': {
slug: 'baz',
public: false,
readonly: false,
users: ['bo', 'bill', 'brooke'],
},
},
};
Say we want to display the services
property as a list on the page. While it's possible to loop over the object itself, it's more convenient to loop over a list that looks like this.
[
{
id: 'service-foo',
slug: 'foo',
public: true,
readonly: true,
users: [ 'frank', 'finn', 'fernando' ]
},
{
id: 'service-bar',
slug: 'bar',
public: false,
readonly: true,
users: [ 'john', 'jane' ]
},
{
id: 'service-baz',
slug: 'baz',
public: false,
readonly: false,
users: [ 'bo', 'bill', 'brooke' ]
}
]
How can we transform our our services
property object to an array?
One obvious way is to do it in plain JavaScript. Like this.
const transform = obj => Object.keys(obj).map(id => ({ id, ...obj[id] }));
console.log(trasform(obj.services));
We map over object's keys and construct a new object consisting of the object's key and its properties with the help of the ES spread operator.
I was curious how to achieve the same results with Rambda - a functional programming library for JavaScript.
Turns out there are two ways to do it.
First one is to use Ramda's pipe operator.
const R = require('rambda');
const transform= R.pipe(
R.toPairs,
R.map(([id, props]) => ({ id, ...props }))
);
console.log(trasform(obj.services));
Second is to use Ramda's compose operator.
const R = require('rambda');
const transform = R.compose(
R.map(([id, props]) => ({ id, ...props })),
R.toPairs
);
console.log(trasform(obj.services));
Both results in the same output. It's only a matter of taste. If you are coming form an FP background, compose
will look more natural to you.
Let's break down Rambda's toPairs operator. Maybe a better name for it would be toTuple
.
console.log(R.toPairs(obj.services));
[
[
'service-foo',
{ slug: 'foo', public: true, readonly: true, users: [Array] }
],
[
'service-bar',
{ slug: 'bar', public: false, readonly: true, users: [Array] }
],
[
'service-baz',
{ slug: 'baz', public: false, readonly: false, users: [Array] }
]
]
The toPairs
operator loops over object's keys and returns key and property a list with two items in it - [key, prop]
.
In functional programming you often work with lists as they are easier to manipulate.
What the difference when it comes to the two composition operators in Rambda - pipe
and compose
? It's subtle. In my opinion it's a matter of taste.
The pipe
operator feels most intuitive to developers. It works like a pipe in Unix (ls -l | grep js
), passing the results from one function to another in the chain from left to right. First function can take multiple arguments, but the rest of the functions in the chain can only have one argument as input. It reminds me a lot about the proposed pipeline operator in JavaScript.
The compose
operator does the opposite of pipe. It performs its operations right to left.
I find it intuitive to think of it as the Russian Doll pattern - f4(f3(f2(f1([1,2,3]))))
, where each function is called inside out.
There are probably a few more ways to transform an object to array. Let me know then so I can learn something new.
]]>The app itself is based on Firebase and Firebase Functions was an obvious choice for sending emails. What was not so obvious is how to put everything together. I went through the pain and finally figured it out.
Below are my notes on how I got everything to work with TypeScript using Mailgun, Handlebars templates for HTML emails and Firebase Functions.
I assume here that you already have a Firebase project set up with the local Firebase emulator running. If not, you can follow my guide - Smooth local Firebase development setup with Firebase emulator and Snowpack. It targets Svelte, but you can skip those parts if you are using some other framework or technology.
There are many transactional email providers, but I chose Mailgun. Mostly because of their cool name and logo, and also because they have a generous free quota (5K emails/month) and a nice Javascript SDK.
I won't go through setting and configuring Mailgun, DNS and all that jazz. They already have good documentation on how to do it. Instead, I will concentrate on wiring everything up in code.
There are several Mailgun libraries on NPM, but we will go with the official one. It's written in Javascript and if you are using Typescript you have to install the types.
$ npm add mailgun.js @types/mailgun-js
Because Mailgun SDK is written in Javascript you have to add esModuleInterop
to your tsconfig.json
.
{
"compilerOptions": {
"module": "commonjs",
"noImplicitReturns": true,
"noUnusedLocals": true,
"esModuleInterop": true,
"outDir": "lib",
"sourceMap": true,
"strict": true,
"target": "es2017"
},
"compileOnSave": true,
"include": [
"src"
]
}
All right! We are ready to roll!
To keep code clean we will keep the email sending logic in a separate file. Create a new email.ts
file in the src
directory.
// email.ts
import * as functions from 'firebase-functions';
import Mailgun from 'mailgun-js';
const apiKey = 'mailgun-api-key';
const domain = 'mg.example.com';
const mg = new Mailgun({ apiKey, domain });
export const send = (subscribers: string[]) =>
mg.messages().send({
from: 'Mailgun Test <noreply@mg.example.com>',
to: subscribers,
subject: 'Mailgun test',
text: 'Hello! How are you today?',
html: '<h1>Hello!</h1><p>How are you today?</p>'
});
So far, so good. With this code we can send an email to a bunch of subscribers whose email addresses are passed in as a list of strings.
Mailgun SDK requires an API key to send email. While you can hardcode it in your code, as we did in the example above, you should never do it. Instead, we can leverage Firebase Remote Config for this and keep our Maingun API key securely stored in it.
$ firebase functions:config:get > .runtimeconfig.json
If you already don't have anything there you will get and empty JSON file. Let's add our Mailgun API key to it.
{
"mg": { "key": "your-mailgun-api-key" }
}
NOTE: to set this value in production later you can use the command below.
$ firebase functions:config:set mg.key="your-mailgun-api-key"
Also, make sure to keep all your config keys lowercase or Firebase will complain!
Now, when the Firebase emulator starts it will pickup the local config. This means that we can get the key from the functions config.
// email.ts
import * as functions from 'firebase-functions';
// ...
const apiKey = functions.config().mg?.key;
Our basic email function is now done. Let's continue with setting up our Handlebars templates.
HTML email design is hard. It's like going back to the 90s again. Tables and all. Luckily there are plenty of apps that can help you with that. Maybe you even have an old copy of Adobe Dreamweaver laying around somewhere? If you a feeling adventurous.
Jokes aside, if you want to learn more about HTML email design I can highly recommend reading An Introduction To Building And Sending HTML Email For Web Developers on Smashing Magazine.
But, we are aiming for the MVP here as this article's focus is on the technical implementation and not on email design.
My goal was to keep the email templates separate from code and Handlebars is a good fit for that. With that said, it was not as straight forward as I first thought.
Handlebars.js is one of the templating languages that allows you to precompile your templates, so you can later import them in code. It took me a while to get it right, but here is how you do it.
First, let's install Handlebars and npm-run-all utility package that we will use for the precompile step.
$ npm add handlebars npm-run-all
Next we need to create two Handlebars templates. One for html emails and one for text emails.
Create an emails
folder in the root of the project and then create two files in it - html.handlebars
and text.handlebars
.
{{!-- html.handlebars --}}
<h1>{{title}}</h1>
<p>{{body}}</p>
<p>All the best, John</p>
Text template is needed for email clients that can't display HTML. I am looking at you Mutt!
{{!-- text.handlebars --}}
{{title}}
{{body}}
All the best, John
Now that we have created the templates we need to precompile them with this command.
$ npx handlebars emails/ -f src/templates.js -c handlebars/runtime
It compiles the templates into templates.js
module and also includes the reference to handlesbars/runtime
module that is needed for some reason.
Now you can import templates.js
in your code and both text
and html
templates will be available.
// email.ts
import * as functions from 'firebase-functions';
import Mailgun from 'mailgun-js';
import * as Handlebars from 'handlebars/runtime';
import './templates';
const apiKey = functions.config().mg?.key;
const domain = 'mg.example.com';
const mg = new Mailgun({ apiKey, domain });
// import our email and text precompiles templates
const html = Handlebars.templates['html'];
const text = Handlebars.templates['text'];
export const send = (subscribers: string[]) => {
const data = { title: 'Mailgun Test', body: 'How are you?' };
return mg.messages().send({
from: 'Mailgun Test <noreply@mg.example.com>',
to: subscribers,
subject: data.title,
text: text(data),
html: html(data),
});
};
Our email send function is now complete, but let's streamline our development environment a bit, so that Handlebars templates are precompiled automatically before we build our functions.
Change the scripts
section in functions' package.json
to this.
"scripts": {
"build": "run-s build:templates build:tsc",
"build:tsc": "tsc",
"build:templates": "handlebars emails/ -f src/templates.js -c handlebars/runtime",
"serve": "npm run build && firebase serve --only functions",
"shell": "npm run build && firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
When we now execute npm run build
Handlebars will compile the templates first before we compile function. This is done with run-s
(s is for serial) which is a part of the npm-run-all
utility package.
Now that we have all the different parts done, let's wire them all together.
// functions/src/index.ts
import * as functions from 'firebase-functions';
import { send } from './email';
export const sendEmail = functions.https.onCall(async data => {
const { subscribers } = data;
try {
await send(subscribers);
return { ok: true };
} catch (err) {
return { ok: false, error: err.message };
}
});
That's it! Now you can send transactional emails with Mailgun through Firebase Functions. I kept the example minimal in order to keep things simple, but you can fetch data from Firestore or send the email in the Firebase trigger functions. Imagination is the limit!
If you what to start both Firebase emulator and function compilation watch, when you start the project, change the scripts
section of your main project's package.json
to this.
"scripts": {
"build": "run-p build:*",
"build:functions": "cd functions && npm run build",
"firebase:deploy:functions": "firebase deploy --only functions",
"firebase:start": "firebase emulators:start --only functions",
"start": "run-p watch:* firebase:start",
"watch:functions": "cd functions && npm run watch"
},
This is just one way to send transactional emails from Firebase functions. To keep templates separate from code we had to precompile our email templates as Firebase functions can't deal with file system very well. I am sure there are other templating languages that support precompilation, but Handlebars is good enough for the task.
You can find complete example code on Github.
https://github.com/codechips/firebase-functions-mailgun-handlebars-example
Thanks for reading!
]]>I learned Vim and I learned RxJS. I didn't say it was easy. Was it worth it? 100%. The key is being persistent and not give up. If I could learn so can you!
I've compiled a list of some of the best RxJS resources for people who are just getting started on their wonderful journey to become RxJS masters.
Observables, multicast, share, subscriptions, hot, cold, async, streams, forkJoin, switchMap, concat. Oh my! Those terms alone can scare one off. But don't be afraid! Once you follow this guide everything will start making sense.
The way I like to think about RxJS is that it's a library that helps us process our data through a data-processing pipeline we define.
When you hear people talk about RxJS you often hear the word streams. Well, it kind of makes sense. The data flows through the processing pipeline just like water flows through a pipe.
RxJS also contains the word reactive in it. Let's talk about it and what reactivity means in this context. I like to think of it as the data pipeline only activates (reacts) only when we tell it to. What do I mean by that? Let me explain.
Reactivity means that we can trigger our data processing pipeline by various inputs or events that we define in the pipeline. But there is a catch. The pipeline is not activated, even when triggered by our defined events, unless we tell it that we are interested by subscribing to it. No subscribers, no reactivity, no data.
This works as a kind of pull model, even though the data is pushed through the pipeline. Let me explain. When we subscribe to our pipeline (pull), we activate the pipeline. It then activates the pipeline from the bottom all the way up by saying "Hey, we have a subscriber! Let's start working!" All the events listeners and triggers will get activated and they will start processing (pushing) data through our pipeline all the way down to us when something is triggered. When we unsubscribe, the pipeline will become inactive again, because there are no subscribers.
Does it make sense? It's reactive because it's lazy. Nothing happens until you subscribe. Only then it starts reacting (or working).
RxJS is also a declarative framework. This is another concept that can be hard to understand. With imperative (normal) programming you describe exactly what you want to happen, step by step. You give precise instructions. With declarative programming, on the other hand, you write code that describes what you want to do, but not necessarily how to do it. You say what you want, but you don't specify exactly how. It's compiler's job to figure it out. You give the compiler commands instead of telling it exactly what to do. These commands consists of RxJS operators chained together. The implementation details are abstracted away from you. You specify the desired outcome and compiler will figure it out for you.
This is good, because you are forced to work on the higher level of abstraction. The implementation details are already in place for you. Less code to write. You just have to define the desired outcome in your program and RxJS will take care of it.
Another hurdle, and maybe the hardest to jump over, is thinking in streams. Thinking in streams is super-hard and will take you some time to learn. But it's really cool once you grok it. When you finally reach that "Aha!" moment you will for sure get a rush. Suddenly, it will all make sense and you will realize how to merge, delay, filter, split and do lots of other cool things with streams.
One essential thing when learning RxJS is to take it slow and be patient. You head will hurt, for sure. Try not to get frustrated. It will all click together soon if you are persistent. Later, you will wonder why you didn't learn RxJS earlier and will be telling all your friends how cool it is and you will start prompting it in your circles.
I've compiled a list of resources that helped me learn RxJS. RxJS is built into Angular, that's why many articles on the internet are Angular based. But you can ignore all Angluar stuff and concentrate only on the juicy RxJS parts.
The guide is broken down into different sections. Start at the top, it will help you learn the core concepts. Later, feel free to jump around to the articles that catch your attention.
When you read an article and learn a new concept, it's not guaranteed to be crystal clear at first. Come back to it sometimes later and re-read it.
After you learn a new concept or operator, try to play around with it to get that hands-on feeling. Spend 20-30 minutes on it. It will be time well invested.
Also, many of the articles included are on Medium. Sorry about that.
I am a Svelte fan, so I also threw in a couple of Svelte related articles for other Svelte fans out there. One thing about Svelte and RxJS combo is that they go very well together. I like to say that RxJS is Svelte's stores on steroids. You get 200% reactivity if you use RxJS.
Another cool thing is that you don't have to use onMount
, when fetching data for example. Why? Because RxJS is lazy (reactive). Since Svelte views RxJS pipelines as Svelte stores, Svelte compiler manages subscriptions for us automatically. Now, how cool is that?!
The hardest part is getting started. RxJS has many operators, but you only need to learn a handful of them to be productive. Once you learn them, others will start making sense.
The introduction to Reactive Programming you've been missing
Classic introduction to Reactive Programming. This should be your starting point. It's a little dated, but explains the concepts well and teaches you how to think in streams.
Recreating a classic FRP tutorial with Svelte and RxJS
My own take on the tutorial above with Svelte and newer version of RxJS. Plus I went a bit further than the original article by removing code duplication.
Good and short introduction to RxJS in form of free online book. Highly recommended!
I explain why Svelte and RxJS is such a nice combo with a few simple examples.
Short article that gives a digestible overview of RxJS and concepts with a few code examples.
Thinking reactive with the SIP principle
Nice article that teaches you to think reactively with RxJS. I like to think of RxJS operators as Lego pieces that you assemble into something larger.
RxJS, where is the If-Else Operator
Coming from imperative background you might wonder where the if
statement is. Well, there is no if
statement sort of. Instead you use operators like map
and filter
to achieve the desired branching logic.
Understanding hot vs cold Observables
Hot and cold observables. You will hear it a lot when learning RxJS and will most likely get burned by it sometimes. This short article explains the concepts very well.
RxJS: Understanding the publish and share Operators
This article explains in-depth how to turn cold observarbles into hot. Heavy reading, but an excellent reference.
RxJS subjects is another concept that you must understand. They are really useful. This article explains subjects on the higher level.
RxJS Subjects Explained with Examples
Good code examples of RxJS subjects. A follow up article to the one above.
Comprehensive Guide to Higher-Order RxJs Mapping Operators
Mapping operators are the core of RxJS and there are quite a few of them. This article explains them well.
Understanding RxJS map, mergeMap, switchMap and concatMap
Another excellent article on various RxJS mapping operators. A must read!
RxJs Error Handling: Complete Practical Guide
You will get errors and exceptions when working with RxJS and you need to know how to handle them. This in-depth article explains how to deal with errors the RxJS way.
Little dated, but still very good RxJS concepts overview from Fireship. With complementary video too!
Once you get down the basics your imperative mind will still struggle to translate it to declarative thinking. You need to revisit the concepts and examine them closer, more in-depth.
After you learn the basics, you only need to learn a handful of operators. Like, really learn them. Especially different mapping operators. Higher-order observables can be tough to grasp.
Below is a collection of intermediate resources. They all require basic RxJS knowledge. Without it they will be either overwhelming or will just not make any sense to you.
Thinking in nested streams with RxJS
Learn how to work with higher-order observables aka nested streams.
RxJS. Transformation Operators in Examples (part 1)
Very good breakdown of the transformation operators such as different buffer and concat operators. Clear code examples.
RxJS. Transformation Operators in Examples (part 2)
Second part of the transformation operators. This time various merge, scan, group and window operators. Excellent code examples!
Combining Observables with forkJoin in RxJS
ForkJoin is RxJS version of Promise.all
. It's really useful to have when you have to deal with parallel HTTP requests for example.
SwitchMap is one operator that you will use often. This is a nice breakdown of how it works using HTTP requests examples.
RxJS: merge() vs. mergeAll() vs. mergeMap()
Merge is also one of the frequently used operators. Make sure you understand all the different variations of it.
The magic of RXJS sharing operators and their differences
Explains the sharing operators in detail. Those cold vs hot observable concepts.
Creating Custom Operators in RxJS
Learn how to create custom observables in RxJS. Helps you understand and solidify your RxJS observable concepts.
Getting to Know the Defer Observable in RxJS
Defer operator is really handy. It might not be something you will use often, but it's still very important operator to know.
Create a tapOnce custom Rxjs Operator
Continuing on the topic of custom RxJS operators. Here is a very good article explaining how to create a custom tapOnce
operator. You will be using tap
operator a lot when you need to debug your pipelines and see what data flows through them.
Showing a loading spinner delayed with rxjs
Clean example of how to show a loading spinner while waiting for something. I am sure you will want to show a loading spinner in your apps.
Debounce with vanilla JavaScript or RxJS
Examples of debounce using plain JS and RxJS. Which one is better? You be the judge.
A text transcript and source code of the paid RxJS course on Egghead.io. Covers lots of ground!
Below are some advanced topics and tips. Take a look at them when you are really sure you understand the core concepts.
Learn the expand
operator with guitar delay pedal example. Very cool!
RxJS examples of the day (part 1)
Good examples of how solve problems in the most effective ways when using RxJS. Lots of learning opportunities.
RxJS: iif is not the same as defer with ternary operator
Comparison between iif
and defer
and when to use what.
Building a safe autocomplete operator in RxJS
RxJS is really handy for autocomplete. Learn how to build a custom autocomplete operator.
Thinking reactively in Angular and RXJS
Learn how to think reactively by building a calendar app.
Best RxJS reference with good examples. My goto place when I need to look up an operator.
Fun game to learn RxJS. You have to code your way through.
RxJS is usually explained with the help of marble diagrams. This is a good interactive reference to many RxJS operators.
Another interactive tool slash reference that will help you understand operators with code examples and marble diagrams.
Lots of good resources and short interactive animations comparing different operators together. Site feels a bit messy, but the information is really good.
Firebase has really nice RxJS bindings. This article explains the basics.
Introducing BLoC Pattern with React and RxJS
BLoC pattern originated in Dart language, but can be used in other frameworks too. Here is a simple example with RxJS in React.
RxJS Cheatsheet VS Code extension
Handy VSCode extension. View RxJS operator documentation in-place.
https://github.com/AlexAegis/svelte-minesweeper
Cool classic Minesweeper clone built in Svelte, RxJS and TypeScript. Lots of learning opportunities by studying the code.
If videos are your thing, here are some of the best ones.
Essential talk that explains observables by building observables. If you plan on watching one talk only, make it this one.
Learn RxJS in 60 Minutes for Beginners
A nice crash course that covers the basics of RxJS. Covers lots of ground.
RxJS Recipes - Uphill Conf 2019
Sweet and short talk on how to solve a problem using RxJS. Learn how to think in streams and patterns. Required watching.
Mastering the Subject: Communication Options in RxJS | Dan Wahlin
A really good explanation of RxJS subjects and how you can use them to communicate between different application components.
The Magic of RxJS - Natalia Tepluhina | JSHeroes 2019
Shows how you can use RxJS to build a Pong game. Heavy code, but very inspirational talk!
Data Composition with RxJS | Deborah Kurata
Really good talk that explains how you can use RxJS to fetch data and do cross-component communication. A must watch!
Thinking Reactively: Most Difficult | Mike Pearson
Learn how to think reactively by building a typeahead search. Very good talk for RxJS newbies.
Understanding RxJS Error Handling
Excellent talk on different exception handling strategies in RxJS.
Why Should You Care About RxJS Higher-order Mapping Operators?
Explains higher-order RxJS mapping operators with clear examples that all can understand.
I switched a map and you'll never guess what happened next
Fun interactive talk that explains mapping operators with the help of hiring agency example.
RxJS Advanced Patterns – Operate Heavily Dynamic UI’s | Michael Hladky
Advanced concepts talk that goes very deep. Expect to scrub a lot in order to understand everything.
Here are some tips from me that can help you on your RxJS learning journey.
tap
operator for debugging your pipelines. Inject tap(console.log)
anywhere in your pipeline to view the data flowing through it.of
, from
, merge
, BehaviourSubject
, combineLatest
, startWith
and all the mapping operators. They will give you a solid base to stand on.You can read articles and watch videos all you want, but in order to truly learn and understand you have to do some coding. Here are some good problems you can try to tackle with RxJS.
Timer App
Try to create a simple timer app. You should be able to start, stop and reset the timer. Bonus points: be able to set a countdown time.
Typeahead Search
Try to implement a simple typeahead TV show search by using Episodate API.
RxJS is a wonderful technology, but it's not widely adopted yet. One of the main reasons might be the lack of good learning resources. Hopefully this resource will help your discover its super-powers.
My main advice is to take it slow and don't give up! It will all click soon enough if you are persistent. Soon you will be wondering how you even could do something without RxJS and will mutter for yourself how easy you could have solved the problem if only this project used RxJS.
]]>Let's be honest. Trend sensitivity in tech is a big problem. Developers are jumping from editor to editor whenever something new, hot and trendy comes out. Always searching for the Holy Grail. Sublime, Atom, VSCode. What comes next?
Vim is a very old editor. It's 25 years old. That's ancient in the tech world. What can possibly justify this piece of software's existence? Something must be good if it still lives, right? What are the benefits?
One of Vim's powers is its simplicity and extensibility. It's a very extensible editor. It's also scriptable. It has macros and tons of other useful features. It's small, doesn't hog your CPU and make your computer fans go crazy. It often comes prepackaged with almost every Unix-like operating system.
It's also a powerful editor for Javascript, TypesScript and Svelte development.
Neovim is the modern fork of Vim. Its goal is to clean and extend Vim with modern features. Personally, I don't use Neovim. I tried it, but I can't honestly tell what makes it better than the original Vim. The gains are not obvious to me. I am sticking to original Vim for now, currently version 8.2.
You might say "But what about Emacs?" I tried Emacs too for a while after getting inspired from watching Emacs Rocks videos. Tried only to realize that I was spending most of my working time configuring Emacs instead of doing the actual work. Also, I already have an operating system, thank you.
Oh, and you know, all those people who just have to crack a joke about exiting Vim, why don't you ask them to try and exit Emacs next time? Look who is laughing now!
I always use Tmux and Vim together. What it Tmux? I would describe it as a tiling window manager for the terminal. You have to tweak it a bit to you liking, but once you master it, windows will start flying.
This is out of scope for the article. I just wanted to mention it.
First things first, we have to configure Vim syntax highlighting for Svelte, Javascript and TypeScript.
For Svelte, vim-svelte is the plugin to use. It provides indentation and syntax highlighting for Svelte components. There is also vim-svelte-plugin that does the same thing.
For Javascript language highlighting, you have a few different options. The most popular seem to be vim-javascript and this is what I use.
There is also vim-js and jats. If you are feeling adventurous check out vim-polyglot, a plugin with more language syntax hightlights that you probably will ever need.
When it comes to TypeScript, I settled on yats.vim. I find it the best, but typescript-vim is another good option.
I am using vim-plug as my Vim plugin manager and if you do too, this is how to add them to Vim config file.
Plug 'evanleck/vim-svelte'
Plug 'pangloss/vim-javascript'
Plug 'HerringtonDarkholme/yats.vim'
Syntax highlighting is a nice to have, but autocomplete is essential for a good coding session. Let's tackle that next.
Honestly, how did I write code in Vim before this extension existed? Apparently I did, but I bet it was painful.
Coc.nvim is a must-have if you are a Vim user. It's an "Intellisense engine for Vim8 & Neovim, full language server protocol support as VSCode" according to their website.
What does it mean? It means it supports the same language protocol standard as VScode, which in turn means you can easily implement an autocompletion for a language if that language has a language server. Crystal clear, right?
Here is Svelte's language server for example.
Installing Coc.nvim is straight forward. Add the line below to your .vimrc
in the right place and run :PlugInstall
.
Plug 'neoclide/coc.nvim', {'branch': 'release'}
Coc.nvim has many extensions and we will install one for Svelte next, along with a few others we will need too.
If you are coding Svelte in Vim coc-svelte is a must. Unfortunately, the original extension's Svelte dependencies are lagging behind, so I keep my own fork of it where I try to keep upstream dependencies in sync.
Add it to your .vimrc
with the line below.
Plug 'codechips/coc-svelte', {'do': 'npm install'}
Beside Svelte extension we also need to install TypeScript Coc extension. It will help us with autocompletion for TypeScript and JavaScript. Install it in Vim by running :CocInstall coc-tsserver
command.
Coc.nvim autoupdates dependencies in the background, but you can always update them manually by running :CocUpdate
command.
If you are using TypeScript, SASS or PostCSS in your Svelte components you have to create a svelte.config.js
file in the root of your project. That file is needed by coc.nvim in order to interpret those parts of the Svelte files correctly. Read my TypeScript and Svelte article on how to do that - How to use Typescript with Svelte.
We also want our files to look nice and therefore we need to format them. There is a coc-prettier extension that does that, but for some reason it breaks Svelte files for me instead. Sometimes it doubles the lines at the end of the file. For that reason I decided to go with vim-prettier extension instead.
For Prettier extension to work properly it needs to find a prettier
CLI and Prettier configuration. There are multiple ways of configuring and setting it up, but I always do it locally for every of my projects. It's the easiest.
$ npm add -D prettier prettier-plugin-svelte
You also need to add Prettier vim plugin to your Vim config and tweak some of its settings.
Plug 'prettier/vim-prettier', { 'do': 'npm install' }
" Prettier Settings
let g:prettier#quickfix_enabled = 0
let g:prettier#autoformat_require_pragma = 0
au BufWritePre *.css,*.svelte,*.pcss,*.html,*.ts,*.js,*.json PrettierAsync
One drawback of Prettier is that you have to install Prettier and prettier Svelte plugin in every Svelte project. It might be possible to use a global Prettier config, because it will search for Prettier configs up the tree.
However, I strongly advice against doing it, because you might want to share same config with your team.
My prettier config usually looks something like this.
module.exports = {
arrowParens: 'avoid',
printWidth: 90,
singleQuote: true,
svelteBracketNewLine: true,
svelteStrictMode: false,
trailingComma: 'none',
plugins: ['prettier-plugin-svelte'],
};
Prettier uses cosmiconfig. That means you can use any name and format it supports.
If you need to configure Coc.nvim and its extensions you can open the config file directly in Vim by running :CocConfig
command. You can also edit the coc-settings.json
file directly. You will find it in your ~/.vim
folder or ~/.config/nvim
if you are using Neovim.
Here is what mine looks like.
{
"diagnostic.messageTarget": "echo",
"signature.target": "echo",
"coc.preferences.hoverTarget": "echo",
"javascript.suggestionActions.enabled": true,
"typescript.suggestionActions.enabled": true,
"prettier.printWidth": 120,
"prettier.singleQuote": true
}
All those "echo" values force Coc to display diagnostic messages in Vim's command line instead of a popup. I don't like distracting popups.
These are Coc.nvim settings in my .vimrc
.
" COC
let g:coc_node_path = '$HOME/.nvm/versions/node/v12.16.3/bin/node'
nmap ff (coc-format-selected)
nmap rn (coc-rename)
nmap gd (coc-definition)
nmap gy (coc-type-definition)
nmap gi (coc-implementation)
nmap gr (coc-references)
set updatetime=300
set shortmess+=c " don't give |ins-completion-menu| messages.
" Use K to show documentation in preview window
nnoremap K :call show_documentation()
function! s:show_documentation()
if (index(['vim','help'], &filetype) >= 0)
execute 'h '.expand('')
else
call CocAction('doHover')
endif
endfunction
Maybe you can get some inspiration from reading them.
It's not sure that Vim will recognize that your Svelte file consists of different sections and languages (JavaScript, CSS) and might comment them out using HTML comments when you try to comment them out.
It happened to me, and if it happens to you, here is how to solve it.
Install the context_filetype Vim plugin. It will help us set the filetype based on the file section.
Plug 'Shougo/context_filetype.vim'
Add the following code to your Vim config.
if !exists('g:context_filetype#same_filetypes')
let g:context_filetype#filetypes = {}
endif
let g:context_filetype#filetypes.svelte =
\ [
\ {'filetype' : 'javascript', 'start' : '<script>', 'end' : '</script>'},
\ {
\ 'filetype': 'typescript',
\ 'start': '<script\%( [^>]*\)\? \%(ts\|lang="\%(ts\|typescript\)"\)\%( [^>]*\)\?>',
\ 'end': '',
\ },
\ {'filetype' : 'css', 'start' : '<style \?.*>', 'end' : '</style>'},
\ ]
let g:ft = ''
Now your comments should behave correctly.
I am using NERDCommenter, a Vim commenting plugin with the best punchline - "Comment functions so powerful - no comment necessary."
For it to work properly with Svelte I had to tweak its settings a bit.
" NERDCommenter settings
let g:NERDSpaceDelims = 1
let g:NERDCompactSexyComs = 1
let g:NERDCustomDelimiters = { 'html': { 'left': '' } }
" Align comment delimiters to the left instead of following code indentation
let g:NERDDefaultAlign = 'left'
fu! NERDCommenter_before()
if (&ft == 'html') || (&ft == 'svelte')
let g:ft = &ft
let cfts = context_filetype#get_filetypes()
if len(cfts) > 0
if cfts[0] == 'svelte'
let cft = 'html'
elseif cfts[0] == 'scss'
let cft = 'css'
else
let cft = cfts[0]
endif
exe 'setf ' . cft
endif
endif
endfu
fu! NERDCommenter_after()
if (g:ft == 'html') || (g:ft == 'svelte')
exec 'setf ' . g:ft
let g:ft = ''
endif
endfu
There are plenty of other useful tips and tricks, but I will stop here.
If you stuck with me this far you should have a fully functional Vim setup ready for Svelte development.
https://github.com/evanleck/vim-svelte
https://github.com/leafOfTree/vim-svelte-plugin
https://github.com/pangloss/vim-javascript
https://github.com/yuezk/vim-js
https://github.com/junegunn/vim-plug
https://github.com/leafgarland/typescript-vim
https://github.com/HerringtonDarkholme/yats.vim
https://github.com/sheerun/vim-polyglot
https://github.com/neoclide/coc-prettier
https://github.com/prettier/vim-prettier
https://github.com/preservim/nerdcommenter
https://github.com/Shougo/context_filetype.vim
https://github.com/davidtheclark/cosmiconfig
My goal has always been to shorten the time between an idea, a thought, and its final implementation on screen. I think I've come pretty close by now by using Vim as my main editor.
However, learning Vim comes with a price. I can barely write anything outside Vim now. Short texts, no problem. Longer texts, forget it. The muscle memory is strong!
One big reason I like Vim is that it's never in my way. I don't like being distracted. It's just me, a black computer screen and my thoughts.
My final advice:
If you want to learn Vim, Vimcasts is a good starting point. Godspeed.
]]>Today I finally decided to go to the bottom of this. What I found was surprising, but I was actually not that surprised. Maybe it's because this is exactly how I imagined them to work?
Anyhow, turns out it's very simple and the Svelte compiler does no magic at all! Read on to learn how Svelte's module scripts work!
Note: This article is code-heavy. It's needed in order to clarify things. The code examples themselves are however kept light. No need to complicate things to get the point across.
You have probably stumbled upon module functions if you've worked with Sapper. This example is taken straight from its documentation.
<script context="module">
export async function preload(page, session) {
const { slug } = page.params;
const res = await this.fetch(`blog/${slug}.json`);
const article = await res.json();
return { article };
}
</script>
<script>
export let article;
</script>
<h1>{article.title}</h1>
How does it work and how is the article
available to the component? Well, in case of Sapper there is some magic going on behind the scenes, but to explain it in simple terms:
Everything you define in a module script block will be available to the component defined in the same file.
Let's break it down and start with the basics. First, we create a simple Svelte component that has a module script block. In order to understand what's going on we will add a few logging statements.
<!-- Component.svelte -->
<script context="module">
console.log('init module');
let foo = 'bar';
const settings = { show: true };
</script>
<script>
import { onMount } from 'svelte';
export let number = 0;
console.log('init component');
onMount(() => console.log(`mounted component #${number}`));
</script>
<div class="component">
<h3>Component #{number}</h3>
<p><code>module[Component].foo = {foo}</code></p>
<p><code>module[Component].settings = {JSON.stringify(settings)}</code></p>
</div>
In the module block we defined two variables, foo
and settings
. Then, in the component itself we access those variable just like we define them in the component itself.
How is that possible? If you look at the Javascript file Svelte compiler generated for us you will see that the things we defined in the module block were copied to the main module file.
// Component.js
console.log("init module");
let foo = "bar";
const settings = { show: true };
function instance($$self, $$props, $$invalidate) {
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots("Component", slots, []);
let { number = 0 } = $$props;
console.log("init component");
onMount(() => console.log(`mounted component #${number}`));
const writable_props = ["number"];
// ...
}
class Component extends SvelteComponentDev {
constructor(options) {
super(options);
init(this, options, instance, create_fragment, safe_not_equal, { number: 0 });
dispatch_dev("SvelteRegisterComponent", {
component: this,
tagName: "Component",
options,
id: create_fragment.name
});
}
// ...
}
export default Component;
All the compiler does is to copy over anything defined in the module block to the top of the Javascipt module file where component is defined.
Since our Svelte component lives in the same module file, everything defined in the module context is automatically available to the component. That's how Javascript scopes work.
<!-- index.svelte -->
<script>
import Component from './Component.svelte';
</script>
<Component number={1} />
<Component number={2} />
<Component number={3} />
When we import the component into our index.svelte
file it will import the component class itself because it's a default export from the Javascript module Component.js
.
Since component is defined in that module the module will be parsed and executed by the Javascript engine and all module level variables will be initialized.
import Component from "./Component.js";
When we view this page in the browser with dev tools open we will see this output in the console.
As you can see from the screenshot the module is executed only once, and also before any component is initialized. It happens when this module is imported by some other file.
When variables and functions are defined on the module scope they will be evaluated only once, when the module is first evaluated (imported), and will be available to all components defined in that module.
As of today, you can only have one component per module in Svelte, but there is an RFC to be able to define multiple components per module, eg in one Svelte file. Maybe we will be able to in Svelte v4.
Hopefully this make it more clear how module level scripts work, but it's not very useful right now because we cannot manipulate these module level variables. Let's fix it.
In order for us to be able to change our shared variables, and for those changes to be picked up by all components, the variables have to be reactive.
Let's convert them to Svelte stores instead.
<!-- Component.svelte -->
<script context="module">
import { writable } from 'svelte/store';
const foo = writable('foo');
const settings = writable({ show: true });
</script>
<script>
import { onMount } from 'svelte';
export let number = 0;
onMount(() => console.log(`mounted component #${number}`));
</script>
</style>
<div class="component">
<h3>Component #{number}</h3>
<div class="flex mt">
<code>module[Component].foo = {$foo}</code>
<button on:click={() => ($foo = `Component #${number}`)}>change foo</button>
</div>
<div class="flex mt">
<code>module[Component].settings = {JSON.stringify($settings)}</code>
<button on:click={() => ($settings.show = !$settings.show)}>toggle settings.show</button>
</div>
</div>
This is what the compiled component file looks like now.
// Component.js
// ...
const foo = writable("foo");
const settings = writable({ show: true });
class Component extends SvelteComponentDev {
// ...
}
export default Component;
Now we can manipulate those variables from any component and all the changes will be instantly reflected in the other components. The power of Svelte's reactivity.
But what if we want to manipulate and access these module variables outside the component itself? We can do that by prepending an export
statement to the variables and function we want to export.
<!-- Component.svelte -->
<script context="module">
import { writable } from 'svelte/store';
// export foo store from module
export const foo = writable('foo');
// this store is private, not exported from the module
const settings = writable({ show: true });
export const setFoo = s => {
foo.set(s);
};
// export function to manipulate module state that is private to the module
export const toggleShow = () =>
settings.update(state => ({ ...state, show: !state.show }));
</script>
<script>
import { onMount } from 'svelte';
export let number = 0;
onMount(() => console.log(`mounted component #${number}`));
</script>
<div class="component">
<h3>Component #{number}</h3>
<p><code>module[Component].foo = {$foo}</code></p>
<p><code>module[Component].settings = {JSON.stringify($settings)}</code></p>
</div>
If you look in the end of the compiled file you will see that the Svelte compiler exports these variables for us along side with the default Component
module export.
// Component.js
const foo = writable("foo");
// this store is private, not exported from the module
const settings = writable({ show: true });
const setFoo = s => {
foo.set(s);
};
const toggleShow = () => settings.update(state => ({ ...state, show: !state.show }));
class Component extends SvelteComponentDev {
// ...
}
export default Component;
export { foo, setFoo, toggleShow };
This means we can import our exports in other places like so.
<!-- index.svelte -->
<script>
import Component, { toggleShow, setFoo, foo } from './Component.svelte';
let value = '';
const setValue = () => {
setFoo(value);
value = '';
};
</script>
<div>
<code>foo = {$foo}</code>
<input type="text" placeholder="New foo value" bind:value />
<button on:click={setValue}>set foo value</button>
<button on:click={toggleShow}>toggle show outside component</button>
</div>
<Component number={1} />
<Component number={2} />
<Component number={3} />
If we look at the compiled index.svelte
file we will find the following import statement.
// index.js
import Component, { toggleShow, setFoo, foo } from "./Component.js";
This is pretty much everything you need to know about Svelte's module script blocks.
If you for some reason don't want to use module scripts you achieve the same functionality with pure Javascript modules. Here is a simple example of how to do it.
<!-- Page.svelte (former index.svelte) -->
<script>
// import module state and Svelte components from main module file
import { Component, toggleShow, setFoo, foo } from './index';
let value = '';
const setValue = () => {
setFoo(value);
value = '';
};
</script>
<div>
<code>foo = {$foo}</code>
<input type="text" placeholder="New foo value" bind:value />
<button on:click={setValue}>set foo value</button>
<button on:click={toggleShow}>toggle show outside component</button>
</div>
<Component number={1} />
<Component number={2} />
<Component number={3} />
We will import our module variables from the index.js
file.
<!-- Component.svelte -->
<script>
import { onMount } from 'svelte';
// import module state stores
import { foo, settings } from './index';
export let number = 0;
onMount(() => console.log(`mounted component #${number}`));
</script>
<div class="component">
<h3>Component #{number}</h3>
<p><code>module[index].foo = {$foo}</code></p>
<p><code>module[index].settings = {JSON.stringify($settings)}</code></p>
</div>
We move our module variables from Component.svelte
to the index.js
module.
// index.js
import { writable } from 'svelte/store';
// create and export shared state stores
export const foo = writable('foo');
export const settings = writable({ show: true });
// export helper functions
export const setFoo = s => foo.set(s);
export const toggleShow = () =>
settings.update(state => ({ ...state, show: !state.show }));
// export Svelte components
export { default as Component } from './Component.svelte';
export { default as Index } from './Page.svelte';
In the compiled Javascript version for Page.svelte
file you will see the following import statement.
// Page.js
import { Component, toggleShow, setFoo, foo } from "./index.js";
And in the Javascript version of Component.svelte
you will find this statement.
// Component.js
// ...
import { foo, settings } from "./index.js";
class Component extends SvelteComponentDev {
// ...
}
export default Component;
As you can see, we managed to replicated Svelte's module block functionality with regular Javscript. I don't know if we gained anything from it though. Feels like it only complicates things for us.
The context module directive is straight-forward when you examine it closely. When you tag your script with context="module"
the code it contains will be copied to the module level of that Svelte file. That's it. How variables and functions defined on the module level can be accessed in the component class is just how JavaScript scoping works.
You can find the full code for this article if you want to play around with it.
https://github.com/codechips/svelte-module-scripts-examples
One might wonder why there's such trivial, and even unnecessary, functionality in Svelte as module level blocks. Modules and Svelte slots allow us to build very powerful abstractions on top of them. I will teach you how in my future posts.
]]>One year ago I wrote a post on how to do client-side authentication in Svelte with Auth0 - Svelte Auth0 integration in 66 LOC. While it's possible to use it in Sapper you shouldn't do it. There is a better, and more secure way, to do in with Express middleware called express-openid-connect.
This post will also show you how to make authenticated requests to your third-party backend API using Auth0 access tokens.
Although the express authentication middleware is maintained by Auth0 it's not locked to it and you can use it with any OpenID Connect provider.
I am using it in a Sapper app running on Vercel with Auth0 as my authentication provider.
This app is an internal app and one nice thing about Auth0 is that you can do fancy stuff in the configuration, such as allow users to sign in with their Google account, but restrict it to your company domain only.
NOTE: The app itself is written in TypeScript and therefore all examples will be in TypeScript as well. If you are not using TypeScript you can just omit all the type imports and their usage in code.
Also, all code examples are copied straight from the app and slightly adjusted. This post is meant to guide you in the right direction and not serve as the complete working example.
Sapper is using Polka as a default server, but I use Express.js in my app. Since the app is written in TypeScript so we also need to install Express.js types.
$ npm add express @types/express
Because we will be making requests to the backend API we also need to install some kind of HTTP client. There are many good clients available. Some of the most popular ones are Axios, Superagent and got. In my app I use wretch, a thin wrapper on top of the Fetch API that also works on the server.
$ npm add wretch node-fetch form-data
With all required dependencies in place we can now start configuring our Sapper app for backend authentication and remote HTTP calls.
I assume that you already have your Auth0 config ready, so I won't explain how to set everything up in the Auth0 console. Two important requirements is that your config is setup as a backend app and not SPA and that you have defined an audience.
The audience part is crucial, otherwise you will not get a correct JWT token back. Also, it's important that you use access_token
for your backend calls and not id_token
. This is a common misconception and mistake I see developers make. Id tokens should only be used for fetching profile information! You must use access token if you plan on setting up and using grants aka API access permissions.
Here is the server.ts
file in all its glory with Auth0 middleware in place.
// src/server.ts
import sirv from 'sirv';
import compression from 'compression';
import * as sapper from '@sapper/server';
import express from 'express';
import { auth, OpenidRequest, OpenidResponse } from 'express-openid-connect';
// node fetch is installed
global.fetch = require('node-fetch');
global.FormData = require('form-data');
global.URLSearchParams = require('url').URLSearchParams;
const {
PORT = 3000,
OAUTH_BASE_URL = 'http://localhost:3000',
NODE_ENV = 'development',
OAUTH_ISSUER_BASE_URL = 'https://your-tenant.auth0.com',
OAUTH_CLIENT_ID = 'your-client-id',
COOKIE_SECRET = 'very-secret-12131415',
OAUTH_CLIENT_SECRET,
OAUTH_AUDIENCE = 'https://example.com/api',
} = process.env;
const dev = NODE_ENV === 'development';
const config = {
attemptSilentLogin: true,
authRequired: false,
auth0Logout: true,
baseURL: OAUTH_BASE_URL,
clientID: OAUTH_CLIENT_ID,
issuerBaseURL: OAUTH_ISSUER_BASE_URL,
secret: COOKIE_SECRET,
clientSecret: OAUTH_CLIENT_SECRET,
authorizationParams: {
scope: 'openid profile offline_access email',
response_type: 'code',
audience: OAUTH_AUDIENCE,
},
};
const app = express().use(
compression({ threshold: 0 }),
sirv('static', { dev }),
express.json(),
auth(config),
(req: OpenidRequest, res: OpenidResponse, next?: (err?: Error) => void) => {
return sapper.middleware({
session: () => {
return {
isAuthenticated: req.oidc.isAuthenticated(),
user: req.oidc.user,
};
},
})(req, res, next);
}
);
if (dev) {
app.listen(PORT, () => console.log(`Listening at http://localhost:${PORT}`);
}
export default app;
As I mentioned earlier this app is running on Vercel and Vercel builder requires us to export the app. I am setting up wretch
in the beginning of the file. This is needed for it to work on the server-side.
I also setup Auth0 config using environment variables. The cookie secret variable is just a random long string that openid connect middleware will use to sign (and maybe also encrypt?) the authentication cookie with. The rest of the values comes from Auth0 config.
You can see that I wrap Sapper middleware into its own function. That is needed, otherwise it will override the openid connect routes. It's important that Sapper middleware comes last in the middleware chain.
The openid connect middleware exposes /login
and /logout
routes in our app that we will leverage. It has tons of other configuration options, which I only understand half of. The documentation is very good and you have plenty of different authentication example scenarios to choose from.
We are done with server configuration. If you set up everything correctly and can start the app it means that we are half-way and have enabled server-side authentication in our app.
Let's put it to use!
Since we've added Auth0 state to the Sapper session with isAuthenticated
and user
properties, we can use them both on server-side and client-side to check the if the user is logged in or not.
As you probably already know, Sapper hydrates the app. It means that the initial request will be processed server-side and then, when you request a new route, Sapper will intercept it, fetch the page in the background and replace it for us on the client-side. This is what makes Sapper apps and page transitions smooth like butter.
You can access Sapper session in any page of your app, but probably the best way to do it is in _layout.svelte
as this page is usually shared by other pages.
// src/routes/_layout.svelte
<script lang="ts">
import { stores } from '@sapper/app';
const { session } = stores();
</script>
{#if $session.isAuthenticated}
<div>
Signed in as <strong>{$session.user.given_name}</strong>
| <a href="/logout">Sign out</a>
{:else}
<a href="/login">Sign In</a>
{/if}
<pre>
{JSON.stringify($session, null, 2)}
</pre>
That's it! Now you should be able to login and logout through Auth0 middleware. To test that authentication works correctly: login, logout and hit the back button in the browser. Are you still logged in or do you end up in the "not authenticated" app state?
All is good, but the app is kind of useless if we can't make authenticated calls to our backend API. Let's tackle that next!
This is the hard part. We want to call our backend API using Auth0's access_token
. It's not as straight-forward as you might think. Because of CORS restrictions I found that the easiest way to call external backend API is through Sapper's backend API. You can say it's kind of an internal proxy call.
What do I mean by that? We need to create a separate backend API route in Sapper for every call we want to make to our external API. So the flow will be "Frontend Ajax Call -> Sapper API route -> External Backend API". "Why the ceremony?" you might ask. Bear with me and I will explain everything.
You see, when you authenticate with Auth0, the middleware will set a cookie for us in the browser and that cookie will be sent and validated on every request to the backend. There are no databases involved. The complete Auth0 session is kept in that cookie. What's stored in the authentication cookie is partially defined by the scopes we defined earlier in the Auth0 config in server.ts
file.
This means that we have access to that information in the Sapper's backend. Let's use it by creating a simple Sapper API endpoint where we fetch some user info from an external API.
Create a new API route at src/routes/api/user.fetch.ts
. This will create a new API route in our Sapper app available as /api/user.fetch
. I like to use this kind of RPC route names. I got inspired by Slack's API and it makes them much nicer to read and understand in my opinion. Single purpose.
// src/routes/api/user.fetch.ts
import wretch from 'wretch';
import { getAccessToken } from './_utils';
import type { OpenidRequest, OpenidResponse } from 'express-openid-connect';
const endpoint = `https://api.example.com/api/users`;
export async function get(req: OpenidRequest, res: OpenidResponse) {
const { id } = req.query;
try {
const accessToken = await getAccessToken(req);
const url = `${endpoint}/${id}`;
const payload = await wretch(url)
.auth(`Bearer ${accessToken}`)
.get()
.json();
return res.json(payload);
} catch (error) {
res
.status(error.status || 500)
.end(error.message || error.text || 'Got unknown error from backend API');
}
}
There is nothing magical going on. We call this route with GET /api/user.fetch?id=123
in the frontend, extract the user ID from the request object, call our external API with the access token and then return the result back to the frontend.
In this backend route you can validate your payloads and massage the response before it's sent back to the client if you need to.
You might have noticed the getAccessToken(req)
call. This is where we fetch the access token. I've abstracted it away so I can reuse it in other API calls.
// src/routes/api/_utils.ts
import type { OpenidRequest } from 'express-openid-connect';
export const getAccessToken = async (req: OpenidRequest) => {
let { access_token, isExpired, refresh } = req.oidc.accessToken;
if (isExpired()) {
({ access_token } = await refresh());
}
return access_token;
};
This is just a helper function that extracts the access token from the request and refreshes it if it's expired. I haven't validated it, but I think that the library makes an call to the Auth0 if the access token has expired and needs to be refresh. Doing that little OAuth jazz dance.
If you want to get access token on the client side, and make an API call directly from your frontend, here is an imaginary API route that can help you achieve it.
// src/routes/api/token.fetch.ts
import { getAccessToken } from './_utils';
import type { OpenidRequest, OpenidResponse } from 'express-openid-connect';
export async function get(req: OpenidRequest, res: OpenidResponse) {
try {
const token = await getAccessToken(req);
return res.json({token});
} catch (error) {
res
.status(error.status || 500)
.end(error.message || error.text || 'Got unknown error from backend API');
}
}
Something like this. You can even put in on the session object. However, I strongly advise you not to call your backend from frontend directly!
First, you have to deal with CORS pain. Second, access tokens are short lived and if you cache them on client-side you have to keep track of expire time and refresh them. More code, more possible bugs and potential security issues.
We now have authentication middleware and our "proxy" API routes setup. Let's wire everything together by fetching the data from our backend API route.
<!-- index.svelte -->
<script lang="ts">
import wretch from 'wretch';
import { onMount } from 'svelte';
import { stores } from '@sapper/app';
const { session } = stores();
onMount(async () => {
$session.userData = await wretch('/api/user.fetch?id=123').get().json();
});
</script>
<pre>
{JSON.stringify($session.userData, null, 2)}
</pre>
This is it. Everything you put in your session
store will be available to other components of your app. If you have some shared data, the best place would probably be to seed it in the _layout.svelte
file because this file is shared by other pages.
https://github.com/auth0/express-openid-connect
https://github.com/axios/axios
https://github.com/elbywan/wretch
https://github.com/expressjs/express
https://github.com/form-data/form-data
https://github.com/lukeed/polka
https://github.com/node-fetch/node-fetch
https://github.com/sindresorhus/got
https://github.com/visionmedia/superagent
As you see, SSR is not easy. Especially when you have to deal with authentication. It often leads to more code to maintain and also to insecurity if security is correctly setup. At least for me. Are the trade-offs worth it? I am not sure.
One issue that I haven't really solved yet is the case when Auth0 session times out. That happens when you leave your browser window open and come back to it a few days later. It looks like you are logged in. You can navigate between pages, but when you try to call a third party backend you get error back because the access token cannot be refreshed because the main session has timed out.
It's not a big blocker for me since this is an internal app, but would be nice to solve it somehow. One idea that comes to mind is to start some kind of timer on the frontend with the token expire time set and when it times out just log the user out. I need to test it soon. If you have any other ideas please shoot me an email and I will add it to this post.
Although the code in this post is maybe not something that you can just copy and paste straight off, but hopefully it will point you in the right direction on how to handle server-side authentication in Sapper.
]]>svelte-check
, right? Wrong! The TypeScript compiler won't either help you catch all errors. It will not whine about you naming your variables incorrectly. Nor will svelte-check, but a good linter will annoy the hell out of you until you learn to do things right.
A short while ago Svelte's Eslint plugin didn't support linting TypeScript in templates. That was a show stopper for many. Now that it's in place there is no excuse not to use it.
In this post I will guide you to the right tools and libraries for building a proper linting pipeline for your Svelte Typescript-enabled project.
First, we need a Svelte project to test this on. Let's use the official Webpack template for a change. The template comes with TypeScript support that we need to enable explicitly.
$ npx degit sveltejs/template-webpack svelte-eslint
$ cd svelte-eslint
$ node scripts/setupTypeScript.js
$ npm install
$ npm run dev
$ open localhost:8080
If you followed the instructions above you should see a simple page with "HELLO WORLD!" displayed on it.
If you use Svelte's official VSCode extension you get auto formatting for free, but I use Vim, so I need to setup Prettier explicitly.
$ npm add -D prettier prettier-plugin-svelte
Even if you use VSCode extension it's a good practice to create a Prettier configuration for every project. We also need it to format the code when linting as you will learn later.
// .prettierrc.js
module.exports = {
arrowParens: 'avoid',
singleQuote: true,
printWidth: 90,
plugins: ['prettier-plugin-svelte'],
semi: false,
svelteSortOrder: 'options-styles-scripts-markup',
svelteStrictMode: false,
svelteBracketNewLine: true,
svelteIndentScriptAndStyle: true,
trailingComma: 'none'
}
Above is my Prettier configuration, but you can adjust it to fit your needs.
When starting a new TypeScript project it's a good idea to turn the strict
mode on directly. For this we need to change the tsconfig.json
to the following.
// tsconfig.json
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "ES2017", "WebWorker"],
"resolveJsonModule": true,
"esModuleInterop": true,
"importsNotUsedAsValues": "remove",
"strict": true
},
"include": ["src/**/*", "src/node_modules/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "static/*"]
}
Again, TypeScript configuration varies greatly. Adjust if needed.
Linting and formatting are two different concepts. Try not to mix them. I know that you can integrate Prettier with Eslint, but don't do it. It's always better to format files separately, before you lint them.
The time has come to setup Eslint with TypeScript support. Let's install the required dependencies first.
$ npm add -D eslint eslint-plugin-svelte3
$ npm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
Now we need to add ESlint configuration file. If you are using Tailwind CSS you have to disable Eslint from linting the <style>
tags in Svelte templates. You can do it in the settings
section of Eslint config by setting svelte2/ignore-styles
setting to an function that returns true
. I know, weird, but it works.
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking'
],
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte']
},
env: {
es6: true,
browser: true
},
overrides: [
{
files: ['*.svelte'],
processor: 'svelte3/svelte3'
}
],
settings: {
'svelte3/typescript': require('typescript'),
// ignore style tags in Svelte because of Tailwind CSS
// See https://github.com/sveltejs/eslint-plugin-svelte3/issues/70
'svelte3/ignore-styles': () => true
},
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['node_modules']
}
If you have done everything correctly, and your editor has linting support enabled, it should just work. If you are not sure, check the guidelines on how to integrate Eslint in your editor.
Next thing we need to do is to add an error to the app to see if the ESlint catches it. Replace the script section in App.svelte
with the code below.
<!-- App.svelte -->
<script lang="ts">
import { writable } from 'svelte/store'
const store = writable([])
$store.length // incorrect no-unsafe-member-access error
export let name: string
</script>
Alright! Time to test-drive the beast. Add the following commands to the scripts
section in your package.json
.
"format": "prettier --write ./src/**/*.{js,svelte,html,ts}",
"lint": "eslint './src/**/*.{js,ts,svelte}'",
"lint:fix": "eslint --fix './src/**/*.{js,ts,svelte}'",
"prelint": "npm run format"
The prelint
command will make the format
command run before doing the actual linting, "format, then lint" style. It will be invoked automatically if you run npm run lint
or npm run lint:fix
. This is neat. One less command to keep in mind.
Try to lint the project and see Eslint catch the $store.length
error above. This error would have slipped though the TypeScript compiler, try running npm build
, but now Eslint caught it for us.
Humans are not machines, not yet at least. It's easy to forget things, because it's ... human. Therefore, we need to automate things as much as possible so nothing can slip through the cracks. Husky and lint-staged are two tools that can help us achieve this.
Husky is a CLI tool that helps working with Git hooks easier. It will install Git hooks that we can in turn hook up different commands to. We can for example use the pre-commit
Git hook to trigger a command and depending on the outcome either allow or disallow a commit.
Another tool, lint-staged
, helps us narrow it down only to the files that have changed. This might seem unnecessary, but as your project grows in size I guarantee that you will appreciate it. Let's install and configure them.
$ npm add -D husky lint-staged
$ npx husky init
Husky should now have installed a pre-commit
hook in the .husky
directory. Open that file in your editor and add the npx lint-staged
command to the bottom.
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# run lint-staged command
npx lint-staged
To explain, every time you will try to commit your changes in Git Husky will call lint-staged
command and that command can in turn call other commands on only the files that are affected by the commit.
You can configure lint-staged
in many different ways, but for our case adding a new lint-staged
option in the package.json
file should do.
"lint-staged": {
"*.{js,ts,svelte}": ["svelte-check", "npm run lint:fix"]
}
This configuration will run the two commands above on any files matching the pattern when you try to do a Git commit. If the checks fail nothing will be committed.
One thing I recommended doing is to add the linting to your build command as the first step. If something managed to slip through the cracks it will be caught there. You probably don't build production builds often, maybe you even have a CI pipeline setup for this, so it will not affect the DX and slow things down for you.
You can write JavaScript in many different ways. Linting helps you stick to the defined rules and keep your code consistent. TypeScript compiler can help you catch some errors, but not all. A good linter will guide you to write better code and become a disciplined developer.
Solid linting pipeline can be daunting to setup. Honestly, I've been skeptical of linting and haven't used it much myself in the past, but after working on a bigger project with many developers involved I realized why people use linters and why they are extra useful in a JavaScript project.
I cannot guarantee that my setup will work for you, things move fast in the frontend world, but at least I hope that you got some inspiration out of it.
]]>Svelte crew managed to get four releases out in February. Incredible! Check out the changes below.
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
If you a curious about the future of Svelte, check out the RFCs repository. Lots of interesting suggestions and discussions going on there.
Sapper had one release last month and is now on version 0.29.1. If you are using TypeScript that release probably broke some types for you, the Preload.Preload
one.
https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md
Still no official news on SvelteKit, but progress is being made.
If you are dying to learn more about SvelteKit check out this ambitious unofficial documentation. A very interesting read!
https://sk-incognito.vercel.app/learn/what-is-sveltekit
I like bundlers as much as I hate them, but they are fun to play with. I tried Snowpack before the official version 2 came out and really liked it, but after using it for a few projects my feelings are mixed. One recent project upgrade from Snowpack v2 to v3 didn't go smooth for me either.
Rollup is currently the official bundler for Svelte, but I think it's more suited for building libraries than web apps. I like Svite, but the author put it on hold after Vite v2 came out. What else is left? Webpack of course!
I must admit that actually never took a close look at Webpack, because I always found its configuration daunting. This ignorance made me reject it. This time I decided to take a deep dive and finally find out what it's made off.
It took me a while to go to the bottom of things, but it was worth the time. I think I've finally found the bundler I've been looking for.
Webpack might not be the fastest bundler, but it's solid as a rock and flexible as a Russian Olympic gymnast. It's also the oldest bundler (est 2012). This is actually good, because the age means it has a mature codebase, mature ecosystem, tons of documentation and all the wrinkles are ironed out.
Webpack's Svelte loader v3 came out in January and it works incredibly well with latest Webpack 5.
https://github.com/sveltejs/svelte-loader
Webpack is slow(er) to start, but once you are up and running, the reloads are blazing fast with hot module reload enabled. The feedback loop is really short.
There is also official Sapper Webpack template with TypeScript support if you want to give it a try.
https://github.com/sveltejs/sapper-template-webpack
If you want to learn more about Webpack check out the links below by @survivejs. Great resource!
https://presentations.survivejs.com/webpack-from-apprentice-to-journeyman
https://survivejs.com/webpack/foreword/
Want to know the differences between the Packs? Read the Snowpack vs. wWbpack.
Vite.js, the next gen ESM bundler, had a v2 release recently, together with a gorgeous documentation site. Its Svelte plugin repo is now archived, but apparently Vite supports Rollup plugins out of the box. Interesting! Just need to find some time to try it out.
Also, I don't know what @benmccann and @lukeed are cooking with Vite, but it sure looks interesting.
https://github.com/benmccann/vite-svelte
Svelte Society released a nice Svelte cheatsheet slash reference. Make sure to bookmark it!
https://sveltesociety.dev/cheatsheet
Svelte's official Eslint plugin finally supports TypeScript in Svelte templates. Enable it at your own risk in the existing project with strict TypeScript settings on.
If you are using Tailwind CSS make sure to disable the style tag parsing in Eslint configuration or you will get errors.
https://github.com/sveltejs/eslint-plugin-svelte3
Talking about TypeScript. Want to speed up you TypeScript compilation? Check out this Svelte preprocess fork that uses Esbuild.
https://github.com/lukeed/svelte-preprocess-esbuild
TypeScript has a complex type system. This article has 10 valuable tips for 10 bad TypeScript habits to break.
https://startup-cto.net/10-bad-typescript-habits-to-break-this-year/
Interesting discussion (or maybe rant) happened on /r/sveltejs recently - Please Stop being Fanboys. The comments are interesting too. What's your stand on this?
Ever wondered what you can use Svelte's new keywork #key for? Me too! Here is a simple REPL that will make everything click.
Clubhouse is all the rage at the moment and Svelte is there too! Mike Nikles started Casual Svelte Chat on weekdays where you can hang out and chat about Svelte. However, I think that he wants to move it from Clubhouse to some other platform. Check out his Twitter on where and when.
ElectroBlocks
Cool Arduino IDE that uses Blockly. It comes with a built-in simulator as well and is built with Sapper and TypeScript.
Unicode Lookup
Snappy Unicode lookup PWA app built with Svelte and TypeScript by @emnudge.
svelte-sheets
Excel Sheets in Svelte. Not Google Sheets, but still pretty cool. Download a sample file from here if you want to play with it.
Editors
Two popular rich text editor for Svelte are Quill.js and Editor.js, but there is also Typewriter and ByteMD that look very promising.
Building a blog with Svelte
Learn how to build a blog in Svelte with these series on DEV.to by Chris Jackson.
Svelte Quick Tips
Check out Svelte Quick Tips by Dana Woodman. This one on basic internationalization.
MiniRx
I always say that you can double Svelte's reactivity by using RxJS. Florian Spier and friends just released MiniRx, a framework agnostic reactive store backed by RxJS.
Firebase Open Source
Fan of Firebase? Check out this collection of Firebase open source projects.
There has been an explosion of Svelte form libraries lately. That's fantastic to see! Forms library are useful, and fun to build, but they will not solve all your problems. More on this further down.
Some of the new Svelte form libraries are:
Formula - Zero Configuration Reactive Forms for Svelte
https://formula.svelte.codes
Felte - A simple form library based on Svelte stores and Svelte actions
https://github.com/pablo-abc/felte
Fastform - a Formik like library
https://github.com/srmullen/fastform
svelte-use-form - Svelte form library that is easy to use and has 0 boilerplate
https://www.npmjs.com/package/svelte-use-form
Some of the other popular Svelte form libraries are:
Sveltik - More or less Formik ported to Svelte
https://github.com/nathancahill/sveltik
Svelte Forms Lib - A lightweight library for managing forms in Svelte with Yup support
https://github.com/tjinauyeung/svelte-forms-lib
Svelte Forms - Form library with built-in validations
https://github.com/chainlist/svelte-forms
Svelte Forms - simple forms library using Svelte actions
https://github.com/svelteschool/svelte-forms
svelte-formup - Form helpers for Svelte
https://github.com/kenoxa/svelte-formup
svelte-formit - heavily inspired by the react-hook-form library
https://github.com/galkatz373/svelte-formit
Many of the form libraries above either have built-in validation or use Yup, but there are other great validation libraries out there.
Vest - My favorite validation library with a different validation approach
https://github.com/ealush/vest
Iodine.js - Micro client-side validation library (1.6kb)
https://github.com/mattkingshott/iodine
Nope - Lightweight clone of Yup
https://github.com/bvego/nope-validator
v8n - "The ultimate JavaScript validation library you've ever needed"
https://github.com/imbrn/v8n
Zod - Low-level TypeScript-first schema declaration and validation library
https://github.com/colinhacks/zod
Validator.js - A library of string validators and sanitizers
https://github.com/validatorjs/validator.js
Superstruct - Lower-level validation library for JavaScript (and TypeScript).
https://github.com/ianstormtaylor/superstruct
async-validator - Validate forms asynchronously
https://github.com/yiminghe/async-validator
Bueno! - Tiny, composable validation library
https://github.com/philipnilsson/bueno
Schema - Schema validator like Yup, but with a different API
https://github.com/zuze-lab/schema
Remember I said earlier that form libraries will not solve all your problems? I know this from experience of building a couple of forms heavy Svelte apps last year. You usually always have a fresh start, but sooner or later you run into edge cases such as parent-child selects, dynamic fields, calculated fields or input formatting. Things that no forms library will solve for you.
Instead of using form libraries, I've developed a set of useful abstractions and patterns for working with forms. It might result in slightly more code, but at least that code is under my control and not upstream in some forms library.
I've distilled and documented all my Svelte forms knowledge in a book. It's the missing manual for Svelte forms. Check it out here.
If you want to use a forms library, there is a whole chapter dedicated to it in the book. You will learn how to build a reactive, validation library agnostic form library with calculator field support in only 100 LOC using nothing, but Svelte.
What do you call a dog who can perform magic?
A labracadabrador
That's all for now. See ya next month!
P.S. Like the content? Subscribe to this newsletter at reactivity.news.
]]>I saw on its website that there was no Svelte example and I decided to fix it. Said and done. I ported its React async validation example to Svelte. This is what the final form looks like.
And here is how I did it.
If you just want to see the code you can skip to the next section, but if you want to follow along we need to setup a new Svelte project. Vite v2 came out recently and I decided to give it a spin. Apparently it supports Rollup plugins out of the box. This means we can use Svelte's Rollup plugin.
Let's set it up.
# pick vanilla JS template
$ npm init @vitejs/app
Switch to the project folder and add Svelte and Svelte Rollup HMR plugin. Since this is a port of the official React example we will reuse its SASS style file. Vite supports PostCSS and SASS out of the box. We just need to install the right dependency.
$ npm add -D sass rollup-plugin-svelte-hot svelte svelte-preprocess
We also need to teach Vite how to deal with Svelte files. For that we need to create a Vite config file.
// vite.config.js
import svelte from 'rollup-plugin-svelte-hot'
export default {
plugins: [svelte()]
}
Create a src
folder in the project directory and move the main.js
to it. Make sure to update the script reference in index.html
file to point to'/src/main.js'.
From now on it's business as usual to create Svelte files. Let's start with a simple App.svelte
file that will use as base for our example. Create it in the src
folder.
<!-- App.svelte -->
<h1>Svelte + Vite rocks!</h1>
Replace the contents of main.js
with this.
// main.js
import App from './App.svelte'
const app = new App({
target: document.getElementById('app')
})
export default app
Start the app with npm run dev
. Works? Good! Then let us continue.
Vest is a validation framework with a slightly different validation approach. Instead of writing your validations with with a DSL like in Yup, you write them as a suite of validation tests.
Here is a basic example of a Vest validation suite.
import vest, { test, enforce } from 'vest'
import { isEmail } from 'validator'
// extend Vest with email validator
enforce.extend({ isEmail })
const validate = vest.create('user_form', (data = {}, currentField) => {
vest.only(currentField)
test('email', 'Email is required', () => {
enforce(data.email).isNotEmpty()
})
test('email', 'Email is not valid', () => {
enforce(data.email).isEmail()
})
test('password', 'Password is required', () => {
enforce(data.password).isNotEmpty()
})
if (data.password) {
test('confirm_password', 'Passwords do not match', () => {
enforce(data.confirm_password).equals(data.password)
})
}
})
let data = {
email: 'johndoe',
password: '',
password_confirm: ''
}
const result = validate(data)
if (result.hasErrors()) {
console.log(result.getErrors())
// { email: ['Email is not valid'], password: ['Password is required'] }
}
We extend Vest with a custom email validator from validator.js. We also validate password_confirm
field only if form values password
field is not empty.
You can see that beside the data we pass additional currentField
argument in the Vest callback. This allows to pass a specific field name, and if there are tests defined for it, Vest suite will run validations for that field only.
Vest has no opinions on how you write your validation suite. This allows us to model our form validation exactly how we need it.
This example was to only to show the basics of writing validation tests in Vest, but I've only scratched the surface of what's possible.
Next, we need to create our registration form. I tried to keep the structure of the original React form as much as I could.
<!-- AuthForm.svelte -->
<script>
import Input from './components/Input.svelte'
import Button from './components/Button.svelte'
import Checkbox from './components/Checkbox.svelte'
export let values = {}
export let submit = () => {}
let errors = {}
let warnings = {}
// we copy the supplied for values to our form state
// this is a good practice and helps us with resetting the form
let formState = { ...values }
// used as an indicator flag for async username availability check
let usernamePending = false
// form submit button status
let disabled = true
</script>
<form on:submit|preventDefault={submit} id="example-form" class="col-xs-10 col-lg-6">
<h2>Authentication</h2>
<Input
name="username"
label="Username"
pending={usernamePending}
placeholder="try: ealush or codechips"
{errors}
bind:value={formState.username}
/>
<Input
name="password"
label="Password"
errors={{ ...errors, ...warnings }}
bind:value={formState.password}
onInput={check}
/>
<Input
name="confirm_password"
label="Confirm Password"
{errors}
bind:value={formState.confirm_password}
/>
<Input
name="email"
label="Email"
{errors}
bind:value={formState.email}
/>
<Checkbox
name="tos"
label="I have read and agreed to the terms of service."
bind:checked={formState.tos}
/>
<footer>
<Button class="btn-submit" type="submit" {disabled}>Submit</Button>
</footer>
</form>
I've omitted the validation part for now, as we will add it later. Notice that I clone the original form values, supplied to the form component in the values
property, into a formState
object. I think it's overall good practice to follow. When working with forms you want to work on a copy of the original data. This also helps with form resets as you will see later.
We also re-bind our exported form component values in the main form. Another neat feature in Svelte.
When it comes to form components I just ported them straight from React to Svelte.
<!-- components/Button.svelte -->
<script>
export let type = 'button'
</script>
<button {type} {...$$props}>
<slot />
</button>
Our button component contains a Svelte slot. This allows us to pass other Svelte components into it or other HTML elements that can make our buttons look more pretty, like adding a spinning submitting indicator icon.
<!-- components/Checkbox.svelte -->
<script>
export let name = ''
export let checked = false
export let label = ''
export let onChange = () => {}
</script>
<label class={`v-center ${$$props.class}`}>
<input type="checkbox" {name} bind:checked on:change={onChange} />
<small>{label}</small>
</label>
I like to always give the exported properties default values, so nothing blows up. This is especially true for input handlers. A noop (no operation) function is a way to go to keep JS interpreter happy.
<!-- components/Input.svelte -->
<script>
export let value = ''
export let label = ''
export let name = ''
export let pending = false
export let errors = {}
export let onInput = () => {}
$: messages = errors[name] || []
</script>
<label class:pending class={$$props.class}>
<div class="row">
<strong class="col-xs-4">{label}</strong>
{#if messages.length}
<span class="col-xs-8 error-container">{messages[0]}</span>
{/if}
</div>
<input type="text" {name} bind:value {...$$props} on:input={onInput} />
</label>
One good thing to know is that you cannot pass type
as dynamic property to the input types when you use bi-directional bindings.
You can see that we have a cryptic $$props
variable in the components. This magic variable is available in every Svelte component and allows you to pass in properties to the container that are not explicitly defined on it. If that doesn't make sense it will click when we will wire up validation to our form.
Let's setup some validation constraints for our registration form. From looking at the form we can see that our fields consists of username, email and password fields. We also have a Terms of Service checkbox that we need to consent to.
Beside errors Vest also supports warnings. Warnings are non-fatal and will let the validation suite pass. Their role is to advise, not hinder.
// validate.js
import vest, { test, enforce } from 'vest'
import { doesUserExist } from './api'
import { isEmail } from 'validator'
// extend vest with email validator
enforce.extend({ isEmail })
const suite = vest.create('user_form', (data = {}, currentField) => {
// if field name is supplied validate only that field
vest.only(currentField)
test('username', 'Username is required', () => {
enforce(data.username).isNotEmpty()
})
test('username', 'Username must be at least 3 characters long', () => {
enforce(data.username).longerThanOrEquals(3)
})
if (!suite.get().hasErrors('username')) {
// use memoization to reduce the number of API calls
test.memo(
'username',
'Username already exists',
() => {
if (data.username) {
return doesUserExist(data.username)
}
},
[data.username]
)
}
test('email', 'Email address is not valid', () => {
enforce(data.email).isEmail()
})
test('password', 'Password is weak, Maybe add a number?', () => {
// mark this test as a warning only
vest.warn()
enforce(data.password).matches(/[0-9]/)
})
if (data.password) {
test('confirm_password', 'Passwords do not match', () => {
enforce(data.confirm_password).equals(data.password)
})
}
test('tos', () => {
enforce(data.tos).isTruthy()
})
})
export default suite
This validation suite is taken straight from the React example. The only new thing I added was the memoization feature of Vest. This helps reduce the number of call to the API when checking the username availability. Vest will remember all the checks we have done so far.
Notice that we don't do any API calls unless all other username
checks pass. This is nice as it helps us reduce the number of unnecessary calls to the API.
Below is our fake username API check. It's a promise that resolves or rejects in case of failure.
// api.js
const usernames = ['ealush', 'codechips']
export const doesUserExist = username => {
return new Promise((resolve, reject) => {
setTimeout(() => {
usernames.includes(`${username}`.toLowerCase()) ? reject() : resolve()
}, 2000)
})
}
I think that you can also reject it with an error if you want to show a custom error message.
We have now come to the final step where we put all the pieces together. Vest has a small CSS class helper utility called classNames
that we will use. Its job is to derive CSS class names from Vest validation result.
Svelte differs from React. Svelte keeps it's state in the component, it's stateful, while React is stateless. It re-renders its component DOM on every change. Therefore, for our classNames
to work we need to make it reactive by using Svelte's reactive variable functionality. Every time Vest result reference changes it will re-trigger this reactive variable.
<!-- AuthForm.svelte -->
<script>
import classNames from 'vest/classNames'
import validate from './validate'
import Input from './components/Input.svelte'
import Button from './components/Button.svelte'
import Checkbox from './components/Checkbox.svelte'
export let values = {}
export let submit = () => {}
// our error and warnings state
let errors = {}
let warnings = {}
// we copy the supplied for values to our form state
// this is a good practice and helps us with resetting the form
let formState = { ...values }
let usernamePending = false
// initial empty validation state
let result = validate.get()
// input check handler
const check = ({ target: { name } }) => runValidate(name)
// generic form validation function used in form submit callback
// and individual form field validations
const runValidate = name => {
result = validate(formState, name)
// extract errors and warnings
errors = result.getErrors()
warnings = result.getWarnings()
// username validation is async and needs special treatment
if (name === 'username') {
usernamePending = true
// for async validation we need to use the done hook
result.done(() => {
errors = result.getErrors()
warnings = result.getWarnings()
usernamePending = false
// reassignment is needed for reactive cn statement
result = result
})
}
}
const reset = () => {
validate.reset()
result = validate.get()
errors = {}
warnings = {}
formState = { ...values }
}
const onSubmit = () => {
runValidate()
if (result.hasErrors()) return
submit(formState)
// reset form state
reset()
}
// Vest CSS class helper utility
$: cn = classNames(result, {
warning: 'warning',
invalid: 'invalid',
valid: 'valid'
})
// reactive variable for the submit button
$: disabled = result.hasErrors()
</script>
<form on:submit|preventDefault={onSubmit} id="example-form" class="col-xs-10 col-lg-6">
<h2>Authentication</h2>
<Input
name="username"
label="Username"
pending={usernamePending}
class={cn('username')}
placeholder="try: ealush or codechips"
{errors}
bind:value={formState.username}
onInput={check}
/>
<Input
name="password"
label="Password"
class={cn('password')}
errors={{ ...errors, ...warnings }}
bind:value={formState.password}
onInput={check}
/>
<Input
name="confirm_password"
label="Confirm Password"
{errors}
class={cn('confirm_password')}
bind:value={formState.confirm_password}
onInput={check}
/>
<Input
name="email"
label="Email"
{errors}
class={cn('email')}
bind:value={formState.email}
onInput={check}
/>
<Checkbox
name="tos"
label="I have read and agreed to the terms of service."
class={cn('tos')}
onChange={check}
bind:checked={formState.tos}
/>
<footer>
<Button class="btn-submit" type="submit" {disabled}>Submit</Button>
</footer>
</form>
Because we have async username validation we need to handle it in the Vest suite's .done()
hook. This hook fires when all async validations have run.
Final step is to include our form component in our App.svelte
. Notice that we pass down the original form state and submit handler as component properties.
<!-- App.svelte -->
<script>
import AuthForm from './AuthForm.svelte'
const submit = data => console.log(data)
const values = {
username: '',
password: '',
confirm_password: '',
email: ''
}
</script>
<AuthForm {values} {submit} />
And here is the final result in action.
https://codesandbox.io/s/svelte-vest-form-validation-example-kf185
This was a quick port of Vest's async validation React example to Svelte. The purpose was to show the basics of Vest validation library. This is however not something I would do in the real project. In a real project I would use Svelte's one-way bindings, because they are more flexible for such use case. Want to know how? It's something I teach in my Svelte forms book that you can get here.
]]>Webpack might not be the sexiest bundler, but it's mature, battle-tested and a lot of teams and projects around the world rely on it. It might not be the fastest bundler, but once it starts the developer experience is on par with other cool ESM bundlers thanks to the HMR support. The reloads are blazing fast!
The thing with Webpack is that it's a real DYI bundler. It's its curse, but also its beauty. It might take you some time to get to the setup you are happy with. The tweaking possibilities are virtually endless. You can tweak every parameter and optimize your production bundle in every imaginable way.
Read on to learn how to start!
Webpack in itself is just a library. You need to add a Webpack runner if you want something to happen. The standard, official runner is webpack-cli, but in this example we will use a lightweight runner instead called webpack-nano.
$ npm init -y
$ npm add -D webpack webpack-nano webpack-merge
Webpack is all about configuration and if you are not familiar with it, things can get quite intense and overwhelming. You can configure Webpack in many different ways, but the base configuration always has some common parts such a loaders and plugins.
Here is a pseudo code of what a configuration file might look like.
module.exports = () => {
return {
// the main app entry file
entry: {
app: ['./src/index.js']
},
// directory to output files to
output: {
path: './dist'
},
// how and if to generate source maps
devtool: 'eval-cheap-module-source-map',
// how to process different file types using Webpack loaders
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.[p]?css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
],
},
// which different Webpack plugins to use
plugins: [
new MiniCssExtractPlugin({ filename: 'app.css' }),
]
}
}
The SurviveJS Webpack book preaches another, more sensible approach, to structure your Webpack configuration.
The most common way to configure Webpack is to use different config files, one for each environment. We will not do that. Instead we will have one main config file called webpack.config.js
where we will handle configuration for all of our environments and another helper file called webpack.parts.js
that we will use to assemble the main configuration file with the help of the webpack-merge library. The only thing this library does is to merge the objects together.
Here is our starting point.
// config/webpack.config.js
const path = require('path')
const { merge } = require('webpack-merge')
const { mode } = require('webpack-nano/argv')
const common = merge([])
const development = merge([])
const production = merge([])
const getConfig = mode => {
switch (mode) {
case 'production':
return merge(common(mode), production, { mode })
case 'development':
return merge(common(mode), development, { mode })
default:
throw new Error(`Unknown mode, ${mode}`)
}
}
module.exports = getConfig(mode)
To run Webpack we can run this command with the different mode
flag.
$ npx wp --config config/webpack.config.js --mode development
$ npx wp --config config/webpack.config.js --mode production
The two commands above will run Webpack in development and production configuration modes.
I like to keep Webpack configuration in a separate config
directory. Therefore we need to tell Webpack explicitly where to find the config file.
Currently the configuration is empty so don't expect miracles to happen when you run them. Let's fix it.
First thing we need to do is to add a development server and an entry index.html
file that holds our application. There are two de facto standard Webpack plugins people often use, webpack-dev-server and html-webpack-plugin, but we will instead use lightweight alternatives - webpack-plugin-serve and mini-html-webpack-plugin.
$ npm add -D webpack-plugin-serve mini-html-webpack-plugin
Let's define parts for them to use in our Webpack parts file.
// config/webpack.parts.js
const path = require('path')
const { MiniHtmlWebpackPlugin } = require('mini-html-webpack-plugin')
const { WebpackPluginServe } = require('webpack-plugin-serve')
exports.devServer = () => ({
watch: true,
plugins: [
new WebpackPluginServe({
port: 3000,
static: path.resolve(process.cwd(), 'dist'),
historyFallback: true
})
]
})
exports.page = ({ title }) => ({
plugins: [new MiniHtmlWebpackPlugin({ publicPath: '/', context: { title } })]
})
exports.generateSourceMaps = ({ type }) => ({ devtool: type })
While on it, I also threw in source maps handling part. Now we can use these sections in our main configuration file.
// config/webpack.config.js
// ...
const parts = require('./webpack.parts')
const common = merge([
{ output: { path: path.resolve(process.cwd(), 'dist') } },
parts.page({ title: 'My Awesome App' }),
])
const development = merge([
{ entry: ['./src/index.ts', 'webpack-plugin-serve/client'] },
{ target: 'web' },
parts.generateSourceMaps({ type: 'eval-source-map' }),
parts.devServer()
])
We added another entry point to the configuration - webpack-plugin-serve/client
. It's needed for live reloads to work. The serve plugin has many different configuration options
and supports HMR by default.
Also, Webpack normally writes and serves the files from disk. You might speed things up by leveraging webpack-plugin-ramdisk.
Next up is adding support for Svelte files. For that we will use Svelte's official svelte-loader.
$ npm add -D svelte svelte-preprocess svelte-loader
Now we have to define a Svelte part in our Webpack parts file. Notice that we also have to pass in the current mode as we need to pass them to Svelte compiler.
// config/webpack.parts.js
// ...
exports.svelte = mode => {
const prod = mode === 'production'
return {
resolve: {
alias: {
svelte: path.dirname(require.resolve('svelte/package.json'))
},
extensions: ['.mjs', '.js', '.svelte', '.ts'],
mainFields: ['svelte', 'browser', 'module', 'main']
},
module: {
rules: [
{
test: /\.svelte$/,
use: {
loader: 'svelte-loader',
options: {
compilerOptions: {
dev: !prod
},
emitCss: prod,
hotReload: !prod,
preprocess: preprocess({
postcss: true
})
}
}
},
{
test: /node_modules\/svelte\/.*\.mjs$/,
resolve: {
fullySpecified: false
}
}
]
}
}
}
Since Svelte is a global dependency we can add it to our base Webpack config.
// config/webpack.config.js
const common = merge([
{ output: { path: path.resolve(process.cwd(), 'dist') } },
parts.page({ title: 'My Awesome App' }),
parts.svelte(mode),
])
Svelte loader configuration is taken straight from Svelte's repo. There is no chance I could write this myself. If you ever looked at Svelte's standard Rollup configuration you will recognize a lot of things, so I will skip the explaining part.
One cool thing I like is that we have granular control over the order the resolved main fields in the package.json
files of external libraries. It's something I missed in Snowpack for example.
Adding TypeScript support is straight-forward. We could add Babel support instead because Babel is much faster than official TypeScript compiler, but we won't do it. Adding Babel only complicates things in my opinion.
$ npm add -D typescript ts-loader @tsconfig/svelte
We also need to add a minimal TypeScript cofig file for things to work.
{
"extends": "@tsconfig/svelte/tsconfig.json",
"include": ["src/**/*"],
"exclude": ["node_modules/*", "public/*"]
}
Adding TypeScript support to Webpack is a one-liner.
exports.typescript = () => ({
module: { rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }] }
})
Now we can use it in our main Webpack config.
// config/webpack.config.js
// ...
const common = merge([
{ output: { path: path.resolve(process.cwd(), 'dist') } },
parts.page({ title: 'My Awesome App' }),
parts.svelte(mode),
parts.typescript()
])
Since this is a global dependency we will add it to our common
config (for now).
You usually have to deal with CSS when building an app. Tailwind CSS is a popular choice today. Here is how to set everything up.
$ npm add -D css-loader postcss-loader postcss tailwindcss autoprefixer postcss-load-config
$ npx tailwindcss init
Create a minimal PostCSS config.
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}
Make sure to add the file pattern to the Tailwind config otherwise you will end up with a very large CSS file in production. If you want to get a small CSS file already in development make sure to enable Tailwind's JIT mode.
// tailwind.config.js
module.exports = {
// mode: 'jit',
purge: [
'./src/**/*.svelte'
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
If we want Svelte component files to be extracted to a common CSS file, and we do, we need to use mini-css-extract-plugin.
// config/webpack.parts.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// ...
exports.postcss = () => ({
loader: 'postcss-loader'
})
exports.extractCSS = ({ options = {}, loaders = [] } = {}) => {
return {
module: {
rules: [
{
test: /\.(p?css)$/,
use: [{ loader: MiniCssExtractPlugin.loader, options }, 'css-loader'].concat(
loaders
),
sideEffects: true
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
})
]
}
}
You can build mini-pipelines of loaders in Webpack. Tailwind is built on top of PostCSS, but you might be working in SASS in your project. Therefore we can pass different CSS loaders in the arguments to the extractCSS
part.
// config/webpack.config.js
const common = merge([
{ output: { path: path.resolve(process.cwd(), 'dist') } },
parts.page({ title: 'My Awesome App' }),
parts.loadSvg(),
parts.svelte(mode),
parts.extractCSS({ loaders: [parts.postcss()] })
])
Our common Webpack config looks like this so far. Notice that we pass our PostCSS loader as an argument to the extractCSS
function. Maximum flexibility!
In Webpack version 4 you were forced to use a separate plugin to handle your assets, but Webpack 5 has built-in support for asset handling.
// config/webpack.parts.js
exports.loadSvg = () => ({
module: { rules: [{ test: /\.svg$/, type: 'asset' }] }
})
You can then use them like this in your code.
<!-- App.svelte -->
<script>
import icon from './assets/icon.svg'
</script>
{@html icon}
Asset handling in Webpack is a huge topic. I recommend that you take a look at Webpack's asset module documentation to learn more.
When it comes to SVG you can also optimize your SVGs by using svgo-loader and to other fancy stuff with other Webpack plugins. Search and you will find!
Webpack doesn't not have native support for environment variables, but it's just one dotenv-webpack plugin away.
// config/webpack.parts.js
const DotenvPlugin = require('dotenv-webpack')
// ...
exports.useDotenv = () => ({
plugins: [new DotenvPlugin()]
})
Whatever you put into your .env
file will be resolved at compile time. Of course, if you have a real environment variable defined, for example in your build server, that will be used.
# .env file
KANYE_API=https://api.kanye.rest
That variable is now available in your code in process.env.KANYE_API
.
For larger projects TypeScript compiler can slow things down. Luckily there is an ESBuild plugin we can use to transpile our TypeScript files during development.
// config/webpack.parts.js
const { ESBuildPlugin } = require('esbuild-loader')
// ...
exports.esbuild = () => {
return {
module: {
rules: [
{
test: /\.js$/,
loader: 'esbuild-loader',
options: {
target: 'es2015'
}
},
{
test: /\.ts$/,
loader: 'esbuild-loader',
options: {
loader: 'ts',
target: 'es2015'
}
}
]
},
plugins: [new ESBuildPlugin()]
}
}
I don't recommend using ESBuild for your production build and it does not do any typechecking. This is not a problem for us. We can use ESBuild in our development environment and TypeScript in production.
// config/webpack.config.js
const development = merge([
{ entry: ['./src/index.ts', 'webpack-plugin-serve/client'] },
{ target: 'web' },
parts.generateSourceMaps({ type: 'eval-source-map' }),
parts.esbuild(),
parts.devServer()
])
const production = merge([
{ entry: ['./src/index.ts'] },
parts.typescript(),
])
Hopefully you start to get a feeling for how we are assembling our main Webpack configuration piece by piece from separate parts.
It's good idea to tree shake and minify your bundle when building for production. TerserJS plugin is built into Webpack 5. No need to install it separately. However, if you want to minimize CSS you can do it in two ways: setup cssnano in PostCSS config or use css-minimizer-webpack-plugin. Let's use the plugin.
$ npm add -D css-minimizer-webpack-plugin
Webpack gives you granular control of how your bundle can be optimized and it would require a separate article. We will instead use the default settings which are good enough.
exports.optimize = () => ({
optimization: {
minimize: true,
splitChunks: { chunks: 'all' }
runtimeChunk: { name: 'runtime' },
minimizer: [`...`, new CssMinimizerPlugin()]
}
})
Word of caution: When building a production bundle make sure to specify NODE_ENV=production
in the package build script if you are using Tailwind CSS. Otherwise Tailwind will not purge your generated CSS file.
There are tons of other Webpack plugins that you can use in your pipeline. Let's use two of them just for demonstration purposes.
// config/webpack.parts.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const WebpackBar = require('webpackbar')
// ...
// clean dist directory on build
exports.cleanDist = () => ({
plugins: [new CleanWebpackPlugin()]
})
// show a nice progress bar in the terminal
exports.useWebpackBar = () => ({
plugins: [new WebpackBar()]
})
Hopefully you get the idea of how to setup different Wepback parts and how webpack-merge
merges them together.
It's always interesting to know what dependencies and the size our production bundle consists of. There are a few plugins that can help you analyze and visualize this. The most popular one is webpack-bundle-analyzer.
// config/webpack.parts.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
// ...
exports.analyze = () => ({
plugins: [
new BundleAnalyzerPlugin({
generateStatsFile: true
})
]
})
This plugin analyzes your final bundle and opens up a browser with a chart in it.
And we are done. This is what our final Webpack configuration looks like.
// config/webpack.config.js
const path = require('path')
const { merge } = require('webpack-merge')
const parts = require('./webpack.parts')
const { mode, analyze } = require('webpack-nano/argv')
const common = merge([
{ output: { path: path.resolve(process.cwd(), 'dist') } },
parts.page({ title: 'Sh*t Kanye says' }),
parts.loadSvg(),
parts.svelte(mode),
parts.extractCSS({ loaders: [parts.postcss()] }),
parts.cleanDist(),
parts.useWebpackBar(),
parts.useDotenv()
])
const development = merge([
{ entry: ['./src/index.ts', 'webpack-plugin-serve/client'] },
{ target: 'web' },
parts.generateSourceMaps({ type: 'eval-source-map' }),
parts.esbuild(),
parts.devServer()
])
const production = merge(
[
{ entry: ['./src/index.ts'] },
parts.typescript(),
parts.optimize(),
analyze && parts.analyze()
].filter(Boolean)
)
const getConfig = mode => {
switch (mode) {
case 'production':
return merge(common, production, { mode })
case 'development':
return merge(common, development, { mode })
default:
throw new Error(`Unknown mode, ${mode}`)
}
}
module.exports = getConfig(mode)
Instead of having one giant configuration file we assembled our configuration file from different configuration parts that we have defined in a separate file. Feels a little like building with Lego, don't you think?
The last part we need to do is to add a few NPM script commands to our package.json
.
"scripts": {
"build": "NODE_ENV=production wp --config config/webpack.config.js --mode production",
"build:analyze": "wp --config config/webpack.config.js --mode production --analyze",
"start": "wp --config config/webpack.config.js --mode development"
}
https://github.com/codechips/svelte-typescript-setups
You can't go wrong by betting on Webpack. Many big projects out there use it and they use it for a good reason. It might not be the fastest, but for sure it's the most reliable and flexible bundler on the market today. The tweaking possibilities are virtually endless.
Webpack also has a huge and mature ecosystem. It has no native support for ESM yet, like Vite or Snowpark, but it's in the roadmap. You have to keep up with all the cool kids, right?
The configuration is the hardest part. I suspect that I currently know around 10% of what you can do with and in Webpack, if not even less. The good new is that its documentation is excellent.
One major thing that scares most of the developers is Webpack's complex configuration. Usually it's abstracted away from you when you use some starter packs. My suggestion is to spend some time learning Webpack. It's time well invested. Bundlers come and go, but Webpack is here to stay.
]]>Svelte had two new releases last month. Incredible pace with no signs of slowing down. Feels like we might go places soon, peeps!
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
Some commits in the Sapper repository, but no releases last months. I don't expect any either. Better to channel this energy into SvelteKit.
https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md
SvelteKit is finally here! Although in Beta, but at least we can study the source code. I can't find the source, but I read that Svelte crew had to make the Github repo public because they run out of quota on the Github actions. That's quite funny if true.
Svelte community is currently bubbling with excitement and some brave souls have already ported their apps from Sapper to SvelteKit.
There is no official changelog yet, but you can follow the progress towards 1.0 on Github. My prediction is that it will be sometimes at the beginning of summer, but I've been wrong before.
SvelteKit is all about adapters, but it's hard to know what adapters are available at the moment. There are official ones and others built by the community.
Another way to adding new functionality to SvelteKit is by using svelte-adders. Check it out!
One of the major underlying changes to SvelteKit is the switch from Snowpack to Vite 2.0. In my last newsletter I posted a link to a secret repo on Github that I found by accident and that involved Svelte and Vite. I guess that was some kind of POC. This pull request explains the motivation for the switch.
Personally, I will wait with migration as my Sapper app is written in TypeScript and uses Auth0 Express middleware. None of the things SvelteKit currently supports. Auth is currently the biggest unknown, because SvelteKit is true serverless framework and normal things don't (yet) work.
I also see that people also started blogging about SvelteKit. Here is an good one How to Build a Website with Svelte and Sveltekit.
I've been closely following the development of Deno. I like its mascot a lot, but what I like the most is its approach to imports. I believe we are currently in the middle of a paradigm shift when it comes to package imports. My guess that NPM will become less relevant in the near future and decentralization will take over. Snowpack already supports this with its streaming imports and my guess that more will follow.
There are a lot of 3rd party packages and frameworks available for Deno https://deno.land/x
Yep, Svelte is there too. https://github.com/crewdevio/Snel
One big unanswered question is how to run Deno apps in the cloud. Heroku supports Deno, but there is also Deno Deploy as an option from the creators of Deno and the company behind it.
Some time ago I tweeted: SPA = your users pay for rendering, SSR you pay the rendering cost. I quickly got reminded that this cost is nothing compared to code maintenance and the cost of duplication of the models on the client and the server. While using solutions like Firebase or GraphQL might ease the pain, but they will not make it go away.
I like Svelte simply because it fits in my head and also because it has a great DevX. But if we take a big step back and look at our app objectively, we will most likely discover code and complexity not directly related to the apps's functionality per se. Like code bundling or state management for example. Be honest, how many times did you pick a fight with your bundler lately? What about all the time you spent trying to keep TypeScript compiler happy? Also, are you sure that you picked the right state management library for your project? The new one that was released yesterday seems much better. Maybe you should switch to it? The complexity, FOMO and yak shaving is real in the frontend world, folks.
The answer to this on everybody's lips seem to be SSR. It's all the rage at the moment, no matter what problem you have at hand. But wouldn't it be cool if you could build reactive SSR apps, but in a much better way and (almost) without writing any JavaScript?
This thought-provoking The Future of Web Software Is HTML-over-WebSockets article really got me thinking. What if?
The article makes a few interesting points:
The statements got me curious and down the rabbit hole I went. Now, I wouldn't use Rails (again), but I got really impressed by Elixir and its Phoenix web framework that offers the same functionality with its LiveView feature that the article mentions.
Watch this keynote by the Phoenix author - Phoenix LiveView - Interactive Apps without Javascript to see what I mean.
The DOM patches Phoenix sends over the wire (or websockets) are ridiculously small thanks to smart architecture and the use of Morphdom library.
The most beautiful part is that you barely have to write any JavaScript. You get the reactivity for free with smart DOM patching. Mind blowing!
If the talk got you curious I suggest you watch The Soul of Erlang and Elixir by Saša Jurić next. It explains well what makes Elixir so special compared to other languages and runtimes.
After that you can read these three short(ish) articles:
I've been learning and experimenting with Elixir and Phoenix for that past month and I can't stop. It was long time ago I had that much fun while feeling productive at the same time. Crazy!
Even if you don't plan on using Elixir I suggest you give it a fair chance. It's always healthy to expand your views. Elixir is a functional programming language and it will for sure influence how you write your JavaScript code for the better. A good starting resource is the free and short online book - The Joy of Elixir. Great storytelling too!
Oh, and if you want to use Svelte with Phoenix, you can. https://github.com/virkillz/sveltex
I did a TypeScript poll on Twitter a while ago where I asked if people use TypeScript in their Svelte projects. I was truly surprised by the answers. Turns out that majority of people do. Not by a lot, but still. The feelings are however mixed. Maybe the YES/NO poll options were too binary. After all, I also use TypeScript, but only for non-Svelte files.
However, in my recent personal apps I've dismissed TypeScript completely as I feel that it brings little gain and more pain. I feel more comfortable with dynamic nature of JS I guess.
There is a lot of fragmentation going on in JavaScript land today. You literally can accomplish the same thing choosing one of the 1001 libraries available. Ever needed to have state management and searched for the best option? Good luck deciding!
Elm is the most known language that makes all those hard decisions for you, but looks like its development has stagnated a bit.
Recently I re-stumbled on Mint, a language designed for writing single page applications, in this post on StackOverflow blog. Will it become a game changer or not? Time will tell.
Q: Why do French eat snails?
A: They don't like fast food
Want to get this newsletter directly in your inbox? Subscribe at reactivity.news.
]]>Another typical example would be some type of interest calculator form. You change one field and that change forces other fields to recalculate their values.
The first thing that comes to mind is to use Svelte's bind
directive, and you are right, but that alone is not enough. We have to use unidirectional (one-way) bindings, made popular by React.
By default, Svelte offers bidirectional (two-way) bindings. You bind the input's values with bind:value
for text type inputs and bind:checked
for checkboxes.
<script>
const values = {
framework: 'Svelte',
userAgrees: false
}
</script>
<div>
<input
type="text"
name="framework"
id="framework"
bind:value={values.framework}
/>
{values.framework}
</div>
<div>
<input
type="checkbox"
name="agree"
id="agree"
bind:checked={values.userAgrees}
/>
{values.userAgrees}
</div>
Let's see the code Svelte compiler generates for us for a simple form. If you study the code generated by the compiler carefully you will find this.
// generated handler for framework input field
function input0_input_handler() {
values.framework = this.value;
$$invalidate(0, values);
}
// generated handler for userAgrees checkbox
function input1_change_handler() {
values.userAgrees = this.checked;
$$invalidate(0, values);
}
// wire up the generated input handlers
listen(input0, "input", /*input0_input_handler*/ ctx[1]),
listen(input1, "change", /*input1_change_handler*/ ctx[2])
There is a lot of internal Svelte magic going on, but the important concept to understand is that default bind directives generate code that roughly matches this JavaScript code.
input0.addEventListener('input', input0_input_handler);
input1.addEventListener('change', input1_change_handler);
As you can see, default two-way bindings are not magical in Svelte, but by using them we are limited to what we can do. Luckily, we have an escape hatch in form of unidirectional bindings.
Instead of outsourcing our input bindings to Svelte we will manage them ourselves. Sure, it's a little more code, but this code give us more granularity and control over what we can accomplish. Now that we know how Svelte compiler does it we can replicate the functionality ourselves.
<script>
const values = {
framework: 'Svelte',
userAgrees: false
}
const inputHandler = e => {
const { name, value } = e.target
values[name] = value
}
const changeHandler = e => {
const { name, checked } = e.target
values[name] = checked
}
</script>
<div>
<input
type="text"
name="framework"
id="framework"
value={values.framework}
on:input={inputHandler}
/>
{values.framework}
</div>
<div>
<input
type="checkbox"
name="userAgrees"
id="userAgrees"
checked={values.userAgrees}
on:change={changeHandler}
/>
{values.userAgrees}
</div>
If we look at the generated code we will find this.
function instance($$self, $$props, $$invalidate) {
const values = { framework: "Svelte", userAgrees: false };
const inputHandler = e => {
const { name, value } = e.target;
$$invalidate(0, values[name] = value, values);
};
const changeHandler = e => {
const { name, checked } = e.target;
$$invalidate(0, values[name] = checked, values);
};
return [values, inputHandler, changeHandler];
}
The handlers are almost the same. Only differences is that they are written by us and not generated by the Svelte compiler. This is important as it allows us to gain control of the logic in them.
The Svelte compiler is smart, it will augment our handlers and wrap state in an internal function. That function invalidates state if it has changed and updates the inputs value. Svelte's reactivity in a nutshell.
For the curious, Svelte compiler is using acorn as JavaScript parser in its compiler toolchain.
Now that we've learned both types of input bindings, let's dive into the calculator field generators.
We will keep complexity low. Our requirements is to generate a slug for our blog post based on it's title. For slug generation we will use the slugify package.
Let us also throw in another common business requirement. Slug should not be auto-generated if the slug input field has been touched, meaning interacted with by the user.
Without peeking at the solution below imagine how you would solve it with Svelte's bidirectional bindings. Hard, right?
It's much easier to think in terms of unidirectional bindings for this kind of problem. Here is the solution to the problem.
<script>
import slugify from 'slugify'
const initial = {
title: '',
slug: ''
}
// always work on the copy of original data
let values = { ...initial }
// keep state of touched fields
let touched = {}
const submit = () => console.log(values)
const handleInput = ({ target: { name, value } }) => {
if (name === 'title') {
values.title = value
// only generate slug if the field has not been touched
if (!touched.slug) values.slug = slugify(value, { lower: true })
}
if (name === 'slug') {
values.slug = slugify(value, { lower: true })
}
}
const handleFocus = ({ target: { name } }) => (touched[name] = true)
const reset = () => {
values = { ...initial }
touched = {}
}
</script>
<form on:submit|preventDefault={submit} on:reset|preventDefault={reset}>
<div>
<label>
<span>Title</span>
<input
type="text"
name="title"
placeholder="Please provide a title"
value={values.title}
on:focus={handleFocus}
on:input={handleInput}
/>
</label>
</div>
<div>
<label>
<span>Slug</span>
<input
type="text"
name="slug"
placeholder="Choose a slug"
value={values.slug}
on:focus={handleFocus}
on:input={handleInput}
/>
</label>
</div>
<div>
<button type="submit">Save</button>
<button type="reset">Reset</button>
</div>
</form>
The solution is naïve, but that's on purpose. I wanted to demonstrate how to think in unidirectional bindings and how they can help you solve problems that Svelte's bidirectional bindings are not able solve in a good way.
I noticed that I reach for Svelte's bidirectional bindings less and less because they are limited when it comes to complex forms. Unidirectional bindings give you a lot more control.
Imagine when you have input fields that are truly dependant on each other, a classical temperature converter comes to mind. I have seen solutions using bidirectional bindings and Svelte's stores to solve this, but the code you end up with is messy and unnecessary complex. However, it's a piece of cake when you reach for unidirectional bindings.
One-way bindings are super-flexible, especially when it comes to advanced form validation. If you want to learn more take a look at my Svelte Forms book. It's packed with cool use cases and easy to understand examples!
]]>One release in the past month. Lots of fixes and new feature Support passing CSS custom properties to component.
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
Feels like a lot of energy is going into SvelteKit right now. The logo on its website is stunning. It also has a very smooth setup experience and minimal amount of dependencies. I encourage you to create a new project. I guarantee you will be pleasantly surprised.
Overall it's still a little shaky and rough around the edges, but we are off to a good start! You can tell that SvelteKit is gaining popularity by the increased number of YT videos and all the SvelteKit related questions being asked on Svelte subreddit.
According to Github it's 77% complete. I predicted at the end of June in the last newsletter. Let's see if my predictions hold up!
I guess we can now officially proclaim Sapper dead. Its development has stalled and for a good reason. Good luck with the migration to SvelteKit! Should be smooth as butter.
https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md
svelte-restate
Fresh out the oven another Svelte state management solution. Immutable store for Svelte with full Typescript support and Redux Devtools integration. BOOM!
The cool thing about Svelte stores is that as long as you fulfill its contract you can pretty much do anything on the inside. No wonder there are so many new solutions popping up. It's fun and easy!
https://github.com/endenwer/svelte-restate
sveltekit-stripe
An example of how to integrate Stripe into a SvelteKit project. You have no good excuse left to not to charge for your app anymore.
https://github.com/srmullen/sveltekit-stripe
State Of JavaScript Libraries and Tools
These kind of posts are always super-fun to read. Svelte is (of course) gaining on every front, but it's cool to see Solid.js and Alpine.js get more attention.
https://moiva.io/blog/2021-q1-state-of-js-frameworks
auth-supabase
Supabase (open-source Firebase alternative) is cool, SvelteKit is cool and auth is hard. Learn how to put everything together by studying this code.
https://github.com/one-aalam/svelte-starter-kit/tree/auth-supabase
1000 experiments
Mad scientist Joshua Nussbaum is doing something crazy mad. He is doing a 1000 experiments experiment. One a day. They are not only code experiments, but still! That's like 3 years in total! Currently on #170.
Gitpod's New Site
Gitpod converted their site to SvelteKit. 200 pages. It's snappy and fast. Check it out with network tab open in Dev Tools!
Webstone
Talking about Gitpod. Mike Nikles, who recently joined the company, has started a project called Webstone. He mentioned it on Twitter after I said that Svelte is currently missing a generator for CRUD. Like forms, DB access and other useful things that some frameworks have. Hopefully this project will make it easier to get started. Contribute!
https://github.com/WebstoneHQ/webstone
Svelte Summit
A week ago this year's Svelte Summit was held. I haven't watched it yet, but maybe I will find some time to catch up.
Svelte vs React
This is not a comparison of the frameworks. Well, actually it is, but not which one is better than the other. After working with Svelte and using external libraries I've noticed that some of the libraries on NPM work better with React that with Svelte.
Why is that? Because Svelte is stateful and React is stateless. React is declarative. Every time you change state it will re-render itself, allowing you to call some library function in the process. Svelte, on the other hand, will not do anything when you modify state unless you explicitly tell it to.
One way to fix this is to use a computed variable. Every time the variable changes Svelte will call some function, but I must admit that it feels a little "hacky" to me.
SolidJS
I've mentioned above that Solid.js is gaining ground. It's a really cool and well designed framework with a React-like API. One good thing is that it, like Svelte, does not use virtual DOM. It also claims to be the fastest framework, but I doubt that it matters unless you are building games or something else requiring high FPS. I bet that your users won't complement you on your form field validations feeling snappy.
I recommend that you check out the FAQ and read through the articles. Lots of knowledge there.
Elm
My recent frustration with TypeScript and its complex, and fake types, made me re-evaluate Elm. I've mentioned in the latest issue that it feels like Elm's development has stagnated. Turns out that's not the case. The Elm community is thriving and everyone who started using Elm say that they can't go back to writing JavaScript again.
For those of you who don't know what Elm is: it's a strongly typed functional language that compiles to JavaScript. It's famous for its "no runtime errors" guarantee and "if it compiles, it runs" motto. It's also known for CDD (Compiler Driven Development) as Elm's compiler has the best errors messages. It tells you exactly what you did wrong and how to fix it.
I've played with Elm a couple of years ago and liked it, but I've abandoned it because tooling (editor support) was not that great. Things have significantly improved since.
One thing I never liked (and still kind of don't) is the way you write HTML in Elm. It's lists all the way down and if you what to use an SVG icon you have to convert it to an Elm type first. There are CLI tools for that or you can do it with Webpack.
Even if you feel that Elm is not for you I recommend that you take a look at it. I guarantee it will influence how you write and structure your normal projects. In a good way.
Start by reading Elm at Rakuten. It's a long article (40 minute reading time!) evaluating Elm after two years in production with many interesting pointers to other places, not only Elm related.
One of the articles mentioned in the article above is TypeScript's Blind Spots that highlights some of the TypeScript type gotchas.
Read also Elm vs. Svelte if you are curious how the two differ.
Svelte with Webpack 5
Webpack might be old (I prefer mature), but it won't go anywhere soon. Many companies and projects around the world rely on it. You can't go wrong on betting on Webpack. Here is an article that shows how to setup a Svelte project using the newest Webpack 5.
https://codechips.me/svelte-and-webpack-5/
Calculator fields in Svelte forms
In some situations Svelte's bidirectional bindings won't cut it and you have to reach for React-line unidirectional (one-way) bindings.
https://codechips.me/svelte-forms-calculator-fields/
RxJS 7
Version 7 of RxJS was finally released. 50% smaller, better typings and more consistent APIs.
JSONBIN
Never heard of it before, but feels like I am a little late to the party. Neat service if you need to store some JSON in your app. And who doesn't, right?
Firebase SDK 9
Finally! Firebase has listened to its users and is releasing version 9 of the SDK which is tree-shakeable. Personally, I am very excited as Firebase SDK (Firestore and Auth) are a hefty (700kb!!) chunk of my app bundle. The app itself is around 150kb. The SDK is currently in beta.
https://firebase.google.com/docs/web/learn-more#modular-version
Litestream
If you don't want to use modern NoSQL solutions plain old SQL will get you far. There is this one project I've been keeping an eye on called Litestream. It's replication of SQLite database. You are probably rolling your eyes, but SQLite will take you far especially if you are on a budget or tinkering with a hobby app. Wrap it in a Hapi or Express server, add Prisma ORM, put it on some PaaS and you've just got yourself a nice and cheap API.
Here is an article that shows just how cheap it is https://mtlynch.io/litestream/
And finally ...
Q: How do you generate a random string?
A: Put a Windows user in front of Vim and ask him to exit
That's it for this month. Thank you for reading!
]]>Recently I gave asdf a try and I don't think I will ever go back to other Node.js managers. Asdf is not only a Node.js manager. It's a tools manager. It can manage many different tools and programming languages that you probably already use in your daily work. Why use specialized tools when you can use one tool to rule them all?
I've written some notes on now to get started with asdf to manage Node.js. Mostly for myself, but hopefully you will find them useful too. Maybe I can at least awaken your curiosity so you can give it a try!
To install Asdf git clone it into .asdf
directory in your home folder or some other location. You are free to choose the location, but I strongly recommend sticking to Asdf's recommendation.
$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf
I am using Zsh as my main shell. To make asdf
command and autocompletion available in the shell I had to add the following to my ~/.zshrc
.
# ~/.zshrc
source $HOME/.asdf/asdf.sh
# add asdf completion
fpath=(${ASDF_DIR}/completions $fpath)
autoload -Uz compinit
compinit
If you source your Zsh config the asdf
command should be available to you. I am on Linux, so make sure to check installation instructions for your distro or operating system by reading Asdf's excellent documentation.
Asdf is useless on its own. To install a tool or programming language you first have to install a plugin for it in asdf. Maybe a best way is to view asdf as a plugin manager. You first install a specific plugin and then you install specific versions of tools or languages using that plugin.
Asdf has a well thought-out design. You keep everything self-contained and isolated and use asdf
top level command to orchestrate everything. Its design also enables modularity and easy plugin authoring. Everything is kept in one local directory - $HOME/.asdf
. No chmod
or sudo
commands required!
You can view all your installed plugins by running asdf plugin list all
command. To see all available Asdf plugins you can run asdf plugin list all
or visit Asdf's plugin page.
You can install many tools and programming languages with Asdf. Here is how to install Node.js using Asdf.
# add nodejs asdf plugin
$ asdf plugin add nodejs
The command may fail if you don't have all plugin's requirements installed on your computer. Often plugins rely on a few specific system packages to be installed. For example Node.js plugin relies on dirmngr, GnuPG, curl and awk to be present.
After you have a working Node plugin installation you can proceed to install a specific version Node.js, but first you have to know versions are available.
# list all node versions
$ asdf list all nodejs
The command will print out a couple of pages of Node.js versions with latest and greatest version at the bottom of the list.
If you want to install the latest version of Node you don't have to specify the version, just use latest
.
$ asdf install nodejs latest
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4009 0 4009 0 0 16430 0 --:--:-- --:--:-- --:--:-- 16430
gpg: key D7062848A1AB005C: public key "Beth Griggs " imported
gpg: Total number processed: 1
gpg: imported: 1
....
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 31.8M 100 31.8M 0 0 3369k 0 0:00:09 0:00:09 --:--:-- 3553k
node-v16.3.0-linux-x64.tar.gz: OK
$
Same goes for Node LTS. Just use lts
instead of latest
. To see what versions you currently have installed run asdf list nodejs
command.
I rely on a few global NPM packages in my daily work. Luckily Asdf's Node.js plugin can install global packages for us automatically if we specify them in ~/.default-npm-packages
file.
$ cat ~/.default-npm-packages
pnpm
npm-check-updates
degit
prettier
neovim
If you don't want auto NPM installs after installing new Node.js version you can install the packages manually. A common gotcha is to forget to reshim
after installation.
$ asdf reshim nodejs
If you forget to run the reshim
command the globally install NPM package will not be available in your path.
Asdf lets you specify specific tool versions per project. For that it relies on the .tool-versions
file for switching between versions. If you are coming from NVM you can have asdf read an existing .nvmrc
or .node-version
file. To do this, add the following line to $HOME/.asdfrc
.
legacy_version_file = yes
To use a specific version of Node.js in your project run asdf local node <version>
command. Just make sure that node version is installed first otherwise the command will fail. If everything went fine Asdf will write to a .tool-versions
file in the project folder. Next time you come back to that folder asdf will use the project-local node version.
You can specify system-wide (global) version of the tool by running asdf global node 16.3.0
command. This will add it to the ~/.tool-versions
file and that version will be use if no local version is specified. Here is my global versions file.
$ cat ~/.tool-versions
bat 0.18.0
erlang 24.0.1
elixir 1.12.1-otp-24
neovim nightly
nodejs 16.3.0
It's a short list for now as I am currently in the tool migration phase.
asdf list <plugin>
- show locally installed tool versionsasdf list all <plugin>
- show all available tool versionsasdf plugin add <plugin>
- install asdf pluginasdf install <plugin> <version>
- install specific tool versionasdf uninstall <plugin> <version>
- remove specific tool versionasdf global <plugin> <version>
- set global tool versionasdf local <plugin> <version>
- set project-specific tool versionasdf reshim <plugin>
- refresh plugin stateasdf help
- probably the only command you need to knowI've recently switched to Neovim where some features are only available in the nightly build. Just for the reference, and to demonstrate now to use Asdf to install something else, here is how I installed Neovim using Asdf.
$ asdf plugin add neovim
$ asdf install neovim nightly
$ asdf global neovim nightly
$ asdf uninstall neovim nightly && asdf install neovim nightly
Side note: A Vim user? I encourage you to give Neovim nightly (upcoming v0.5) a try!
Why use many specialized package managers when you can have one manager to rule them all? Of course, it's not always smooth sailing. Asdf can be a bit quirky to set it up if you are unlucky.
Although Asdf supports many tools I have some personal threshold. For example, I would not install Postgres with it. For that I would either use Docker or install it using native OS package manager. For everything else I will probably use Asdf in the near future.
]]>Two releases in the past month ... or maybe none? Not sure. For in-depth details see Svelte's official blog post.
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
From 77% to 65% complete in a month. I predicted at the end of June in the last newsletter, but the odds are getting lower. This is no surprise. The more you fix the more you discover.
https://github.com/sveltejs/kit/milestone/2
How is Svelte different than React?
Josh Nuss teaches us history of JS and shares his thoughts on how Svelte is different from React.
https://dev.to/joshnuss/why-svelte-59a5
SvelteKit Theme Switch
How to use Svelte stores and TypeScript to implement CSS theme switcher
https://dev.to/nico_bachner/sveltekit-theme-switch-a58
Simple Page Transitions with SvelteKit
How to implement nice page transitions using Svelte HOC and key property
https://dev.to/evanwinter/page-transitions-with-svelte-kit-35o6
Shadow Generator built in Svelte
Nice shadow generator built with Svelte and Tailwind
https://pagereview.io/css-shadow
Modular Synthesizer
Very cool app built with Svelte and Tone.js. Just don't get stuck playing around!
SvelteKit Authentication with Magic Links
This is a demo application for using Magic auth with SvelteKit. Magic provides secure, passwordless authentication for your application.
https://github.com/srmullen/sveltekit-magic
SvelteKitAuth
One thing SvelteKit hasn't officially solved yet in authentication. Hopefully this lib will be the goto solution.
https://github.com/Dan6erbond/sk-auth
IBM Carbon components in Svelte
Seems like IBM is one of the bigger companies that is betting on Svelte. Nice!
https://carbon-svelte.vercel.app/
Deep Space Network
App showcasing Carbon Components and Svelte
svelte-starter-kit
Neat SvelteKit boilerplate with all common bells and whistles
https://github.com/one-aalam/svelte-starter-kit
Svelte Motion
Cool new animation library for Svelte
https://svelte-motion.gradientdescent.de/
pluma-svelte-forms
Another forms library for Svelte
https://github.com/plumacloud/pluma-svelte-forms
Solid.js
In the past month I've been reading up on Solid.js. Very interesting reading! Ryan Carniato has really thought everything through when designing Solid taking the best parts and inspiration from number of different frameworks.
If there is one thing I can recommend in this edition s to read all the articles by him! You can find the links in Solid's Github repo.
Feels like Solid.js is gaining a lot of traction at the moment. This is a framework to watch!
https://github.com/solidjs/solid
A Look at Compilation in JavaScript Frameworks
Ryan's latest article where he dives deep down into the compilation step in some of the JavaScript frameworks
https://dev.to/this-is-learning/a-look-at-compilation-in-javascript-frameworks-3caj
Awesome Forms with Solidjs
A simple example of building a simple address form with Solid and RxJS.
https://dev.to/johncarroll/awesome-forms-with-solidjs-18gi
SolidJS - React meets Svelte?
"SolidJS is a frontend UI framework whose syntax is very similar to React but is compiled to modern javascript like Svelte is allowing people to get the speed and bundle sizes of Svelte but use the React syntax they are used to"
https://dev.to/alexmercedcoder/solidjs-react-meets-svelte-4fmm
The Rest
Feels like PWAs are on the rise. Here is a good starting point to learn more about them.
https://web.dev/progressive-web-apps/
PureScript Flame
A re-implementation of Elm architecture in PureScript.
https://github.com/easafe/purescript-flame
HTMX
Cool library that makes a lot of hard things easy by only using HTML attributes
Quicklink
Quicklink is a tiny library that prefetches page resources for you. It's kind of SvelteKit's prefetch attribute.
instant.page
Library with almost on par functionality with Quicklink. A direct competitor?
https://instant.page/
Aurelia Web Framework
A really interesting framework with great documentation. Weird that I haven't seen it before. I wonder if it's popular?
CSS Bed
Sometimes you have to throw together an app really quickly. Classless CSS frameworks can be really useful in such cases.
Yjs
Building next Google Docs or Figma? Then check out this library
SPA vs Phoenix LiveView
Lately I've been playing with Phoenix LiveView. It's a totally different approach to regular frontend apps. In LiveView you keep state on the server. Here is a short and easy-read article that teaches us the differences. Maybe it will arouse your interest to learn more about LiveView.
https://rafaelantunes.com.br/how-liveview-differs-from-a-traditional-spa
Surface UI
A library built on top of Phoenix LiveView that lets you build rich interactive user-interfaces, writing minimal custom Javascript. Still in early development, but looks very promising.
I am not sure what I think about web components yet, I've never needed to write one, but the idea of having framework agnostic components is certainly appealing. There are many use cases for web components, but where do you draw the line?
How we use Web Components at GitHub
An interesting read about how Github builds and thinks about web components
https://github.blog/2021-05-04-how-we-use-web-components-at-github/
Custom Elements Everywhere
A summary site that checks custom element support in many frontend frameworks.
https://custom-elements-everywhere.com/
Shoelace
A nice style library of web components. How to use it together with Svelte is yet to figure out.
Q: How does a farmer count all his animals in the barn?
A: With a cowculator
That's all for this month. Godspeed!
]]>SolidJS prides itself by claiming that it's currently the fastest framework out there. Speed is often not the problem for most apps. Unless we are talking about low-powered devices. Network speed is also an issue. Large app and slow Internet connection is a bad combination. That's why we see all the SSR frameworks such as SvelteKit, Next, Nuxt and others gain in popularity. Hydration is cool but it also makes the code more complex and requires a server, well, or a cloud function in most cases.
Svelte prides itself by creating the smallest JS bundle. That doesn't apply if your app has many components, but is often not a problem unless your app is exceptionally large. Some time ago Ryan managed to optimize Solid's bundle size to be on par if not even smaller than Svelte's.
Svelte also prides itself by being used for low-powered devices. This got me thinking. If Solid is the fastest framework with the smallest bundle size it must be a perfect framework for low end devices and also in general. You get faster asset downloads and theoretically you should get more juice out of it because it's the fastest.
Another bonus, in my eyes, is that Solid does all this without virtual DOM.
But what about developer experience? How is it compared to Svelte? I decided to find out by porting my simple Svelte app to Solid.
The sample application is simple, but has a few moving parts. Its primary purpose is to fetch and display a random quote from Kanye Rest API. It also has a timer to test HMR reloads, Tailwind CSS to test how it is to work with CSS in Solid and a fade CSS transition to test how this works in Solid.
Here is a codesandbox to see it in action.
SolidJS has a nice set of Vite-based app starter templates. The Tailwind is supported by WindiCSS. I am actually not sure if it's Tailwind CSS anymore. By judging Windi's website it looks like is taken its own path to become a Tailwind alternative.
Anyway, for my project I chose the WindiCSS + TypeScript template as I wanted to see how autocompletion performs in the editor.
$ pnpx degit solidjs/templates/ts-windicss solidjs-playground
The main file is simple. I was first looking for the windi.css
file until I found out that this file is virtual.
// index.tsx
import "windi.css";
import { render } from "solid-js/web";
import App from "./App";
render(() => <App />, document.getElementById("root"));
If you know a bit of React this code should look familiar.
In Solid.js components are constructors. They are called once and then they cease to exist. This is how I understand it at least.
// App.tsx
import type { Component } from "solid-js";
const App: Component = () => {
return (
<p class="text-4xl text-green-700 text-center py-20">Hello tailwind!</p>
);
};
export default App;
I wanted to see how good Solid's HMR is and for that I created a small timer component. All it does is to calculate the time difference in seconds from the app loads and now. For that I used the excellent date-fns
library.
import { createSignal, createMemo, onCleanup } from 'solid-js'
import { differenceInSeconds, format } from 'date-fns'
export enum Interval {
OneSec = 1,
FiveSec = 5,
TenSec = 10,
}
interface TimeProps {
frequency: Interval
}
export const Time = (props: TimeProps = { frequency: Interval.OneSec }) => {
const loadTime = new Date()
const [spent, setSpent] = createSignal(0)
const timer = setInterval(
() => setSpent(differenceInSeconds(new Date(), loadTime)),
props.frequency * 1000
)
const timeSpent = createMemo(() => format(new Date(spent() * 1000), 'mm:ss'))
onCleanup(() => clearInterval(timer))
return (
<p class='px-2 py-1 text-center text-indigo-900 bg-indigo-400'>
You've just wasted <strong>{timeSpent()}</strong> of your life on Kanye
</p>
)
}
Solid signals are the basic reactive primitives. You can some what compare them to React's useState directives or normal Svelte variables.
Because Solid components are constructors and only run once you have to set up your component lifecycles. In the time component I used onCleanup
method to cancel the interval timer.
To calculate and format the time in seconds I used createMemo
directive. You can compare it to Svelte's reactive variables. I actually could have done it in a simpler way by only creating a function and not wrapping it in createMemo
directive. createMemo
is used for better caching for example to reduce work required to access expensive operations like DOM node creation. According to Solid's guidelines it's often better to derive signals. Solid's documentation states: What can be derived, should be derived.
In the component constructor you can see that we pass the time interval as frequency
component property.
// App.tsx
import type { Component } from 'solid-js'
import { Time, Interval } from './Time'
import Wisdom from './Wisdom'
const App: Component = () => {
return (
<div>
<Time frequency={Interval.FiveSec} />
<Wisdom />
</div>
)
}
export default App
Props are a little confusing to me. It's not clear if I can set default props. Solid is sensitive when it comes to how you must handle the props. Because of how its reactivity primitives work you are not allowed to destructure props and I feel this can lead to a lot of frustration and time spent debugging.
This component is the core of the app. It's inside it where quote handling is done. Here it is in all its glory.
// Wisdom.tsx
import { createResource, Switch, Match } from 'solid-js'
import { Transition } from 'solid-transition-group'
import usa from './assets/usa.svg'
type Quote = {
quote: string
}
// this is defined in .env file in the project root folder
const KANYE_API = import.meta.env.VITE_KANYE_API as string
const fetchQuote = async () => (await fetch(KANYE_API)).json()
// Solid primitive to handle async resources
const [quote, { refetch }] = createResource<Quote>(fetchQuote)
export default function Wisdom() {
return (
<div class='container p-5 mx-auto max-w-3xl lg:mt-24'>
<h1 class='text-5xl md:text-7xl font-black text-indigo-900 lg:items-center lg:flex'>
<img src={usa} alt='USA' class='inline-block w-32 h-20' />
<div class='leading-none mt-5 lg:mt-0 lg:ml-4'>
Sh<span class='text-red-700'>*</span>t Kanye says
</div>
</h1>
<div class='mt-5 text-5xl md:text-7xl font-extrabold leading-none text-indigo-800'>
<Transition name='fade'>
<Switch fallback={<p>Failed to fetch quote</p>}>
<Match when={quote.loading}>
<p class="releative">Loading ...</p>
</Match>
{/* if you don't handle errors the whole app breaks */}
<Match when={quote.error}>
<p class="relative text-red-600">{quote.error.message}</p>
</Match>
<Match when={quote()}>{q => <p class="relative">{q.quote}</p>}</Match>
</Switch>
</Transition>
</div>
<div class='mt-10'>
{/* no way to easy style components with Tailwind */}
<button onClick={refetch} class='btn-fetch'>
Preach to me!
</button>
</div>
</div>
)
}
There are some Solid specific directives such as createResource
, refresh
, Switch
, Match
and Transition
. Don't worry if they don't make sense. We will go through them later.
Styling in JSX works just as you expect, but unlike in React you can use normal class
instead of className
property name. As a Svelte developer I am spoiled by having scoped components styles in Svelte. Solid has an official style library - solid-styled-components. It's a css-in-js style library. For some reason I find it a bit inelegant to work with. I still gave it a try, but couldn't get it to work with Tailwind's @apply
directive so I extracted the button's style to the global CSS stylesheet.
SVG handling is not something framework specific, but bundler specific. Because this starter project is Vite-based Vite takes perfect care of that for me. I wanted to import and render raw SVG file markup. If you use Vite you can append ?raw
to your SVG imports to get the raw markup. I couldn't get it to work in Solid. so I used an image tag instead. Maybe there is a way to do it, but I was short on time.
import usa from './assets/usa.svg'
<img src={usa} alt='USA' class='inline-block w-32 h-20' />
Rendering raw SVG files is a breeze in a Svelte setup by using the @html
directive.
Solid has a neat function called createResource
that help you work with async functions. It's not reactive by default. The way it runs is by reacting to changes in the id supplied as a function argument. I don't have an id because I am fetching random quotes. Luckily there is a refetch
function that lets you trigger fetching manually.
// Wisdom.tsx
import { createResource, Switch, Match } from 'solid-js'
type Quote = {
quote: string
}
// this is defined in .env file in the project root folder
const KANYE_API = import.meta.env.VITE_KANYE_API as string
const fetchQuote = async () => (await fetch(KANYE_API)).json()
const [quote, { refetch }] = createResource<Quote>(fetchQuote)
It's nice that you can supply a type to the function. Autocompletion in editor works out of the box, but the bundler doesn't catch a mistyped property name in JSX and gives you no error in dev console nor in the terminal.
Solid comes with many helper components that lets you handle common template control flows. There is <For>
<Show>
, <Suspense>
and others. In this app I used <Switch>/<Match>
component to display different quote states. This is an improvement over React where it's common practice to use raw JS code to handle these cases.
// Wisdom.tsx
<Switch fallback={<p>Failed to fetch quote</p>}>
<Match when={quote.loading}>
<p>Loading ...</p>
</Match>
{/* if you don't handle errors the whole app breaks */}
<Match when={quote.error}>
<p class="text-red-600">{quote.error}</p>
</Match>
<Match when={quote()}>{q => <p class="relative">{q.quote}</p>}</Match>
</Switch>
One weird error I encountered is that if you get an error in the fetchQuote
function it breaks the whole app unless you handle it. This happened to me when I got disconnected from the WIFI and couldn't fetch a quote. So it looks like you have to handle the error cases explicitly.
You should always handle error cases, but should the timer in a separate component be affected by a network error? I am not sure. In my opinion the components should be isolated, but apparently they are not in Solid.
This was an interesting problem. I wanted to implement a simple CSS fade transition for switching between quotes. In Svelte I would have used the #key
block together with built-in Svelte transitions for that and be done, but in Solid I had to import a separate transition package for it - solid-transition-group.
There are multiple ways to build CSS transitions using this official library. The official example already had a fade transition so I just copied all the code and CSS classes from it.
// index.css
.fade-enter-active {
transition: opacity 200ms ease-in-out;
}
.fade-exit-active {
transition: opacity 200ms ease-in-out;
}
.fade-enter, .fade-exit-to {
opacity: 0;
}
The transition package has a known limitation. Transition and Transition Group work on detecting changes on DOM children. Therefore it only supports single DOM childs and not text or fragments. Because of that I had to wrap all the elements in paragraph tags.
In the code snippet below name='fade'
is used to generate transition class names.
// Wisdom.tsx
import { Transition } from 'solid-transition-group'
<Transition name='fade'>
<Switch fallback={<p>Failed to fetch quote</p>}>
<Match when={quote.loading}>
<p class="releative">Loading ...</p>
</Match>
{/* if you don't handle errors the whole app breaks */}
<Match when={quote.error}>
<p class="relative text-red-600">{quote.error.message}</p>
</Match>
<Match when={quote()}>{q => <p class="relative">{q.quote}</p>}</Match>
</Switch>
</Transition>
The implemented transition is a flaky experience. Sometimes it works, sometimes doesn't. Sometimes it works and suddenly it just stops. Maybe I did something wrong. I also feel that the whole thing is overly complex. Working with CSS transitions is so much easier in Svelte, because they are built-in.
Solid has a really small runtime, 6 kB minified and gzipped which takes 1ms to download on a decent connection. It's five times smaller compared to full jQuery library, which is 30.4 kB.
The whole production build is around 10 kb. It's actually more or less on par with the bundle size of the Svelte version.
vite v2.4.2 building for production...
✓ 274 modules transformed.
dist/assets/usa.ede8af9e.svg 0.88kb
dist/index.html 0.55kb
dist/assets/index.9f119c76.js 2.11kb / brotli: 0.92kb
dist/assets/index.2c586559.css 4.85kb / brotli: 1.27kb
dist/assets/vendor.ce7411c6.js 31.43kb / brotli: 8.68kb
Impressive! Size does matter.
Here is the complete app source code: https://github.com/codechips/solidjs-playground
Overall I am impressed by SolidJS. It's an advanced framework and code can get complex, but at the same time I think if you master it you can write fast and efficient code that can make your apps fly. Because of the complexity I wouldn't recommend it as your first framework.
To me personally SolidJS is a next step up for the advanced Svelte developer who wants to try something new, wants to have one-way bindings as default and fine-grained reactivity. Or a React developer who likes JSX, but who is burned out from React.
However, I wouldn't recommend Solid just yet if your app needs advanced CSS animations. It's doable, but in Svelte you get it our of the box and it's simpler to do too. I also couldn't figure out how to use framework agnostic JS libraries, such as tom-select for example, something that is easy to do in Svelte.
SolidJS is still a young framework. This often means that there are not many third-party libraries for it yet. Even though it's primarily based on JSX and feels React-ish you cannot re-use any of the existing React libraries.
Although Solid has support for SSR there is no SSR framework, ala Next.js or SvelteKit for it yet. I am exited to see what community will build on top it. I cross my fingers and hope for the best, because Solid is a great and promising UI library.
]]>One release last month. Feels like there is currently a lot of focus on SSR. For details see Svelte's official blog post.
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
Still a lot of stuff to nail down, but that doesn't stop people using it in production.
https://github.com/sveltejs/kit/milestones
Svelte in large companies
More and more larger companies are betting on Svelte. I know about Ikea, but sadly don't know the story behind the decision. Among others are Square, Brave Search, and Decathlon. That last site is blazing fast and a joy to browse. A rare find in today's sluggish world of ecommerce.
Snappy PWA
Parsnip is a mobile-first, progressive-web-app that helps you to learn to cook at home. Built entirely with Svelte, Routify, TailwindCSS, and Firebase. Feels really snappy on mobile! Here is the story and tech behind it.
Timeline of rontend frameworks
Cool timeline animation video about the popularity of JS frameworks. Hard to imagine that even React was small back in the days and nice to see that Svelte is on the rise!
https://statisticsanddata.org/data/the-most-popular-javascript-frameworks-2011-2021/
SolidJS hits 1.0
SolidJS, the fastest React inspired framework without virtual DOM, has finally hit release 1.0 and with it came a new website. It has a nice tutorial that was clearly inspired by Svelte's. In my opinion Solid is an advanced framework and some concept might be had to grasp if you are new to web development. I really dig Solid and hope that it gains in popularity.
https://www.solidjs.com/tutorial
Utility CSS framework in SASS
It was only a matter of time before someone would build a CSS utility framework ala Tailwind in SASS. There is UniformCSS and YoghurtCSS. Both look interesting!
Enrichment Calculator
Nice app build with Svelte and IBM Carbon Components. No, it won't help you get rich. Sorry!
https://enrichment-calc.netlify.app/
Alpine.js 3.0
Alpine.js. which describes itself as "Tailwind but for JS", has hit 3.0. It now has an website. Check it out!
Imba
The friendly full-stack language with "groundbreaking memoized DOM" that compiles down to JS. Looks cool and reminds me a bit of CoffeeScript, but on steroids.
Another RxJS + Svelte example
I think that RxJS is really cool and wish that more people discovered this library. Here is a Svelte REPL that showcases some common RxJS operators and how to use them as Svelte stores
https://svelte.dev/repl/b8d09f9bfd4b48e19db4a88181b6ccd2?version=3.38.3
Migrating from Sapper to SvelteKit
More and more people are migrating their Sapper apps to Svelte. I should probably do the same soon. It's always interesting to read up on their experience. Warning for strong colors!
https://shipbit.de/blog/migrating-from-sapper-to-svelte-kit/
astro.build
YASSG (Yet Another Static Site Generator) from the people behind Snowpack. Only this one is framework agnostic and renders the entire site to static HTML.
https://astro.build/blog/introducing-astro
What do you call 2000 pounds of bones? A skele-ton!
That's it for this month!
]]>Six new releases in the past months. Incredible! I thought that summer vacations in the Northern hemisphere would slow things down, but it's actually the opposite. Maybe people don't take vacations or maybe that's what they actually do and contribute to Svelte on their vacations.
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
Slowly moving forward. As I am writing this there are 36 open and 96 closed issues in the 1.0 milestone.
https://github.com/sveltejs/kit/milestones
SveltePress
SveltePress is a documentation tool built on top of SvelteKit. It's something like VuePress, or maybe Docsify, and is an easy way to get your project or API documentation going.
https://github.com/GeopJr/SveltePress
Snel
A tool to compile Svelte to JavaScript files to create web application using Deno and Svelte. Don't know if anyone is using it in production, but interesting to see that people are building things on top of Deno runtime.
https://github.com/crewdevio/Snel
Lightweight i18n library
Internationalization is often a pain. typesafe-i18n is an opinionated, fully type-safe, lightweight localization library for TypeScript and JavaScript projects with no external dependencies that works with Svelte and SvelteKit.
https://github.com/ivanhofer/typesafe-i18n
svelte-restate
Because Svelte's store contract is so simple it's easy to create a custom store implementation. Restate is an immutable store for Svelte with full Typescript support and Redux Devtools integration. It is highly inspired by re-frame (ClojureScript) subscriptions.
https://github.com/endenwer/svelte-restate
Felte Forms Library
Felte is an ambitious forms library for Svelte and Solid. I like forms. I work with them a lot and also wrote a book[link] on the subject. I haven't had the time to play with Felte yet, but it sure looks interesting!
https://github.com/pablo-abc/felte
SvelteKit + Supabase
Supabase is an exciting alternative to Firebase. This template offers everything you need to get off the ground quickly to build apps on top of SvelteKit and Supabase.
https://github.com/one-aalam/svelte-starter-kit
Firestore Lite
One of the biggest pain points working with Firebase what its large bundle size. Well, no more! The new version of Firebase SDK is tree-shakeable and there is even leaner Firestore Lite.
https://firebase.googleblog.com/2021/07/introducing-the-new-firebase-js-sdk.html
React vs Svelte vs Solid & MicroFrontends
Interesting interview with Ryan Carniato, creator of SolidJS. Give it a listen!
https://show.nikoskatsikanis.com/episodes/ryan-carniato
3 lessons from React
Ryan is also a great teacher and is not dogmatic about any frameworks. Here are some of his thoughts on becoming a better Svelte developer by looking at how things are done in other frameworks.
I still don't know what to make with (and off), web components, but I am sure they will become more popular as the web and web development progress. Did you know that you both build web components in Svelte and also use web components built with other tools in Svelte. Confusing right? Fear not! These links will make everything clear.
Using custom elements in Svelte
Learn how to use web components build in Lit, and the gotchas that come with it, in your Svelte apps.
https://css-tricks.com/using-custom-elements-in-svelte/
svelte-custom-element-template
Sample project how to build custom elements, aka web components, in Svelte with TypeScript.
https://github.com/Dan6erbond/svelte-custom-element-template
Svelte for Web Components development: Pitfalls and workarounds
Learn some of the pitfalls and how to solve them when developing web components in Svelte.
I have a secret fascination with Lisp. Many years ago I had the chance to work with Clojure and remember that I liked the experience a lot. Then I got more involved in web development and with it came JS and Node.js. So no more Clojure.
I recently dove deep down into ClojureScript ecosystem and its funny package names. While the entry barrier is high I truly believe that teams can be 10X productive in ClojureScript than in other languages. First, it's simple, stable and doesn't change often. Second, ClojureScript is purely functional and in Lisp code is data and data is code. I urge to to research what it means for your code on your own.
Reagent
A nice guide to Reagent, a ClojureScript wrapper around React. It gives you a way to easily create React components.
https://purelyfunctional.tv/guide/reagent/
Re-frame
Re-frame is a mature framework built on top of Reagent with great documentation. Lots of takeaways and learnings even if you don't plan to build anything with it.
https://day8.github.io/re-frame/
Fulcro
Fulcro is a library for development of single-page full-stack web applications and a spiritual successor to Om.
https://fulcro.fulcrologic.com/
awesome-clojurescript
Awesome list of all things ClojureScript and its creative package names.
https://github.com/hantuzun/awesome-clojurescript
shadow-cljs
One of the biggest hurdles coming to ClojureScript is to understand how to setup the project. shadow-cljs is one of the popular tools in the ecosystem, but many people don't understand where to draw the line between ClojureScript compiler and other tools. This article explains how ClojureScript compiles the files to JavaScript.
https://code.thheller.com/blog/shadow-cljs/2019/03/01/what-shadow-cljs-is-and-isnt.html
I think we can all agree that Tailwind CSS has become the most popular utility CSS framework and all popular things get copied. In the last issue I wrote about YoghurtCSS and UniformCSS, two SASS based utility frameworks. There is also Halfmoon and H-grid.
If you are a fan of Material Design there is Svelte Material UI, which is a collection of Svelte components.
Some people believe native apps will take off for real because the experience is so much better. Some believe it will be PWAs. Only time will tell.
I was curious if there is something like React Native for Svelte and it turns out there are a few frameworks. However, I don't know how mature it is or if anyone has built something with them in production.
Svelte Native
Svelte + NativeScript. Looks like an official framework, but not sure if it is.
https://svelte-native.technology/
svelte-capacitor
Project template to build hybrid mobile apps using Svelte and CapacitorJS with live reloading on Android and iOS.
https://github.com/drannex42/svelte-capacitor
svelte-nodegui
Build performant, native, cross-platform desktop apps with Svelte.
Framework7 + Svelte
Framework7 has bindings for Svelte that lets you build Build full featured iOS, Android & Desktop apps with it.
Building a PWA with Svelte
Some comments and notes on the experience of building a PWA with Svelte and Framework7, plus a starter project.
https://github.com/tncrazvan/svelte-f7-starter
The Future of the Web
Interesting view on the future of the Web.
https://www.hazem.cool/blog/the-future-of-the-web
Websites using Svelte
List of some of the public sites using Svelte.
https://www.wappalyzer.com/technologies/javascript-frameworks/svelte
Collection of responsive patterns
Useful list of patterns and modules for responsive designs.
https://bradfrost.github.io/this-is-responsive/patterns.html
The Future of Webdev
Personal thoughts on the future of web development. It's not TypeScript and WebAssembly, nor React and GraphQL.
https://suzdalnitski.medium.com/the-future-of-webdev-b5fe293b5f2c
How does JavaScript work?
Deep technical series on how JavaScript works internally. Start with this article.
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf
What do sprinters eat before the race?
Nothing. They fast.
That's all for this month!
]]>Svelte development shows no signs of slowing down. Four new releases in August. Boom!
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
Although SvelteKit is moving forward they need your help to get to 1.0. Doesn't matter if you are a total beginner. I am sure there is something for everyone.
https://github.com/sveltejs/kit/issues/2100
https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md
How to build a decentralized chat dapp
Build a decentralized chat app using the GUN JavaScript library and Svelte. Learn how to use web3 and blockchain. This video is from Fireship so you know it's good.
Create a PWA with SvelteKit
SvelteKit is a hybrid framework meaning you can have dynamic pages and you can have statically generated pages. This article is a good starting point for understanding how to build a PWA in SvelteKit.
https://dev.to/100lvlmaster/create-a-pwa-with-sveltekit-svelte-a36
The Truth About Svelte
Oldie but goodie. Written in 2019 before the releases of Svelte 3. Tech moves fast!
https://gist.github.com/Rich-Harris/0f910048478c2a6505d1c32185b61934
Philosophy of Svelte
Personal reflection on Svelte. I agree with most of the things in the article, but it's not all roses. There are some cases where other frameworks would make more sense to use.
https://blog.scottlogic.com/2021/01/18/philosophy-of-svelte.html
Evidence - a YC backeed company is betting on Svelte and SvelteKit
I've read about startups betting on Svelte before, but they all faded away after the initial hype. My guess that it wasn't Svelte's fault. Evidence, a startup backed by Y Combinator bet on Svelte too. Vey cool and let's hope that they will do well.
https://news.ycombinator.com/item?id=28304781
Cool App: Thread Bare
I was blown away after watching this Youtube video I saw in this tweet. If you want to build highly interactive apps Svelte is your friend.
SvelteKit and Prisma
SvelteKit is all the rage. Prisma is all the hotness. Here is a short and sweet tutorial from Mike Nikles on how to use them together.
https://www.mikenikles.com/blog/svelte-kit-prisma-a-match-made-in-digital-heaven
Svelte developers are happier and get paid more
StackOverflow's Developer Survey is always a fun read. Svelte did really good in 2021 and Svelte developers are getting paid more that React developers according to results. If you are still hesitating if Svelte is something you should bet on here is your answer.
https://insights.stackoverflow.com/survey/2021#most-loved-dreaded-and-wanted-webframe-love-dread
https://insights.stackoverflow.com/survey/2021#top-paying-technologies-web-frameworks
JavaScript vs JavaScript. Fight!
Interesting article from Ryan Carniato that tries to clear up many confusions in today's frontend world. Did you know that SvelteKit, Nuxt.js and Next.js are SPAs? No? Go read the article!
https://dev.to/this-is-learning/javascript-vs-javascript-fight-53fa
Svelte Summit Fall 2021
Want to speak at the Svelte Summit in November? Go and submit your talk!
https://sessionize.com/svelte-summit-fall-2021/
Fragments over the Wire, HTML over the Wire, HTML over Websockets I don't know what the official name is, but I know for sure that this is one of the possible futures of Web development and I really dig it.
The reasons for this are plenty, but here are a flew of them according to me.
No need to write JavaScript
Writing pure frontend code can be daunting and take a lot of time. Write everything in the backend and outsource the rest to the framework.
Write everything in one language
Everyone knows that context switching is a real productivity killer. Writing everything in one language improves your focus on the business domain problems and not on the framework ceremonies.
Faster time to market
From my own experience these frameworks let you go faster and thus reduce time to market. Perfect for SME, POCs and side-projects.
Just as there are pros there are cons. Here are some of the reasons where they might not make sense.
Different teams for frontend and backend
If you work in a bigger company where you have different frontend and backend teams. The frontend devs will not have anything to do unless they are on the UX side of the frontend spectrum.
Highly interactive apps
Because the state is usually managed on the server these apps can introduce some network lag. If high interactivity is a requirement you will be better off if you offload that to custom written frontend app.
Offline support
Many of these frameworks rely on Websockets. Don't use them if you offline support is a must.
Here are some interesting articles on the topic.
The Future of Web Software Is HTML-over-WebSockets
The article that started it all and got me really excited about the concept. I guess that I read it in the middle of one of my frontend fatigues.
https://alistapart.com/article/the-future-of-web-software-is-html-over-websockets/
FrOW: HTML Fragments over the Wire
Explains the history of how we got here and has a few interesting links that can take you down the rabbit hole.
https://github.com/guettli/frow--fragments-over-the-wire
Blazor
Microsoft doesn't want to missout on all the fun. Blazor is their contibution to this space that sits on top of ASP.NET. I haven't explored it myself in depth yet, but that page contains many buzzwords. WebAssembly, SignalR, JS iterop. Well, what can I say? Microsoft.
https://github.com/AdrienTorris/awesome-blazor
Kweb
No, Kweb is not K-Pop. Sorry! Kweb is a new way to create beautiful, efficient, and scalable websites in Kotlin, where server-browser communication is handled automatically.
https://github.com/kwebio/kweb-core/
Other FROW frameworks
There are a few other frameworks out there. The most well-known are.
One thing that sets Svelte apart from other frameworks is that it has CSS transitions built-in. Overall, working with CSS and animations in Svelte is so much easier than in other frameworks. At least that I've worked with earlier.
Svelte is cool, but if you don't use Svelte for your project yet here a some nice libraries that can make your pages more interactive without doing that much work.
Transition.js
Simple JS library that provides a convenient way to create CSS transitions pragmatically.
Highway.js
A lightweight, easy-to-use, flexible and modern library that help you create AJAX navigations with animations on a website.
Swup.js
Another transition library that it ridiculously easy to use. Just add it to you page and choose among different animation themes.
Barba.js
A progressive enhancement library to create fluid and smooth transitions between your website's pages. Mighty impressive website!
Q: How do you make holy water?
A: You boil the hell out of it.
That's it for this month!
]]>Things were simpler back in the days. jQuery was king and we relied on it to get things done. And we did get things done too. Browser wars made the frontend world accelerate at incredible speed. There were many new cool APIs we could use, but they were often browser specific. This created a lot of inconsistencies and forced us to use polyfills. Modernizr was a popular library that we relied on to detect what was possible for us to do in the browser.
The speed of browser advancements propelled the development of new tools. Bower was a popular, but short-lived package manager for the web that many developers relied on. We used different task runners such as Grunt and Gulp to get things done. The frontend ecosystem was getting complex, but we could still wrap our heads around it.
We were getting things done with the tools and libraries we had, but it wasn't enough. We wanted more and better tools. Webpack was gaining traction and Browserify was released and this is where I personally lost track of everything. I think it was around 2014 where things got blurry and made it impossible to keep track of all the news. This is also the year when I had my first frontend burnout and decided to move back to backend development. It became too much. I craved simplicity.
Let's fast forward to today and see if things have improved. The popular frameworks of the past are merely ghosts of their former glory. Today, React is the undisputed king followed by many other modern frameworks such as Svelte, Vue and Angular. TypeScript has become a de-facto standard language of web development. For sure, frameworks and languages have advanced for the better, but did they actually make our life simpler or allowed us to reduce time to market?
My opinion is that npm install
broke the Internet. There were a few different attempts to bring package management to Node.js and NPM won. Today it's the standard of JS package management, but in the beginning it was actually for managing packages for Node.js only, hence the name - Node Package Manager.
Not that long after developers realized that they could also publish frontend libraries to NPM. I think it was Bower package manager that started the trend. Sometimes later, Bower quickly became out of fashion when most of developers switched to different JS bundlers. Fast forward to today and think of one frontend project that doesn't use a bundler and downloads half of the Internet when you run npm install
.
That's the norm today. Why is it like this? First, NPM's recommendation to package creation was to create small focused packages that do one thing and do it well and I was a big proponent of this myself telling all other developers to think this way. Second, it's often easier to find and install a package from NPM than to write it yourself. Is it a good thing? I am not sure. Remember the leftpad incident? Also, if you rely on some NPM package and find a bug in it, how much effort is required to fix it versus if you would have written the functionality yourself?
It's easy to add new packages to your project and if it's easy we tend to do it. We have too much freedom of choice. It's also a little too easy to publish packages on the NPM and everyone wants to scratch their own itch. I am not saying it's bad. On the contrary, sharing knowledge and contributing to open source is good, but does it really justify itself as NPM package? Maybe a simple Github gist is enough? Copy, paste, adjust.
Here is a thought experiment. Would you be able to build the same app with the same level of interactivity if you could only add your dependencies as scripts in the document head? How cautious would you be?
JavaScript is a cool language that mixes OOP with FP. Pick and choose. It's also a messy language. Actually not messy, but maybe the right word would be - flexible, and if you know your way around the language and all its quirks you will do just fine. Many existing websites and apps were written using plain JS.
When TypeScript entered the scene (almost) everyone cheered and thought that it will make all of their JS problems go away. Sure, it might have solved some of the existing problems, better IDE autocomplete comes to mind, but what people don't realize is that by adopting TypeScript they just made their developer lives more complex. If you ask me, TypeScript is a weird language with a fake type system that gives you fake confidence. I too do use TypeScript for some projects, but it feels like the only thing I do is try to keep the TypeScript compiler happy.
TypeScript is rarely frictionless, especially its setup. I bet there are very few developers that throw together a working tsconfig.json
on the first try. It comes with a cost and it's something you have to think about. Because it's so wide-spread today, it's hard not to use TypeScript when building open source libraries. I mean, you don't want people to think you are a weirdo, right?
Adopting TypeScript is not enough. There is a whole JS surrounding ecosystem that you have to adopt to as well. Because JS is such a loose language you have to solve linting and code formatting. Should you adopt ESlint or StandardJS? Is there room for Prettier? If you go with ESlint what plugins do you need? Should you adopt Airbnb style or is there something else, something better and newer? You might want to introduce automatic linting too so you can't commit bad code. Husky might help you with it.
The tooling ecosystem surrounding JS adds additional complexity layer to your project. Now, I am sure that you can reach the perfect setup for your project, but how many many hours of frustration, searching and reading documentation did it take you to get there?
It's clear that the frontend world is getting more and more complex and those people who tell you it's still young and we are still trying to find good solutions to existing problems are ignorant. Two decades is a long time for the dust to settle, but if you keep adding more wind all the time your sight of view will only get shorter. The wind is added by a lot of new frameworks and libraries, the flexibility and shortness of the JavaScript as a language and NPM. It's too easy to publish and consume NPM packages today and everyone is doing it. The flexibility and the possibility is what got us here. I understand that innovation is hard, it's blood, sweat and tears, but what if we have been running in the wrong direction this whole time? The direction of complexity instead of simplicity.
In fact it's so bad that I've heard that some of the developers have developed the green field project anxiety. There are too many choices and they rather take on maintenance projects than make the tough decisions. React fatigue and overall frontend fatigue is real. How do I know that the tech stack I am betting on will still be relevant in two years? SSR or SPA, routing, state management, style libraries. It's easy to end up in the state of analysis paralysis and it's not a nice place to be in.
We also tend to get things backwards. Developers think that in order to get things done you have to use [insert some cool framework name here]. I've witnessed this myself. I have seen code schools teaching newbies without any IT experience how to build apps with React without teaching them the basics of HTML and how the web works. That's crazy, but also sad. It's sad how much money people pay in hope to get a job after they finish the coding course.
It's not the fault of the people who taught the course. It's Economics 101, demand vs supply. If someone can make money out of you they will. React should be the last thing you learn because React is super-complex, but there is a surge for React developers so let's skip the essentials and concentrate on the needs of the market.
Choosing boring technology for the project is boring. I myself is guilty of this too. Luckily I can figure most of the things out because I have a lot of experience under my belt, but it can imagine how frustrating it can be for a newbie. Heck, sometimes even I don't figure stuff out. Not because I can't, but because it's hard and I don't want to invest my time in it, spend my mental energy. To me it's a clear sign of how complex current frontend stack is.
This leads me into the main topic of the article - the dreaded frontend burnout.
This burnout type is sneaky. You don't usually notice it because it doesn't really feel as a burnout, but more like a never-ending mild frustration at first.
It's a slow burnout. It's like drinking coffee from your favorite coffee cup that has a small, but sharp crack in it just at the right place and you scratch your lip every time you take a sip. First it's annoying, but then you get used to it, only to realize months later that you have had enough, and you throw that cup into the wall with anger.
When you reach this moment it's usually too late. That's why you have to pay close attention to the following signs.
Bringing your work home. Not physically, but mentally. If you come home with the constant nagging feeling that you didn't finish what you planned to accomplish today. The feeling of mild frustration.
Yak shaving. If you feel that the only thing you do is fight with the tools and project related things and it leads to a lot of time spent tweaking, solving, dependency management, but no actual coding. Frustration builds up.
Mentally taxing. If you come home mentally drained and the only thing you have energy left to do is to watch Netflix. You have no power to do anything else.
Lost opportunity cost. If you start doubting if building UIs and web sites was the right career choice for you. You ask yourself if things would be different if you programmed an operating system or backend APIs, but you are building interactive UIs and it's just as complex.
Negative feedback loop. If you feel that things are getting worse at work every day and you are not getting anything done.
Work/life balance. If you feel that work bleeds into your private life and work/life boundaries become fuzzy. It can be as simple as thinking about the unfinished ticket in the evening or fail to fall asleep because you are thinking about work. This is especially common if you work remotely.
Performance drop. If you feel tired and lethargic when you come to work and can't get anything done. You have hard time concentrating on the task at hand and procrastinate. Nothing feels exciting anymore.
Framework envy. If you are thinking that things would be easier, more productive or fun if only you used another framework or technology in the project.
Quitting. If you are thinking about switching jobs. This is a common fallacy. People often think it's their current employer that is the problem, but oftentimes it's not.
Denial. If people close to you tell that you have changed lately or you are not present mentally, and you blame it on the tough intense period at work, it's definitely a sign. It's called self-denial.
These are just a few signs that you are nearing the point of no return and it's only a matter of time before the burnout hits you. There are also always physical symptoms associated with those feelings. The most common are: lack of energy, irritation, agitation and sleeping trouble. Pay close attention to your body, not only to your mind.
If you can relate to any of those signs it can mean that you've caught this early and it might not be too late yet. Recognizing and accepting is the first and most important step.
My best advise here would be to take a break and go out for long walks in the nature. They help clear your mind plus you get the physical exercise as a bonus. Also, make sure to book some social events with friends or family even if you feel that you have to force yourself. Next step is have an honest talk with your manager and explain how you feel. It might be hard to do, but you will be glad you did it.
There are things in my opinion we can do to reverse this trend, and they are exciting too, but it's a topic for another upcoming article.
I fully understand, and accept, that the frontend world is turbulent at the moment, and it will be a bumpy ride before we get to somewhat stable state, but please be careful. Watch out for the signals yourself, watch your friends and colleagues. Pay close attention to how you feel and your body. Burnout is never worth it.
Think objectively. Development is fun, but sometimes it can be too fun. Do you really need to use this frontend framework? Do you really need to use SSR? Is SEO a requirement? Remember that complexity increases with every choice you make. It might not be obvious at first when you get everything setup and running, but it will be obvious later when you find yourself in the hole you that have been digging yourself and it's too deep to climb up from.
We, as programmers, often tend to complicate things. It's in our DNA. We like a good challenge, to solve complex problems and we are really good at it too. The problem is that the complexity often steals our time. Time that is better spend building features instead that bring real value to the users.
Next time you are about to start a new project always ask yourself: What is the simplest solution I can get away with?
It's totally fine if you don't agree with me on everything. I just wanted to get it off my chest.
]]>When you create a new SvelteKit project with npm init svelte@next my-app
command you have a few options to choose from. The choices you make will affect how easy it will be to integrate Tailwind CSS. For example, here are the choices that I made during my project creation.
As you can see in the screenshot above I chose TypeScript support. In order to get TypeScript working in Svelte you have to use svelte-preprocess library. That library is also the key to get Tailwind working in our project. Its job is to support and process many different languages in Svelte templates and it also has built-in support for PostCSS that Tailwind is based on.
Don't worry if you didn't choose TypeScript as an option in your project. You can install svelte-preprocess
separately and implement it in your svelte.config.js
like this.
// svelte.config.js
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte'
}
};
export default config;
Now that we have PostCSS support in place let's move on and install and configure Tailwind CSS and other required libraries.
First things first, we need to install Tailwind and supporting NPM packages and Tailwind configuration file.
$ npm add -D tailwindcss autoprefixer postcss-load-config
$ npx tailwindcss init
For this to work we also need to create PostCSS configuration in our project directory.
// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
tailwindcss: {},
},
}
We are almost ready to go, but if you start you project you will get an exception that has the following error message in it somewhere.
Instead rename tailwind.config.js to end in .cjs, change the requiring
code to use import(), or remove "type" : "module" from [...]/package.json
What Node basically says is that it expects a different type of module. The solution is simple. Change extensions for Tailwind and PostCSS configs from .js
to .cjs
.
In order for Tailwind to know what CSS it needs to purge during production builds you have to add purge patterns to your Tailwind config.
// tailwind.config.cjs
module.exports = {
// add this section
purge: [
'./src/**/*.html',
'./src/**/*.svelte'
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
We now have a correct Tailwind and PostCSS setup in place. Let's use it.
The last step is to create a Tailwind CSS file and import it in the project. Create an app.css
file under src
directory, if it's not there already, and add the following to it.
/* app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1, h2, h3, h4 {
@apply font-bold;
}
}
Next step is to require that file in your template. Here I added it to my main layout file.
// __layout.svelte
<script lang="ts">
import '../app.css';
import Header from '$lib/components/Header.svelte';
import Login from '$lib/components/Login.svelte';
import { auth } from '$lib/auth';
</script>
<main>
<Header name={$auth.company?.name} />
<div class="max-w-4xl mx-auto py-8">
{#if $auth.loading}
<div class="text-gray-600">Loading ...</div>
{:else if $auth.user}
<slot />
{:else}
<Login />
{/if}
</div>
</main>
If you start the application now the Tailwind CSS should kick in.
<style lang="postcss">
.content {
@apply text-lg text-gray-600;
}
</style>
You can also use Tailwind directives in your style tags. I usually add lang="postcss"
to get rid of editor warnings, but you can safely omit it if you want as it has no effect on the functionality.
That's all there is to it. Tailwind CSS integration in SvelteKit in 3 easy steps. If I now counted correctly that is.
]]>Four new releases last month. Svelte is currently at v3.44.1
https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md
Currently at v1.0.0-next.192 and at 82% done towards 1.0
https://github.com/sveltejs/kit/blob/master/packages/kit/CHANGELOG.md
Transitional Apps
Rich Harris gave a talk at the Jamstack conference trying to coin a new term transitional apps. In it he shares his thoughts on the shortcomings of SPAs and emerging technologies like HTML-over-the-wire. Funny, because if you think about it the only type of apps that could satisfy all the requirements are ironically only native apps.
https://www.youtube.com/watch?v=860d8usGC0o
Phoenix LiveView 0.17
BEAM is an awesome VM, Elixir is an awesome language, Phoenix is an awesome web framework and with the 0.17 release of the Phoenix LiveView framework building live apps just became so much better. Watch this keynote from Chris McCord, the creator of Phoenix framework, from the ElixirConf 2021 to understand why.
https://www.youtube.com/watch?v=Of1phFsC4ZI
New Suspense SSR Architecture in React 18
Talking about LiveView, React doesn't want to be left behind, so they are moving to the server too, or actually already did. I stumbled on this discussion on Twitter by accident, and while it explains React Suspense in great detail, one cannot wonder about all the complexity. Do things really need to be this complex?
https://github.com/reactwg/react-18/discussions/37
SvelteKit on Firebase
One great thing about SvelteKit is its adapter architecture. If you are a fan of Firebase you can utilise the Firebase Hosting CDN with dynamic content served by SvelteKit on Firebase Cloud Functions. Adapter is still in beta, but still worth checking out.
https://github.com/jthegedus/svelte-adapter-firebase
SvelteKit SSR Forms
In frontend apps today we don't usually don't submit our forms the "old school" way anymore, but opt out of the default browser behavior. If you what to have a fallback to the server here is a nice article that explains the basics of it in SvelteKit.
https://dev.to/danawoodman/getting-form-body-data-in-your-sveltekit-endpoints-4a85
The Single-Page-App Morality Play
I am not sure how to describe the article. Maybe "personal thoughts on SPAs"? Just go and read it.
https://www.baldurbjarnason.com/2021/single-page-app-morality-play
Human friendly dates in JavaScript
Sometimes you have to show human friendly dates in your app. You know like 2 minutes ago or just now. There are plenty of libraries available to you on NPM, but it's actually pretty simple to write yourself.
https://alexwlchan.net/2020/05/human-friendly-dates-in-javascript/
Parcel 2 is released
After long time of development Parcel v2, the zero-config bundler, is finally released. I honestly don't know how it compares to other modern bundlers like Vite or Snowpack, but let's see if it catches on.
Rome will be rewritten in Rust
Talking about bundlers and tools. Rome team announced that it will be rewritten in Rust. For those of you who don't know Rome, it's a linter, compiler, bundler, and more for JavaScript, TypeScript, JSON, HTML, Markdown, and CSS. A one stop tool, but by being all that isn't it spreading yourself thin and what will the Rust-rewrite bring to the table except hopefully being a little faster?
https://rome.tools/blog/2021/09/21/rome-will-be-rewritten-in-rust
Favico.js
I was blown away by this small favicon library. It even allows you to stream your webcam as an icon. Check it out if you are building a messaging, e-commerce or real-time collaboration app.
http://lab.ejci.net/favico.js/
Markdown Editor
Cherry Markdown Editor is a Javascript Markdown editor that is lightweight and easy to extend. There are plenty of others, but I personally like this one. Check it out!
https://github.com/Tencent/cherry-markdown
Popper.js
Popper is a tiny tooltip library (3k) that Tippy.js is based on.
Flowbite
An open-source component (400+) library based on Tailwind CSS and an alternative to Tailwind UI. Paid pro version available.
Z-index tutorial
Z-index can sometimes be hard to understand. This interactive tutorial is using burgers. Be warned it can make you hungry!
https://imjignesh.com/how-to-use-css-z-index/
Dieter Ram and software design
Dieter Ram was an iconic designer. Here is how his 10 principles of good design applies to software engineering.
https://github.com/zedr/dieter-rams-10-applied-to-software
OpenAPI vs AsyncAPI
Today's software is shifting more and more towards event-driven async architectures. Even though it might not apply directly in the frontend, it will still influence the flows and design. There are two major standards today - OpenAPI and AsyncAPI. See how they compare and know the difference.
https://www.asyncapi.com/blog/openapi-vs-asyncapi-burning-questions
Tailwind CSS and SvelteKit - The Easy Way
SvelteKit is popular, Tailwind CSS is popular. Here is a short post on how to make them play nice together.
https://codechips.me/tailwindcss-sveltekit-the-easy-way/
Q: Why did the kids eat their homework?
A: Cause the teacher said it was a piece of cake!
That's it for this month. Thanks for reading.
]]>onMount
hook or explicitly turn off SSR for that page.
SSR has its use cases, but it also makes things more complicated. I couldn't resist the urge to learn more how SvelteKit deals with forms in SSR mode. Below are my findings and lessons learned.
My personal first rule of SSR: if your app doesn't need SEO, don't use SSR.
Let me unpack this.
First, if you use SSR it means the app needs some kind of server to render the pages, serverless or not. This can add some substantial server costs. It might be a luxury problem and with in your budget, but still something to keep in mind.
Second, SSR adds additional layer of complexity to your app. Say what you want, but it is a fact. Do you really need or want that?
But wouldn't app's bundle size increase if I turn off SSR? It's a valid question and the answer is - NO. Or at least that's what I think. SvelteKit is good at code splitting and will only load JS and assets when you visit the page.
You can turn off SSR for the whole app, but it's not SvelteKit's default behaviour.
Why might you want to use SSR? Back in the early days of the web when Ajax call were just invented some smart developers came up with two great concepts - progressive enhancement and graceful degradation. To me they are both equal in that way that they both accomplish the same goal. Progressive enhancement starts with the basic HTML and gradually adds capabilities to the page depending on what features browser supports. Graceful degradation is the same, but starts assuming that browser supports everything and then gracefully degrades to bare minimum of the features that the browser supports.
You might ask what that has to do with SSR? To me personally it means that if the JavaScript is broken on the page your app should still work and the users should still be able to submit the existing forms. You might think to yourself "that doesn't apply to me, I have tests," but the web is too unpredictable. Your tests is no guarantee even if they pass. Always assume the worse. I am not talking about becoming a total pessimist, but about programming defensively.
For our SSR for example we will use a simple login form, because you know, everyone can relate to one and have probably used one at some point in their life.
The example is built on top of SvelteKit's demo starter project. Go here to learn how to create one.
SIDENOTE: the code in all examples is naïve and simple. The main point is to illustrate and explain the core flows. It's happy path all the way!
<h2>Login</h2>
<div>
<form action="/login" method="post">
<div>
<label>
<span>Email</span>
<input type="text" name="email" placeholder="name@company.com" />
</label>
</div>
<div>
<label>
<span>Password</span>
<input type="password" name="password" placeholder="*******" />
</label>
</div>
<div>
<button type="submit">Login</button>
<button type="reset">Reset</button>
</div>
</form>
</div>
SvelteKit uses a file based router. Because we put the index page in the login
folder under routes
you can access it at /login
in your browser. If you submit the form the page will just reload itself because we have nothing in place to catch it. We need a SvelteKit endpoint to process the form.
Simply explained, endpoints in SvelteKit are exported functions with naming conventions. If your endpoint file exports a function with get
, post
, put
, del
names SvelteKit will map them to corresponding HTTP methods. Unlike Svelte code, they live in plain JavaScript or TypeScript files under the routes
folder.
To process the login form we only need to support the HTTP POST
method. I like to keep my code and its responsibilities co-located therefore we will create our endpoint file in the same login
folder as our login page.
export const post = async ({ request, url }) => {
const form = await request.formData();
const payload = Object.fromEntries(form);
console.log(payload);
const endpoint = new URL(`http://${url.host}/login`);
return {
headers: { Location: endpoint.toString() },
status: 302
}
}
In order to keep things simple we need to start simple. Earlier when we submitted the form there was no API handler to catch it and that's why the page just reloaded itself.
We now have an API handler that can process the form's POST request. If you look at the code you might ask what's going on and why we need a redirect?
You see, SvelteKit is not your traditional web framework like Rails or Django where the templates are tightly coupled with code. There is no way for us to return a SvelteKit page from the SvelteKit API endpoint. Because of that we have to redirect the user back to the login page.
You also need to parse the form body in your handler. It's your responsibility and SvelteKit won't do it for you.
The handler function takes a SvelteKit request as its only argument and it contains a lot of useful information we can leverage to make smart decisions. Right now host
and body
are the only ones we are interested in.
You might ask why we need to create a new URL object and not just hardcode the path? There is a good reason for it that we will talk about later.
So what does this endpoint handler do? Not much yet, but if you try to submit a form you should see the form values printed in the server console. It's a start.
If you have large hands you might hit a few wrong characters when entering the password. We should be able to notify our users if something went wrong or if the login was successful. Unfortunately there is no easy way to do it in SvelteKit because endpoints are decoupled from the pages, but there is a hack for it. We can leverage URL query parameters to our advantage.
Remember that we created an URL object in the example earlier and I told you that there was a reason for it? If you look at the code below you will understand why.
export const post = async ({ request, url }) => {
const form = await request.formData();
const payload = Object.fromEntries(form);
const endpoint = new URL(`http://${url.host}/login`);
if (
payload.username == "jane@example.com"
&& payload.password == "qwerty"
) {
endpoint.searchParams.append('success', true);
} else {
endpoint.searchParams.append('error', 'Wrong credentials')
}
return {
headers: { Location: endpoint.toString() },
status: 302
}
}
We are leveraging URL object's search params property to augment our redirect URL with useful information.
You next question is probably how to present this information to our end users. We can use SvelteKit's load
function to parse that information on the serverside and pass it to our login page instance.
Load function is central when dealing with SSR in SvelteKit and has many different use cases. If our example we are only interested in the query parameters.
<script context="module">
export async function load({ url }) {
const query = url.searchParams || {};
if (query.has('error')) {
return {
props: {
error: query.get('error')
}
};
}
if (query.has('success')) {
return {
props: {
success: query.get('success') === 'true'
}
};
}
// you have to return an empty object in case none
// of the if statements match
return {};
}
</script>
<script>
export let error = undefined;
export let success = false;
</script>
<h2>Login</h2>
<div>
{#if success}
<p class="success">You are successfuly logged in</p>
{/if}
{#if error}
<p class="error">{error}</p>
{/if}
<form action="/login" method="post">
...
</form>
</div>
The code shouldn't be too complex to understand. The important thing is that our module props
name we return have matching exported variables in page's script block. In our case it's success
and error
.
One important thing to remember is to always return something back in the load function. If all of our if
statements fall through and we don't return an empty object you will get a blank page back.
We have improved our flow and added meaningful user feedback, but I don't feel that comfortable with the solution. If you get an error page back you can change the message by tampering with the query parameters. This is not a big problem in our case, but what if we have logic that depends on boolean variables, like success
? If you get this wrong it can lead to unwanted concequences.
It would be much better if you could pass an object to your page internally instead of relying on query parameters. I am not the only one who wants this.
Hey! What year is this? 1997? SSR forms? Seriously? Who submits their forms via plain HTTP today, right? Yes yes. I get it. We have come a long way and I think that most of us expect the page not to reload when we submit a form.
Hopefully by now you understand how SSR forms in SvelteKit work. Let's fast forward to today and talk about clientside form submits. SvelteKit starter demo has a nice example of the todo list that we can get some inspiration from. It uses Svelte action to enhance the form. We can use that example as base to add progressive enhancement to the login form.
export function enhance(form, { done, error }) {
const onSubmit = async (e) => {
e.preventDefault();
const payload = new FormData(form);
try {
const res = await fetch(form.action, {
method: form.method,
headers: {
accept: 'application/json'
},
body: JSON.stringify(Object.fromEntries(payload))
});
if (res.ok) {
// get response payload
const resp = await res.json();
// pass response to done handler
done(resp, form);
} else {
const body = await res.text();
console.log(res.status); // 401
console.log(res.statusText); // Unauthorized
// return error as error object
error(new Error(body), form);
}
} catch (err) {
console.log(err);
error(err, form);
}
};
form.addEventListener('submit', onSubmit);
return {
destroy() {
form.removeEventListener('submit', onSubmit);
}
};
}
The action takes two functions as arguments - done
and error
. When the user submits the form we serialize the form as JSON and submit it to our API endpoint with the URL and method from the form object.
When the response comes back we check if it was successful or not and pass the result to the handler function, that were passed as action arguments, depending on the outcome. I am not sure what the logic for res.ok
flag is, but my guess it's all HTTP 2XX statuses.
I don't know about you, but I find the enhance
action pretty elegant. It can also be used with any form. In my opinion this is a perfect example of the power of Svelte actions.
The code example below illustrates how we can incorporate enhance
function into our example.
<script context="module">
export async function load({ url }) {
const query = url.searchParams || {};
if (query.has('error')) {
return {
props: {
error: query.get('error')
}
};
}
if (query.has('success')) {
return {
props: {
success: query.get('success') === 'true'
}
};
}
// you have to return an empty object in case none
// of the if statements match
return {};
}
</script>
<script>
import { enhance } from './_utils';
export let error = undefined;
export let success = false;
const onDone = (resp, form) => {
console.log(resp);
success = resp.success;
error = undefined;
form.reset();
};
const onError = (err, form) => {
// we return error as JSON from the endpoint
// so we can try to parse it here
const text = JSON.parse(err.message).message;
error = text;
success = false;
form.reset();
};
const reset = () => {
error = undefined;
success = false;
};
</script>
<h2>Login</h2>
<div>
{#if success}
<p class="success">You are successfuly logged in</p>
{/if}
{#if error}
<p class="error">{error}</p>
{/if}
<form
action="/login"
method="post"
use:enhance={{ done: onDone, error: onError }}>
...
</form>
</div>
Even though we can now do client side form submission it won't work because we need to handle JSON payloads in our API endpoint. Something that we need to fix next.
API endpoints in SvelteKit have no opinion on how you want to deal with different types of payloads. The information in request will help you make the decision, but the rest is up to you.
Because our API endpoint has to deal with both JSON and form-encoded payloads we need to have logic in place to see what type of request we are dealing with. The simplest way is to create a helper function to check the headers if it's a JSON request and then return back an object. SvelteKit will serialize the response to JSON for us if we return an object or array in the response body.
const isJSON = (req) =>
req.headers.get('accept') == 'application/json';
const email = 'jane@example.com';
const password = 'qwerty';
/**
* @type {import('@sveltejs/kit').RequestHandler}
*/
export const post = async ({ request, url }) => {
// check if request is JSON
if (isJSON(request)) {
const data = await request.json();
if (data.email == email && data.password == password) {
return {
body: { success: true }
};
} else {
// You can also throw an error here which will result in 500 error
// throw new Error("Something went bonks")
return {
status: 401,
body: { message: 'Unauthorized' }
};
}
}
const form = await request.formData();
const payload = Object.fromEntries(form);
// construct new redirect url
const endpoint = new URL(`http://${url.host}/login`);
if (payload.email == email && payload.password == password) {
// add query params
endpoint.searchParams.append('success', 'true');
} else {
endpoint.searchParams.append('error', 'Please check your credentials');
// You can also return HTTP error here instead
// return {
// status: 401,
// body: 'Wrong credentials'
// }
}
// redirect back to /login page
return {
headers: { Location: endpoint.toString() },
status: 302
};
};
We have some if
branching in our function body to check if we are dealing with JSON and do an early return if this is the case. Otherwise we treat all other requests and plain HTTP requests.
I was hoping that SvelteKit would deserialize JSON body for us automatically, but it doesn't look that way. We have to do it ourselves. If something blows up SvelteKit will respond with HTTP 500 Internal Error
so make sure to wrap your code in try/catch blocks and handle the errors yourself instead.
If you try to submit the form now it should work and if you disable JavaScript and submit the form it should also work. Mission accomplished!
After you take a deeper look at SvelteKit's SSR forms they don't seem that hard, but the current documentation is thin and the examples are scarce.
SvelteKit is not your "traditional" web framework, but it's your "transitional" framework, right? Things might not work like you expect them to and there are also some features that I personally miss. For example, it would be nice to add isJSON
property to the request object and deserialize the payload on the core level so we don't have to parse JSON manually. Maybe there is a good reason for it or maybe the functionality is just not in place yet. Time will tell!
If you are interested in learning more about how to work with forms in Svelte make sure to check out my book - Svelte Forms: The Missing Manual. It's packed with examples and I guarantee that they all work in SvelteKit.
]]>I have always been active, a sports nerd. For the past 10 years I've been practicing BJJ (Brazilian jiu-jitsu), a type of martial arts that is used a lot in UFC. It's a fantastic and fun sport, but it also comes together with injuries. A year ago I accidentally twisted my knee during practice and it has been troubling me ever since. I've done my fair share of rehab, but I've also carried some extra body weight that I felt caused unnecessary strain on my knee. I felt like losing some weight might help a bit.
Sometimes at the end of summer I spoke to a friend who tried this weight loss method called Keto yielding great results. I've heard of it before, but never felt like I needed to try it myself as I never had any real weight issues. However, inspired by his success story and excitement I decided to try it for three months with a goal of losing 10 kgs. If I reached the weight goal sooner I would stop and resume my normal life.
I honestly thought I was going to crush my goal two months in because of the level of my progress, but it actually took me two and a half months to reach it and I continued for the whole three month period because I felt I was on a roll.
During the whole period I also got much less physical exercise than I usually do, maybe like 1/5 of my normal level. That's because I pulled my hamstring pretty bad at the end of the summer when wakeboarding.
Looking back I went from 92 kgs to 82 kgs without breaking any sweat.
The weight loss method in itself is deceivingly simple. Drop all carbs from you diet and increase everything else. That means protein, fat and vegetables. Go especially heavy on the fat.
Most people are afraid of fat. They think that if you eat fat you become fat. That is partially true. If you eat a lot of carbs and sugar and also add fat to your diet it will be stored in your body, but if you skip carbs fat will be used as energy.
The "drop all carbs" part of this diet is incredibly hard because when you start looking the carbs are everywhere and in very large amounts too. According to the Keto diet you are only allowed to eat at most 20 g of carbs a day.
You are not allowed to eat starch (pasta, bread, cereal, potato, corn, beans, legumes, beer and most vegetables that grow below ground) and sugar (candy, milk, fruit and half of the things you normally buy at your local grocery store).
Doesn't sound so hard in theory, but really hard in practice. Carbs are everywhere!
I started my experiment on September 1st and decided that my last day would be Black Friday, which falls on November 26 this year.
First two weeks were incredibly hard. Not in terms of eating, but more of knowing what to eat. Finding inspiring recipes, doing grocery shopping, cooking. Not only that, I often had to cook separate meals for myself as the rest of the family were not onboard with my little experiment and wanted to eat normal meals.
During the first two weeks I lost 2 kgs. I was impressed! That's a lot! From my research it mostly wasn't fat, but water. One gram of carbohydrates binds four grams of water inside your body and as you deplete your body's carb storage the water exits your body as well.
There is also a well-known concept called Keto fever, a state when your body enters ketosis, meaning it switches from carbs to fat as the primary source of energy. I honestly thought it was a fad as nothing happened to me. Or so I thought. It hit me on Day 5. I woke up feeling weak and drowsy and with a terrible headache. Took some painkillers that made me feel a little better. It was gone the next day.
Keto is also more effective when paired with intermittent fasting. I've been doing intermittent fasting for years, the 16/8 method. I am not strict about it. I just skip breakfast as I am never hungry when I wake up. It has served me well, but I am not sure if it has helped me lose weight. Maybe more like it has helped me not gain any.
One thing that struck me is that Keto diet is boring af. Sure, it's fun to eat large stakes, vegetables and fat sauces in the beginning, but that fun lasts for about a week. After that it becomes very one-sided and blunt. You have to try and find food and recipes not that you like, but that you like a bit more than you dislike. Also you have to use a lot of spices to make everything more interesting.
A little more than one month into my experiment my weight loss continued, maybe not at the same pace as in the beginning, but it was still steady. By that time I was down 5 kgs. I was also a little worried that I was losing weight a little too fast. Normal weight loss progress is around 1/2 kg a week and I was losing around 1 kg a week. However, the fast progress also made me confident that I will crush my weight goal way ahead of the deadline.
Many people around me were curious when I told them about my experiment. Two of the common recurring questions were:
Don't you feel tired all the time? Do you get anything done during the day?
On the contrary! I was full of energy and my blood sugar levels were super stable. I didn't have any energy dips at all, the ones you can get after eating a large plate of pasta for lunch.
I am not a doctor, but I think the reason for that is stable insulin levels. Insulin is the hormone your body produces to decrease and regulate your blood sugar levels when you eat carbs or sugar. I didn't have that problem as I didn't consume any of that.
Beside my stable insulin levels my digestive system has never felt better. During my experiment not once did I have a bloated stomach, the kind you can get when you eat lots of carbs.
Another benefit of the diet is that I totally lost my sugar and fast food cravings. I am a real sugar junkie and I also wouldn't say no to a big fat juicy burger, but for some reason I didn't crave that anymore.
As I said earlier I thought that I would crush my goal in two months, but my weight loss actually slowed down. I was still losing weight, but not at the same pace as in the beginning. I think there were a few reasons for that. First, I lost a lot of weight fast and maybe there wasn't that much more fat to lose. Second, I was less strict with what I ate, because honestly, the whole thing was getting pretty boring.
I got a little worried that I might even miss my goal so I got myself back into the groove by eating more strictly again. I also doubled down my efforts on physical exercise by taking a least 40 minutes walks in the morning, often on my way to work. I didn't do it every day, more like 3-4 times a week.
It paid off. Today I weighted myself for the last time. 82.1 kg.
As a bonus I also got my six pack back! I will spare you the selfie. I am not that kind.
I celebrated the whole thing with a few homemade pizzas together with my family. At last! Eating pizza again was good, but honestly not as good as I remember it.
Below is a collection of personal advice and tips that worked for me if you want to try this yourself.
If you decide that you want to give Keto a try it's important to set an end date for the experiment. Don't choose too short period. I would say give it a minimum of two months. You can also set a weight goal, but that can be hard to reach. It's better to set a date and see how much weight you can lose during that period.
Keto is the most strict of the diets and can be challenging to follow fully. If you want to be less strict you might want to give LCHF, its little brother, a try. It's basically the same thing, only it doesn't force you to remove all carbs from your diet.
I find it personally easier to go all in, cold-turkey. You might want to start with LCHF and gradually transition to Keto.
Don't be afraid of fat. Eat more of it as it will make you feel full. If you leave the table and still feel hungry it's probably a sign that you didn't eat enough of fat. Don't worry about cholesterol, but try to stay away from vegetable seed oils as I understand they are not very good for you. Eat a lot of nuts and avocados.
Concentrate on what you eat instead of how much you eat. Concentrate on eating the good food and avoiding eating bad food. Search around. There is a lot of information on the Internet, but take it with a grain of salt.
Going Keto basically means reducing your food palette. It can get boring pretty quickly and feel like you are eating the same food over and over again. That's why it's important to find food and meals that you actually enjoy. For example, my friend eats a lot of eggs, I eat a lot of fat yoghurt with hazelnuts and a little honey. Find the spices and sauces you like, for example Tabasco or Sriracha. Also use a lot of mayonnaise, butter and olive oil to bring out and enhance the flavours.
The Keto community is desperately trying to come up with good substitutes for normal food like bread, pasta, pizza and deserts. You will find a lot of weird recipes and I urge you to stay away from them. I tried a few of those and it was actually as bad as I expected.
Try to be strict, but at the same time don't be dogmatic about it. If you are eating out with friends try to choose something that is least bad or just make an exception and eat what you feel like eating that very moment. I've done it several times. It didn't affect my progress and made me enjoy the moment instead. Allow you to eat some fruits if you feel like it or even a small scoop of ice cream, if you can't resist the urge. Don't feel bad about it. After all, you are a human, not a machine. For example, I ate a lot of dark chocolate during the weekends and enjoyed some red wine and fine spirits.
Remember, nothing happens overnight, so don't expect any fast miracles (pun intended). Everyone is different. Just because I saw fast progress doesn't mean you will too. The important thing is not to get discouraged and give up too early. Trust the process and trust yourself.
Make a habit of weighing yourself every second week. I usually did it on Sunday mornings. If you weight yourself every day you will easily get discouraged because your weight can fluctuate a lot from day to day.
A long walk is the best weight loss exercise. Not only does it help you clear your head and get the blood flowing, but it also helps you burn calories. Make a habit to take a least 40 min long walks, preferably in the morning. Make it into a ritual and I promise you will notice a big difference in a few weeks. Put on a good podcast if it makes it easier or just let your mind roam free and just observe what pops up.
It's interesting how the Keto diet forces you to become an amateur nutrition expert as a side effect. I've become an expert on reading nutrition facts on the grocery products, something that I almost never cared about before.
It's also scary to learn how much added sugar our everyday food contains and it's really hard to find something that actually good for you. No wonder why most restaurants normally serve a lot of carbs, because carbs are cheap. Good and healthy food costs, but it's worth paying for.
It's clear what you eat matters much more than how much you exercise, but just because of that fact don't neglect the exercise. See it as a booster.
Personally, this short experiment was successful for me, but I honestly feel that Keto diet is not sustainable in the long run. During it I ate a lot of meat and especially red meat. Although it was effective I don't think it's good for me or for the environment.
Will I do it again? I don't know, but one thing is sure, I am never going back to my pre-Keto way of eating. I now know too much of what good and healthy food means for me, my body and my well-being, even if it comes with a price tag.
]]>It's the concept I call - the delayed fuse.
I actually got the name and the idea from a movie The Wolf of Wall Street. In the movie there is a scene where main characters take rare, but expired drugs aaaaaand nothing happens. Anticlimax.
https://www.youtube.com/watch?v=aicfFxaYVyM
Nothing happens until it actually happens and when it happens, it happens big.
This is the mental model in a nutshell, but let's expand it a bit.
For me personally it's a concept, a mental model, and a framework that has three parts to it: the fuse, the blast and the shockwave.
Let's first talk about good ol' dynamite. You know, the kind you see in old movies where a couple of villains want to blow up a bank vault and take all the money. In those movies there is usually a scene where the fuse is lit and people run for cover not to be affected by the blast.
This is important because the length of the fuse is your safety margin.
There are different kind of fuses. Some burn fast and some burn slow. This knowledge allows you to plan. Controlling the length of the fuse gives you enough time to get to a safe space.
This concept is also true for hand grenades and traditional OTC fireworks and I bet you have seen a couple of clips on Youtube where things don't go as planned.
https://www.youtube.com/watch?v=eIUiYtXHusc
But what does that have to do with anything? It has to do with action, reaction and time. The time between action and reaction is the delay.
The gist of this is that nothing happens overnight, and you probably know that, but knowing it doesn't help you worry less or to get less discouraged when you launch something (product, newsletter, course, saas) aaaaand nothing happens. It's hard to hide the disappointment.
But thinking in terms of the delayed fuse helps you plan and also control the impact, which leads me into the second part of the concept - the blast.
After the fuse has burned out there is a reaction, a blast. This is your launch. However, before the blast, while the fuse is still burning, you can have plenty of time. During that time your goal is to gather as many people as possible and tell them that things are about to blow, or if it's fireworks, to enjoy the show.
This is you building your audience. How do you do that? By being helpful, by sharing your knowledge and good vibes.
Things are easier if you already have an audience around you, just let them know. But if you don't, you have to work hard for it and prove your worth first. It's not hard, but it takes time. Just be helpful, share your knowledge and tips with people around you.
The time factor is important and it's the length of the fuse and its burn rate that controls it. Don't choose a fuse too short and don't pick one has fast burn rate if you don't have any audience yet. Let things take time.
It's equally important not to choose a fuse that burns for too long. If you do, people will get tired of waiting for the show. You need to find a balance here.
Another positive thing of choosing a slightly longer fuse is that it will allow you to safely extinguish it and prevent the blast. That is if you fail to attract an audience.
How ever you now choose to do it you must let your audience know. The man in the weapon shop in the video above lit the fuse, but failed to notify his audience. The blast sure was impactful, but not so much the reaction of his audience. This is what happens when you don't warn your audience and just drop the bomb on them.
The situation is the equivalent of locking yourself in the room working on your next big thing with hopes that the day you are finally done you will instantly become an overnight success only by just letting the world know about it once. The world just doesn't work that way. Sorry.
If you did everything right you hopefully have gathered a few people around you to enjoy the show. Things are about to blast. The impact, the blast radius and how far the shockwave will travel, mainly depend on the number of people you managed to gather around you at that very moment.
If you set off a firecraker it will go poof and not KABOOM. If you have many people in your audience some of them might still get impressed and excited, but most probable is that many of them will instead just say meh, go home and not think about it anymore.
But if your audience is small and you set off a Tsar Bomba instead of a tiny firecracker, I am sure it will be something for them to remember and talk about. Both the blast radius and the shockwave will be huge. The sound and the light will reach people living far far away. The shockwave, in this context, is other people talking about the launch and the product and that way helping you spread your message.
The two examples are extreme opposites of each other. Both can create a sizeable impact, but if you try to maximize both factors the impact will have the biggest effect.
Small blast + Large audience = Impact
Big blast + Small audience = Impact
Your goal should be to attract as many people as possible by telling them about the awesome show that is about to happen and to get them curious.
The takeaway is that almost nothing happens overnight. There is always a delay.
You lit the fuse, nothing happens, nothing happens, and then KABOOM!
Understanding the delayed fuse concept can help you cope with stress and worry, because you now know that delays are natural. It can also help you plan and control the impact - the blast radius and the shockwave.
But if you don't light the fuse nothing will ever happen.
]]>Thought I might as well write it down in case I forget it. Here are the instructions.
I normally use pnpm
, but the commands for npm
should be the same.
$ pnpm create vite
Follow the instructions and choose Svelte
template. TypeScript or plain, doesn't really matter.
Next step is to add Tailwind CSS and then initialize it.
$ pnpm add -D tailwindcss autoprefixer postcss-load-config
$ pnpx tailwindcss init tailwind.config.cjs
Make sure to include the correct file glob patterns in the content
property of the Tailwind CSS configuration file.
module.exports = {
content: [
'./index.html',
'./src/**/*.svelte'
],
theme: {
extend: {},
},
plugins: [],
}
Vite has built-in support for PostCSS, but we still need to create PostCSS config and add Tailwind to it.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
You also might want to create a main CSS file in case you plan to support re-usable styles in your application.
@tailwind base;
@tailwind components;
@tailwind utilities;
Voila! We now have a minimal working Tailwind setup. Let's put it to use.
You don't need any svelte.config.js
file with svelte-prerocess
nonsense, @sveltejs/vite-plugin-svelte got you covered here.
<script>
import "./index.css";
</script>
<h1 class="text-4xl font-extrabold">Yo!</h1>
<p class="meta">This is some meta text</p>
<style lang="postcss">
.meta {
@apply mt-4 text-gray-600;
}
</style>
That's all there is to it. Happy hacking!
]]>Your hobbies often cost money, but your interests can help you earn money.
Want to know how? Read on to learn how you can find and leverage your interests.
Your skills is something you currently get paid for. It's what you are good at. It's your job.
You might not like doing what you are doing, or you might do, but your skills put food on the table.
Skills is a compound of your previous interests, formal or informal education and serendipity. They are you.
All the previous choices you have made and the experiences you have had make up your current skill set.
Most of the knowledge workers sell their skills in the form of knowledge. They sell their expertise in form of time in exchange for money.
Depending on your skill set you can get paid a lot or a little, but you are most likely selling your time for money. A salary.
Skills are not your interests. But they might have been.
You hobbies is something you don't get paid for. If you get paid for them they are not hobbies. It's a job.
Hobbies often cost money. Travelling is a hobby, so is gardening or working out. Maybe your hobby is photography, cross-stitching or hiking. Maybe it's your dog.
Hobbies don't have to cost much but they often do. And they are worth spending money on.
Hobbies is something you do during your free time and you do it because it's fun. A hobby's main purpose is usually relaxation, meditation or pure enjoyment. Your hobbies help you forget all your worries and allow you to fully immerse yourself in the now. If only for a short moment.
You often use the money generated from your skills to sponsor your hobbies. Your hobbies are important for your well-being. You should try to keep them alive and set aside time for them regularly.
Hobbies are not your interests.
So what are interests if they are not your hobbies? In short, they are the seeds of your new skills. Skills yet to be learned. A seed that can grow into a beautiful tree that bears many fruits in the future.
Let me explain. The diagram below helps visualize the concept.
Here is the thing. Your interests don't have to be related to your current skill set, but they should help you expand it.
The interests are often found at the intersections. It's the missing skill. If you invest time and energy in a newly found interest you will expand your skills' circle after a while. Growth.
Interests have nothing to do with your passion or your purpose. You don't have to know your purpose. That's a much deeper question that very few people know the answer to.
Interests is you, learning.
But how do you find your interests?
There are a few questions you can ask yourself that can help identify your interests:
Spend time on them. Look for patterns.
Also, think about activities that make you lose track of time. That usually means you enjoy them.
Now that you’ve come up with things you’re interested in, choose the one that you feel most excited about right now. Don't worry, you can always come back to your other interests later.
Don't underestimate the power of the unconscious mind. There is energy waiting to be released.
Strike while the iron is hot.
I said in the beginning that you can make money from your interests. That is only if you invest time and energy in them.
But don't worry about right now. That comes later. First thing you need to do is to get started.
Let your enthusiasm and curiosity be the primary driving factors, and the journey be the main focus.
The journey itself will help you generate ideas for how you can make money. Just keep your eyes open.
The C.L.E.A.R. is the perfect protocol to base your interest journey on (not sure where I heard it first).
Let's break it down.
Curiosity
Your interest starts with your curiosity and fascination. Something that captures your attention. Do you research and find as much information on it as possible. Books, articles, videos, people, forums. Overwhelm yourself first, then try to distill everything to its essence.
Learning
Start learning and experimenting so you can get a better understanding of it. Do small projects and experiments and document your findings, both good and bad, in public. This is important!
Enthusiasm
First steps can be hard and frustrating, but don't give up! Let it take time. Remember, nobody is instantly good at something. Chances are the more you learn the more enthusiastic you get. Mastering a new skill provides satisfaction. Continue to share your learnings and your enthusiasm. Rant!
Awareness
As you become better and more skilled you will become aware yourself that you are getting pretty good at this. You start seeing opportunities and people around will start to interact with you. They will become aware of you. This will only happen if you continue sharing what you learn. Your journey.
Recognition
As you continue your journey and document it people will start giving you recognition. They will start seeing you as an expert.
Everything is clear, right? Good!
This is not some magic formula. Many people follow this protocol.
I did and it worked for me. My primary interest for that past year was Svelte web framework. I started from zero and by following the C.L.E.A.R. protocol I wrote a book, got consulting gigs and conference talks offers. All this from curiosity driven by interests.
You have to invest time in your interests. Continuity. If you don't, you might lose interest (pun intended). Setting aside time is crucial. You can either follow the Seinfeld (don't break the chain) method or decide on the days when you dedicate a big chunk of time to them, like on weekends.
The more you do the faster you will master it. There are no shortcuts. Sorry. You have to put in the time.
Your journey will be full of pitfalls and frustrations, but don't let that discourage you. Nothing is easy. Keep pursuing your interests and work through the hardships so you can learn, improve and get better. Everything takes time and dedication. Make sure you do the work and use your failures as learning experiences going forward.
There will be a few pitfalls as you get deeper into your passion, but don’t let those discourage you. Even if you aren’t naturally good at something you’re passionate about, keep pursuing them and working through the hardships so you can improve and get better. Passions take time and dedication, so make sure you spend time doing the work and use any failures as learning experiences going forward.
While it’s important to persevere, it’s also important to recognize when to stop pursuing something. If you aren’t getting the same level of enjoyment that you were before, then it may be time to move onto something different.
Just as it’s important to persevere, it’s important to recognize when to stop. If you aren’t getting the same level of enjoyment or satisfaction, if it becomes a chore or you feel that you have learned enough, it might be time to move onto something different, more exciting.
After all, you are more skilled now than before you started. You just turned your interest into a new skill.
Now go and use it.
P.S. If you need help getting started or need someone to hold you accountable shoot me an email. I have an interesting working theory and an awesome playbook that I would like to validate, so the help will be mutual.
]]>But the elephant does nothing. Instead, it quietly stands still chained to the pole.
Why?
Elephant's past that is holding it back. You see, when the elephant was just a baby it got chained by its foot to the very same pole. Of course the elephant didn't like it and tried to break free. However, it was still to small and too weak to break free. But the elephant didn't give up on the first try. It tried over and over again.
Every time the elephant tried to break free, the chain that was tied to its foot started to dig into its flesh. It quickly created a painful wound. As the time went by every new attempt to break free started to feel.
In very short period of time the elephant internalized that its escape attempts are associated with pain. It eventually gave up and accepted its destiny, being chained to the pole its whole life.
I don't remember where I first heard the story but it really resonated with me.
Hope it did with you too.
Don't let your past chain you down, my friend.
Below are my three favorite ones. Try them out and see if they make your life a little better.
The most useful principle and the one I live by the most. When faced with an unforeseen problem I just deal with it. No need to dwell on the past or its cause. Sure, I might have had other plans, we all do, but things normally don't go according to plans. Life is too unpredictable and it also what makes it exciting.
Save yourself the frustration and just deal with the problem in front of you. This simple mindset will help you channel your energy in the right direction. Dwelling on the past and the reason is like ramping up your can engine while it's in neutral. You will only waste fuel. Take action to move forward instead.
I also find it important to ask WHAT and not WHY question in situations like these. Sure, it might be important to ask why but only if the answer helps you to know the what. Remember, you cannot change something that already happened, you can only deal with things in the present and future moments.
A great tool for anger management. We all face tough situations every now and then. Our problems always seem so big. It's easy to get caught up in anger or frustration.
But if you zoom out far enough and look at the situation from a higher perspective it will become small. Often almost trivial.
Take a couple of minutes to do this exercise. See the situation a fem meters from above, then zoom out more and more until Earth becomes as small as a grain of sand.
It's not until you zoom out and look at it from above you realize how small your problem really is. You realize how little time you have and that it's not worth to spend it on small things.
To put things in perpective take a look the short movie - Powers of Ten.
https://www.youtube.com/watch?v=0fKBhvDjuy0
This visualization can be done as short daily meditation. Take five minutes of your daily commute or when waiting in line to contemplate.
Assume the worst. The death of your loved ones. Your own death. Losing your job or your house. This visualization can be very challenging to practice, but it will prepare you for the worst. Not only that, it will also make you appreciate things you already have in your life and to not take them for granted.
Don't practice this before falling asleep. Negative visualization practice is powerful and can be very uncomfortable. It can keep you awake long into the night.
Every now and then, before falling asleep, think about all the people and things you have in your life. Roof over your head, food on the table, cloth to wear, maybe health or loving partner. Children. Your pet. Your job.
It will help you realize that being rich is often not about money and possessions and that you are already rich. That you have all the things you need.
And we don't need much. Honestly.
]]>I drove to our summer house in the middle of the winter. The house was cold. It was almost as cold inside as it was outside. I had to quickly get warm to stop the shivering.
I decided to make a fire.
Fire has always fascinated people. Without fire we would still eat raw meat. Without fire we would not have iron and technological advances. After water, fire is the most essential necessity to human existence.
It took me a good while to get the fire going. House was cold. Fireplace was cold. Wood was cold and wet. It's been covered in snow.
After a few failed attempts and lots of wasted old newspapers the warmth finally start spreading around the room. The steam from my breath finally disappeared and I could finally see from the windows as they started to defrost. I started to defrost.
I sat on the floor next to the fireplace mesmerized by the flames and sparking wood and thoughts carried me away.
I thought about the skill of starting a fire. It's a skill that you have to learn. Just like with everything, the more fires you make the better you get.
Heck, it's not even a skill. It's an art. It's more art than science even though some scientific principles still apply.
Then I started to think on what it takes to make a fire and fire in general. Below are my notes.
Dry wood vs wet wood. Wet wood requires a lot of energy to start burning. Dry wood ignites easily. Even without paper.
Starters. You often need something highly flammable to start the fire. Paper or candle wax. Tree bark burns really well. You can even use gasoline but that can be dangerous.
Arrangement. For the fire to start you need to arrange the wood carefully. It cannot be too dense. You need to leave plenty of space for the air to enter. You also have to place paper strategically in the right places. At the bottom.
First failure. I often fail to light the fire on the first try. Maybe the wood is cold or a little wet. Second time is usually easier as the wood might have dried a little or got a little warmer.
Material. You can use many things for your fire but there is a reason most people use wood. It's cheap and it's been tested by time.
Professionals. Some people are pros when it comes to fires. Scouts, hunters, lumberjacks, hikers. They make it looks so easy. They know exactly what to do. But they also had to start somewhere. There are many fires ahead of me and you.
Campfire. When it comes to wood arrangement there are a few different ways to do it. Everyone has their own preference and it's usually they way they were taught. Some wood arrangements have advantages of lasting longer. For example, there is a special camp file arrangement where you put the biggest logs at the bottom and then build you way up by size with the smallest pieces on top. Then you light the fire at the top and it works itself down.
Learning. If you want to know how to make fire, you have to find someone who can teach you. This is the way it works. You don't learn this stuff in school. Someone shows you once and then is mostly trial and error until you get the hang of it.
Small adjustment. Sometimes a slight adjustment of on piece of wood will kick off a dying fire or kill it. It's especially true in the beginning.
Spark. Every fire needs some initial energy, a spark of some kind. You have to start it yourself (action). Sure, you can wait for the lightning to strike, but I wouldn't take that chance.
Size. Only a fool would use large blocks of wood to try to start a fire. First, you have to find dry wood and they you have to chop it into thinner and smaller pieces. Smaller pieces require less energy to start burning. Only after you see that the wood is burning, and has burned for a while, it's safe to add larger pieces.
Density. Wood density matters. Heavier wood makes fire last longer as there is more energy in it.
Illusion. If you use a lot of paper you can create an illusion that the wood is burning. But it's can be deceiving. The fire dies as soon as the paper has burned down. I can't count the times I've been burned by this. There are no guarantees.
Purpose. What is the purpose of your fire? To keep you warm? Because it's cozy? Maybe you want to cook something? Just for fun? Or maybe you need to get the sauna warm?
Intensity. Fire burns most intensely in the beginning.
Keep it burning. You have to add more wood if you want to keep the fire alive.
Energy storage. You can store fire's energy to heat up the space. There are tiled stoves that radiate heat even after the fire stops burning.
Cheats. If you only have wet wood you can cheat and use gasoline as a starter. But it's dangerous. Gasoline is highly flammable and explosive.
Time. It takes time to learn how to make good fire on the first try. You can still fail even if you have done it 1000 times before. Conditions are different each time.
Modern fire. It's never been easier to start a fire. Today you have matches, lighters and other help. It's easy to take them for granted but they are actually not that old. A match was invented in 1826.
Practice. Don't be so hard on yourself when you fail. Practice makes perfect. Even if I think I am pretty good at starting a fire I've met people that are much better. And they all do it differently.
Now. "What does that have to do with anything?" you might ask.
Go back and re-read everything but this time replace fire with your business, company or product.
]]>Sometimes I go for a late evening walk to wind down. When it's dark and silent outside. It makes you see the same road that you walked during the morning in totally new light. The street lights, or if you are lucky, the moon.
Luckily, I live in the city surrounded by water and lots of green areas. City is where I do all my walking. I love to walk in the woods but unfortunately it's hard to find them nearby (walking distance).
Cities are actually great to walk in and can be a very interesting experience. Every time I travel to a new city I make sure to go for a walk. Alone. Preferably in the evening, when it's dark outside. It might seem weird but I love to see the lights from the street lights. Neon signs on the buildings. Car lights. Random people passing by. Couples in love. And the silence of the evening. There is something magical about it.
Usually I walk without any destination in mind. I walk just for the sake of walking. Maybe you could call me a flâneur.
However, back home I have a few different static routes to chose from. I know how long time each one takes and I can decide on the which one to take depending on how much time I have.
I know these routes by heart. They are well prepped, asphalted roads specifically made for people who like to walk or jog. People like me and you. People who do it because it's what everyone says you should do. It's good for your body and health.
However, I don't do it for the sake of my health. I do it for the sake of walking.
And if you do it right it can be a super interesting and pleasant experience.
But what is right?
When I walk I see many people either run or walk fast. 80% of people have headphones on (with probably some interesting podcast on) or they walk and talk on the phone.
(Some people also run and talk on the phone. I will never understand it.)
When you have your headphones plugged in you literally try to shut off your surroundings. Why? Are you trying to maximize your life?
If you do one thing do one thing only, not two.
Walking is thinking. Walking is observing the world. Walking is enjoying life.
I've come to discover that there are actually two types of walking - automatic and exploratory.
Automatic is when you go for a walk on a common route that you know by heart. It's a great way to engage in thinking activity. Because you know the route you can just put your feet on autopilot and engage your brain instead.
Actually, you don't need to engage your brain that much if you don't want to. All you need to do is to think about something hard enough before you go for a walk. During the walk you can outsource the problem to your unconscious self and just observe the thoughts as they pop up in your head. Maybe something useful will pop up maybe not. From my experience it usually does.
Second mode of walking is exploratory. This is my favorite mode of walking. It's when you deviate from the main road and walk on some road you never walked before. I try to do it often because it's fun.
This type of walking is very active. You are forced to be present, to be aware. You have to make decisions. You have to scan your surroundings because you are in the unknown territory. You have to be in the now.
It's also beautiful. You see new things. You might discover something interesting. You might learn something. You explore your surroundings.
How do you do it?
When you walk on the road just turn left or right to some other connecting road. It's the road you never walked before but it's OK. You approximately know your main direction anyway. Now you just have to figure out how to get there and see where you end up.
Most roads that connect to the main road are smaller but still well maintained. Not that many people walk there but still many do. If the road is there it must lead somewhere, right? Just take it.
The road you took might have a fork in it. It's up to you decide if you should take left or right. You still have the approximate sense of direction in your head. Use your best judgment and take a chance.
It's most likely that the road you chose at the fork might be even smaller and less maintained, but still it's a road and you can see that others have walked there before you. So it's safe to assume that you are not a trailblazer.
Maybe, after walking for some time, you discover that that road comes to a dead end. (It happened to me a couple of times.) That's not the end of the world. You can always go back the way you came to the main road and take the path most traveled. The safe path. The path everyone walks.
But there is a big chance that if you carefully look around, standing there at the dead end, you might notice a small path leading into the woods or a small fence that you can climb over. You don't know where this new path leads but it's still an option, right? It's now up to you decide if you want to continue your adventure or turn back.
There is no shame in turning back. After all, you just lost some time and energy. But if you turn back will you not regret that you never took the road less traveled? That tiny, bare visible path?
If there is a path it means that someone has walked if before you. Not many but some.
Where does that path lead?
Aren't you curious to find out?
Remember, you can always turn back.
Heck, you might just substitute the word walk with work in this article.
]]>Let's first define what terms micro-chore and micro-interaction mean to me.
A micro-chore is an action, or physical task, that can take anything between 2 seconds to 15 minutes to do. A chore is something you don't enjoy doing. It's called a chore for a reason. Because doing them is usually a low value activity. It's your $10/hour work with no leverage.
Doing dishes, folding laundry, closing a cabinet or a door, picking something up from the floor, taking out the garbage. All those are examples of chores that add little value to your life but that you still need to do.
I used to get extra annoyed when someone else failed to do something expected of them. It could be trivial things. For example when someone in my family didn't close the kitchen cabinet or failed to put their dirty plate in the dishwasher after a meal.
But then I realized that I was spending more energy being annoyed that it would take me to do the damn chore myself. So I started doing the chores myself. The problem was that they were still boring, because I was still feeling annoyed while doing them.
And one day it all clicked. I learned to let go of the feeling of annoyance with one simple trick.
The trick is to approach every chore with mindfulness. To give it your full attention. Sounds cheesy but there is actually a good reason for it. When you do the chore and you feel annoyed or irritated you waste your energy. But if you approach it with full focus and mindfulness you actually train your focus, attention and the art of letting go.
Every small chore then becomes a micro exercise in mindfulness. Focus, complete attention, letting go. It makes every experience unique in its own way if you can concentrate fully on it.
Boring suddenly becomes interesting.
Someone cuts you off in traffic or someone walks into you in the street and yells something. There are many types of micro-interactions in our daily lives. The kind I want to talk about is the non-relational type. It's an interaction with someone you don't know or have no relationship with.
I used to get annoyed and I always had even more annoying post-reaction. "I should've said something smart back", "I should have done this or that." I use to think a lot of what I could have said or done, all the smart one-liners. I noticed that I got carried away in the evil spiral of annoyance because my ego got hurt.
Then I came up with a rule for myself. There is no reason to get annoyed or upset if nothing serious happened.
You are dealing with people and people are unpredictable. You getting annoyed means they managed to get to you, intentionally or not. They somehow managed to affect your feelings, your ego, your pride, your self-worth and they forced you to spend your mental energy on it.
Why let them do that? You don't know them and have no relationship to them. Strange, isn't it? Just let it go.
Again, you get a chance to train yourself of letting go of the things that are not important to you or your life.
How to practice? When the situation happens try to bring your attention to the now. Focus on external things. Your surroundings, the sounds around you. I've noticed that if I bring my attention inward, it can be on my feelings and emotions, I get caught up in them.
Now I am gradually learning to focus inward and inspect my feelings. It's actually a very interesting exercise to do. You learn to view your feelings and emotions objectively and see them for what they are. You become the silent observer.
I know it all sounds like new age bullshit, but when you learn to observe your feelings and emotions objectively, when you hear your inner critic, you cannot stop wondering who is the observer and who is that voice in your head? This is the practice of detaching from your ego, your inner critic.
Remember, you often spend more energy being annoyed about something than actually doing it or just letting it go.
Next time it happens ask yourself - Is it worth it?
]]>This got me thinking, and my thinking normally involves lots of research. I am currently down in the psychology rabbit hole trying to understand different theories about how our mind works. Freud, Jung, Adler and friends.
One interesting theory is the Affect theory. It states that there are nine primary affects. An affect is an emotion or subjectively experienced feeling. These affects can be either positive, neutral or negative.
So you just learned that you have a core set of affects (or emotions). Great, now what? The hard thing is to notice them because your true core emotions are often suppressed by your other (false) emotions. It happens unconsciously and it happens for a good reason. To protect you, or sooner your ego, which is your personality. All this is explained by the Triangle of Conflict model.
Simply explained in layman terms: a situation triggers a core (raw) emotion in you. This emotion triggers an alarm and a committee is gathered to decide how you should deal with this emotion. This committee (your superego) is based on your upbringing, past traumas, values and a bunch of other stuff. If this committee decides that this emotion is somehow harmful or dangerous to your ego it replaces the core emotion with some other emotion (laughter, sarcasm, etc) or simply suppresses the emotion. It's a form of defense and is done for your own good. This process also happens at the speed of light without you even noticing it.
To explain it in computer terms, you have a built-in proxy that sits between your unconscious mind and your concious mind. This proxy decides if it should let your true feelings pass, drop them or replace them with something else.
Turns out this Change Triangle, as it's also called, is a useful tool to get to know your feelings better.
The first basic step is to start noticing your core emotions. Because everything happens automatically, and so fast, you can't process things in real-time. It's psychically impossible. What you can do is to carefully think about a situation in retrospect and see if you can identify your core emotions and if they were replaced by some other emotion. If you stick to this habit you will start noticing patterns in your behavior. This knowledge will slowly some shine light on your true feelings and you will start to notice small changes in yourself. Your behaviour and your reactions to various life situations will change.
I've been doing this for a while now and have identified that I normally suppress a lot of feelings than change them. My ego doesn't allow me to feel because it thinks that feelings are dangerous for me.
Even noticing my core emotions is super hard for me. I like to compare them to the shy kids sitting in the back of the classroom. When I ask a question there are a bunch of kids who sit in the front raising their hands, almost jumping out of their chairs, yelling "Me! Me! Pick me! I know this!" These kids are logic and reason. The over-achievers, the Besserwissers.
But then you see a tiny thin and pale hand that is slowly raised in the back of the classroom. Scared, unsure. You tell the kids in the front to be quiet and ask the shy kid in the back to speak. Everyone turns around and the room goes quiet. The kid in the back starts to speak in a quiet voice. All the kids in the front start laughing because that's the stupidest thing they've ever heard. You hush at them to be quiet and ask the kid to continue talking. When the kid is done talking the room is silent. You can hear old florescent lights buzzing. Nobody is laughing now. Because deep down everybody knows that this is the best answer, even if it can be hard to admit.
Start paying attention to the kids in the back of your classroom and you will get true answers and not only the right answers.
]]>