In the lineup of possible JS frameworks, a newer one has come out recently, and tickles my fancy: Svelte, designed by the infamous Rich Harris, is a "framework without the framework".
You can read the official Svelte guide, but I thought it would be useful to walk you through what a "serious" web application might look like when made with Svelte.
Note: This tutorial has been updated 2018-07-16 to follow Svelte version 2.
In this tutorial we will build a very basic web app that lets you create and edit records like a real business-type app would do. We'll build an animal address book. It's like an address book, but for animals! ππΆπ¦
Each section of this tutorial is in a sub-folder of this repo. You can build and view all the different sections like this:
git clone https://github.com/saibotsivad/svelte-app-tutorial.git
cd svelte-app-tutorial
npm install
npm run build
npm run start
Then open http://localhost:8001/ in your browser.
Much like Rich Harris' other JS contribution, RactiveJS, Svelte promotes the idea that components are written as plain old HTML files.
Here's a very simple component:
<!-- HelloWorld.html -->
<h1>Hello {name}!</h1>
That's the whole component!
The {name}
is a mustache-like syntax unique
to Svelte (it has some extra bits of functionality). When the component property
updates, the template will too.
Svelte compiles the component to a JavaScript file. Later in the tutorial we'll use a build system (like Rollup, Gulp, Grunt, or Browserify). The simplest way to get started is to use svelte-cli:
npm install -g svelte-cli
svelte compile --format iife HelloWorld.html > HelloWorld.js
Note: The flag
--format iife
means that we can use the compiled JavaScript file in the browser directly. It's handy for this tutorial and when you're playing around, but later we will learn how to compile the Svelte components into modules and bundle multiple components into a single JavaScript file like you would in a real web app.
To use the component, create an index.html
file that looks like this:
<!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
</head>
<body>
<div id="main"></div>
<script src="HelloWorld.js"></script>
<script>
var app = new HelloWorld({
target: document.getElementById('main'),
data: {
name: 'world'
}
})
setTimeout(function() {
app.set({ name: 'everyone' })
}, 3000)
</script>
</body>
</html>
If you open this file in your browser (try open index.html
from the
command line) you'll see the header element render as "Hello world!",
and 5 seconds later change to "Hello everyone!".
Components are written as single HTML files that we compile into JavaScript files.
We can use those components as composable widgets, without needing to use a full framework.
Our address book will have two main views: a list of all the animals we know, and a view for looking at each animal separately. Let's start by building the list view.
Let's start with a list of entries that looks like this:
<ul>
<li>Goat: [email protected]</li>
<li>Dog: [email protected]</li>
</ul>
We want to spice up the list elements, so let's try something like this:
<li>
<h1>π Goat</h1>
·
<a href="mailto:[email protected]">email</a>
·
<a href="https://twitter.com/EverythingGoats">twitter</a>
</li>
Let's write that as a Svelte component:
<!-- ListEntry.html -->
<li>
<h1>{entry.emoji} {entry.name}</h1>
{#if entry.email}
·
<a href="mailto:{entry.email}">email</a>
{/if}
{#if entry.twitter}
·
<a href="https://twitter.com/{entry.twitter}">twitter</a>
{/if}
</li>
This component doesn't have any internal "state", it just takes the properties it's given and displays them. These are the best kind of components, because they are the easiest to reason about.
The #if
and /if
properties are Svelte template syntax markers for
conditionally displaying the content between the curly braces if the
referenced property (in this case if entry.email
or entry.twitter
)
exists.
Let's make the parent component, to see how we use child components:
<!-- List.html -->
<ul>
{#each animals as animal}
<ListEntry entry="{animal}" />
{/each}
</ul>
<script>
import ListEntry from './ListEntry.html'
export default {
components: {
ListEntry
}
}
</script>
The #each
and /each
properties are Svelte template syntax markers
for looping through lists and adding the content between curly braces
for each list entry.
Note: In addition to the
#each list as item
you can reference the entry index using#each list as item, index
and then use theindex
property like a normal variable in the template.
JavaScript code for the component goes inside the <script>
element,
and you can specify things like: default data, helper functions that
you can use in your component, and more cool things.
The important thing to note is that the <script>
element needs to
export
an object as the default export, and you'll need to
import
and list each child component it uses in the exported
components
object.
Note: The syntax
{ ListEntry }
is ES6 shorthand for{ ListEntry: ListEntry }
.
For now, this component only needs to specify that the List
component uses the ListEntry
component.
Let's compile all this together, and make sure our app works so far:
svelte compile --format iife ListEntry.html > ListEntry.js
svelte compile --format iife List.html > List.js
Note: For the early sections of this tutorial, when you compile a component it might complain "no name is specified for the imported module". This error is fine for now, but later on in this tutorial we will make the build smarter so those errors go away.
Then we will make an index.html
like this:
<!DOCTYPE html>
<html>
<head>
<title>Animal Phone Book</title>
<!--
IMPORTANT! If you are testing with a server that does not
set the `charset` of the content type, some browsers will
not display the emoji correctly. You can overcome this by
setting the charset as a <meta> tag:
-->
<meta charset="UTF-8">
</head>
<body>
<div id="list"></div>
<script src="ListEntry.js"></script>
<script src="List.js"></script>
<script>
var list = new List({
target: document.getElementById('list'),
data: {
animals: [{
name: 'Goat',
emoji: 'π',
email: '[email protected]',
twitter: 'EverythingGoats'
},{
name: 'Dog',
emoji: 'πΆ',
email: '[email protected]',
twitter: 'dog_rates'
}]
}
})
</script>
</body>
</html>
If you open this in your browser, you should see a page that, although not very pretty, lists two entries: one for Goat, and one for Dog.
Using components inside other components requires you to specify which
component you're using in the default export of the <script>
tag, and
passing parameters to a child component is as simple as setting a
named attribute on the HTML element:
<ChildComponent componentProperty="{parentProperty}" />
<script>
import ChildComponent from './ChildComponent.html'
export default {
components: { ChildComponent }
}
</script>
Our little app displays data, but we want to be able to add, edit, and delete animal contacts from our list. Let's think about how to add animals first.
You can imagine a normal input form that looks like this:
<h1>Add Animal</h1>
<form action="http://site.com/api/add_animal" method="POST">
<p>
<label for="animal-name">Name</label>
<input type="text" name="animal-name">
</p>
<p>
<label for="animal-emoji">Emoji</label>
<input type="text" name="animal-emoji">
</p>
<p>
<label for="animal-email">Email</label>
<input type="text" name="animal-email">
</p>
<p>
<label for="animal-twitter">Twitter</label>
<input type="text" name="animal-twitter">
</p>
<button type="submit">Create Animal</button>
</form>
If we imagine a "form" element (like the one above) as a composed set of components, we could imagine wanting to use that "form" like so:
<h1>Add Animal</h1>
<FormAddAnimal
on:submit="saveData(event)"
/>
So then we would write our "form" component like so:
<!-- FormAddAnimal.html -->
<h1>Add Animal</h1>
<p>
<label for="animal-name">Name</label>
<input type="text" name="animal-name" bind:value="animal.name">
</p>
<p>
<label for="animal-emoji">Emoji</label>
<input type="text" name="animal-emoji" bind:value="animal.emoji">
</p>
<p>
<label for="animal-email">Email</label>
<input type="text" name="animal-email" bind:value="animal.email">
</p>
<p>
<label for="animal-twitter">Twitter</label>
<input type="text" name="animal-twitter" bind:value="animal.twitter">
</p>
<button on:click="create(animal)">Create Animal</button>
<script>
const emptyAnimal = () => ({
name: '',
emoji: '',
email: '',
twitter: ''
})
export default {
data() {
return {
animal: emptyAnimal()
}
},
methods: {
create(animal) {
this.fire('submit', animal)
this.set({ animal: emptyAnimal() })
}
}
}
</script>
bind:value
so that changing the <input>
text will
update our bound value.undefined
. (Try removing one.)create(animal)
means that clicking the button
will call the function in the methods
object.this.fire('submit', animal)
means that the component
will emit an event named submit
, with the value being
the bound values.emptyAnimal
is a function so that it's never modified.)Let's add this to our index.html
:
<!DOCTYPE html>
<html>
<head>
<title>Animal Phone Book</title>
<meta charset="UTF-8">
</head>
<body>
<div id="list"></div>
<div id="add"></div>
<script src="FormAddAnimal.js"></script>
<script src="ListEntry.js"></script>
<script src="List.js"></script>
<script>
var list = new List({
target: document.getElementById('list'),
data: {
animals: [{
name: 'Goat',
emoji: 'π',
email: '[email protected]',
twitter: 'EverythingGoats'
},{
name: 'Dog',
emoji: 'πΆ',
email: '[email protected]',
twitter: 'dog_rates'
}]
}
})
var addAnimal = new FormAddAnimal({
target: document.getElementById('add')
})
addAnimal.on('submit', function(animal) {
var animals = list.get().animals
animals.push(animal)
list.set({ animals })
})
</script>
</body>
</html>
Here we have added two components to our main page. The
addAnimal
variable acts as an event emitter, and when the
component fires the submit
event we get the current list
of animals and add to it.
If you check out the demo at this point,
you can enter a name, email, twitter handle, and emoji for your
new animal. (On Mac, try the control+command+space
shortcut to
bring up an emoji picker.)
Components can fire and handle events. This is the primary way of passing data between components.
Methods have access to this
and can modify the component data.
<!-- MyComponent.html -->
<button on:click="fire('something', 3)">fire</button>
<!-- Consumer.html -->
<MyComponent on:something="stuff(event)" />
<script>
export default {
methods: {
stuff(number) {
// number === 3
}
}
}
</script>
Since we've figured out how our components should interact, we have behaviour that should be tested. The tests serve as solid proof that our component does what we think, and they serve also as a concrete assertion of the component API.
Our tests will assert that when the button is clicked, the component will emit an object with the form fields.
Right away this should raise questions in your head:
animal
object contain all properties,
so some will be empty strings? Or should the animal
object
only contain the properties with data?The great thing about writing good tests is that you can make those decisions and assertions and, if you ever decide to change your mind, it will be clear that you did it intentionally, and that you didn't accidentally break something.
Lower the friction of writing tests as much as possible.
We want it to be as easy as possible to write tests, and as
easy as possible to reference tests. In this tutorial I've written
tests using tape, and placed
the test files next to the component files, with the extension
*.spec.js
to make it easier to find them.
To start with, we will make a test file for FormAddAnimal.html
and simply assert that the component can be created without throwing
any errors:
const test = require('tape')
const FormAddAnimal = require('./FormAddAnimal.html')
test('creating the component does not throw error', t => {
const component = new FormAddAnimal()
t.end()
})
You should note that require('./FormAddAnimal.html')
will fail,
because NodeJS cannot natively require HTML files. In order to
make the component tests work, we will need to first compile the
HTML component files into JavaScript.
The easiest way I have found is to use a combination of
After all those are installed, the command to run them together is:
browserify -t [ sveltify ] FormAddAnimal.spec.js | tape-run
This does the following:
FormAddAnimal.spec.js
file to browserify
sveltify
transform (-t
) which will handle the
require
of HTML files and compile them firsttape-run
, which uses
the electron browser in headless mode by defaultAfter you've confirmed that to be working, we can add another test to the same file:
test('modified input values in fired event', t => {
// attach the component to <body>
const component = new FormAddAnimal({
target: window.document.querySelector('body'),
// set the form values
data: {
animal: {
name: 'Bob',
emoji: 'π¦'
}
}
})
// test the value of the fired event
component.on('submit', animal => {
t.equal(animal.name, 'Bob', 'name should be emitted')
t.equal(animal.emoji, 'π¦', 'emoji should be emitted')
t.notOk(animal.email, 'email should not exist')
t.notOk(animal.twitter, 'twitter should not exist')
// complete the test
t.end()
})
// simulate clicking the button
const click = new window.MouseEvent('click')
window.document.querySelector('button').dispatchEvent(click)
})
Another way to write tests is to write your component so it uses computed properties instead of putting the logic in your template. Then you can test the computed property instead of testing your template, and that is usually an easier test to read:
<!-- Example.html -->
{#if someValue}
<p>This displays if `calories` is greater than 5.</p>
{/if}
<script>
export default {
computed: {
someValue(calories) {
return calories > 5
}
}
}
</script>
Then your test could simply be:
const test = require('tape')
const FormAddAnimal = require('./FormAddAnimal.html')
test('computed value is true when over 5', t => {
const component = new FormAddAnimal({
data: { calories: 20 }
})
t.ok(component.get('someValue'), 'should be truthy')
t.end()
})
The same is true for using methods in your component, instead of making complex templates.
Avoid logic in the template section of your component where possible. Prefer computed properties and methods.
Since you can interact with a component using public methods, testing components is as simple or complex as you want to make it.
You'll get plenty of mileage off simple tape
tests,
using browserify and sveltify, especially if you can put
your logic into computed properties or methods.
<!-- MyComponent.html -->
<button on:click="fire('something', 3)">fire</button>
// MyComponent.spec.js
const test = require('tape')
const MyComponent = require('./MyComponent.html')
test('create component', t => {
const component = new MyComponent()
t.end()
})
browserify -t [ sveltify ] MyComponent.spec.js | tape-run