Tag: Svelte

How to Build Your First Custom Svelte Transition

The Svelte transition API provides a first-class way to animate your components when they enter or leave the document, including custom Svelte transitions. By default, the transition directive uses CSS animations, which generally offer better performance and allow the browser’s main thread to remain unblocked. The API is as simple as this: <element transition:transitionFunction />. You can also specify in or out directives which are uni-directional transitions, only running when the element is mounted or unmounted.

An animated example of a custom Svelte transition showing a to do list. An item is typed and animated into the list of items when entered. Clicking a done button animates the item out of view.
Example of a working Svelte transition (jump to demo)

Svelte offers a runtime svelte/transition package that ships with seven prepackaged Svelte transition functions, all of which can be dropped in and tweaked to your heart’s desire. Pairing this with the svelte/easing package, allows for a wide swath of interactions, without writing any of the transition code yourself. Play around with different transitions and easing functions to get a feel for what is possible.

Looking for instructions on how to get started with Svelte? We have a solid overview for you to check out.

The Svelte Custom Transition API

If you need even more control than what the Svelte Transition API offers out of the box, Svelte permits you to specify your own custom transition function, as long as you adhere to a few conventions. From the docs, here’s what the custom transition API looks like:

transition = (node: HTMLElement, params: any) => {   delay?: number,   duration?: number,   easing?: (t: number) => number,   css?: (t: number, u: number) => string,   tick?: (t: number, u: number) => void } 

Let’s break it down. A transition function takes a reference to the DOM node where the transition directive is used and returns an object with some parameters that control the animation and, most importantly, a css or tick function.

The css function’s job is to return a string of CSS that describes the animation, typically including some kind of transform or opacity change. Alternatively, you can opt to return a tick function, which lets you control every aspect of the animation with the power JavaScript, but pays a performance penalty since this type of transition does not use CSS animations.

Both the css and tick functions take two parameters called (t, u) by convention. t is a decimal number that travels from 0.00 to 1.00 while the element is entering the DOM and from 1.00 back to 0.00 when the element is leaving. The u parameter is the inverse of t or 1 - t at any given moment. For example, if you return a string of transform: scale($ {t}), your element would smoothly animate from 0 to 1 on enter, and vice versa on exit.

These concepts may seem a bit abstract, so let’s solidify them by building our own custom Svelte transition!

Building your first custom Svelte transition

First, let’s set up some boilerplate that allows us to toggle an element’s existence in the DOM using a Svelte #if block. Remember, Svelte transitions only run when an element is actually leaving or entering the DOM.

<script>   let showing = true </script>  <label for="showing">   Showing </label> <input id="showing" type="checkbox" bind:checked={showing} />  {#if showing}   <h1>Hello custom transition!</h1> {/if}

You should be able to toggle the checkbox and see our element starkly appear and disappear in place.

Next, let’s set up our custom Svelte transition function and get it wired up to our element.

<script>   let showing = true   // Custom transition function   function whoosh(node) {     console.log(node)   } </script>  <label for="showing">   Showing </label> <input id="showing" type="checkbox" bind:checked={showing} />  {#if showing}   <h1 transition:whoosh>Hello custom transition!</h1> {/if}

Now, if you toggle the checkbox, you will see the <h1> element logged to the console. This proves we have the custom transition connected properly! We won’t actually use the DOM node in our example, but it’s often useful to have access to the element to reference its current styles or dimensions.

For our element to do any animation at all, we need to return an object that contains a css (or tick) function. Let’s have our css function return a single line of CSS that scales our element. We’ll also return a duration property that controls how long the animation takes.

<script>   function swoop() {     return {       duration: 1000,       css: () => `transform: scale(.5)`     }   }   let showing = true </script>  <!-- markup -->

We’ve got something moving! You will notice our element jumps straight to .5 scale when toggling the checkbox. This is something, but it would feel much better if it smoothly transitioned. That’s where the (t, u) parameters come in.

<script>   function swoop() {     return {       duration: 1000,       css: (t) => `transform: scale($ {t})`     }   }   let showing = true </script>  <!-- markup -->

Now we are talking! Remember, t rolls smoothly from 0.00 to 1.00 when an element enters, and vice versa when it leaves. This allows us to achieve the smooth effect we want. In fact, what we just wrote is essentially the built-in scale transition from the svelte/transition package.

Let’s get a little bit fancier. To live up to our custom Svelte transition’s namesake, swoop, let’s add a translateX to our transform, so that our element zooms in and out from the side.

I want to challenge you to attempt the implementation first before we continue. Trust me, it will be fun! Assume that we want to translate to 100% when the element is leaving and back to 0% when it enters.

[waiting…]

How did it go? Want to compare answers?

Here’s what I got:

css: (t, u) => `transform: scale($ {t}) translateX($ {u * 100}%);`

It’s okay if you have something different! Let me break down what I did.

The key thing here is the usage of the second parameter in the css function. If we think about our animation while the element is entering the screen, we want to end up at scale(1) translateX(0%), so we can’t use unaltered t for both the scale and the transform. This is the convenience behind the u parameter — it is the inverse of t at any given moment, so we know it will be 0 when t is 1! I then multiplied u by 100 to get the percentage value and tacked on the % sign at the end.

Learning the interplay between t and u is an important piece of the custom transition puzzle in Svelte. These two parameters enable a world of dynamism for your animations; they can be divided, multiplied, twisted, or contorted into whatever needs you have.

Let’s slap my favorite svelte/easing function on our transition and call it a day:

<script>   import { elasticOut } from 'svelte/easing'   function swoop() {     return {       duration: 1000,       easing: elasticOut,       css: (t, u) => `transform: scale($ {t}) translateX($ {u * 100}%)`     }   }   let showing = true </script>  <label for="showing">   Showing </label> <input id="showing" type="checkbox" bind:checked={showing} />  {#if showing}   <h1 transition:swoop>Hello custom transition!</h1> {/if}

Wrapping up

Congratulations! You can now build a custom Svelte transition function. We have only scratched the surface of what is possible but I hope you feel equipped with the tools to explore even further. I would highly recommend reading the docs and going through the official tutorial to gain even more familiarity.


How to Build Your First Custom Svelte Transition originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,

Introducing Svelte, and Comparing Svelte with React and Vue

Josh Collingsworth is clearly a big fan of Svelte, so while this is a fun and useful comparison article, it’s here to crown Svelte the winner all the way through.

A few things I find compelling:

One of the things I like most about Svelte is its HTML-first philosophy. With few exceptions, Svelte code is entirely browser-readable HTML and JavaScript. In fact, technically, you could call Svelte code as a small superset of HTML.

And:

Svelte is reactive by default. This means when a variable is reassigned, every place it’s used or referenced also updates automatically. (React and Vue both require you to explicitly initialize reactive variables.)

I do find the component format nice to look at, like how you just write HTML. You don’t even need a <template> around it, or to return anything. I imagine Astro took inspiration from this in how you can also just chuck a <style> tag in there and scope styles if you want. But I think I prefer how the “fenced” JavaScript at the top only runs during the build by default.


P.S. I really like Josh’s header/footer random square motif so I tried to reverse engineer it:

To Shared LinkPermalink on CSS-Tricks


The post Introducing Svelte, and Comparing Svelte with React and Vue appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

How I Built a Cross-Platform Desktop Application with Svelte, Redis, and Rust

At Cloudflare, we have a great product called Workers KV which is a key-value storage layer that replicates globally. It can handle millions of keys, each of which is accessible from within a Worker script at exceptionally low latencies, no matter where in the world a request is received. Workers KV is amazing — and so is its pricing, which includes a generous free tier.

However, as a long-time user of the Cloudflare lineup, I have found one thing missing: local introspection. With thousands, and sometimes hundreds of thousands of keys in my applications, I’d often wish there was a way to query all my data, sort it, or just take a look to see what’s actually there.

Well, recently, I was lucky enough to join Cloudflare! Even more so, I joined just before the quarter’s “Quick Wins Week” — aka, their week-long hackathon. And given that I hadn’t been around long enough to accumulate a backlog (yet), you best believe I jumped on the opportunity to fulfill my own wish.

So, with the intro out of the way, let me tell you how I built Workers KV GUI, a cross-platform desktop application using Svelte, Redis, and Rust.

The front-end application

As a web developer, this was the familiar part. I’m tempted to call this the “easy part” but, given that you can use any and all HTML, CSS, and JavaScript frameworks, libraries, or patterns, choice paralysis can easily set in… which might be familiar, too. If you have a favorite front-end stack, great, use that! For this application, I chose to use Svelte because, for me, it certainly makes and keeps things easy.

Also, as web developers, we expect to bring all our tooling with us. You certainly can! Again, this phase of the project is no different than your typical web application development cycle. You can expect to run yarn dev (or some variant) as your main command and feel at home. Keeping with an “easy” theme, I’ve elected to use SvelteKit, which is Svelte’s official framework and toolkit for building applications. It includes an optimized build system, a great developer experience (including HMR!), a filesystem-based router, and all that Svelte itself has to offer.

As a framework, especially one that takes care of its own tooling, SvelteKit allowed me to purely think about my application and its requirements. In fact, as far as configuration is concerned, the only thing I had to do was tell SvelteKit that I wanted to build a single-page application (SPA) that only runs in the client. In other words, I had to explicitly opt out of SvelteKit’s assumption that I wanted a server, which is actually a fair assumption to make since most applications can benefit from server-side rendering. This was as easy as attaching the @sveltejs/adapter-static package, which is a configuration preset made exactly for this purpose. After installing, this was my entire configuration file:

// svelte.config.js import preprocess from 'svelte-preprocess'; import adapter from '@sveltejs/adapter-static';  /** @type {import('@sveltejs/kit').Config} */ const config = {   preprocess: preprocess(),    kit: {     adapter: adapter({       fallback: 'index.html'     }),     files: {       template: 'src/index.html'     }   }, };  export default config;

The index.html changes are a personal preference. SvelteKit uses app.html as a default base template, but old habits die hard.

It’s only been a few minutes, and my toolchain already knows it’s building a SPA, that there’s a router in place, and a development server is at the ready. Plus, TypeScript, PostCSS, and/or Sass support is there if I want it (and I do), thanks to svelte-preprocess. Ready to rumble!

The application needed two views:

  1. a screen to enter connection details (the default/welcome/home page)
  2. a screen to actually view your data

In the SvelteKit world, this translates to two “routes” and SvelteKit dictates that these should exist as src/routes/index.svelte for the home page and src/routes/viewer.svelte for the data viewer page. In a true web application, this second route would map to the /viewer URL. While this is still the case, I know that my desktop application won’t have a navigation bar, which means that the URL won’t be visible… which means that it doesn’t matter what I call this route, as long as it makes sense to me.

The contents of these files are mostly irrelevant, at least for this article. For those curious, the entire project is open source and if you’re looking for a Svelte or SvelteKit example, I welcome you to take a look. At the risk of sounding like a broken record, the point here is that I’m building a regular web app.

At this time, I’m just designing my views and throwing around fake, hard-coded data until I have something that seems to work. I hung out here for about two days, until everything looked nice and all interactivity (button clicks, form submissions, etc.) got fleshed out. I’d call this a “working” app, or a mockup.

Desktop application tooling

At this point, a fully functional SPA exists. It operates — and was developed — in a web browser. Perhaps counterintuitively, this makes it the perfect candidate to become a desktop application! But how?

You may have heard of Electron. It’s the most well-known tool for building cross-platform desktop applications with web technologies. There are a number of massively popular and successful applications built with it: Visual Studio Code, WhatsApp, Atom, and Slack, to name a few. It works by bundling your web assets with its own Chromium installation and its own Node.js runtime. In other words, when you’re installing an Electron-based application, it’s coming with an extra Chrome browser and an entire programming language (Node.js). These are embedded within the application contents and there’s no avoiding them, as these are dependencies for the application, guaranteeing that it runs consistently everywhere. As you might imagine, there’s a bit of a trade-off with this approach — applications are fairly massive (i.e. more than 100MB) and use lots of system resources to operate. In order to use the application, an entirely new/separate Chrome is running in the background — not quite the same as opening a new tab.

Luckily, there are a few alternatives — I evaluated Svelte NodeGui and Tauri. Both choices offered significant application size and utilization savings by relying on native renderers the operating system offers, instead of embedding a copy of Chrome to do the same work. NodeGui does this by relying on Qt, which is another Desktop/GUI application framework that compiles to native views. However, in order to do this, NodeGui requires some adjustments to your application code in order for it to translate your components into Qt components. While I’m sure this certainly would have worked, I wasn’t interested in this solution because I wanted to use exactly what I already knew, without requiring any adjustments to my Svelte files. By contrast, Tauri achieves its savings by wrapping the operating system’s native webviewer — for example, Cocoa/WebKit on macOS, gtk-webkit2 on Linux, and Webkit via Edge on Windows. Webviewers are effectively browsers, which Tauri uses because they already exist on your system, and this means that our applications can remain pure web development products.

With these savings, the bare minimum Tauri application is less than 4MB, with average applications weighing less than 20MB. In my testing, the bare minimum NodeGui application weighed about 16MB. A bare minimum Electron app is easily 120MB.

Needless to say, I went with Tauri. By following the Tauri Integration guide, I added the @tauri-apps/cli package to my devDependencies and initialized the project:

yarn add --dev @tauri-apps/cli yarn tauri init

This creates a src-tauri directory alongside the src directory (where the Svelte application lives). This is where all Tauri-specific files live, which is nice for organization.

I had never built a Tauri application before, but after looking at its configuration documentation, I was able to keep most of the defaults — aside from items like the package.productName and windows.title values, of course. Really, the only changes I needed to make were to the build config, which had to align with SvelteKit for development and output information:

// src-tauri/tauri.conf.json {   "package": {     "version": "0.0.0",     "productName": "Workers KV"   },   "build": {     "distDir": "../build",     "devPath": "http://localhost:3000",     "beforeDevCommand": "yarn svelte-kit dev",     "beforeBuildCommand": "yarn svelte-kit build"   },   // ... }

The distDir relates to where the built production-ready assets are located. This value is resolved from the tauri.conf.json file location, hence the ../ prefix.

The devPath is the URL to proxy during development. By default, SvelteKit spawns a devserver on port 3000 (configurable, of course). I had been visiting the localhost:3000 address in my browser during the first phase, so this is no different.

Finally, Tauri has its own dev and build commands. In order to avoid the hassle of juggling multiple commands or build scripts, Tauri provides the beforeDevCommand and beforeBuildCommand hooks which allow you to run any command before the tauri command runs. This is a subtle but strong convenience!

The SvelteKit CLI is accessed through the svelte-kit binary name. Writing yarn svelte-kit build, for example, tells yarn to fetch its local svelte-kit binary, which was installed via a devDependency, and then tells SvelteKit to run its build command.

With this in place, my root-level package.json contained the following scripts:

{   "private": true,   "type": "module",   "scripts": {     "dev": "tauri dev",     "build": "tauri build",     "prebuild": "premove build",     "preview": "svelte-kit preview",     "tauri": "tauri"   },   // ...   "devDependencies": {     "@sveltejs/adapter-static": "1.0.0-next.9",     "@sveltejs/kit": "1.0.0-next.109",     "@tauri-apps/api": "1.0.0-beta.1",     "@tauri-apps/cli": "1.0.0-beta.2",     "premove": "3.0.1",     "svelte": "3.38.2",     "svelte-preprocess": "4.7.3",     "tslib": "2.2.0",     "typescript": "4.2.4"   } }

After integration, my production command was still yarn build, which invokes tauri build to actually bundle the desktop application, but only after yarn svelte-kit build has completed successfully (via the beforeBuildCommand option). And my development command remained yarn dev which spawns the tauri dev and yarn svelte-kit dev commands to run in parallel. The development workflow is entirely within the Tauri application, which is now proxying localhost:3000, allowing me to still reap the benefits of a HMR development server.

Important: Tauri is still in beta at the time of this writing. That said, it feels very stable and well-planned. I have no affiliation with the project, but it seems like Tauri 1.0 may enter a stable release sooner rather than later. I found the Tauri Discord to be very active and helpful, including replies from the Tauri maintainers! They even entertained some of my noob Rust questions throughout the process. 🙂

Connecting to Redis

At this point, it’s Wednesday afternoon of Quick Wins week, and — to be honest — I’m starting to get nervous about finishing before the team presentation on Friday. Why? Because I’m already halfway through the week, and even though I have a good-looking SPA inside a working desktop application, it still doesn’t do anything. I’ve been looking at the same fake data all week.

You may be thinking that because I have access to a webview, I can use fetch() to make some authenticated REST API calls for the Workers KV data I want and dump it all into localStorage or an IndexedDB table… You’re 100% right! However, that’s not exactly what I had in mind for my desktop application’s use case.

Saving all the data into some kind of in-browser storage is totally viable, but it saves it locally to your machine. This means that if you have team members trying to do the same thing, everyone will have to fetch and save all the data on their own machines, too. Ideally, this Workers KV application should have the option to connect to and sync with an external database. That way, when working in team settings, everyone can tune into the same data cache to save time — and a couple bucks. This starts to matter when dealing with millions of keys which, as mentioned, is not uncommon with Workers KV.

Having thought about it for a bit, I decided to use Redis as my backing store because it also is a key-value store. This was great because Redis already treats keys as a first-class citizen and offers the sorting and filtering behaviors I wanted (aka, I can pass along the work instead of implementing it myself!). And then, of course, Redis is easy to install and run either locally or in a container, and there are many hosted-Redis-as-service providers out there if someone chooses to go that route.

But, how do I connect to it? My app is basically a browser tab running Svelte, right? Yes — but also so much more than that.

You see, part of Electron’s success is that, yes, it guarantees a web app is presented well on every operating system, but it also brings along a Node.js runtime. As a web developer, this was a lot like including a back-end API directly inside my client. Basically the “…but it works on my machine” problem went away because all of the users were (unknowingly) running the exact same localhost setup. Through the Node.js layer, you could interact with the filesystem, run servers on multiple ports, or include a bunch of node_modules to — and I’m just spit-balling here — connect to a Redis instance. Powerful stuff.

We don’t lose this superpower because we’re using Tauri! It’s the same, but slightly different.

Instead of including a Node.js runtime, Tauri applications are built with Rust, a low-level systems language. This is how Tauri itself interacts with the operating system and “borrows” its native webviewer. All of the Tauri toolkit is compiled (via Rust), which allows the built application to remain small and efficient. However, this also means that we, the application developers, can include any additional crates — the “npm module” equivalent — into the built application. And, of course, there’s an aptly named redis crate that, as a Redis client driver, allows the Workers KV GUI to connect to any Redis instance.

In Rust, the Cargo.toml file is similar to our package.json file. This is where dependencies and metadata are defined. In a Tauri setting, this is located at src-tauri/Cargo.toml because, again, everything related to Tauri is found in this directory. Cargo also has a concept of “feature flags” defined at the dependency level. (The closest analogy I can come up with is using npm to access a module’s internals or import a named submodule, though it’s not quite the same still since, in Rust, feature flags affect how the package is built.)

# src-tauri/Cargo.toml [dependencies] serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.0.0-beta.1", features = ["api-all", "menu"] } redis = { version = "0.20", features = ["tokio-native-tls-comp"] }

The above defines the redis crate as a dependency and opts into the "tokio-native-tls-comp" feature, which the documentation says is required for TLS support.

Okay, so I finally had everything I needed. Before Wednesday ended, I had to get my Svelte to talk to my Redis. After poking around a bit, I noticed that all the important stuff seemed to be happening inside the src-tauri/main.rs file. I took note of the #[command] macro, which I knew I had seen before in a Tauri example earlier in the day, so I studied copied the example file in sections, seeing which errors came and went according to the Rust compiler.

Eventually, the Tauri application was able to run again, and I learned that the #[command] macro is wrapping the underlying function in a way so that it can receive “context” values, if you choose to use them, and receive pre-parsed argument values. Also, as a language, Rust does a lot of type casting. For example:

use tauri::{command};  #[command] fn greet(name: String, age: u8) {   println!("Hello {}, {} year-old human!", name, age); }

This creates a greet command which, when run,expects two arguments: name and age. When defined, the name value is a string value and age is a u8 data type — aka, an integer. However, if either are missing, Tauri throws an error because the command definition does not say anything is allowed to be optional.

To actually connect a Tauri command to the application, it has to be defined as part of the tauri::Builder composition, found within the main function.

use tauri::{command};  #[command] fn greet(name: String, age: u8) {   println!("Hello {}, {} year-old human!", name, age); }  fn main() {   // start composing a new Builder chain   tauri::Builder::default()     // assign our generated "handler" to the chain     .invoke_handler(       // piece together application logic       tauri::generate_handler![         greet, // attach the command       ]     )     // start/initialize the application     .run(       // put it all together       tauri::generate_context!()     )     // print <message> if error while running     .expect("error while running tauri application"); }

The Tauri application compiles and is aware of the fact that it owns a “greet” command. It’s also already controlling a webview (which we’ve discussed) but in doing so, it acts as a bridge between the front end (the webview contents) and the back end, which consists of the Tauri APIs and any additional code we’ve written, like the greet command. Tauri allows us to send messages across this bridge so that the two worlds can communicate with one another.

A component diagram of a basic Tauri application.
The developer is responsible for webview contents and may optionally include custom Rust modules and/or define custom commands. Tauri controls the webviewer and the event bridge, including all message serialization and deserialization.

This “bridge” can be accessed by the front end by importing functionality from any of the (already included) @tauri-apps packages, or by relying on the window.__TAURI__ global, which is available to the entire client-side application. Specifically, we’re interested in the invoke command, which takes a command name and a set of arguments. If there are any arguments, they must be defined as an object where the keys match the parameter names our Rust function expects.

In the Svelte layer, this means that we can do something like this in order to call the greet command, defined in the Rust layer:

<!-- Greeter.svelte --> <script>   function onclick() {     __TAURI__.invoke('greet', {       name: 'Alice',       age: 32     });   } </script>  <button on:click={onclick}>Click Me</button>

When this button is clicked, our terminal window (wherever the tauri dev command is running) prints:

Hello Alice, 32 year-old human!

Again, this happens because of the println! function, which is effectively console.log for Rust, that the greet command used. It appears in the terminal’s console window — not the browser console — because this code still runs on the Rust/system side of things.

It’s also possible to send something back to the client from a Tauri command, so let’s change greet quickly:

use tauri::{command};  #[command] fn greet(name: String, age: u8) {   // implicit return, because no semicolon!   format!("Hello {}, {} year-old human!", name, age) }  // OR  #[command] fn greet(name: String, age: u8) {   // explicit `return` statement, must have semicolon   return format!("Hello {}, {} year-old human!", name, age); }

Realizing that I’d be calling invoke many times, and being a bit lazy, I extracted a light client-side helper to consolidate things:

// @types/global.d.ts /// <reference types="@sveltejs/kit" />  type Dict<T> = Record<string, T>;  declare const __TAURI__: {   invoke: typeof import('@tauri-apps/api/tauri').invoke; }  // src/lib/tauri.ts export function dispatch(command: string, args: Dict<string|number>) {   return __TAURI__.invoke(command, args); }

The previous Greeter.svelte was then refactored into:

<!-- Greeter.svelte --> <script lang="ts">   import { dispatch } from '$ lib/tauri';    async function onclick() {     let output = await dispatch('greet', {       name: 'Alice',       age: 32     });     console.log('~>', output);     //=> "~> Hello Alice, 32 year-old human!"   } </script>  <button on:click={onclick}>Click Me</button>

Great! So now it’s Thursday and I still haven’t written any Redis code, but at least I know how to connect the two halves of my application’s brain together. It was time to comb back through the client-side code and replace all TODOs inside event handlers and connect them to the real deal.

I will spare you the nitty gritty here, as it’s very application-specific from here on out — and is mostly a story of the Rust compiler giving me a beat down. Plus, spelunking for nitty gritty is exactly why the project is open source!

At a high-level, once a Redis connection is established using the given details, a SYNC button is accessible in the /viewer route. When this button is clicked (and only then — because of costs) a JavaScrip function is called, which is responsible for connecting to the Cloudflare REST API and dispatching a "redis_set" command for each key. This redis_set command is defined in the Rust layer — as are all Redis-based commands — and is responsible for actually writing the key-value pair to Redis.

Reading data out of Redis is a very similar process, just inverted. For example, when the /viewer started up, all the keys should be listed and ready to go. In Svelte terms, that means I need to dispatch a Tauri command when the /viewer component mounts. That happens here, almost verbatim. Additionally, clicking on a key name in the sidebar reveals additional “details” about the key, including its expiration (if any), its metadata (if any), and its actual value (if known). Optimizing for cost and network load, we decided that a key’s value should only be fetched on command. This introduces a REFRESH button that, when clicked, interacts with the REST API once again, then dispatches a command so that the Redis client can update that key individually.

I don’t mean to bring things to a rushed ending, but once you’ve seen one successful interaction between your JavaScript and Rust code, you’ve seen them all! The rest of my Thursday and Friday morning was just defining new request-reply pairs, which felt a lot like sending PING and PONG messages to myself.

Conclusion

For me — and I imagine many other JavaScript developers — the challenge this past week was learning Rust. I’m sure you’ve heard this before and you’ll undoubtedly hear it again. Ownership rules, borrow-checking, and the meanings of single-character syntax markers (which are not easy to search for, by the way) are just a few of the roadblocks that I bumped into. Again, a massive thank-you to the Tauri Discord for their help and kindness!

This is also to say that using Tauri was not a challenge — it was a massive relief. I definitely plan to use Tauri again in the future, especially knowing that I can use just the webviewer if I want to. Digging into and/or adding Rust parts was “bonus material” and is only required if my app requires it.

For those wondering, because I couldn’t find another place to mention it: on macOS, the Workers KV GUI application weighs in at less than 13 MB. I am so thrilled with that!

And, of course, SvelteKit certainly made this timeline possible. Not only did it save me a half-day-slog configuring my toolbelt, but the instant, HMR development server probably saved me a few hours of manually refreshing the browser — and then the Tauri viewer.

If you’ve made it this far — that’s impressive! Thank you so much for your time and attention. A reminder that the project is available on GitHub and the latest, pre-compiled binaries are always available through its releases page.


The post How I Built a Cross-Platform Desktop Application with Svelte, Redis, and Rust appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Using Custom Elements in Svelte

Svelte fully supports custom elements (e.g. <my-component>) without any custom configuration or wrapper components and has a perfect score on Custom Elements Everywhere. However, there are still a few quirks you need to watch out for, especially around how Svelte sets data on custom elements. At Alaska Airlines, we experienced many of these issues first-hand as we integrated the custom elements from our design system into a Svelte application.

While Svelte supports compiling to custom elements, that is not within the scope of this post. Instead, I will focus on using custom elements built with the Lit custom element library in a Svelte application. These concepts should transfer to custom elements built with or without a supporting library.

Property or attribute?

To fully understand how to use custom elements in Svelte, you need to understand how Svelte passes data to a custom element.

Svelte uses a simple heuristic to determine whether to pass data to a custom element as a property or an attribute. If a corresponding property exists on the custom element at runtime, Svelte will pass the data as a property. Otherwise, it will pass it as an attribute. This seems simple, but has interesting implications.

For instance, let’s say you have a coffee-mug custom element that takes a size property. You can use it in a Svelte component like so:

<coffee-mug class="mug" size="large"></coffee-mug>

You can open this Svelte REPL to follow along. You should see the custom element render the text “This coffee mug’s size is: large ☕️.”

When writing the HTML inside the component, it seems like you’re setting both class and size as attributes. However, this is not the case. Right-click on the “This coffee mug’s size is” text in the REPL’s output and click “Inspect.” This will bring open the DevTools inspector. When you inspect the rendered HTML, you’ll notice that only class was set as an attribute — it’s as if size simply disappeared! However, size is getting set somehow, because “large” still appears in the element’s rendered text.

This is because size is a property on the element, but class is not. Because Svelte detects a size property, it chooses to set that property instead of an attribute. There is no class property, so Svelte sets it as an attribute instead. That’s not a problem or something that changes how we expect the component to behave, but can be very confusing if you’re unaware of it, because there’s a disconnect between the HTML you think you’re writing and what Svelte actually outputs.

Svelte isn’t unique in this behavior — Preact uses a similar method to determine whether to set an attribute or a property on custom elements. Because of that, the use cases I discuss will also occur in Preact, though the workarounds will be different. You will not run into these issues with Angular or Vue because they have a special syntax that lets you choose to set an attribute or a property.

Svelte’s heuristic makes it easy to pass complex data like arrays and objects which need to be set as properties. Consumers of your custom elements shouldn’t need to think about whether they need to set an attribute or a property — it just magically works. However, like any magic in web development, you eventually run into some cases that require you to dig a little deeper and understand what’s going on behind the scenes.

Let’s go through some use cases where custom elements behave strangely. You can find the final examples in this Svelte REPL.

Attributes used as styling hooks

Let’s say you have a custom-text element that displays some text. If the flag attribute is present, it prepends a flag emoji and the word “Flagged:” to the text. The element is coded as follows:

import { html, css, LitElement } from 'lit'; export class CustomText extends LitElement {   static get styles() {     return css`       :host([flag]) p::before {         content: '🚩';       }     `;   }   static get properties() {     return {       flag: {         type: Boolean       }     };   }   constructor() {     super();     this.flag = false;   }   render() {     return html`<p>       $  {this.flag ? html`<strong>Flagged:</strong>` : ''}       <slot></slot>     </p>`;   } } customElements.define('custom-text', CustomText);

You can see the element in action in this CodePen.

However, if you try to use the custom element the same way in Svelte, it doesn’t entirely work. The “Flagged:” text is shown, but the emoji is not. What gives?

<script>   import './custom-elements/custom-text'; </script>  <!-- This shows the "Flagged:" text, but not 🚩 --> <custom-text flag>Just some custom text.</custom-text>

The key here is the :host([flag]) selector. :host selects the element’s shadow root (i.e. the <custom-text> element), so this selector only applies if the flag attribute is present on the element. Since Svelte chooses to set the property instead, this selector doesn’t apply. The “Flagged:” text is added based on the property, which is why that still showed.

So what are our options here? Well, the custom element shouldn’t have assumed that flag would always be set as an attribute. It is a custom element best practice to keep primitive data attributes and properties in sync since you don’t know how the consumer of the element will interact with it. The ideal solution is for the element author to make sure any primitive properties are reflected to attributes, especially if those attributes are used for styling. Lit makes it easy to reflect your properties:

static get properties() {   return {     flag: {       type: Boolean,       reflect: true     }   }; }

With that change, the flag property is reflected back to the attribute, and everything displays as expected.

However, there may be cases where you don’t have control over the custom element definition. In that case, you can force Svelte to set the attribute using a Svelte action.

Using a Svelte action to force setting attributes

Actions are a powerful Svelte feature that run a function when a certain node is added to the DOM. For example, we can write an action that will set the flag attribute on our custom-text element:

<script>   import './custom-elements/custom-text';   function setAttributes(node) {     node.setAttribute('flag', '');   } </script>  <custom-text use:setAttributes>   Just some custom text. </custom-text>

Actions can also take parameters. For instance, we could make this action more generic and accept an object containing the attributes we want to set on a node.

<script>   import './custom-elements/custom-text';   function setAttributes(node, attributes) {     Object.entries(attributes).forEach(([k, v]) => {       if (v !== undefined) {         node.setAttribute(k, v);       } else {         node.removeAttribute(k);       }     });   } </script>  <custom-text use:setAttributes={{ flag: true }}>   Just some custom text. </custom-text>

Finally, if we want the attributes to react to state changes, we can return an object with an update method from the action. Whenever the parameters we pass to the action change, the update function will be called.

<script>   import './custom-elements/custom-text';   function setAttributes(node, attributes) {     const applyAttributes = () => {       Object.entries(attributes).forEach(([k, v]) => {         if (v !== undefined) {           node.setAttribute(k, v);         } else {           node.removeAttribute(k);         }       });     };     applyAttributes();     return {       update(updatedAttributes) {         attributes = updatedAttributes;         applyAttributes();       }     };   }   let flagged = true; </script> <label><input type="checkbox" bind:checked={flagged} /> Flagged</label> <custom-text use:setAttributes={{ flag: flagged ? '' : undefined }}>   Just some custom text. </custom-text>

Using this approach, we don’t have to update the custom element to reflect the property — we can control setting the attribute from inside our Svelte app.

Lazy-loading custom elements

Custom elements are not always defined when the component first renders. For example, you may wait to import your custom elements until after the web component polyfills have loaded. Also, in a server-side rendering context such as Sapper or SvelteKit, the initial server render will take place without loading the custom element definition.

In either case, if the custom element is not defined, Svelte will set everything as attributes. This is because the property does not exist on the element yet. This is confusing if you’ve grown accustomed to Svelte only setting properties on custom elements. This can cause issues with complex data such as objects and arrays.

As an example, let’s look at the following custom element that displays a greeting followed by a list of names.

import { html, css, LitElement } from 'lit'; export class FancyGreeting extends LitElement {   static get styles() {     return css`       p {         border: 5px dashed mediumaquamarine;         padding: 4px;       }     `;   }   static get properties() {     return {       names: { type: Array },       greeting: { type: String }     };   }   constructor() {     super();     this.names = [];   }   render() {     return html`<p>       $  {this.greeting},       $  {this.names && this.names.length > 0 ? this.names.join(', ') : 'no one'}!     </p>`;   } } customElements.define('fancy-greeting', FancyGreeting);

You can see the element in action in this CodePen.

If we statically import the element in a Svelte application, everything works as expected.

<script>   import './custom-elements/fancy-greeting'; </script> <!-- This displays "Howdy, Amy, Bill, Clara!" --> <fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

However, if we dynamically import the component, the custom element does not become defined until after the component has first rendered. In this example, I wait to import the element until the Svelte component has been mounted using the onMount lifecycle function. When we delay importing the custom element, the list of names is not set properly and the fallback content is displayed instead.

<script>   import { onMount } from 'svelte';   onMount(async () => {     await import('./custom-elements/fancy-greeting');   }); </script> <!-- This displays "Howdy, no one!"--> <fancy-greeting greeting="Howdy" names={['Amy', 'Bill', 'Clara']} />

Because the custom element definition is not loaded when Svelte adds fancy-greeting to the DOM, fancy-greeting does not have a names property and Svelte sets the names attribute — but as a string, not as a stringified array. If you inspect the element in your browser DevTools, you’ll see the following:

<fancy-greeting greeting="Howdy" names="Amy,Bill,Clara"></fancy-greeting> 

Our custom element tries to parse the names attribute as an array using JSON.parse, which throws an exception. This is handled automatically using Lit’s default array converter, but the same would apply to any element that expects an attribute to contain a valid JSON array.

Interestingly, once you update the data passed to the custom element Svelte will start setting the property again. In the below example, I moved the array of names to the state variable names so that I can update it. I also added an “Add name” button that will append the name “Rory” to the end of the names array when clicked.

Once the button is clicked, the names array is updated, which triggers a re-render of the component. Since the custom element is now defined, Svelte detects the names property on the custom element and sets that instead of the attribute. This causes the custom element to properly display the list of names instead of the fallback content.

<script>   import { onMount } from 'svelte';   onMount(async () => {     await import('./custom-elements/fancy-greeting');   });   let names = ['Amy', 'Bill', 'Clara'];   function addName() {     names = [...names, 'Rory'];   } </script>  <!-- Once the button is clicked, the element displays "Howdy, Amy, Bill, Clara, Rory!" --> <fancy-greeting greeting="Howdy" {names} /> <button on:click={addName}>Add name</button>

As in the previous example, we can force Svelte to set the data how we want using an action. This time, instead of setting everything as an attribute, we want to set everything as a property. We will pass an object as a parameter that contains the properties we want to set on the node. Here’s how our action will be applied to the custom element:

<fancy-greeting   greeting="Howdy"   use:setProperties={{ names: ['Amy', 'Bill', 'Clara'] }} />

Below is the the implementation of the action. We iterate over the properties object and use each entry to set the property on the custom element node. We also return an update function so that the properties are reapplied if the parameters passed to the action change. See the previous section if you want a refresher on how you can react to state changes with an action.

function setProperties(node, properties) {   const applyProperties = () => {     Object.entries(properties).forEach(([k, v]) => {       node[k] = v;     });   };   applyProperties();   return {     update(updatedProperties) {       properties = updatedProperties;       applyProperties();     }   }; }

By using the action, the names are displayed properly on first render. Svelte sets the property when first rendering the component, and the custom element picks that property up once the element has been defined.

Boolean attributes

The final issue we ran into is how Svelte handles boolean attributes on a custom element. This behavior has recently changed with Svelte 3.38.0, but we’ll explore pre- and post-3.38 behavior since not everyone will be on the latest Svelte version.

Suppose we have a <secret-box> custom element with a boolean property open that indicates whether the box is open or not. The implementation looks like this:

import { html, LitElement } from 'lit'; export class SecretBox extends LitElement {   static get properties() {     return {       open: {         type: Boolean       }     };   }   render() {     return html`<div>The box is $  {this.open ? 'open 🔓' : 'closed 🔒'}</div>`;   } } customElements.define('secret-box', SecretBox);

You can see the element in action in this CodePen.

As seen in the CodePen, you can set the open property to true multiple ways. Per the HTML spec, the presence of a boolean attribute represents the true value, and its absence represents false.

<secret-box open></secret-box> <secret-box open=""></secret-box> <secret-box open="open"></secret-box>

Interestingly, only the last of the above options shows “The box is open” when used inside a Svelte component. The first two show “The box is closed” despite setting the open attribute. What’s going on here?

As with the other examples, it all goes back to Svelte choosing properties over attributes. If you inspect the elements in the browser DevTools, no attributes are set — Svelte has set everything as properties. We can console.log the open property inside our render method (or query the element in the console) to discover what Svelte set the open property to.

// <secret-box open> logs '' // <secret-box open=""> logs '' // <secret-box open="open"> logs 'open' render() {   console.log(this.open);   return html`<div>The box is $  {this.open ? 'open 🔓' : 'closed 🔒'}</div>`; }

In the first two cases, open equals an empty string. Since an empty string is falsy in JavaScript, our ternary statement evaluates to the false case and shows that the box is closed. In the final case, the open property is set to the string “open” which is truthy. The ternary statement evaluates to the true case and shows that the box is open.

As a side note, you don’t run into this issue when you lazy load the element. Since the custom element definition is not loaded when Svelte renders the element, Svelte sets the attribute instead of the property. See the above section for a refresher.

There’s an easy way around this issue. If you remember that you’re setting the property, not the attribute, you can explicitly set the open property to true with the following syntax.

<secret-box open={true}></secret-box>

This way you know you’re setting the open property to true. Setting to a non-empty string also works, but this way is the most accurate since you’re setting true instead of something that happens to be truthy.

Until recently, this was the only way to properly set boolean properties on custom elements. However, with Svelte 3.38, I had a change released that updated Svelte’s heuristic to allow setting shorthand boolean properties. Now, if Svelte knows that the underlying property is a boolean, it will treat the open and open="" syntaxes the same as open={true}.

This is especially helpful since this is how you see examples in many custom element component libraries. This change makes it easy to copy-paste out of the docs without having to troubleshoot why a certain attribute isn’t working how you’d expect.

However, there is one requirement on the custom element author side — the boolean property needs a default value so that Svelte knows it’s of boolean type. This is a good practice anyway if you want that property to be a boolean.

In our secret-box element, we can add a constructor and set the default value:

constructor() {   super();   this.open = true; }

With that change, the following will correctly display “The box is open” in a Svelte component.

<secret-box open></secret-box> <secret-box open=""></secret-box>

Wrapping up

Once you understand how Svelte decides to set an attribute or a property, a lot of these seemingly strange issues start to make more sense. Any time you run into issues passing data to a custom element inside a Svelte application, figure out if it’s being set as an attribute or a property and go from there. I’ve given you a few escape hatches in this article to force one or the other when you need to, but they should generally be unnecessary. Most of the time, custom elements in Svelte just work. You just need to know where to look if something does go wrong.


Special thanks to Dale Sande, Gus Naughton, and Nanette Ranes for reviewing an early version of this article.


The post Using Custom Elements in Svelte appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Svelte for the Experienced React Dev

This post is an accelerated introduction to Svelte from the point of view of someone with solid experience with React. I’ll provide a quick introduction, and then shift focus to things like state management and DOM interoperability, among other things. I plan on moving somewhat quickly, so I can cover a lot of topics. At the end of the day, I’m mainly hoping to spark some interest in Svelte.

For a straightforward introduction to Svelte, no blog post could ever beat the official tutorial or docs.

“Hello, World!” Svelte style

Let’s start with a quick tour of what a Svelte component looks like.

<script>   let number = 0; </script>  <style>   h1 {     color: blue;   } </style>  <h1>Value: {number}</h1>  <button on:click={() => number++}>Increment</button> <button on:click={() => number--}>Decrement</button> 

That content goes in a .svelte file, and is processed by the Rollup or webpack plugin to produce a Svelte component. There’s a few pieces here. Let’s walk through them.

First, we add a <script> tag with any state we need.

We can also add a <style> tag with any CSS we want. These styles are scoped to the component in such a way that, here, <h1> elements in this component will be blue. Yes, scoped styles are built into Svelte, without any need for external libraries. With React, you’d typically need to use a third-party solution to achieve scoped styling, such as css-modules, styled-components, or the like (there are dozens, if not hundreds, of choices).

Then there’s the HTML markup. As you’d expect, there are some HTML bindings you’ll need to learn, like {#if}, {#each}, etc. These domain-specific language features might seem like a step back from React, where everything is “just JavaScript.” But there’s a few things worth noting: Svelte allows you to put arbitrary JavaScript inside of these bindings. So something like this is perfectly valid:

{#if childSubjects?.length}

If you jumped into React from Knockout or Ember and never looked back, this might come as a (happy) surprise to you.

Also, the way Svelte processes its components is very different from React. React re-runs all components any time any state within a component, or anywhere in an ancestor (unless you “memoize”), changes. This can get inefficient, which is why React ships things like useCallback and useMemo to prevent un-needed re-calculations of data.

Svelte, on the other hand, analyzes your template, and creates targeted DOM update code whenever any relevant state changes. In the component above, Svelte will see the places where number changes, and add code to update the <h1> text after the mutation is done. This means you never have to worry about memoizing functions or objects. In fact, you don’t even have to worry about side-effect dependency lists, although we’ll get to that in a bit.

But first, let’s talk about …

State management

In React, when we need to manage state, we use the useState hook. We provide it an initial value, and it returns a tuple with the current value, and a function we can use to set a new value. It looks something like this:

import React, { useState } from "react";  export default function (props) {   const [number, setNumber] = useState(0);   return (     <>       <h1>Value: {number}</h1>       <button onClick={() => setNumber(n => n + 1)}>Increment</button>       <button onClick={() => setNumber(n => n - 1)}>Decrement</button>     </>   ); }

Our setNumber function can be passed wherever we’d like, to child components, etc.

Things are simpler in Svelte. We can create a variable, and update it as needed. Svelte’s ahead-of-time compilation (as opposed to React’s just-in-time compilation) will do the footwork of tracking where it’s updated, and force an update to the DOM. The same simple example from above might look like this:

<script>   let number = 0; </script>  <h1>Value: {number}</h1> <button on:click={() => number++}>Increment</button> <button on:click={() => number--}>Decrement</button>

Also of note here is that Svelte requires no single wrapping element like JSX does. Svelte has no equivalent of the React fragment <></> syntax, since it’s not needed.

But what if we want to pass an updater function to a child component so it can update this piece of state, like we can with React? We can just write the updater function like this:

<script>   import Component3a from "./Component3a.svelte";            let number = 0;   const setNumber = cb => number = cb(number); </script>  <h1>Value: {number}</h1>  <button on:click={() => setNumber(val => val + 1)}>Increment</button> <button on:click={() => setNumber(val => val - 1)}>Decrement</button>

Now, we pass it where needed — or stay tuned for a more automated solution.

Reducers and stores

React also has the useReducer hook, which allows us to model more complex state. We provide a reducer function, and it gives us the current value, and a dispatch function that allows us to invoke the reducer with a given argument, thereby triggering a state update, to whatever the reducer returns. Our counter example from above might look like this:

import React, { useReducer } from "react";  function reducer(currentValue, action) {   switch (action) {     case "INC":       return currentValue + 1;     case "DEC":       return currentValue - 1;   } }  export default function (props) {   const [number, dispatch] = useReducer(reducer, 0);   return (     <div>       <h1>Value: {number}</h1>       <button onClick={() => dispatch("INC")}>Increment</button>       <button onClick={() => dispatch("DEC")}>Decrement</button>     </div>   ); }

Svelte doesn’t directly have something like this, but what it does have is called a store. The simplest kind of store is a writable store. It’s an object that holds a value. To set a new value, you can call set on the store and pass the new value, or you can call update, and pass in a callback function, which receives the current value, and returns the new value (exactly like React’s useState).

To read the current value of a store at a moment in time, there’s a get function that can be called, which returns its current value. Stores also have a subscribe function, which we can pass a callback to, and that will run whenever the value changes.

Svelte being Svelte, there’s some nice syntactic shortcuts to all of this. If you’re inside of a component, for example, you can just prefix a store with the dollar sign to read its value, or directly assign to it, to update its value. Here’s the counter example from above, using a store, with some extra side-effect logging, to demonstrate how subscribe works:

<script>   import { writable, derived } from "svelte/store";            let writableStore = writable(0);   let doubleValue = derived(writableStore, $  val => $  val * 2);            writableStore.subscribe(val => console.log("current value", val));   doubleValue.subscribe(val => console.log("double value", val)) </script>  <h1>Value: {$  writableStore}</h1>  <!-- manually use update --> <button on:click={() => writableStore.update(val => val + 1)}>Increment</button> <!-- use the $   shortcut --> <button on:click={() => $  writableStore--}>Decrement</button>  <br />  Double the value is {$  doubleValue}

Notice that I also added a derived store above. The docs cover this in depth, but briefly, derived stores allow you to project one store (or many stores) to a single, new value, using the same semantics as a writable store.

Stores in Svelte are incredibly flexible. We can pass them to child components, alter, combine them, or even make them read-only by passing through a derived store; we can even re-create some of the React abstractions you might like, or even need, if we’re converting some React code over to Svelte.

React APIs with Svelte

With all that out of the way, let’s return to React’s useReducer hook from before.

Let’s say we really like defining reducer functions to maintain and update state. Let’s see how difficult it would be to leverage Svelte stores to mimic React’s useReducer API. We basically want to call our own useReducer, pass in a reducer function with an initial value, and get back a store with the current value, as well as a dispatch function that invokes the reducer and updates our store. Pulling this off is actually not too bad at all.

export function useReducer(reducer, initialState) {   const state = writable(initialState);   const dispatch = (action) =>     state.update(currentState => reducer(currentState, action));   const readableState = derived(state, ($  state) => $  state);    return [readableState, dispatch]; }

The usage in Svelte is almost identical to React. The only difference is that our current value is a store, rather than a raw value, so we need to prefix it with the $ to read the value (or manually call get or subscribe on it).

<script>   import { useReducer } from "./useReducer";            function reducer(currentValue, action) {     switch (action) {       case "INC":         return currentValue + 1;       case "DEC":         return currentValue - 1;     }   }   const [number, dispatch] = useReducer(reducer, 0);       </script>  <h1>Value: {$  number}</h1>  <button on:click={() => dispatch("INC")}>Increment</button> <button on:click={() => dispatch("DEC")}>Decrement</button>

What about useState?

If you really love the useState hook in React, implementing that is just as straightforward. In practice, I haven’t found this to be a useful abstraction, but it’s a fun exercise that really shows Svelte’s flexibility.

export function useState(initialState) {   const state = writable(initialState);   const update = (val) =>     state.update(currentState =>       typeof val === "function" ? val(currentState) : val     );   const readableState = derived(state, $  state => $  state);    return [readableState, update]; }

Are two-way bindings really evil?

Before closing out this state management section, I’d like to touch on one final trick that’s specific to Svelte. We’ve seen that Svelte allows us to pass updater functions down the component tree in any way that we can with React. This is frequently to allow child components to notify their parents of state changes. We’ve all done it a million times. A child component changes state somehow, and then calls a function passed to it from a parent, so the parent can be made aware of that state change.

In addition to supporting this passing of callbacks, Svelte also allows a parent component to two-way bind to a child’s state. For example, let’s say we have this component:

<!-- Child.svelte --> <script>   export let val = 0; </script>  <button on:click={() => val++}>   Increment </button>  Child: {val}

This creates a component, with a val prop. The export keyword is how components declare props in Svelte. Normally, with props, we pass them in to a component, but here we’ll do things a little differently. As we can see, this prop is modified by the child component. In React this code would be wrong and buggy, but with Svelte, a component rendering this component can do this:

<!-- Parent.svelte --> <script>   import Child from "./Child.svelte";            let parentVal; </script>  <Child bind:val={parentVal} /> Parent Val: {parentVal}

Here, we’re binding a variable in the parent component, to the child’s val prop. Now, when the child’s val prop changes, our parentVal will be updated by Svelte, automatically.

Two-way binding is controversial for some. If you hate this then, by all means, feel free to never use it. But used sparingly, I’ve found it to be an incredibly handy tool to reduce boilerplate.

Side effects in Svelte, without the tears (or stale closures)

In React, we manage side effects with the useEffect hook. It looks like this:

useEffect(() => {   console.log("Current value of number", number); }, [number]);

We write our function with the dependency list at the end. On every render, React inspects each item in the list, and if any are referentially different from the last render, the callback re-runs. If we’d like to cleanup after the last run, we can return a cleanup function from the effect.

For simple things, like a number changing, it’s easy. But as any experienced React developer knows, useEffect can be insidiously difficult for non-trivial use cases. It’s surprisingly easy to accidentally omit something from the dependency array and wind up with a stale closure.

In Svelte, the most basic form of handling a side effect is a reactive statement, which looks like this:

$  : {   console.log("number changed", number); }

We prefix a code block with $ : and put the code we’d like to execute inside of it. Svelte analyzes which dependencies are read, and whenever they change, Svelte re-runs our block. There’s no direct way to have the cleanup run from the last time the reactive block was run, but it’s easy enough to workaround if we really need it:

let cleanup; $  : {   cleanup?.();   console.log("number changed", number);   cleanup = () => console.log("cleanup from number change"); }

No, this won’t lead to an infinite loop: re-assignments from within a reactive block won’t re-trigger the block.

While this works, typically these cleanup effects need to run when your component unmounts, and Svelte has a feature built in for this: it has an onMount function, which allows us to return a cleanup function that runs when the component is destroyed, and more directly, it also has an onDestroy function that does what you’d expect.

Spicing things up with actions

The above all works well enough, but Svelte really shines with actions. Side effects are frequently tied to our DOM nodes. We might want to integrate an old (but still great) jQuery plugin on a DOM node, and tear it down when that node leaves the DOM. Or maybe we want to set up a ResizeObserver for a node, and tear it down when the node leaves the DOM, and so on. This is a common enough requirement that Svelte builds it in with actions. Let’s see how.

{#if show}   <div use:myAction>     Hello                   </div> {/if}

Note the use:actionName syntax. Here we’ve associated this <div> with an action called myAction, which is just a function.

function myAction(node) {   console.log("Node added", node); }

This action runs whenever the <div> enters the DOM, and passes the DOM node to it. This is our chance to add our jQuery plugins, set up our ResizeObserver, etc. Not only that, but we can also return a cleanup function from it, like this:

function myAction(node) {   console.log("Node added", node);    return {     destroy() {       console.log("Destroyed");     }   }; }

Now the destroy() callback will run when the node leaves the DOM. This is where we tear down our jQuery plugins, etc.

But wait, there’s more!

We can even pass arguments to an action, like this:

<div use:myAction={number}>   Hello                 </div>

That argument will be passed as the second argument to our action function:

function myAction(node, param) {   console.log("Node added", node, param);    return {     destroy() {       console.log("Destroyed");     }   }; }

And if you’d like to do additional work whenever that argument changes, you can return an update function:

function myAction(node, param) {   console.log("Node added", node, param);    return {     update(param) {       console.log("Update", param);     },     destroy() {       console.log("Destroyed");     }   }; }

When the argument to our action changes, the update function will run. To pass multiple arguments to an action, we pass an object:

<div use:myAction={{number, otherValue}}>   Hello                 </div>

…and Svelte re-runs our update function whenever any of the object’s properties change.

Actions are one of my favorite features of Svelte; they’re incredibly powerful.

Odds and Ends

Svelte also ships a number of great features that have no counterpart in React. There’s a number of form bindings (which the tutorial covers), as well as CSS helpers.

Developers coming from React might be surprised to learn that Svelte also ships animation support out of the box. Rather than searching on npm and hoping for the best, it’s… built in. It even includes support for spring physics, and enter and exit animations, which Svelte calls transitions.

Svelte’s answer to React.Chidren are slots, which can be named or not, and are covered nicely in the Svelte docs. I’ve found them much simpler to reason about than React’s Children API.

Lastly, one of my favorite, almost hidden features of Svelte is that it can compile its components into actual web components. The svelte:options helper has a tagName property that enables this. But be sure to set the corresponding property in the webpack or Rollup config. With webpack, it would look something like this:

{   loader: "svelte-loader",   options: {     customElement: true   } }

Interested in giving Svelte a try?

Any of these items would make a great blog post in and of itself. While we may have only scratched the surface of things like state management and actions, we saw how Svelte’s features not only match up pretty with React, but can even mimic many of React’s APIs. And that’s before we briefly touched on Svelte’s conveniences, like built-in animations (or transitions) and the ability to convert Svelte components into bona fide web components.

I hope I’ve succeeded in sparking some interest, and if I have, there’s no shortage of docs, tutorials, online courses, etc that dive into these topics (and more). Let me know in the comments if you have any questions along the way!


The post Svelte for the Experienced React Dev appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

How to Build a FullStack Serverless HN Clone With Svelte and Fauna

Svelte is a free and open-source front end JavaScript framework that enables developers to build highly performant applications with smaller application bundles. Svelte also empowers developers with its awesome developer experience.

Svelte provides a different approach to building web apps than some of the other frameworks such as React and Vue. While frameworks like React and Vue do the bulk of their work in the user’s browser while the app is running, Svelte shifts that work into a compile step that happens only when you build your app, producing highly-optimized vanilla JavaScript.

The outcome of this approach is not only smaller application bundles and better performance, but also a developer experience that is more approachable for people that have limited experience of the modern tooling ecosystem.

Svelte sticks closely to the classic web development model of HTML, CSS, and JS, just adding a few extensions to HTML and JavaScript. It arguably has fewer concepts and tools to learn than some of the other framework options.

Project Setup

The recommended way to initialize a Svelte app is by using degit which sets up everything automatically for you.

You will be required to either have yarn or npm installed.

# for Rollup npx degit "sveltejs/sapper-template#rollup" hn-clone # for webpack npx degit "sveltejs/sapper-template#webpack" hn-clone
cd hn-clone yarn #or just npm install

Project Structure

── package.json ├── README.md ├── rollup.config.js ├── scripts │   └── setupTypeScript.js ├── src │   ├── ambient.d.ts │   ├── client.js │   ├── components │   │   └── Nav.svelte │   ├── node_modules │   │   └── images │   │   	└── successkid.jpg │   ├── routes │   │   ├── about.svelte │   │   ├── blog │   │   │   ├── index.json.js │   │   │   ├── index.svelte │   │   │   ├── _posts.js │   │   │   ├── [slug].json.js │   │   │   └── [slug].svelte │   │   ├── _error.svelte │   │   ├── index.svelte │   │   └── _layout.svelte │   ├── server.js │   ├── service-worker.js │   └── template.html ├── static │   ├── favicon.png │   ├── global.css │   ├── logo-192.png │   ├── logo-512.png │   └── manifest.json └── yarn.lock

The Application

In this tutorial, we will build a basic HN clone with the ability to create a post and comment on that post.

Setting Up Fauna

yarn add faunadb

Creating Your Own Database on Fauna

To hold all our application’s data, we will first need to create a database. Fortunately, this is just a single command or line of code, as shown below. Don’t forget to create a Fauna account before continuing!

Fauna Shell

Fauna’s API has many interfaces/clients, such as drivers in JS, GO, Java and more, a cloud console, local and cloud shells, and even a VS Code extension! For this article, we’ll start with the local Fauna Shell, which is almost 100% interchangeable with the other interfaces.

npm install -g fauna-shell

After installing the Fauna Shell with npm, log in with your Fauna credentials:

$   fauna cloud-login Email: email@example.comPassword: **********

Now we are able to create our database.

fauna create-database hn-clone

Create Collections

Now that we have our database created, it’s time to create our collections.

In Fauna, a database is made up of one or more collections. The data you create is represented as documents and saved in a collection. A collection is like an SQL table. Or rather, a collection, is a collection of documents.

A fair comparison with a traditional SQL database would be as below.

FaunaDB Terminology SQL Terminology
Database Database
Collection Table
Document Row
Index Index

For our two microservices, we will create two collections in our database. Namely:

  1. a posts collection, and
  2. a comments collection.

To start an interactive shell for querying our new database, we need to run:

fauna shell hn-clone

We can now operate our database from this shell.

$   fauna shell hn-clone  Starting shell for database hn-clone Connected to https://db.fauna.com Type Ctrl+D or .exit to exit the shell hn-clone>

To create our posts collection, run the following command in the shell to create the collection with the default configuration settings for collections.

hn-clone> CreateCollection({ name: "posts" })

Next, let’s do the same for the comments collections.

hn-clone> CreateCollection({ name: "comments" })

Creating a Posts/Feed Page

To view all our posts we will create a page to view all our posts in a time ordered feed.

In our src/routes/index.svelte file add the following content. This will create the list of all available posts that are stored in our Fauna database.

<script context="module">   import faunadb, { query as q } from "faunadb";   import Comment from "../components/Comment.svelte";   const client = new faunadb.Client({     secret: process.env.FAUNA_SECRET,   });     export async function preload(page, session) {       let posts = await client.query(       q.Get(         q.Paginate(q.Documents(q.Collection("posts"))       )     )   );   console.log(posts)     return { posts }; } </script>   <script>   import Post from "../components/Post.svelte";     export let posts;   console.log(posts); </script>   <main class="container">   {#each posts as post}     <!-- content here -->     <div class="card">       <div class="card-body">         <p>{post}</p>           <Comment postId={post.id}/>       </div>     </div>   {/each} </main>

Creating a Comments Component

To create a comment we will create a component to send our data to Fauna using the query below.

let response = await client.query(   q.Create(     q.Collection('comments'),     { data: { title: comment, post: postId } },)   )

Our final component will have the following code.

<script>    export let postId;   import faunadb, { query as q } from 'faunadb'      const client = new faunadb.Client({secret: process.env.FAUNA_SECRET})      let comment;     let postComment = async () => {     if (!comment) { return }     let response = await client.query(     q.Create(       q.Collection('comments'),       { data: { title: comment, post: postId } },     )   )          console.log(response)   } </script>   <div class="row">   <div class="col-sm-12 col-lg-6 col-sm-8">     <p></p>     <textarea type="text" class="form-control" bind:value={comment} placeholder="Comment" ></textarea>     <p></p>     <button class="btn btn-warning" style="float:right;" on:click={postComment}>Post comment</button>   </div> </div>

We run the dev server:

yarn dev

When you visit http://localhost:5000 you will be greeted with the feeds with a panel to comment on the same page.

Conclusion

For this tutorial, we are able to see how fast it can be to develop a full stack application with Fauna and Svelte.

Svelte provides a highly productive, powerful and fast framework that we can use to develop both backend and frontend components of our full stack app while using the pre-rendered framework Sapper.

Secondly, we can see how Fauna is indeed a powerful database; with a powerful FQL, which supports complex querying and integration with the serverless and JAMStack ecosystem through its API first approach. This enables developers to simplify code and ship faster.

I hope you find Fauna to be exciting, like I do, and that you enjoyed this article. Feel free to follow me on Twitter @theAmolo if you enjoyed this!


The post How to Build a FullStack Serverless HN Clone With Svelte and Fauna appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Coordinating Svelte Animations With XState

This post is an introduction to XState as it might be used in a Svelte project. XState is unique in the JavaScript ecosystem. It doesn’t keep your DOM synced with your application state, nor does it help you with asynchrony, or streams of data; XState helps manage your application’s state by allowing you to model your state as a finite state machine (FSM).

A deep dive into state machines and formal languages is beyond the scope of this post, but Jon Bellah does that in another CSS-Tricks article. For now, think of an FSM as a flow chart. Flow charts have a number of states, represented as bubbles, and arrows leading from one state to the next, signifying a transition from one state to the next. State machines can have more than one arrow leading out of a state, or none at all if it’s a final state, and they can even have arrows leaving a state, and pointing right back into that same state.

If that all sounds overwhelming, relax, we’ll get into all the details, nice and slow. For now, the high level view is that, when we model our application as a state machine, we’ll be creating different “states” our application can be in (get it … state machine … states?), and the events that happen and cause changes to state will be the arrows between those states. XState calls the states “states,” and the arrows between the states “actions.”

Our example

XState has a learning curve, which makes it challenging to teach. With too contrived a use case it’ll appear needlessly complex. It’s only when an application’s code gets a bit tangled that XState shines. This makes writing about it tricky. With that said, the example we’ll look at is an autocomplete widget (sometimes called autosuggest), or an input box that, when clicked, reveals a list of items to choose from, which filter as you type in the input.

For this post we’ll look at getting the animation code cleaned up. Here’s the starting point:

This is actual code from my svelte-helpers library, though with unnecessary pieces removed for this post. You can click the input and filter the items, but you won’t be able to select anything, “arrow down” through the items, hover, etc. I’ve removed all the code that’s irrelevant to this post.

We’ll be looking at the animation of the list of items. When you click the input, and the results list first renders, we want to animate it down. As you type and filter, changes to the list’s dimensions will animate larger and smaller. And when the input loses focus, or you click ESC, we animate the list’s height to zero, while fading it out, and then remove it from the DOM (and not before). To make things more interesting (and nice for the user), let’s use a different spring configuration for the opening than what we use for the closing, so the list closes a bit more quickly, or stiffly, so unneeded UX doesn’t linger on the screen too long.

If you’re wondering why I’m not using Svelte transitions to manage the animations in and out of the DOM, it’s because I’m also animating the list’s dimensions when it’s open, as the user filters, and coordinating between transition, and regular spring animations is a lot harder than simply waiting for a spring update to finish getting to zero before removing an element from the DOM. For example, what happens if the user quickly types and filters the list, as it’s animating in? As we’ll see, XState makes tricky state transitions like this easy.

Scoping the Problem

Let’s take a look at the code from the example so far. We’ve got an open variable to control when the list is open, and a resultsListVisible property to control whether it should be in the DOM. We also have a closing variable that controls whether the list is in the process of closing.

On line 28, there’s an inputEngaged method that runs when the input is clicked or focused. For now let’s just note that it sets open and resultsListVisible to true. inputChanged is called when the user types in the input, and sets open to true. This is for when the input is focused, the user clicks escape to close it, but then starts typing, so it can re-open. And, of course, the inputBlurred function runs when you’d expect, and sets closing to true, and open to false.

Let’s pick apart this tangled mess and see how the animations work. Note the slideInSpring and opacitySpring at the top. The former slides the list up and down, and adjusts the size as the user types. The latter fades the list out when hidden. We’ll focus mostly on the slideInSpring.

Take a look at the monstrosity of a function called setSpringDimensions. This updates our slide spring. Focusing on the important pieces, we take a few boolean properties. If the list is opening, we set the opening spring config, we immediately set the list’s width (I want the list to only slide down, not down and out), via the { hard: true } config, and then set the height. If we’re closing, we animate to zero, and, when the animation is complete, we set resultsListVisible to false (if the closing animation is interrupted, Svelte will be smart enough to not resolve the promise so the callback will never run). Lastly, this method is also called any time the size of the results list changes, i.e., as the user filters. We set up a ResizeObserver elsewhere to manage this.

Spaghetti galore

Let’s take stock of this code.

  • We have our open variable which tracks if the list is open.
  • We have the resultsListVisible variable which tracks if the list should be in the DOM (and set to false after the close animation is complete).
  • We have the closing variable that tracks if the list is in the process of closing, which we check for in the input focus/click handler so we can reverse the closing animation if the user quickly re-engages the widget before it’s done closing.
  • We also have setSpringDimensions that we call in four different places. It sets our springs depending on whether the list is opening, closing, or just resizing while open (i.e. if the user filters the list).
  • Lastly, we have a resultsListRendered Svelte action that runs when the results list DOM element renders. It starts up our ResizeObserver, and when the DOM node unmounts, sets closing to false.

Did you catch the bug? When the ESC button is pressed, I’m only setting open to false. I forgot to set closing to true, and call setSpringDimensions(false, true). This bug was not purposefully contrived for this blog post! That’s an actual mistake I made when I was overhauling this widget’s animations. I could just copy paste the code in inputBlured over to where the escape button is caught, or even move it to a new function and call it from both places. This bug isn’t fundamentally hard to solve, but it does increase the cognitive load of the code.

There’s a lot of things we’re keeping track of, but worst of all, this state is scattered all throughout the module. Take any piece of state described above, and use CodeSandbox’s Find feature to view all the places where that piece of state is used. You’ll see your cursor bouncing across the file. Now imagine you’re new to this code, trying to make sense of it. Think about the growing mental model of all these state pieces that you’ll have to keep track of, figuring out how it works based on all the places it exists. We’ve all been there; it sucks. XState offers a better way; let’s see how.

Introducing XState

Let’s step back a bit. Wouldn’t it be simpler to model our widget in terms of what state it’s in, with events happening as the user interacts, which cause side effects, and transitions to new states? Of course, but that’s what we were already doing; the problem is, the code is scattered everywhere. XState gives us the ability to properly model our state in this way.

Setting expectations

Don’t expect XState to magically make all of our complexity vanish. We still need to coordinate our springs, adjust the spring’s config based on opening and closing states, handle resizes, etc. What XState gives us is the ability to centralize this state management code in a way that’s easy to reason about, and adjust. In fact, our overall line count will increase a bit, as a result of our state machine setup. Let’s take a look.

Your first state machine

Let’s jump right in, and see what a bare bones state machine looks like. I’m using XState’s FSM package, which is a minimal, pared down version of XState, with a tiny 1KB bundle size, perfect for libraries (like an autosuggest widget). It doesn’t have a lot of advanced features like the full XState package, but we wouldn’t need them for our use case, and we wouldn’t want them for an introductory post like this.

The code for our state machine is below, and the interactive demo is over at Code Sandbox. There’s a lot, but we’ll go over it shortly. And to be clear, it doesn’t work yet.

const stateMachine = createMachine(   {     initial: "initial",     context: {       open: false,       node: null     },     states: {       initial: {         on: { OPEN: "open" }       },       open: {         on: {           RENDERED: { actions: "rendered" },           RESIZE: { actions: "resize" },           CLOSE: "closing"         },         entry: "opened"       },       closing: {         on: {           OPEN: { target: "open", actions: ["resize"] },           CLOSED: "closed"         },         entry: "close"       },       closed: {         on: {           OPEN: "open"         },         entry: "closed"       }     }   },   {     actions: {       opened: assign(context => {         return { ...context, open: true };       }),       rendered: assign((context, evt) => {         const { node } = evt;         return { ...context, node };       }),       close() {},       resize(context) {},       closed: assign(() => {         return { open: false, node: null };       })     }   } );

Let’s go from top to bottom. The initial property controls what the initial state is, which I’ve called “initial.” context is the data associated with our state machine. I’m storing a boolean for whether the results list is currently open, as well as a node object for that same results list. Next we see our states. Each state is a key in the states property. For most states, you can see we have an on property, and an entry property.

on configures events. For each event, we can transition to a new state; we can run side effects, called actions; or both. For example, when the OPEN event happens inside of the initial state, we move into the open state. When the RENDERED event happens in the open state, we run the rendered action. And when the OPEN event happens inside the closing state, we transition into the open state, and also run the resize action. The entry field you see on most states configures an action to run automatically whenever a state is entered. There are also exit actions, although we don’t need them here.

We still have a few more things to cover. Let’s look at how our state machine’s data, or context, can change. When we want an action to modify context, we wrap it in assign and return the new context from our action; if we don’t need any processing, we can just pass the new state directly to assign. If our action does not update context, i.e., it’s just for side effects, then we don’t wrap our action function in assign, and just perform whatever side effects we need.

Affecting change in our state machine

We have a cool model for our state machine, but how do we run it? We use the interpret function.

const stateMachineService = interpret(stateMachine).start();

Now stateMachineService is our running state machine, on which we can invoke events to force our transitions and actions. To fire an event, we call send, passing the event name, and then, optionally, the event object. For example, in our Svelte action that runs when the results list first mounts in the DOM, we have this:

stateMachineService.send({ type: "RENDERED", node });

That’s how the rendered action gets the node for the results list. If you look around the rest of the AutoComplete.svelte file, you’ll see all the ad hoc state management code replaced with single line event dispatches. In the event handler for our input click/focus, we run the OPEN event. Our ResizeObserver fires the RESIZE event. And so on.

Let’s pause for a moment and appreciate the things XState gives us for free here. Let’s look at the handler that runs when our input is clicked or focused before we added XState.

function inputEngaged(evt) {   if (closing) {     setSpringDimensions();   }   open = true;   resultsListVisible = true; } 

Before, we were checking to see if we were closing, and if so, forcing a re-calculation of our sliding spring. Otherwise we opened our widget. But what happened if we clicked on the input when it was already open? The same code re-ran. Fortunately that didn’t really matter. Svelte doesn’t care if we re-set open and resultsListVisible to the values they already held. But those concerns disappear with XState. The new version looks like this:

 function inputEngaged(evt) {   stateMachineService.send("OPEN"); }

If our state machine is already in the open state, and we fire the OPEN event, then nothing happens, since there’s no OPEN event configured for that state. And that special handling for when the input is clicked when the results are closing? That’s also handled right in the state machine config — notice how the OPEN event tacks on the resize action when it’s run from the closing state.

And, of course, we’ve fixed the ESC key bug from before. Now, pressing the key simply fires the CLOSE event, and that’s that.

Finishing up

The ending is almost anti-climactic. We need to take all of the work we were doing before, and simply move it to the right place among our actions. XState does not remove the need for us to write code; it only provides a structured, clear place to put it.

{   actions: {     opened: assign({ open: true }),     rendered: assign((context, evt) => {       const { node } = evt;       const dimensions = getResultsListDimensions(node);       itemsHeightObserver.observe(node);       opacitySpring.set(1, { hard: true });       Object.assign(slideInSpring, SLIDE_OPEN);       slideInSpring.update(prev => ({ ...prev, width: dimensions.width }), {         hard: true       });       slideInSpring.set(dimensions, { hard: false });       return { ...context, node };     }),     close() {       opacitySpring.set(0);       Object.assign(slideInSpring, SLIDE_CLOSE);       slideInSpring         .update(prev => ({ ...prev, height: 0 }))         .then(() => {           stateMachineService.send("CLOSED");         });     },     resize(context) {       opacitySpring.set(1);       slideInSpring.set(getResultsListDimensions(context.node));     },     closed: assign(() => {       itemsHeightObserver.unobserve(resultsList);       return { open: false, node: null };     })   } }

Odds and ends

Our animation state is in our state machine, but how do we get it out? We need the open state to control our results list rendering, and, while not used in this demo, the real version of this autosuggest widget needs the results list DOM node for things like scrolling the currently highlighted item into view.

It turns out our stateMachineService has a subscribe method that fires whenever there’s a state change. The callback you pass is invoked with the current state machine state, which includes a context object. But Svelte has a special trick up its sleeve: its reactive syntax of $ : doesn’t only work with component variables and Svelte stores; it also works with any object with a subscribe method. That means we can sync with our state machine with something as simple as this:

$  : ({ open, node: resultsList } = $  stateMachineService.context);

Just a regular destructuring, with some parens to help things get parsed correctly.

One quick note here, as an area for improvement. Right now, we have some actions which both both perform a side effect, and also update state. Ideally, we should probably split these up into two actions, one just for the side effect, and the other using assign for the new state. But I decided to keep things as simple as possible for this article to help ease the introduction of XState, even if a few things wound up not being quite ideal.

Parting thoughts

I hope this post has sparked some interest in XState. I’ve found it to be an incredibly useful, easy to use tool for managing complex state. Please know that we’ve only scratched the surface. We focused on the minimal fsm package, but the entire XState library is capable of a lot more than what we covered here, from nested states, to first-class support for Promises, and it even has a state visualization tool! I urge you to check it out.

Happy coding!


The post Coordinating Svelte Animations With XState appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

How to Use Tailwind on a Svelte Site

Let’s spin up a basic Svelte site and integrate Tailwind into it for styling. One advantage of working with Tailwind is that there isn’t any context switching going back and forth between HTML and CSS, since you’re applying styles as classes right on the HTML. It’s all the in same file in Svelte anyway, but still, this way you don’t even need a <style> section in your .svelte files.

If you are a Svelte developer or enthusiast, and you’d like to use Tailwind CSS in your Svelte app, this article looks at the easiest, most-straightforward way to install tailwind in your app and hit the ground running in creating a unique, modern UI for your app.

If you like to just see a working example, here’s a working GitHub repo.

Why Svelte?

Performance-wise, Svelte is widely considered to be one of the top JavaScript frameworks on the market right now. Created by Rich Harris in 2016, it has been growing rapidly and becoming popular in the developer community. This is mainly because, while very similar to React (and Vue), Svelte is much faster. When you create an app with React, the final code at build time is a mixture of React and vanilla JavaScript. But browsers only understand vanilla JavaScript. So when a user loads your app in a browser (at runtime), the browser has to download React’s library to help generate the app’s UI. This slows down the process of loading the app significantly.

How’s Svelte different? It comes with a compiler that compiles all your app code into vanilla JavaScript at build time. No Svelte code makes it into the final bundle. In this instance, when a user loads your app, their browser downloads only vanilla JavaScript files, which are lighter. No framework UI library is needed. This significantly speeds up the process of loading your app. For this reason, Svelte applications are usually very small and lightning fast.

The only downside Svelte currently faces is that since it’s still new and doesn’t have the kind of ecosystem and community backing that more established frameworks like React enjoy.

Why Tailwind?

Tailwind CSS is a CSS framework. It’s somewhat similar to popular frameworks, like Bootstrap and Materialize, in that you apply classes to elements and it styles them. But it is also atomic CSS in that one class name does one thing. While Tailwind does have Tailwind UI for pre-built componentry, generally you customize Tailwind to look how you want it to look, so there is less risk of “looking like a Bootstrap site” (or whatever other framework that is less commonly customized).

For example, rather than give you a generic header component that comes with some default font sizes, margins, paddings, and other styling, Tailwind provides you with utility classes for different font sizes, margins, and paddings. You can pick the specific ones you want and create a unique looking header with them.

Tailwind has other advantages as well:

  • It saves you the time and stress of writing custom CSS yourself. With Tailwind, you get thousands of out-of-the-box CSS classes that you just need to apply to your HTML elements.
  • One thing most users of Tailwind appreciate is the naming convention of the utility classes. The names are simple and they do a good job of telling you what their functions are. For example, text-sm gives your text a small font size**.** This is a breath of fresh air for people that struggle with naming custom CSS classes.
  • By utilizing a mobile-first approach, responsiveness is at the heart of Tailwind’s design. Making use of the sm, md, and lg prefixes to specify breakpoints, you can control the way styles are rendered across different screen sizes. For example, if you use the md prefix on a style, that style will only be applied to medium-sized screens and larger. Small screens will not be affected.
  • It prioritizes making your application lightweight by making PurgeCSS easy to set up in your app. PurgeCSS is a tool that runs through your application and optimizes it by removing all unused CSS classes, significantly reducing the size of your style file. We’ll use PurgeCSS in our practice project.

All this said Tailwind might not be your cup of tea. Some people believe that adding lots of CSS classes to your HTML elements makes your HTML code difficult to read. Some developers even think it’s bad practice and makes your code ugly. It’s worth noting that this problem can easily be solved by abstracting many classes into one using the @apply directive, and applying that one class to your HTML, instead of the many.

Tailwind might also not be for you if you are someone who prefers ready-made components to avoid stress and save time, or you are working on a project with a short deadline.

Step 1: Scaffold a new Svelte site

Svelte provides us with a starter template we can use. You can get it by either cloning the Svelte GitHub repo, or by using degit. Using degit provides us with certain advantages, like helping us make a copy of the starter template repository without downloading its entire Git history (unlike git clone). This makes the process faster. Note that degit requires Node 8 and above.

Run the following command to clone the starter app template with degit:

npx degit sveltejs/template project-name

Navigate into the directory of the starter project so we can start making changes to it:

cd project-name

The template is mostly empty right now, so we’ll need to install some required npm packages:

npm install

Now that you have your Svelte app ready, you can proceed to combining it with Tailwind CSS to create a fast, light, unique web app.

Step 2: Adding Tailwind CSS

Let’s proceed to adding Tailwind CSS to our Svelte app, along with some dev dependencies that will help with its setup.

npm install tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9   # or  yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9

The three tools we are downloading with the command above:

  1. Tailwind
  2. PostCSS
  3. Autoprefixer

PostCSS is a tool that uses JavaScript to transform and improve CSS. It comes with a bunch of plugins that perform different functions like polyfilling future CSS features, highlighting errors in your CSS code, controlling the scope of CSS class names, etc.

Autoprefixer is a PostCSS plugin that goes through your code adding vendor prefixes to your CSS rules (Tailwind does not do this automatically), using caniuse as reference. While browsers are choosing to not use prefixing on CSS properties the way they had in years past, some older browsers still rely on them. Autoprefixer helps with that backwards compatibility, while also supporting future compatibility for browsers that might apply a prefix to a property prior to it becoming a standard.

For now, Svelte works with an older version of PostCSS. Its latest version, PostCSS 8, was released September 2020. So, to avoid getting any version-related errors, our command above specifies PostCSS 7 instead of 8. A PostCSS 7 compatibility build of Tailwind is made available under the compat channel on npm.

Step 3: Configuring Tailwind

Now that we have Tailwind installed, let’s create the configuration file needed and do the necessary setup. In the root directory of your project, run this to create a tailwind.config.js file:

npx tailwindcss init  tailwind.config.js

Being a highly customizable framework, Tailwind allows us to easily override its default configurations with custom configurations inside this tailwind.config.js file. This is where we can easily customize things like spacing, colors, fonts, etc.

The tailwind.config.js file is provided to prevent ‘fighting the framework’ which is common with other CSS libraries. Rather than struggling to reverse the effect of certain classes, you come here and specify what you want. It’s in this file that we also define the PostCSS plugins used in the project.

The file comes with some default code. Open it in your text editor and add this compatibility code to it:

future: {   purgeLayersByDefault: true,   removeDeprecatedGapUtilities: true, },

Tailwind 2.0 (the latest version), all layers (e.g., base, components, and utilities) are purged by default. In previous versions, however, just the utilities layer is purged. We can manually configure Tailwind to purge all layers by setting the purgeLayersByDefault flag to true.

Tailwind 2.0 also removes some gap utilities, replacing them with new ones. We can manually remove them from our code by setting removeDeprecatedGapUtilities to true.

These will help you handle deprecations and breaking changes from future updates.

PurgeCSS

The several thousand utility classes that come with Tailwind are added to your project by default. So, even if you don’t use a single Tailwind class in your HTML, your project still carries the entire library, making it rather bulky. We’ll want our files to be as small as possible in production, so we can use purge to remove all of the unused utility classes from our project before pushing the code to production.

Since this is mainly a production problem, we specify that purge should only be enabled in production.

purge: {   content: [     "./src/**/*.svelte",   ],   enabled: production // disable purge in dev },

Now, your tailwind.config.js should look like this:

const production = !process.env.ROLLUP_WATCH; module.exports = {   future: {     purgeLayersByDefault: true,     removeDeprecatedGapUtilities: true,   },   plugins: [    ],   purge: {     content: [      "./src/**/*.svelte",      ],     enabled: production // disable purge in dev   }, };

Rollup.js

Our Svelte app uses Rollup.js, a JavaScript module bundler made by Rich Harris, the creator of Svelte, that is used for compiling multiple source files into one single bundle (similar to webpack). In our app, Rollup performs its function inside a configuration file called rollup.config.js.

With Rollup, We can freely break our project up into small, individual files to make development easier. Rollup also helps to lint, prettify, and syntax-check our source code during bundling.

Step 4: Making Tailwind compatible with Svelte

Navigate to rollup.config.js and import the sveltePreprocess package. This package helps us handle all the CSS processing required with PostCSS and Tailwind.

import sveltePreprocess from "svelte-preprocess";

Under plugins, add sveltePreprocess and require Tailwind and Autoprefixer, as Autoprefixer will be processing the CSS generated by these tools.

preprocess: sveltePreprocess({   sourceMap: !production,   postcss: {     plugins: [      require("tailwindcss"),       require("autoprefixer"),     ],   }, }),

Since PostCSS is an external tool with a syntax that’s different from Svelte’s framework, we need a preprocessor to process it and make it compatible with our Svelte code. That’s where the sveltePreprocess package comes in. It provides support for PostCSS and its plugins. We specify to the sveltePreprocess package that we are going to require two external plugins from PostCSS, Tailwind and Autoprefixer. sveltePreprocess runs the foreign code from these two plugins through Babel and converts them to code supported by the Svelte compiler (ES6+). Rollup eventually bundles all of the code together.

The next step is to inject Tailwind’s styles into our app using the @tailwind directive. You can think of @tailwind loosely as a function that helps import and access the files containing Tailwind’s styles. We need to import three sets of styles.

The first set of styles is @tailwind base. This injects Tailwind’s base styles—mostly pulled straight from Normalize.css—into our CSS. Think of the styles you commonly see at the top of stylesheets. Tailwind calls these Preflight styles. They are provided to help solve cross-browser inconsistencies. In other words, they remove all the styles that come with different browsers, ensuring that only the styles you employ are rendered. Preflight helps remove default margins, make headings and lists unstyled by default, and a host of other things. Here’s a complete reference of all the Preflight styles.

The second set of styles is @tailwind components. While Tailwind is a utility-first library created to prevent generic designs, it’s almost impossible to not reuse some designs (or components) when working on a large project. Think about it. The fact that you want a unique-looking website doesn’t mean that all the buttons on a page should be designed differently from each other. You’ll likely use a button style throughout the app.

Follow this thought process. We avoid frameworks, like Bootstrap, to prevent using the same kind of button that everyone else uses. Instead, we use Tailwind to create our own unique button. Great! But we might want to use this nice-looking button we just created on different pages. In this case, it should become a component. Same goes for forms, cards, badges etc.

All the components you create will eventually be injected into the position that @tailwind components occupies. Unlike other frameworks, Tailwind doesn’t come with lots of predefined components, but there are a few. If you aren’t creating components and plan to only use the utility styles, then there’s no need to add this directive.

And, lastly, there’s @tailwind utilities. Tailwind’s utility classes are injected here, along with the ones you create.

Step 5: Injecting Tailwind Styles into Your Site

It’s best to inject all of the above into a high-level component so they’re accessible on every page. You can inject them in the App.svelte file:

<style global lang="postcss">   @tailwind base;   @tailwind components;   @tailwind utilities; </style>

Now that we have Tailwind set up in, let’s create a website header to see how tailwind works with Svelte. We’ll create it in App.svelte, inside the main tag.

This is what we have so far in the browser. Tailwind gives us everything we need to customize this into something unique, like the header we’re about to create.

Step 6: Creating A Website Header

Starting with some basic markup:

<nav>   <div>     <div>       <a href="#">APP LOGO</a>        <!-- Menus -->       <div>         <ul>           <li>             <a href="#">About</a>           </li>           <li>             <a href="#">Services</a>           </li>           <li>             <a href="#">Blog</a>           </li>           <li>             <a href="#">Contact</a>           </li>         </ul>       </div>      </div>   </div> </nav>

This is the header HTML without any Tailwind CSS styling. Pretty standard stuff. We’ll wind up moving the “APP LOGO” to the left side, and the four navigation links on the right side of it.

What we have with zero styling whatsoever.

Now let’s add some Tailwind CSS to it:

<nav class="bg-blue-900 shadow-lg">   <div class="container mx-auto">     <div class="sm:flex">       <a href="#" class="text-white text-3xl font-bold p-3">APP LOGO</a>              <!-- Menus -->       <div class="ml-55 mt-4">         <ul class="text-white sm:self-center text-xl">           <li class="sm:inline-block">             <a href="#" class="p-3 hover:text-red-900">About</a>           </li>           <li class="sm:inline-block">             <a href="#" class="p-3 hover:text-red-900">Services</a>           </li>           <li class="sm:inline-block">             <a href="#" class="p-3 hover:text-red-900">Blog</a>           </li>           <li class="sm:inline-block">             <a href="#" class="p-3 hover:text-red-900">Contact</a>           </li>         </ul>       </div>      </div>   </div> </nav>

OK, let’s break down all those classes we just added to the HTML. First, let’s look at the <nav> element:

<nav class="bg-blue-900 shadow-lg">

We apply the class bg-blue-900 gives our header a blue background with a shade of 900, which is dark. The class shadow-lg class applies a large outer box shadow. The shadow effect this class creates will be 0px at the top, 10px on the right, 15px at the bottom, and -3px on the left.

Next is the first div, our container for the logo and navigation links:

<div class="container mx-auto">

To center it and our navigation links, we use the mx-auto class. It’s equivalent to margin: auto, horizontally centering an element within its container.

Onto the next div:

<div class="sm:flex">

By default, a div is a block-level element. We use the sm:flex class to make our header a block-level flex container, so as to make its children responsive (to enable them shrink and expand easily). We use the sm prefix to ensure that the style is applied to all screen sizes (small and above).

Alright, the logo:

<a href="#" class="text-white text-3xl font-bold p-3">APP LOGO</a>

The text-white class, true to its name, make the text of the logo white. The text-3xl class sets the font size of our logo (which is configured to 1.875rem)and its line height (configured to 2.25rem). From there, p-3 sets a padding of 0.75rem on all sides of the logo.

That takes us to:

<div class="ml-55 mt-4">

We’re giving the navigation links a left margin of 55% to move them to the right. However, there’s no Tailwind class for this, so we’ve created a custom style called ml-55, a name that’s totally made up but stands for “margin-left 55%.”

It’s one thing to name a custom class. We also have to add it to our style tags:

.ml-55 {   margin-left: 55%; }

There’s one more class in there: mt-4. Can you guess what it does? If you guessed that it seta a top margin, then you are correct! In this case, it’s configured to 1rem for our navigation links.

Next up, the navigation links are wrapped in an unordered list tag that contains a few classes:

<ul class="text-white sm:self-center text-xl">

We’re using the text-white class again, followed by sm:self-center to center the list—again, we use the sm prefix to ensure that the style is applied to all screen sizes (small and above). Then there’s text-xl which is the extra-large configured font size.

For each list item:

<li class="sm:inline-block">

The sm:inline-block class sets each list item as an inline block-level element, bringing them side-by-side.

And, lastly, the link inside each list item:

<a href="#" class="p-3 hover:text-red-900">

We use the utility class hover:text-red-900 to make each red on hover.

Let’s run our app in the command line:

npm run dev 

This is what we should get:

And that is how we used Tailwind CSS with Svelte in six little steps!

Conclusion

My hope is that you now know how to integrate Tailwind CSS into our Svelte app and configure it. We covered some pretty basic styling, but there’s always more to learn! Here’s an idea: Try improving the project we worked on by adding a sign-up form and a footer to the page. Tailwind provides comprehensive documentation on all its utility classes. Go through it and familiarize yourself with the classes.

Do you learn better with video? Here are a couple of excellent videos that also go into the process of integrating Tailwind CSS with Svelte.


The post How to Use Tailwind on a Svelte Site appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Svelte and Spring Animations

Spring animations are a wonderful way to make UI interactions come to life. Rather than merely changing a property at a constant rate over a period of time, springs allow us to move things using spring physics, which gives the impression of a real thing moving, and can appear more natural to users.

I’ve written about spring animations previously. That post was based on React, using react-spring for the animations. This post will explore similar ideas in Svelte.

CSS devs! It’s common to think of easing when it comes to controling the feel of animations. You could think of “spring” animations as a subcategory of easing that are based on real-world physics.

Svelte actually has springs built into the framework, without needing any external libraries. We’ll rehash what was covered in the first half my previous post on react-spring. But after that, we’ll take a deep-dive into all the ways these springs can be used with Svelte, and leave the real world implementation for a future post. While that may seem disappointing, Svelte has a number of wonderful, unique features with no counterpart in React, which can be effectively integrated with these animation primitives. We’re going to spend some time talking about them.

One other note: Some of the demos sprinkled throughout may look odd because I configured the springs to be extra “bouncy” to create more obvious effect. If you the code for any of them, be sure to find a spring configuration that works for you.

Here’s a wonderful REPL Rich Harris made to show all the various spring configurations, and how they behave.

A quick primer on Svelte Stores

Before we start, let’s take a very, very quick tour of Svelte stores. While Svelte’s components are more than capable of storing and updating state, Svelte also has the concept of a store, which allows you to store state outside of a component. Since Svelte’s Spring API uses Stores, we’ll quickly introduce the salient parts here.

To create an instance of a store, we can import the writable type, and create it like so:

import { writable } from "svelte/store"; const clicks = writable(0);

The clicks variable is a store that has a value of 0. There’s two ways to set a new value of a store: the set and update methods. The former receives the value to which you’re setting the store, while the latter receives a callback, accepting the current value, and returning the new value.

function increment() {   clicks.update(val => val + 1); } function setTo5() {   clicks.set(5); }

State is useless if you can’t actually consume it. For this, stores offer a subscribe method, which allows you to be notified of new values — but when using it inside of a component, you can prefix the store’s name with the $ character, which tells Svelte to not only display the current value of the store, but to update when it changes. For example:

<h1>Value {$  clicks}</h1> <button on:click={increment}>Increment</button> <button on:click={setTo5}>Set to 5</button>

Here’s a full, working example of this code. Stores offer a number of other features, such as derived stores, which allow you to chain stores together, readable stores, and even the ability to be notified when a store is first observed, and when it no longer has observers. But for the purposes of this post, the code shown above is all we need to worry about. Consult the Svelte docs or interactive tutorial for more info.

A crash course on springs

Let’s walk through a quick introduction of springs, and what they accomplish. We’ll take a look at a simple UI that changes a presentational aspect of some elements — opacity and transform — and then look at animating that change.

This is a minimal Svelte component that toggles the opacity of one <div>, and toggles the x-axis transform of another (without any animation).

<script>   let shown = true;   let moved = 0;    const toggleShow = () => (shown = !shown);   const toggleMove = () => (moved = moved ? 0 : 500); </script>  <div style="opacity: {shown ? 1 : 0}">Content to toggle</div> <br /> <button on:click={toggleShow}>Toggle</button> <hr /> <div class="box" style="transform: translateX({moved}px)">I'm a box.</div> <br /> <button on:click={toggleMove}>Move it!</button>

These changes are applied instantly, so let’s look at animating them. This is where springs come in. In Svelte, a spring is a store that we set the desired value on, but instead of instantly changing, the store internally uses spring physics to gradually change the value. We can then bind our UI to this changing value, to get a nice animation. Let’s see it in action.

<script>   import { spring } from "svelte/motion";    const fadeSpring = spring(1, { stiffness: 0.1, damping: 0.5 });   const transformSpring = spring(0, { stiffness: 0.2, damping: 0.1 });    const toggleFade = () => fadeSpring.update(val => (val ? 0 : 1));   const toggleTransform = () => transformSpring.update(val => (val ? 0 : 500));   const snapTransform = () => transformSpring.update(val => val, { hard: true }); </script>  <div style="opacity: {$  fadeSpring}">Content to fade</div> <br /> <button on:click={toggleFade}>Fade Toggle</button>  <hr />  <div class="box" style="transform: translateX({$  transformSpring}px)">I'm a box.</div> <br /> <button on:click={toggleTransform}>Move it!</button> <button on:click={snapTransform}>Snap into place</button>

We get our spring function from Svelte, and set up different spring instances for our opacity, and transform animations. The transform spring config is purposefully set up to be extra springy, to help show later how we can temporarily turn off spring animations, and instantly apply desired changes (which will come in handy later). At the end of the script block are our click handlers for setting the desired properties. Then, in the HTML, we bind our changing values directly to our elements… and that’s it! That’s all there is to basic spring animations in Svelte.

The only remaining item is the snapTransform function, where we set our transform spring to its current value, but also pass an object as the second argument, with hard: true. This has the effect of immediately applying the desired value with no animation at all.

This demo, as well as the rest of the basic examples we’ll look at in this post, is here:

Animating height

Animating height is trickier than other CSS properties, since we have to know the actual height to which we’re animating. Sadly, we can’t animate to a value of auto. That wouldn’t make sense for a spring, since the spring needs a real number so it can interpolate the correct values via spring physics. And as it happens, you can’t even animate auto height with regular CSS transitions. Fortunately, the web platform gives us a handy tool for getting the height of an element: a ResizeObserver, which enjoys pretty good support among browsers.

Let’s start with a raw height animation of an element, producing a “slide down” effect that we gradually refine in other examples. We’ll be using ResizeObserver to bind to an element’s height. I should note that Svelte does have an offsetHeight binding that can be used to more directly bind an element’s height, but it’s implemented with some <iframe> hacks that cause it to only work on elements that can receive children. This would probably be good enough for most use cases, but I’ll use a ResizeObserver because it allows some nice abstractions in the end.

First, we’ll bind an element’s height. It’ll receive the element and return a writable store that initializes a ResizeObserver, which updates the height value on change. Here’s what that looks like:

export default function syncHeight(el) {   return writable(null, (set) => {     if (!el) {       return;     }     let ro = new ResizeObserver(() => el && set(el.offsetHeight));     ro.observe(el);     return () => ro.disconnect();   }); }

We’re starting the store with a value of null, which we’ll interpret as “haven’t measured yet.” The second argument to writable is called by Svelte when the store becomes active, which it will be as soon as it’s used in a component. This is when we fire up the ResizeObserver and start observing the element. Then, we return a cleanup function, which Svelte calls for us when the store is no longer being used anywhere.

Let’s see this in action:

<script>   import syncHeight from "../syncHeight";   import { spring } from "svelte/motion";    let el;   let shown = false;   let open = false;   let secondParagraph = false;    const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });   $  : heightStore = syncHeight(el);   $  : heightSpring.set(open ? $  heightStore || 0 : 0);    const toggleOpen = () => (open = !open);   const toggleSecondParagraph = () => (secondParagraph = !secondParagraph); </script>  <button on:click={ toggleOpen }>Toggle</button> <button on:click={ toggleSecondParagraph }>Toggle More</button> <div style="overflow: hidden; height: { $  heightSpring }px">   <div bind:this={el}>     <div>...</div>     <br />     {#if secondParagraph}     <div>...</div>     {/if}   </div> </div>

Our el variable holds the element we’re animating. We tell Svelte to set it to the DOM element via bind:this={el}. heightSpring is our spring that holds the height value of the element when it’s open, and zero when it’s closed. Our heightStore is what keeps it up to date with the element’s current height. el is initially undefined, and syncHeight returns a junk writable store that basically does nothing. As soon as el is assigned to the <div> node, that line will re-fire — thanks to the $ : syntax — and get our writable store with the ResizeObserver listening.

Then, this line:

$  : heightSpring.set(open ? $  heightStore || 0 : 0);

…listens for changes to the open value, and also changes to the height value. In either case, it updates our spring store. We bind the height in HTML, and we’re done!

Be sure to remember to set overflow to hidden on this outer element so the contents are properly clipped as the elements toggles between its opened and closed states. Also, changes to the element’s height also animate into place, which you can see with the “Toggle More” button. You can run this in the embedded demo in the previous section.

Note that this line above:

$  : heightStore = syncHeight(el);

…currently causes an error when using server-side rendering (SSR), as explained in this bug. If you’re not using SSR you don’t need to worry about it, and of course by the time you read this that bug may have been fixed. But the workaround is to merely do this:

let heightStore; $  : heightStore = syncHeight(el);

…which works but is hardly ideal.

We probably don’t want the <div> to spring open on first render. Also, the opening spring effect is nice, but when closing, the effect is janky due to some content flickering. We can fix that. To prevent our initial render from animating, we can use the { hard: true } option we saw earlier. Let’s change our call to heightSpring.set to this:

$  : heightSpring.set(open ? $  heightStore || 0 : 0, getConfig($  heightStore));

…and then see about writing a getConfig function that returns an object with the hard property that was set to true for the first render. Here’s what I came up with:

let shown = false;  const getConfig = val => {   let active = typeof val === "number";   let immediate = !shown && active;   //once we've had a proper height registered, we can animate in the future   shown = shown || active;   return immediate ? { hard: true } : {}; };

Remember, our height store initially holds null and only gets a number when the ResizeObserver starts running. We capitalize on this by checking for an actual number. If we have a number, and we haven’t yet shown anything, then we know to show our content immediately, and we we do that by setting the immediate value. That value ultimately triggers the hard config value in the spring, which we saw before.

Now let’s tweak the animation to be a bit less, well, springy when we close our content. That way, things won’t flicker when they close. When we initially created our spring, we specified stiffness and damping, like so

const heightSpring = spring(0, { stiffness: 0.1, damping: 0.3 });

It turns out the spring object itself maintains those properties, which can be set anytime. Let’s update this line:

$  : heightSpring.set(open ? $  heightStore || 0 : 0, getConfig($  heightStore));

That detects changes to the open value (and the heightStore itself) to update the spring. Let’s also update the spring’s settings based on whether we’re opening or closing. Here’s what it looks like:

$  : {   heightSpring.set(open ? $  heightStore || 0 : 0, getConfig($  heightStore));   Object.assign(     heightSpring,     open ? { stiffness: 0.1, damping: 0.3 } : { stiffness: 0.1, damping: 0.5 }   ); }

Now when we get a new open or height value, we call heightSpring.set just like before, but we also set stiffness and damping values on the spring that are applied based on whether the element is open. If it’s closed, we set damping up to 0.5, which reduces the springiness. Of course, you’re welcome to tweak all these values and configure them as you’d like! You can see this in the “Animate Height Different Springs” section of the demo.

You might notice our code is starting to grow pretty quickly. We’ve added a lot of boilerplate to cover some of these use cases, so let’s clean things up. Specifically, we’ll make a function that creates our spring and that also exports a sync function to handle our spring config, initial render, etc.

import { spring } from "svelte/motion";  const OPEN_SPRING = { stiffness: 0.1, damping: 0.3 }; const CLOSE_SPRING = { stiffness: 0.1, damping: 0.5 };  export default function getHeightSpring() {   const heightSpring = spring(0);   let shown = false;    const getConfig = (open, val) => {     let active = typeof val === "number";     let immediate = open && !shown && active;     // once we've had a proper height registered, we can animate in the future     shown = shown || active;     return immediate ? { hard: true } : {};   };    const sync = (open, height) => {     heightSpring.set(open ? height || 0 : 0, getConfig(open, height));     Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);   };    return { sync, heightSpring }; }

There’s a lot of code here, but it’s all the code we’ve been writing so far, just packaged into a single function. Now our code to use this animation is simplified to just this

const { heightSpring, sync } = getHeightSpring(); $  : heightStore = syncHeight(el); $  : sync(open, $  heightStore);

You can see in the “Animate Height Cleanup” section of the demo.

Some Svelte-specific tricks

Let’s pause for a moment and consider some ways Svelte differs from React, and how we might leverage that to improve what we have even further.

First, the stores we’ve been using to hold springs and change height values are, unlike React’s hooks, not tied to component rendering. They’re plain JavaScript objects that can be consumed anywhere. And, as alluded to above, we can imperatively subscribe to them so that they manually observe changing values.

Svelte also something called actions. These are functions that can be added to a DOM element. When the element is created, Svelte calls the function and passes the element as the first argument. We can also specify additional arguments for Svelte to pass, and provide an update function for Svelte to re-run when those values change. Another thing we can do is provide a cleanup function for Svelte to call when it destroys the element.

Let’s put these tools together in a single action that we can simply drop onto an element to handle all the animation we’ve been writing so far:

export default function slideAnimate(el, open) {   el.parentNode.style.overflow = "hidden";    const { heightSpring, sync } = getHeightSpring();   const doUpdate = () => sync(open, el.offsetHeight);   const ro = new ResizeObserver(doUpdate);    const springCleanup = heightSpring.subscribe((height) => {     el.parentNode.style.height = `$  { height }px`;   });    ro.observe(el);    return {     update(isOpen) {       open = isOpen;       doUpdate();     },     destroy() {       ro.disconnect();       springCleanup();     }   }; }

Our function is called with the element we want to animate, as well as the open value. We’ll set the element’s parent to have overflow: hidden. Then we use the same getHeightSpring function from before, set up our ResizeObserver, etc. The real magic is here.

const springCleanup = heightSpring.subscribe((height) => {   el.parentNode.style.height = `$  {height}px`; });

Instead of binding our heightSpring to the DOM, we manually subscribe to changes, then set the height ourselves, manually. We wouldn’t normally do manual DOM updates when using a JavaScript framework like Svelte but, in this case, it’s for a helper library, which is just fine in my opinion.

In the object we’re returning, we define an update function which Svelte will call when the open value changes. We update the original argument to this function, which the function closes over ( i.e. creates a closure around) and then calls our update function to sync everything. Svelte calls the destroy function when our DOM node is destroyed.

Best of all, using this action is a snap:

<div use:slideAnimate={open}>

That’s it. When open changes, Svelte calls our update function.

Before we move on, let’s make one other tweak. Notice how we remove the springiness by changing the spring config when we collapse the pane with the “Toggle” button; however, when we make the element smaller by clicking the “Toggle More” button, it shrinks with the usual springiness. I dislike that, and prefer shrinking sizes move with the same physics we’re using for collapsing.

Let’s start by removing this line in the getHeightSpring function:

Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);

That line is inside the sync function that getHeightSpring created, which updates our spring settings on every change, based on the open value. With it gone, we can start our spring with the “open” spring config:

const heightSpring = spring(0, OPEN_SPRING);

Now let’s change our spring settings when either the height of our content changes, or when the open value changes. We already have the ability to observe both of those things changing — our ResizeObserver callback fires when the size of the content changes, and the update function of our action fires whenever open changes.

Our ResizeObserver callback can be changed, like this:

let currentHeight = null; const ro = new ResizeObserver(() => {   const newHeight = el.offsetHeight;   const bigger = newHeight > currentHeight;    if (typeof currentHeight === "number") {     Object.assign(heightSpring, bigger ? OPEN_SPRING : CLOSE_SPRING);   }   currentHeight = newHeight;   doUpdate(); });

currentHeight holds the current value, and we check it on size changes to see which direction we’re moving. Next up is the update function. Here’s what it looks like after our change:

update(isOpen) {   open = isOpen;   Object.assign(heightSpring, open ? OPEN_SPRING : CLOSE_SPRING);   doUpdate(); },

Same idea, but now we’re only checking whether open is true or false. You can see these iterations in the “Slide Animate” and “Slide Animate 2” sections of the demo.

Transitions

We’ve talked about animating items already on the page so far, but what about animating an object when it first renders? And when it un-mounts? That’s called a transition, and it’s built into Svelte. The docs do a superb job covering the common use cases, but there’s one thing that’s not yet (directly) supported: spring-based transitions.

/explanation Note that what Svelte calls a “transition” and what CSS calls a “transition” are very different things. CSS means transitioning one value to another. Svelte is referring to elements as they “transition” into and out of the DOM entirely (something that CSS doesn’t help with much at all).

To be clear, the work we’re doing here is made for adding spring-based animations into Svelte’s transitions. This is not currently supported, so it requires some tricks and workarounds that we’ll get into. If you don’t care about using springs, then Svelte’s built-in transitions can be used, which are significantly simpler. Again, check the docs for more info.

The way transitions work in Svelte is that we provide a duration in milliseconds (ms) along with an optional easing function, then Svelte provides us a callback with a value running from 0 to 1, representing how far along the transition is, and we turn that into whatever CSS we want. For example:

const animateIn = () => {   return {     duration: 2000,     css: t => `transform: translateY($  {t * 50 - 50}px)`   }; };

…is used like this:

<div in:animateIn out:animateOut class="box">   Hello World! </div>

When that <div> first mounts, Svelte:

  • calls our animateIn function,
  • rapidly calls the CSS function on our resulting object ahead of time with values from 0 to 1,
  • collects our changing CSS result, then
  • compiles those results into a CSS keyframes animation, which it then applies to the incoming <div>.

This means that our animation will run as a CSS animation — not as JavaScript on the main thread — offering a nice performance boost for free.

The variable t starts at 0, which results in a translation of -50px. As t gets closer to 1, the translation approaches 0, its final value. The out transition is about the same, but in reverse, with the added feature of detecting the box’s current translation value, starting from there. So, if we add it then quickly remove it, the box will start to leave from its current position rather than jumping ahead. However, if we then re-add it while it’s leaving, it will jump, something we’ll talk about in just moment.

You can run this in the “Basic Transition” section of the demo.

Transitions, but with springs

While there’s a number of easing functions that alter the flow of an animation, there’s no ability to directly use springs. But what we could do is find some way to run a spring ahead of time, collect the resulting values, and then, when our css function is called with the a t value running from 0 to 1, look up the right spring value. So, if t is 0, we obviously need the first value from thespring. When t is 0.5, we want the value right in the middle, and so on. We also need a duration, which is number_of_spring_values * 1000 / 60 since there’s 60 frames per second.

We won’t write that code here. Instead, we’ll use the solution that already exists in the svelte-helpers library, a project I started. I grabbed one small function from the Svelte codebase, spring_tick, then wrote a separate function to repeatedly call it until it’s finished, collecting the values along the way. That, along with a translation from t to the correct element in that array (or a weighted average if there’s not a direct match), is all we need. Rich Harris gave a helping hand on the latter, for which I’m grateful.

Animate in

Let’s pretend a big red <div> is a modal that we want to animate in, and out. Here’s what an animateIn function looks like:

import { springIn, springOut } from "svelte-helpers/animation"; const SPRING_IN = { stiffness: 0.1, damping: 0.1 };  const animateIn = node => {   const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);   return {     duration,     css: t => `transform: translateY($  { tickToValue(t) }px)`   }; };

We feed the values we want to spring to, as well as our spring config to the springIn function. That gives us a duration, and a function for translating the current tickToValue into the current value to apply in the CSS. That’s it!

Animate out

Closing the modal is the same thing, with one small tweak

const SPRING_OUT = { stiffness: 0.1, damping: 0.5, precision: 3 };  const animateOut = node => {   const current = currentYTranslation(node);   const { duration, tickToValue } = springOut(current ? current : 0, 80, SPRING_OUT);   return {     duration: duration,     css: t => `transform: translateY($  { tickToValue(t) }px)`   }; };

Here, we’re check the modal’s current translation position, then use that as a starting point for the animation. This way, if the user opens and then quickly closes the modal, it’ll exit from its current position, rather than teleporting to 0, and then leaving. This works because the animateOut function is called when the element un-mounts, at which point we generate the object with the duration property and css function so the animation can be computed.

Sadly, it seems re-mounting the object while it’s in the process of leaving does not work, at least well. The animateIn function is not called de novo, but rather the original animation is re-used, which means it’ll always start at -80. Fortunately this almost certainly would not matter for a typical modal component, since a modal is usually removed by clicking on something, like the background overlay, meaning we are unable to re-show it until that overlay has finished animating out. Besides, repeatedly adding and removing an element with bidirectional transitions might make for a fun demo, but they’re not really common in practice, at least in my experience.

One last quick note on the outgoing spring config: You may have noticed that I set the precision ridiculously high (3 when the default is 0.01). This tells Svelte how close to get to the target value before deciding it is “done.” If you leave the default at 0.01, the modal will (almost) hit its destination, then spend quite a few milliseconds imperceptibly getting closer and closer before deciding it’s done, then remove itself from the DOM. This gives the impression that the modal is stuck, or otherwise delayed. Moving the precision to a value of 3 fixes this. Now the modal animates to where it should go (or close enough), then quickly goes away.

More animation

Let’s add one final tweak to our modal example. Let’s have it fade in and out while animating. We can’t use springs for this, since, again, we need to have one canonical duration for the transition, and our motion spring is already providing that. But spring animations usually make sense for items actually moving, and not much else. So let’s use an easing function to create a fade animation.

If you need help picking the right easing function, be sure to check out this handy visualization from the Svelte docs. I’ll be using the quintOut and quadIn functions.

import { quintOut, quadIn } from "svelte/easing";

Our new animateIn function looks pretty similar. Our css function does what it did before, but also runs the tickToValue value through the quintOut easing function to get our opacity value. Since t runs from 0 to 1 during an in transition, and 1 to 0 during an out transition, we don’t have to do anything further to it before applying to opacity.

const SPRING_IN = { stiffness: 0.1, damping: 0.1 }; const animateIn = node =>; {   const { duration, tickToValue } = springIn(-80, 0, SPRING_IN);   return {     duration,     css: t => {       const transform = tickToValue(t);       const opacity = quintOut(t);       return `transform: translateY($  { transform }px); opacity: $  { opacity };`;     }   }; };

Our animateOut function is similar, except we want to grab the element’s current opacity value, and force the animation to start there. So, if the element is in the process of fading in, with an opacity of, say, 0.3, we don’t want to reset it to 1, and then fade it out. Instead, we want to fade it out from 0.3.

Multiplying that starting opacity by whatever value the easing function returns accomplishes this. If our t value starts at 1, then 1 * 0.3 is 0.3. If t is 0.95, we do 0.95 * 0.3 to get a value, which is a little less than 0.3, and so on.

Here’s the function:

const animateOut = node => {   const currentT = currentYTranslation(node);   const startOpacity = +getComputedStyle(node).opacity;   const { duration, tickToValue } = springOut(     currentT ? currentT : 0,     80,     SPRING_OUT   );   return {     duration,     css: t => {       const transform = tickToValue(t);       const opacity = quadIn(t);       return `transform: translateY($  { transform }px); opacity: $  { startOpacity * opacity }`;     }   }; }; 

You can run this example in the demo with the “Spring Transition With Fade component.

Parting thoughts

Svelte is a lot of fun! In my (admittedly limited) experience, it tends to provide extremely simple primitives, and then leaves you to code up whatever you need. I hope this post has helped explain how the spring animations can be put to good use in your web applications.

And, hey, just a quick reminder to consider accessibility when working with springs, just as you would do with any other animation. Pairing these techniques with something like prefers-reduced-motion can ensure that only folks who prefer animations are the ones who get them.


The post Svelte and Spring Animations appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Integrating TypeScript with Svelte

Svelte is one of the newer JavaScript frameworks and it’s rapidly rising in popularity. It’s a template-based framework, but one which allows for arbitrary JavaScript inside the template bindings; it has a superb reactivity story that’s simple, flexible and effective; and as an ahead-of-time (AOT) compiled framework, it has incredibly impressive perf, and bundle sizes. This post will focus on configuring TypeScript inside of Svelte templates. If you’re new to Svelte, I’d urge you to check out the introductory tutorial and docs.

If you’d like to follow along with the code (or you want to debug what you might be missing in your own project) you can clone the repo. I have branches set up to demonstrate the various pieces I’ll be going over.

Basic TypeScript and Svelte setup

Let’s look at a baseline setup. If you go to the initial-setup branch in the repo, there’s a bare Svelte project set up, with TypeScript. To be clear, TypeScript is only working in stand-alone .ts files. It’s not in any way integrated into Svelte. Accomplishing the TypeScript integration is the purpose of this post.

I’ll go over a few pieces that make Svelte and TypeScript work, mainly since I’ll be changing them in a bit, to add TypeScript support to Svelte templates.

First, I have a tsconfig.json file:

{   "compilerOptions": {     "module": "esNext",     "target": "esnext",     "moduleResolution": "node"   },   "exclude": ["./node_modules"] }

This file tells TypeScript that I want to use modern JavaScript, use Node resolution, and exclude a node_modules from compilation.

Then, in typings/index.d.ts I have this:

declare module "*.svelte" {   const value: any;   export default value; }

This allows TypeScript to co-exist with Svelte. Without this, TypeScript would issue errors any time a Svelte file is loaded with an import statement. Lastly, we need to tell webpack to process our Svelte files, which we do with this rule in webpack.config.js:

{   test: /.(html|svelte)$ /,   use: [     { loader: "babel-loader" },     {       loader: "svelte-loader",       options: {         emitCss: true,       },     },   ], }

All of that is the basic setup for a project using Svelte components and TypeScript files. To confirm everything builds, open up a couple of terminals and run npm start in one, which will start a webpack watch, and npm run tscw in the other, to start a TypeScript watch task. Hopefully both will run without error. To really verify the TypeScript checking is running, you can change:

let x: number = 12;

…in index.ts to:

let x: number = "12";

…and see the error come up in the TypeScript watch. If you want to actually run this, you can run node server in a third terminal (I recommend iTerm2, which allows you to run these terminals inside tabs in the same window) and then hit localhost:3001.

Adding TypeScript to Svelte

Let’s add TypeScript directly to our Svelte component, then see what configuration changes we need to make it work. First go to Helper.svelte, and add lang="ts" to the script tag. That tells Svelte there’s TypeScript inside the script. Now let’s actually add some TypeScript. Let’s change the val prop to be checked as a number, via export let val: number;. The whole component now looks like this:

<script lang="ts">   export let val: number; </script>  <h1>Value is: {val}</h1>

Our webpack window should now have an error, but that’s expected.

Showing the terminal with an error output.

We need to tell the Svelte loader how to handle TypeScript. Let’s install the following:

npm i svelte-preprocess svelte-check --save

Now, let’s go to our webpack config file and grab svelte-preprocess:

const sveltePreprocess = require("svelte-preprocess");

…and add it to our svelte-loader:

{   test: /.(html|svelte)$ /,   use: [     { loader: "babel-loader" },     {       loader: "svelte-loader",       options: {         emitCss: true,         preprocess: sveltePreprocess({})       },     },   ], }

OK, let’s restart the webpack process, and it should build.

Add checking

So far, what we have builds, but it doesn’t check. If we have invalid code in a Svelte component, we want that to generate an error. So, let’s go to App.svelte, add the same lang="ts" to the script tag, and then pass an invalid value for the val prop, like this:

<Helper val={"3"} />

If we look in our TypeScript window, there are no errors, but there should be. It turns out we don’t type check our Svelte template with the normal tsc compiler, but with the svelte-check utility we installed earlier. Let’s stop our TypeScript watch and, in that terminal, run npm run svelte-check. That’ll start the svelte-check process in watch mode, and we should see the error we were expecting.

Showing the terminal with a caught error.

Now, remove the quotes around the 3, and the error should go away:

Showing the same terminal window, but no errors.

Neat!

In practice, we’d want both svelte-check and tsc running at the same time so we catch both errors in both our TypeScript files and Svelte templates. There’s a bunch of utilities on npm that allow can do this, or we can use iTerm2, is able to split multiple terminals in the same window. I’m using it here to run the server, webpack build, tsc build, and svelte-check build.

Showing iTerm2 window with four open terminals in a two-by-two grid.

This setup is in the basic-checking branch of the repo.

Catching missing props

There’s still one problem we need to solve. If we omit a required prop, like the val prop we just looked at, we still won’t get an error, but we should, since we didn’t assign it a default value in Helper.svelte, and is therefore required.

<Helper /> // missing `val` prop

To tell TypeScript to report this as an error, let’s go back to our tsconfig, and add two new values

"strict": true, "noImplicitAny": false 

The first enables a bunch of TypeScript checks that are disabled by default. The second, noImplicitAny, turns off one of those strict checks. Without that second line, any variable lacking a type—which is implicitly typed as any—is now reported as an error (no implicit any, get it?)

Opinions differ widely on whether noImplicitAny should be set to true. I happen to think it’s too strict, but plenty of people disagree. Experiment and come to your own conclusion.

Anyway, with that new configuration in place, we should be able to restart our svelte-check task and see the error we were expecting.

Showing terminal with a caught error.

This setup is in the better-checking branch of the repo.

Odds and ends

One thing to be aware of is that TypeScript’s mechanism for catching incorrect properties is immediately, and irreversibly switched off for a component if that component ever references $ $ props or $ $ restProps. For example, if you were to pass an undeclared prop of, say, junk into the Helper component, you’d get an error, as expected, since that component has no junk property. But this error would immediately go away if the Helper component referenced $ $ props or $ $ restProps. The former allows you to dynamically access any prop without having an explicit declaration for it, while $ $ restProps is for dynamically accessing undeclared props.

This makes sense when you think about it. The purpose of these constructs is to dynamically access a property on the fly, usually for some sort of meta-programming, or to arbitrarily pass attributes on to an html element, which is common in UI libraries. The existence of either of them implies arbitrary access to a component that may not have been declared.

There’s one other common use of $ $ props, and that’s to access props declared as a reserved word. class is a common example of this. For example:

const className = $ $ props.class;

…since:

export let class = "";

…is not valid. class is a reserved word in JavaScript but there’s a workaround in this specific case. The following is also a valid way to declare that same prop—thanks to Rich Harris for helping with this.

let className; export { className as class };

If your only use of $ $ props is to access a prop whose name is reserved, you can use this alternative, and maintain better type checking for your component.

Parting thoughts

Svelte is one of the most promising, productive, and frankly fun JavaScript frameworks I’ve worked with. The relative ease with which TypeScript can be added is like a cherry on top. Having TypeScript catch errors early for you can be a real productivity boost. Hopefully this post was of some help achieving that.


The post Integrating TypeScript with Svelte appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]