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

@djledda/ladder

v1.1.0

Published

other libraries provide you with a whole framework - this is just a ladder

Downloads

1

Readme

Ladder

Most libraries give you a whole framework. This is just a ladder.

What's in the box?

Ladder is a tiny* TypeScript-only library to quickly start using "hyperscript" style functions and optionally JSX in your code.

Ladder includes:

  • An (almost) bare-bones h function, JSX compatible.
  • Optional Rung hierarchical UI-node primitive.
  • Optional Capsule reactive value primitive.
  • Optional basic implementation of the pub/sub model: Publishers and Subscribers

Capsules can be saved to directly from the h-function to insert the resultant node into the capsule. They can also be used as the value of a prop to automatically watch for updates and update the HTML node they were used on accordingly.

Everything else is up to you. You have full control over how the app works.

Here is an example app:

import { Rung, Capsule, bootstrap, h, frag } from 'ladder';

class App extends Rung {
    private counter = Capsule.new<number>(0);
    private rungs = Capsule.new<HTMLDivElement | null>(null);

    constructor() {
        super({});
        this.counter.watch((count) => this.onCounterUpdate(count));
    }

    private onCounterUpdate(count: number) {
        const rungs = Array<Node>(count);
        for (let i = 0; i < rungs.length; i++) {
            rungs[i] = <div className={'rung'}/>;
        }
        this.rungs.val?.replaceChildren(...rungs);
    }

    // using JSX
    build() { 
        return <>
            <h1>Ladder</h1>
            <button onclick={() => this.counter.val--}>-</button>
            <span>{this.counter}</span>
            <button onclick={() => this.counter.val++}>+</button>
            <div saveTo={this.rungs}/>
        </>;
    }

    // using pure ts
    build() {
        return frag(null, 
            h("h1", {}, "Ladder"),
            h("button", {onclick: () => this.counter.val--}, "-"),
            h("span", {}, this.counter),
            h("button", {onclick: () => this.counter.val++}, "+"),
            h("div", {saveTo: this.rungs}),
        ); 
    }
}

bootstrap(new App(), "app");

The bootstrap function injects the results of build into the HTML node with the app id.

Rungs

A Rung is any class derived from the internal Rung abstract class. A Rung must implement the build method, returning an instance of the HTML Node primitive (e.g. using the h function included). Once a Rung is 'built', it is done. All subsequent render calls to the Rung will return the prebuilt DOM tree. It is then up to you manipulate the Rung's internal DOM-tree yourself, should anything need to change.

Rungs can be included directly as a child in the JSX and the render function is called automatically. Should you need to rerun the build function, you can call redraw internally and the resultant node will be inserted at its predecessor's position in the DOM directly.

This build function will insert the result of the render call of the this.coolRung instance directly.

class MyCoolRung extends Rung {
    // ... great code ...
}

class SuperRung extends Rung {
    private coolRung = new MyCoolRung();

    constructor() {
        super({});
    }

    build() {
        return <div>Check out this rung here: {this.coolRung}</div>;
    }
}

Rungs are intentionally not able to be used in JSX. JSX should only be used for HTMLElements, as Rungs are not declarative, rather, they are simple objects.

Capsules

A Capsule is a primitive used to store a single value that can be watched for changes.

It can be any object fulfilling the following interface (included in the library):

interface ICapsule<T extends Captable = Captable> {
    watch(watcher: (newVal: T) => void, after?: boolean): ISubscription;
    toString(): string;
    val: T;
}

type Captable = { toString(): string; } | string | null;

interface ISubscription {
    unbind(): void;
}

I.e. Capsules must encapsulate values that can either be null or be able to be cast to a string.

Capsules can be used in h/JSX as a HTML attribute, a child node, or as the value of the special saveTo property:

Taking the build method from the initial example:

class App extends Rung {
    // ...
    build() {
        return <>
            <h1>Ladder</h1>
            <button onclick={() => this.counter.val--}>-</button>
            <span>{this.counter}</span>
            <button onclick={() => this.counter.val++}>+</button>
            <div saveTo={this.rungs}/>
        </>;
    }
    // ...
}

this.counter and this.rungs are both Capsules and as such the node generated as a child of the <span> for this.counter will update when the watcher callback is fired. Similarly, the <div> node at the end of the fragment is saved to this.rungs to be used in the Rung.

Pub/Sub

Often, littering your code with reactive primitives isn't the best idea. You might want to notify your dependants of any updates after a series of complex operations that are applied to multiple different values that are used in different places. Notifying your dependants manually is useful for this kind of use case.

Ladder includes Publisher and Subscription primitives to include in your Rungs or elsewhere in your program.

For example, suppose you have a Track data class that emits events:

const enum TrackEvents {
    NewTimeSig="tr-0",
    NewBarCount="tr-1",
    NewName="tr-2",
    DisplayTypeChanged="tr-3",
    Baked="tr-4",
    DeepChange="tr-5",
}

class Track implements IPublisher<TrackEvents> {
    // ...
}

And a Rung that listens for some of them:

type TrackSubs =
    | TrackEvents.NewName
    | TrackEvents.NewTimeSig
    | TrackEvents.NewBarCount
    | TrackEvents.DisplayTypeChanged;

class TrackView extends Rung implements ISubscriber<TrackSubs> {
    // ...
}

The track view can then subscribe to its track instance member using the same strings using track.addSubscriber(this, <array-of-track-subs>). To respond to fired events, TrackView implements notify:

class TrackView extends Rung implements ISubscriber<TrackSubs> {
    // ...
    notify(publisher: Track, event: TrackSubs): void {
        switch (event) {
        case TrackEvents.NewName:
        case TrackEvents.NewTimeSig:
        case TrackEvents.NewBarCount:
        case TrackEvents.DisplayTypeChanged:
        case TrackEvents.LoopLengthChanged:
            // respond!
            break;
        }
    }
    // ...
}

addSubscriber returns ISubscription, with the same interface as a Capsule. You must call unbind if you want to stop listening e.g. when doing cleanup tasks.

Here is a trick to write the list of subscribed events once and generate a type from it, reducing duplicate code and consistency mess:

const TrackSubs = [
    TrackEvents.NewName,
    TrackEvents.NewTimeSig,
    TrackEvents.NewBarCount,
    TrackEvents.DisplayTypeChanged,
];

type TrackSubs = typeof TrackSubs[number]; // Yes, the names can be identical!

class TrackView extends Rung implements ISubscriber<TrackSubs> {
    // ...
}

Miscellaneous Helpers

There are also the two methods q and frag that wrap document.createTextNode and document.createDocumentFragment respectively to reduce bloat when using pure JavaScript.


That's about it. You can use as much or as little as you want, hopefully you find it useful for simple or complicated apps for which modern JS Frameworks are just too much overhead in terms of either setup, performance, or restrictivity.

*All components of Ladder are about ~2.5KiB transpiled and minified, ~1KiB gzipped.