Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/ryanmorr/reflex

Reactive DOM
https://github.com/ryanmorr/reflex

dom javascript reactive ui

Last synced: 23 days ago
JSON representation

Reactive DOM

Awesome Lists containing this project

README

        

# reflex

[![Version Badge][version-image]][project-url]
[![License][license-image]][license-url]
[![Build Status][build-image]][build-url]

> Reactive DOM

## Install

Download the [CJS](https://github.com/ryanmorr/reflex/raw/master/dist/cjs/reflex.js), [ESM](https://github.com/ryanmorr/reflex/raw/master/dist/esm/reflex.js), [UMD](https://github.com/ryanmorr/reflex/raw/master/dist/umd/reflex.js) versions or install via NPM:

```sh
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:

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

const count = store(0);

const element = html`


Count: ${count}


count.update((val) => val + 1)}>Increment

`;

document.body.appendChild(element);
```

## API

### `store(value?)`

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

```javascript
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:

```javascript
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:

```javascript
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:

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

// Create an element
const el = html`

`;

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

// Create an SVG element
const rect = html``;

// Create a document fragment for multiple root nodes
const frag = html`

`;

// Supports attributes
const div = html`

`;

// Supports spread attributes
const section = html``;

// Supports styles as an object
const header = html``;

// Supports styles as a string
const em = html``;

// Supports functions for setting child nodes
const header = html`${(parentElement) => html`

Title

`}`;

// Supports functions for setting attributes (except event listeners)
const footer = html` 'foo'}>`;

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

#### 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:

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

const name = store('John');

// Interpolate a store into an element
const element = html`

My name is ${name}
`;

// 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:

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

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

// Interpolate a promise like anything else
const element = html`

Hello ${promise}
`;

// 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:

```javascript
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`${children}`;
};

// 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`

Time: ${time}
`;
};

// 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:

```javascript
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`

`;
};
```

#### 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:

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

const element = html`

/* initialize element */}>
`;
```

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:

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

// Use a store to group multiple element references
const foo = store();
const element = html`







`;

// 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:

```javascript
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:

```javascript
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:

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

const value = store('foo');
const element = html``;
```

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

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

const clicked = store();
const button = html`Click Me`;
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:

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

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

const element = html`


    ${each(items, (item, index, array) => html`
  • ${index + 1}: ${item}
  • `)}

`;
```

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

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

const items = store([]);

const element = html`

${each(items,
(item) => html`

${item}
`,
() => html`
No Results
`
)}

`;
```

------

### `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:

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

// Embed a store in the DOM
const store = store('foo');
const element = html`

${store}
`;

// 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:

```javascript
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:

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

// Create an element-store binding
const foo = store('foo');
const element = html`

${foo}
`;

// 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](https://github.com/ryanmorr/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](http://unlicense.org/).

[project-url]: https://github.com/ryanmorr/reflex
[version-image]: https://img.shields.io/github/package-json/v/ryanmorr/reflex?color=blue&style=flat-square
[build-url]: https://github.com/ryanmorr/reflex/actions
[build-image]: https://img.shields.io/github/actions/workflow/status/ryanmorr/reflex/node.js.yml?style=flat-square
[license-image]: https://img.shields.io/github/license/ryanmorr/reflex?color=blue&style=flat-square
[license-url]: UNLICENSE