3.0.0 • Published 12 months ago

@ryanmorr/reflex v3.0.0

Weekly downloads
-
License
Unlicense
Repository
github
Last release
12 months ago

reflex

Version Badge License Build Status

Reactive DOM

Install

Download the CJS, ESM, UMD versions or install via NPM:

npm install @ryanmorr/reflex

Usage

Reflex is a small, but versatile UI library that combines declarative DOM building with reactive stores that bind data to DOM nodes, automatically keeping the DOM in sync when the data is changed:

import { html, store } from '@ryanmorr/reflex';

const count = store(0);

const element = html`
    <div>
        <p>Count: ${count}</p>
        <button onclick=${() => count.update((val) => val + 1)}>Increment</button>
    </div>
`;

document.body.appendChild(element);

API

store(value?)

Create a reactive store that encapsulates a value and can notify subscribers when the value changes:

import { store } from '@ryanmorr/reflex';

// Create a store with an initial value
const count = store(0);

// Get the store value
count.value(); //=> 0

// Set the store value
count.set(1);

// Set the store value with a callback function
count.update((val) => val + 1);

// Subscribe a callback function to be invoked when the value changes,
// it returns a function to unsubscribe from future updates
const unsubscribe = count.subscribe((nextVal, prevVal) => {
    // Do something
});

derived(...stores, callback)

Create a reactive store that is based on the value of one or more other stores:

import { derived, store } from '@ryanmorr/reflex';

const firstName = store('John');
const lastName = store('Doe');
const fullName = derived(firstName, lastName, (first, last) => `${first} ${last}`);

fullName.value(); //=> "John Doe"

firstName.set('Jane');

fullName.value(); //=> "Jane Doe"

// Subscribe to be notified of changes
const unsubscribe = fullName.subscribe((nextVal, prevVal) => {
    // Do something
});

If the callback function defines an extra parameter in its signature, the derived store is treated as asynchronous. The callback function is provided a setter for the store's value and no longer relies on the return value:

import { derived, store } from '@ryanmorr/reflex';

const query = store();

// Perform an ajax request when the query changes
// and notify subscribers with the results
const results = derived(query, (string, set) => {
    fetch(`path/to/server/${encodeURIComponent(string)}`).then(set);
});

html(strings, ...values?)

Create DOM nodes declaratively via tagged template literals:

import { html } from '@ryanmorr/reflex';

// Create an element
const el = html`<div></div>`;

// Create a text node
const text = html`Hello World`;

// Create an SVG element
const rect = html`<rect x="10" y="10" width="100" height="100" />`;

// Create a document fragment for multiple root nodes
const frag = html`<div></div><span></span>`;

// Supports attributes
const div = html`<div id="foo" class=${'bar'} />`;

// Supports spread attributes
const section = html`<section ...${{id: 'foo', class: 'bar'}} />`;

// Supports styles as an object
const header = html`<header style=${{width: '100px', height: '100px'}} />`;

// Supports styles as a string
const em = html`<em style=${'color: red; text-decoration: underline red;'} />`;

// Supports functions for setting child nodes
const header = html`<header>${(parentElement) => html`<h1>Title</h1>`}</header>`;

// Supports functions for setting attributes (except event listeners)
const footer = html`<footer class=${(element, attributeName) => 'foo'}></footer>`;

// Supports event listeners (indicated by a prefix of "on")
const button = html`<button onclick=${(e) => console.log('clicked!')}>Click Me</button>`;

Bindings

When a reactive store is interpolated into a DOM element created with html, it creates a reactive binding that will automatically update that portion of the DOM, and only that portion, when the internal store value changes:

import { html, store } from '@ryanmorr/reflex';

const name = store('John');

// Interpolate a store into an element
const element = html`<div>My name is ${name}</div>`;

// The store value is appended as a text node
element.textContent; //=> "My name is John"

// The store is bound to the text node, changing
// the store value automatically updates the text
// node and only that text node
name.set('Jim');

// After rendering is completed
element.textContent; //=> "My name is Jim"

Similarly to stores, promises can also be interpolated into a DOM element created with html, setting the value of the node/attribute when the promise resolves:

import { html } from '@ryanmorr/reflex';

const promise = Promise.resolve('World');

// Interpolate a promise like anything else
const element = html`<div>Hello ${promise}</div>`;

// After the promise resolves and rendering is completed
element.textContent; //=> "Hello World"

Components

Functional components are also supported. Since reflex is not virtual DOM, a component is only executed once, making both stateless and stateful components easy:

import { html, store } from '@ryanmorr/reflex';

// A simple component to wrap a common pattern with props and child nodes
const Stateless = ({id, children}) => {
    return html`<section id=${id}>${children}</section>`;
};

// Create the component and return a DOM element
const section = html`<${Stateless} id="foo">bar<//>`;

// A component that holds state
const Stateful = () => {
    const getTime = () => new Date().toLocaleTimeString();
    const time = store(getTime());
    setInterval(() => time.set(getTime()), 1000);
    return html`<div>Time: ${time}</div>`;
};

// Create the stateful component just like a stateless one
const div = html`<${Stateful} />`;

If the component function defines an extra parameter as part of its signature, it is provided a function for registering callbacks to be invoked when the component is mounted to the DOM. Optionally, the mount callback can return a cleanup function that is executed when the component is disposed:

import { html } from '@ryanmorr/reflex';

const Component = (props, mount) => {

    mount((element) => {
        // Executed when the component is appended to 
        // the DOM and is provided the root element(s)

        return () => {
            // Executed when the component is disposed
        };
    });

    return html`<div></div>`;
};

Refs

When creating elements with html, the ref attribute can be used to invoke a function when the element is first created. This is useful for initializing elements and collecting references to deeply nested elements:

import { html } from '@ryanmorr/reflex';

const element = html`<div ref=${el => /* initialize element */}></div>`;

Additionally, assigning a store as the value of a ref attribute will add the element to an internal array within the store. Subscribers of the store will be notified when elements are added and removed:

import { html, store, dispose } from '@ryanmorr/reflex';

// Use a store to group multiple element references
const foo = store();
const element = html`
    <ul>
        <li ref=${foo}></li>
        <li ref=${foo}></li>
        <li ref=${foo}></li>
        <li ref=${foo}></li>
    </ul>
`;

// Returns an array of all elements in the store
const elements = foo.value();

// Subscribe to be called when elements are added or removed
foo.subscribe((nextElements, prevElements) => {
    // Do something
});

// Disposing an element will automatically remove it from the store
dispose(element.lastChild);

effect(...stores?, callback)

Create a side effect that is executed every time the DOM has been updated and return a function to stop future calls:

import { effect } from '@ryanmorr/reflex';

const stop = effect(() => {
    // DOM has been updated
});

Providing one or more dependencies will create a side effect that is guaranteed to execute after a store value changes and any portion of the DOM that depends on that store has been updated:

import { effect, store } from '@ryanmorr/reflex';

const id = store('foo');
const content = store('bar');

const stop = effect(id, content, (idVal, contentVal) => {
    // Invoked anytime `id` or `content` changes and the DOM has been updated
});

bind(store)

Create a two-way binding between a store and a form field, allowing the store to be automatically updated with the current value of the form element when the user changes it, and vice-versa. It supports inputs, checkboxes, radio buttons, selects, and textareas:

import { bind, html, store } from '@ryanmorr/reflex';

const value = store('foo');
const element = html`<input value=${bind(value)} />`;

Alternatively, bind can be used to support stores as event listeners:

import { bind, html, store } from '@ryanmorr/reflex';

const clicked = store();
const button = html`<button onclick=${bind(clicked)}>Click Me</button>`;
clicked.subscribe((event) => console.log('clicked'));

each(store, callback, fallback?)

Efficiently diffs and renders lists when provided a reactive store that encapsulates an iterable value. Upon reconciliation, the each function uses a strict equality operator (===) to compare the indexed values of the iterable and determine if an element has been removed or relocated:

import { each, html, store } from '@ryanmorr/reflex';

const items = store([1, 2, 3, 4, 5]);

const element = html`
    <ul>
        ${each(items, (item, index, array) => html`<li>${index + 1}: ${item}</li>`)}
    </ul>
`;

Provide a fallback function as an optional third argument to render content when the store contains an empty iterable or non-iterable value:

import { each, html, store } from '@ryanmorr/reflex';

const items = store([]);

const element = html`
    <section>
        ${each(items, 
            (item) => html`<div>${item}</div>`,
            () => html`<div>No Results</div>`
        )}
    </section>
`;

tick()

Reflex uses deferred rendering to batch DOM updates. The tick function returns a promise that is resolved when all previously queued DOM updates have been rendered:

import { tick } from '@ryanmorr/reflex';

// Embed a store in the DOM
const store = store('foo');
const element = html`<div>${store}</div>`;

// Change a store value to trigger a re-render
store.set('bar');

// The DOM is up-to-date when the `tick` promise resolves
await tick();

cleanup(element, callback)

Register a callback function to be invoked when an element is disposed. An element is disposed implicitly only during an each DOM reconciliation or explicitly when the dispose function is called on the element or an ancestor element:

import { cleanup } from '@ryanmorr/reflex';

cleanup(element, () => console.log('element and child nodes disposed'));

dispose(element)

Destroy all node-store bindings to prevent future DOM updates and invoke any registered cleanup functions for an element and its descendants. It will also remove the element and its descendants from any store it was added to via the ref attribute:

import { dispose, store, html, tick } from '@ryanmorr/reflex';

// Create an element-store binding
const foo = store('foo');
const element = html`<div>${foo}</div>`;

// Update element
foo.set('bar');
await tick();
console.log(element.textContent); //=> "bar"

// Destroy the element-store binding
dispose(element);

// The element is no longer updated
foo.set('baz');
await tick();
console.log(element.textContent); //=> "bar"

CSS

For a CSS-in-JS solution, refer to fusion, a similar library that brings reactivity to CSS variables, media queries, keyframes, and element queries among other helpers. It is also 100% compatible with reflex.

License

This project is dedicated to the public domain as described by the Unlicense.

3.0.0

12 months ago

2.0.1

2 years ago

2.0.0

2 years ago

1.0.0

3 years ago

0.1.0

4 years ago