Embroidery

GitHub / npm

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.

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

Using npm

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

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 cannot 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.

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 APIs return JSON or XML and leave 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.

Written by Anton, Nov 2020
Updated Sep 2024