Embroidery

GitHub / npm

Introduction

What is Embroidery?
If you’ve ever written code that produces HTML you probably at some point have to do some kind of DOM manipulation or event listening. You want to click a button to update a feed, do an AJAX form submission or maybe toggle CSS classes. While you definitely can just use regular Javascript or even jQuery it can feel a bit cumbersome or repetitive.

You might reach for React or Vue. These libraries are great but also introduce a lot of new concepts. Embroidery is no where near as feature complete as these.

Embroidery’s goal is to make it easier to add Javascript behaviors to your HTML while keeping your code very close to the metal. It aims to be 2kB (minified and gzipped) or less. Currently it’s 1.9kB.

This is just a toy project and mainly serves a learning purpose. It’s not meant to be used in production.

Getting started

There are a few ways to get going with Embroidery.

With a bundler (like webpack, rollup, parcel, etc)
First install Embroidery

npm install embroidery // (or yarn, pnpm etc)

And then in your main js file

import { Embroidery } from 'embroidery'
const app = Embroidery.start()
app.register({ /* your controllers */})

Using a CDN (without a bundler)

import { Embroidery } from 'https://unpkg.com/[email protected]/dist/embroidery.m.js'
const app = Embroidery.start()
app.register({ /_ your controllers _/})

Usage

Let’s dive in! Start by adding some HTML.

<!-- HTML somewhere in your web app -->
<div data-controller="myController">
    <input data-target="name" type="text" />
    <button data-action="click->greet">Greet</button>
    <span data-target="output"></span>
</div>

Then create a controller. This can be a separate file or part of a combined js file.

// my-controller.js
export const myController = {
  greet({ name, output }) {
    output.textContent = name.value
  },
}

You register your controller in your main js file.

import { Embroidery } from 'embroidery'
import { myController } from 'my-controller'

let app = Embroidery.start()
app.register({ myController })

There are a few things to look at here: controllers, targets and actions.

Controllers are the wrapper of your Embroidery code. The name in data-controller (HTML) maps to the name of the exported const in the js file - they always need to be the same. Controllers can not be kebab-cased since that is not a valid variable name in js. I prefer using camelCase but you can use snake_case if you want.

Targets can be accessed as properties by destructuring the first parameter in the action’s function. In the above case we get the input value, name, and set the textContent on output.

Actions are the event and name of the function that should run when the event is triggered. Events are regular DOM events. As you can see they are separated by an arrow: ->, where the first part is the event and the last part is the name of the function.

Try to change click to hover!

Actions come with a few default events. For example buttons and links have the default event click.

Element     Event
a           'click'
button      'click'
form        'submit'
input       'input'
select      'change'
textarea    'input'

You can also chain actions!

<div data-controller="manyActions">
  <div data-action="mouseover->doThis mouseout->doThat">Do this or that</div>
</div>

Multiple targets

If you have multiple elements on the same level you can access them as an array. You’ll have to append [] to the target name to mark them as being part of an array.

Note!
The name to access the target in the controller will change to <targetName>Targets. So the target name snowboard[] becomes snowboardTargets.

<div>
  <div data-target="hello[]">Hello to you</div>
  <div data-target="hello[]">Hello to me</div>
  <div data-target="hello[]">Hello to everyone</div>
</div>
// hello-controller.js
export const helloController = {
    init({ helloTargets }) {
        helloTargets.forEach((target) => {
        //...
        })
    },
}

Managing state

State in Embroidery is similar to mutable class variables. Here’s an example of how to store the state of an input value.

export const controller = {
    name: '',
    myName({ name }) { // name is an <input /> in this case
        this.name = name.value
    },
    submit() {
        fetch('/update', {
            method: 'POST',
            body: JSON.stringify({
            name: this.name
            })
        })
    }
}

(Very) Experimental: Partials

Most REST API’s return JSON or XML and leaves it up to you on how to organize and style the data. But there might be times when you can take a shortcut and return HTML asynchronously instead. 37signals calls this html-over-the-wire and (re)popularized this technique in HEY. It can be a way to reduce the data payload and defer content that does not need to be rendered immediately on the server to arrive later on the client.

Embroidery supports this as well:

<div data-partial="/endpoint-that-returns-html"></div>

What’s cool about this is that you can have controllers embedded in the partial HTML and Embroidery will pick them up at runtime!

How is Embroidery different from Alpine.js and Stimulus?

First of all, Embroidery would not exist if it weren’t for the following libs. I am very thankful for what they’ve done and that it allowed me to base my work on theirs. They are very similar. My intention is that Embroidery should be easier to pick up but I believe both Alpine and Stimulus have a few more bells and whistles.

Alpine.js’s niche is to be the “Tailwind of Javascript”, so you write your code in Alpine specific directive strings. This can certainly work for many use cases but might introduce other annoyances, like code formatting. Reusability can also be tricky, and I found myself most often copy/pasting code between templates.

Stimulus and Embroidery have a similar syntax. The big difference is that Stimulus uses classes (you inherit from the Stimulus controller) rather than POJO’s, relies on strings for naming targets and requires a bit more repetition.

Written by Anton, Nov 2020
Updated Feb 2023