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

helium-html

v0.4.0

Published

A simple UI library that is easy to use and plays well with others.

Downloads

8

Readme

helium-html

A simple UI library that is easy to use and plays well with others.

import { html } from 'helium-html';

const name = 'James';
const avatar = html`<img src=${ `avatar/${name}.png` } alt=${name} />`;
// hello world
const card = html`<div class="card">
  <h3>hello ${name}</h1>
  ${avatar}
</div>
`;

document.body.append(card)

With Helium you can use the html tag to write HTML which is transformed into a DOM node. This is no simple interpolation; if you were to add a node via substitution then that node will be appended into the result, not a copy. This makes it relatively simple to compose a document in a concise, coherent and modular way.

But what about dynamic state? Well by passing an Observable as an attribute or node it will be bound to that node and any changes will be reflected automatically. This provides a simple way for state to modify content. Creating stateful components is as simple as writing a function that accepts an Observable as a parameter.

import { html, Store } from 'helium-html';

function card (name: Store<string>) {
  const src = name.map(n => `avatar/${n}.png`);
  const avatar = html`<img src=${src} alt=${name} />`;

  return html`<div class="card">
    <h3>hello ${name}</h1>
    ${avatar}
  </div>
  `;
}

const name = new Store('James');

document.body.append(card(name))

setTimeout(() => name.update('Paul'), 5000);

No V-DOM

Observables are bound to a specific piece of your template. If you have a large template with a single piece of dynamic text, only that text will be updated. This differs from the likes of react which do a 'virtual' render of the entire template, compare with the current state and modify only the parts which change. Skipping this has obvious performance advantages, but naive use can lead to large re-renders.

The following is a simple example showing 2 ways to reflect state changes. Both take the same parameters, can be used in similar ways and produce the same HTML. In function a the Observable state is passed into the template expression. In function b state changes are observed and the value is passed into the template expression.

import { html, children } from 'helium-html'

// GOOD
function a (name: Store<string>) {
  return html`<div>${name}</div>`;
}

// BAD
function b (name: Store<string>) {
  return name.map(v => html`<div>${v}</div>`);
}

const name = new Store('Dave');

document.body.append(html`
  ${a(name)}
  ${b(name)}
`);

When the value of name is updated for a only the text node containing the value is updated. For b a new div and text node is created, which replace the previous one. In this example the performance cost of doing the latter is likely to be low, but with a more complicated template possibly containing nested templates the cost could be much higher. The moral is to unwrap Observables as late as possible.

Variadic children

Having a variable number of child nodes with an element can obviously cause some unnecessary re-rendering. A helper children is provided to resolve this issue.

The following example shows 2 ways to render a list of items. Both take the same parameters, can be used in similar ways and produce the same HTML. In function a the items$ observable is passed to the children helper, which deals with subscription and ensures that only new items need to be rendered. In function b the items$ observable is subscribed to directly, then individual items are iterated over and rendered.

import { html, children } from 'helium-html'

// GOOD
function a (items$: Store<string[]>) {
  return html`<ul>${children(items$, item => html`<li>${ item }</li>`)}</ul>`;
}

// BAD
function b (items$: Store<string[]>) {
  return html`<ul>${items$.map(items => items.map(item => html`<li>${ item }</li>` ))}</ul>`;
}

const names = new Store(['Dave', 'James']);

document.body.append(html`
  ${a(names)}
  ${b(names)}
`);

names.modify(names => names.concat(['Paul']));

When the names store is updated a renders only the new element, and appends it to the list. b renders all 3 nodes and replaces the first 2 nodes in the list with new, but identical, nodes. The children helper does this by using an identity function. By default this is value => value. When the source updates each value generates an ID based on the identity function, and checks if the previous list contains a node with a matching ID. If it does then that previous node is used, otherwise the render function is called with that entry. This allows for reordering entries, adding new entries and removing old ones without additional cost.

The issue with this design is that it becomes hard to update the contents of the child elements without re-rendering the entire child. It's possible to circumvent this by placing the variables for the child elements inside it's own Store so that the child element can subscribe the changes without having to rely on a complete re-render.

Props, state, attributes and components

A casual comparison to the likes of React ( with hooks ) things appear very similar. But not all the concepts line up in the same way. Components are functions that return a Node. Properties are arguments you pass to a function, not psuedo-attributes. Attributes are attributes and state is represented using observables.

import React, { useState } from 'react';

function Example({ initial }: { initial: number }) {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

function App() {
  return <Example initial={{ 42 }}/>
}

import { html } from 'helium-html';

function Example(initial: number) {
  const count = new Store(initial);
  const increment = () => count.modify(value => value + 1);

  return html`<div>
    <p>You clicked ${count} times</p>
    <button onclick=${increment}>
      Click me
    </button>
  </div>`;
}

function App() {
  return Example(42);
}

Memoised parsing

Templates are parsed lazily and stored for reuse. So you only pay the parsing cost once, the first time you use the template.

Typo correction

Missing closing tags are corrected for automatically. Unmatched closing tags are ignored.

Observables

helium-html uses it's own Observable like classes for dynamic state. These are inspired by Rxjs and the Observable proposal, but follow a subtly different design and implementation which is intended to be simpler to use and debug. As a tradeoff they cannot do as much, but in part I hope this will encourage simpler usage as Rxjs can become incredibly complex.

They do not support the Symbol.Observable method, and are not compatible with the stage 1 Observable proposal.

Observable is an interface that describes 2 methods; watch and pipe. watch subscribes to the Observable with a given callback that is called for each emitted value. pipe accepts an Operator and returns a new Observable based on the Operator, allowing for a source to be chained via various transforms.

Emitter is an Observable that is analogous to a Rxjs Subject. It implements the method emit which sends a value to all it's subscribers immediately. Store is an Observable that is analogous to a Rxjs BehaviourSubject. It contains a value which is sent to new subscribers immediately, updates to the value are sent to all subscribers. The initial value is set via the constructor, and changed using the update method.

AbstractObservable is an abstract class which implements Observable. It is used as a base for Emitter and Store. It is exposed so that developers may implement their own Observable classes with custom behavior. It provides a default implementation for pipe as well as the common operators filter and map. However, requires the implementor to define their own watch method, which is where the bulk of the class specific behavior for Observables occur.

Helium Observables do not support error or completion events. Any faults are thrown back to the trigger. If you wish to pass any error state onto a subscriber I advise utilizing a Result style algebraic type that wraps the success/failure state of an operation. Any subscription to an Observable returns a Disposable object, by calling the dispose method of this object any subscriptions will be cancelled.

Project status

This project is currently in pre-release state. Implementation of Observables, common operators and core behavior of the library have been completed and extensive tests written. Some things might still change and break before reaching 1.0.0, after which normal semver rules will be observed for breaking changes.