Skip to content

Signy from scratch

Build a shopping list, one idea at a time. No framework experience needed.

This page is for you if you know some HTML and a little JavaScript and have never used a UI library. You will build a small shopping list. Each step adds one idea, and every example on the page is complete: copy it into your file and it works.

1. What a UI library does for you

A page often has to change when data changes: a counter goes up, an item appears in a list. In plain JavaScript you find the element and change it yourself, every time.

That works for one number. With ten numbers shown in ten places, you have to remember to update each of them, and it is easy to miss one. Signy turns that around: you change the data, and the page follows by itself.

2. One HTML file

You need no installation and no build tool. Create a file called shopping.html, paste this in, and open it in your browser.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Shopping list</title>
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      // The code of each step goes here
    </script>
  </body>
</html>

The first script tag loads Signy and gives you a global object called Signy. Every example below starts by taking what it needs from it, such as const { Signal, html } = Signy;. In your file, replace the id in the last line of each example with "#app".

3. A signal is a box with a value

A signal holds one value and remembers who reads it. You read it with .value and change it by giving .value a new value. Here is the apple counter again.

There is no line that updates the text. The text reads the signal, so Signy updates it whenever the signal changes. That is the whole idea.

The one rule. Write ${() => this.apples.value}, with () => in front. That little arrow makes a function, and Signy calls it again when the value changes. Without it, ${this.apples.value} puts the value in once and it never changes again.

4. A component is a piece of the page

The example above is a component: a JavaScript class that describes one piece of the page. If you have not used classes:

  • class Apples extends Component { ... } creates a new kind of component, based on the one Signy provides.
  • apples = new Signal(0); is a field: a value each component carries. Inside the class you reach it as this.apples.
  • render() returns the HTML of the component. Signy calls it once, when the component appears. It never calls it again: the functions inside keep the page up to date.
  • application(Apples).mount("#app") puts the component into the element with the id app.

The HTML is written between backticks after html. That is a template. The ${...} parts put JavaScript values into it.

5. Reacting to clicks and typing

onClick=${...} runs a function when the element is clicked, and onInput=${...} runs one each time the user types. The function gets an event, and event.currentTarget.value is the text in the field.

Notice <input /> ends with />. In a template, elements that have no closing tag, such as input, img and br, need that slash.

6. Showing and hiding

To show one thing or another, use Show. It takes a when function, what to show when it is true (children) and what to show otherwise (fallback).

7. Lists

A signal can hold a list. To change it, give the signal a new list. Changing the old list in place, with push(), does not tell anyone. For shows one row per item and updates only the rows that changed.

  • [...this.items.value, newItem] makes a new list with everything from the old one plus the new item.
  • filter() makes a new list without the removed item.
  • Each item has an id, and keyBy: "id" tells For to use it. That way For knows which row is which, even after you add or remove items.

8. Values computed from other values

The number of items comes from the list. Instead of keeping it in a second signal, compute it with Computed. It updates when the list does.

9. Splitting into components

When a component grows, move a part into its own component and use it like a tag: <${Total} />. Values you pass on the tag are its props, and it reads them as this.props.

A prop is read once, like everything in render(). For a value that changes, pass a function and call it inside () =>. What you put between the opening and closing tag arrives as this.props.children.

10. When something does not work

What you seeWhyFix
The number on the page never changesThe value is put in onceWrite ${() => this.count.value}, not ${this.count.value}
The list does not change after push()The signal still holds the same list, so nobody is toldGive it a new list: this.items.value = [...this.items.value, item]
A list made with .map() never changes.map() runs once, when the page is builtUse For
The input keeps its text after you clear the signalThe field's text belongs to the browser once someone typedReset the form with form.reset(), or set the field through a ref
The page is empty, or half of it is missingAn <input> or <br> without the slashWrite <input /> and <br />
Nothing shows at allAn error in your codeOpen the browser console (F12) and read the first red message

11. Where to go next