npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

yoffee

v0.0.13

Published

Minimal HTML one-way binding library

Downloads

75

Readme

YOFFEE.JS

npm bundle size

Simple, Efficient, Declarative HTML templates with one-way data binding.

Why should I use this over the likes of react and vue?

  • Faster
  • Simpler
  • Lighter
  • Unopinionated

Official Documentation

Discord Channel

yoffee doesn't force you to use webpack or any other bundler - the code runs natively in the browser. Try the following counter button example:

<script type="module">
    import {html, createYoffeeElement} from "https://unpkg.com/yoffee@latest/dist/yoffee.min.js"

    createYoffeeElement("counter-button", () => {
        const state = {
            clicks: 0
        }

        return html(state)`
            <button onclick=${() => state.clicks += 1}>
                I've been clicked ${() => state.clicks} times
            </button>
        `
    })
</script>
<counter-button></counter-button>

Try it live on JSFiddle

Installation

From CDN: Include the import statement in your script.

import {html, createYoffeeElement} from "https://unpkg.com/yoffee@latest/dist/yoffee.min.js"

From NPM: npm install yoffee, Then include in your script:

import {html, createYoffeeElement} from "yoffee"

Overview

Or alternatively, visit the docs site here.

yoffee lets you write HTML templates in JavaScript with template literals. yoffee stays as unopinionated as possible by sticking to HTML with no special syntax.

yoffee provides two main exports, html and createYoffeeElement. html creates a reactive DOM Element, but it's not wrapped in a web component yet. That's what createYoffeeElement is for.

html can be used standalone:

let text = "World!"
let element = html()`
<div>
    Hello ${text}
</div>
`;
// This works
document.body.appendChild(element)

element is a regular HTML element (More accurately, a DocumentFragment) that we can insert into the DOM using standard appendChild.

Data Binding

yoffee provides a way to update an element by binding it to a state object. When a property on the state object changes, yoffee automatically updates only the relevant part of the element:

let state = {
    text: "World?"
}

let element = html(state)`
<div>
    Hello ${() => state.text}
</div>
`;
state.text = "World!"

In the above example, when state.text changed, yoffee modified the div's content.

Notice that we used an arrow function () => state.text instead of just state.text. When using state's properties, always use arrow functions, otherwise yoffee won't update the template.

Speed

yoffee is extremely fast. yoffee saves references to DOM elements, and when state changes, it updates only the relevant elements instead of the whole root element.

Consider the following code that contains two expressions and some static content:

let element = html(state)`
<div class=${() => state.class}>
    ${() => state.content}
    <div>Some other irrelevant static content...</div>
</div>
`;
state.class = "classy";
state.content = "a content";

First state.class was set, and then state.content.

Instead of overwriting the whole div twice, yoffee first updates the property class, then the textNode content. The other irrelevant text didn't change.

Faster than React

React revels in its speed by minimizing DOM updates. In order to minimize them, React generates a diff between virtual DOMs on each update. In the above example, React would have created the whole div in memory, compared the current and new divs, and only updated the diff in the DOM. Yoffee on the other hand keeps a reference to elements in the DOM, with no need for the diff process.

Examples

Text

`<div> 
    ${() => state.text} some text between, ${() => state.moreText}
</div>`

Conditionals:

`
<div>
    ${() => state.a ? "a" : "b"}
    ${() => state.condition && "am i here?"}
</div>
`

CSS:

`<style> 
    #my-element {
        color: ${() => state.color};
    }
</style>`

Events:

`<button onclick=${() => state.a+=1}>
    ${() => state.a}
</button>`

Attributes:

`<div dir=${() => state.dir}>what is my direction?</div>`

Attribute names:

`<div ${() => state.attrName}>i have some attr</div>`

Attribute dict:

let state = {
    inputAttrs: {
        dir: "left",
        placeholder: "i am placeholder"
    }
}
html()`<input ${() => state.inputAttrs}></input>`

Nesting template (HTML element) inside a template:

let state = {
    someInsideData: {name: "old name"}
}

let element = html(state)`
<div>
    I have other elements inside of me
    ${() => html(state.someInsideData)`
        <div>${() => state.someInsideData.name}</div>
    `}
</div>
`;

// Modify prop of inner template
state.someInsideData.name = "new name"

// Modify whole inner template (prop of outer template)
state.someInsideData = {name: "new name"}

List of elements:

let state = {
    items: [{
        name: "Mojojojo"
    }, {
        name: "harambe"
    }]
};

let element = html(state)`
<div>
${() => state.items.map(item => html(item)`
    <div>${() => item.name}</div>
`)}
</div>
`;

// Modify prop of specific item
state.items[0].name += "s";

// Modify the whole list
state.items = [{name: "new name"}, {name: "another"}]

A single expression can contain multiple properties:

`<div>${() => state.a + state.b}</div>`

A single dom node can contain multiple expressions - here we see style attribute node:

`<div style="color:${() => state.color}; width:${() => state.width}px;">`

Multiple states in single yoffee template:

html(state1, state2)`
<div>
    ${() => state1.text}
    ${() => state2.text}
</div>
`;
state1.text = "i am text"
state2.text = "i am some other unrelated text"

Multiple templates with one state (good for displaying global state):

html(state)`
<div>text is ${() => state.text}</div>
`
html(state)`
<div>${() => state.text} is text</div>
`
;
state.text = "life"

Web components - Yoffee Element example:

<body>
<script type="module">
    import {html, createYoffeeElement} from "https://unpkg.com/yoffee@latest/dist/yoffee.min.js"

    createYoffeeElement("my-list-item", props => html(props)`
        <button onclick=${() => props.clicked()}>
            click me for the ${() => props.clicks}th time.
        </button>
    `)

    createYoffeeElement("some-list", props => {
        const state = {
            items: [
                {clicks: 0},
                {clicks: 0}
            ],
            margin: 20
        }

        return html(props, state)`
            <style>
                :host {
                    display: block;
                    margin: ${() => state.margin}px;
                }
            </style>
            <div>
                ${() => state.items.map(item => html(item)`
                    <my-list-item
                        clicks="${() => item.clicks}"
                        clicked=${() => () => item.clicks += 1}
                        data=${() => item.data}></my-list-item>
                `)}
            </div>
            <button onclick=${() => state.items = [...state.items, {clicks: 0}]}>
                add a button
            </button>
        `
    });
</script>
<some-list></some-list>
</body>

Try it live on JSFiddle

Comprehensive features example:

import {html} from "https://unpkg.com/yoffee@latest/dist/yoffee.min.js"

window.state = {
        name: "Inigo Montoystory",
        color: "red",
        age: 3,
        clicks: 1,
        placeholder: "this is hint",
        amAlive: true
    };
    window.innerState = {
        deathColor: "blue"
    };
    window.secondState = {
        age: 10
    }

    let element = html(state, secondState)`
<div>
    My name is <span style="color: ${() => state.color}">
        ${() => state.name}
    </span>

    <div>i will live ${() => state.age + 1}ever</div>
    <div>second state age is  ${() => secondState.age} yars</div>
    <div>i am ${"static"}</div>

    <button onclick=${() => state.clicks += 1}>
        click me baby ${() => state.clicks} more time
    </button>

    <style>
     #thing {
        color: ${() => state.color};
     }
    </style>
    <div id="thing">colorful things</div>

    <input placeholder=${() => state.placeholder}>

    <div>
        ${() => state.amAlive ? "yes" : html(innerState)`
            <span style="color: ${() => innerState.deathColor}; font-size: ${() => innerState.deathColor === "blue" ? "40px" : "13px"};">NO</span>`}
    </div>
</div>
`;

    // element is a regular html element
    document.body.appendChild(element);

    // modifying the state
    state.name = "John Cena!!!";

    // switching the color
    setInterval(() => state.color = state.color === "blue" ? "red" : "blue", 500);

Try it live on JSFiddle

How does it work?

Consider the following example:

html(state)`
<div id="parent">
    <div id="child">${() => state.content}</div>
</div>
`;
state.content = "new content"

when the last line is called, yoffee only updates #child's content, by rerunning the expression () => state.content. yoffee does several things to make that possible:

  • Wrap state object with setters and getters
    • Setters notify yoffee that content property has changed and should be rerendered. (when state.content = "new content" is called)
    • Getters allow us to know which property corresponds to which expression in the html: when () => state.content is called, the getter for content is called, letting yoffee know that content property corresponds to that expression.
  • Analyze the resulting HTML element to keep a reference to each of the nodes containing expressions. For example, yoffee keeps a reference to the #child's TextNode which will be changed when content's setter is called. It does so by inserting randomly generated IDs into the expressions, the then finding them.

Why do bound expression have to be functions?

When an expression isn't a function, yoffee can't rerun it when state's properties are changed - in fact, no property is linked to a static expression. Consider this expression:

${state.a}

yoffee can't possibly know that the property a is linked to this expression, because only the value of a is passed.

Its possible to use eval to convert expressions into callbacks (add ()=> to the above code) but that would slow performance and be prone to errors and security problems.

Contribution

Feel free to contact me about bugs, features and anything you'd like.

If you like this project and you feel like contributing, questions about the code and PRs are very welcome :)