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

https://github.com/lagunoff/typescript-sdom

Fast and lightweight VirtualDOM alternative
https://github.com/lagunoff/typescript-sdom

elm functional-reactive-programming react typescript virtual-dom

Last synced: 2 months ago
JSON representation

Fast and lightweight VirtualDOM alternative

Awesome Lists containing this project

README

        

[![Generic badge](https://img.shields.io/badge/status-experimental-red.svg)](https://shields.io/)
## Explanation
SDOM stands for Static DOM, just like VirtualDOM, SDOM is a
declarative way of describing GUIs. SDOM solves performance problems
in VirtualDOM by sacrificing some expressiveness. Basically, only the
attributes and the contents of `Text` nodes can change over time, the
overall shape of DOM tree remains constant, thus Static DOM. The
idea is by [Phil Freeman](https://github.com/paf31) see his
[post](https://blog.functorial.com/posts/2018-03-12-You-Might-Not-Need-The-Virtual-DOM.html)
and purescript
[implementation](https://github.com/paf31/purescript-sdom).

Here is a pseudocode emphasising the difference between VirtualDOM and SDOM approach
```ts
type Model = { text: string; active: boolean };

// The whole tree is recomputed on each render
const virtualdom = (model: Model) => h.div({ class: model.active ? 'active' : '' },
h.span(model.text)
);

// Only the props and the text nodes are recomputed
const sdom = h.div({ class: model => model.active ? 'active' : '' },
h.span(model => model.text)
);
```

## Installation
```sh
$ yarn add typescript-sdom
```

## Simplest app
demo: [https://lagunoff.github.io/typescript-sdom/simple/](https://lagunoff.github.io/typescript-sdom/simple/)
```ts
import * as sdom from '../../src';
const h = sdom.create();

const view = h.div(
{ style: `text-align: center` },
h.h1('Local time', { style: date => `color: ` + colors[date.getSeconds() % 6] }),
h.p(date => date.toString()),
);

const colors = ['#F44336', '#03A9F4', '#4CAF50', '#3F51B5', '#607D8B', '#FF5722'];
const model = { value: new Date() };
const el = view.create(sdom.observable.create(model), sdom.noop);
document.body.appendChild(el);
setInterval(tick, 1000);

function tick() {
sdom.observable.next(model, new Date());
}
```

## Representation
Simplified definitions of the main data types:
```ts
export type SDOM = {
create(o: Observable, sink: Sink): El;
};
export type Sink = (x: T) => void;
export type Observable = { subscribe: Subscribe<{ prev: T; next: T; }>>; getValue(): T; };
export type Subscribe = (onNext: (x: T) => void, onComplete: () => void) => Unlisten;
export type Unlisten = () => void;
export type Subscription = { onNext: (x: T) => void; onComplete: () => void; };
export type ObservableValue = { value: T; subscriptions?: Subscription<{ prev: T; next: T }>>[]; };
```

`SDOM` describes a piece of UI that consumes data of
type `Model` and produces events of type `Msg` also value of type `El`
is the end-product of running `SDOM` component, in case of browser
apps `El` is a subset of type `Node` (could be Text, HTMLDivElement
etc), but the definition of `SDOM` is supposed to work in other
settings as well by changing `El` parameter to the relevant type. The
module [src/observable.ts](src/observable.ts) contains minimal
implementation of subscription-based functionality for dealing with
values that can change over time. `Observable` and
`ObservableValue` are the most important definitions from that
module. `ObservableValue` is the source that contains changing
value and `Observable` provides interface for querying that value
and also to setup notifications for future changes.

## Examples



Simple app

source |
demo



Variants

source |
demo



TodoMVC

source |
demo


## Links
- [https://github.com/paf31/purescript-sdom](https://github.com/paf31/purescript-sdom)
- [https://blog.functorial.com/posts/2018-03-12-You-Might-Not-Need-The-Virtual-DOM.html](https://blog.functorial.com/posts/2018-03-12-You-Might-Not-Need-The-Virtual-DOM.html)

## Todos
- [ ] Similar approach for non-web GUIs (ReactNative, QTQuick)
- [ ] Investigate the possibility of using generator-based effects in `update` e.g. [redux-saga](https://github.com/redux-saga/redux-saga), add examples
- [ ] Better API and docs for `src/observable.ts`
- [ ] Add benchmarks
- [ ] Improve performance for large arrays with https://github.com/ashaffer/mini-hamt

## API reference
#### h

`function h(name: string, ...rest: Array | SDOM | ((m: unknown) => string)>): SDOM;`

An alias for `elem`. Also a namespace for the most [common html
tags](./src/html.ts) and all public API. All functions exposed by
`h` have their `Model` and `Msg` parameters bound, see docs for
`create`, see also [todomvc](examples/todomvc/src/index.ts) for
usage examples

#### type SDOM

```ts
export type SDOM = {
create(model: Store, sink: Sink>): Elem;
};
```

`SDOM` constructor for dynamic `Elem` node

#### create

`function create(): H;`

Bind type parameters for `h`. This function does nothing at runtime
and just returns `h` singleton which exposes all API with bound
`Model` and `Msg` parameters. Without this typescript is not able
to unify types if you use directly exported functions from the
library. You dont need this in JS code.

```ts
type Model = { counter: number };
type Msg = 'Click';
const h = sdom.create();
const view = h.div(
h.p(m => `You clicked ${m.counter} times`),
h.button('Click here', { onclick: () => 'Click' }),
);
const model = { value: { counter: 0 } };
const el = view.create(sdom.store.create(model), sdom.noop);
assert.instanceOf(el.childNodes[0], HTMLParagraphElement);
assert.instanceOf(el.childNodes[1], HTMLButtonElement);
```

#### attach

`function attach(view: SDOM, rootEl: HTMLElement, init: Model, sink: Sink): SDOMInstance;`

Start the application and attach it to `rootEl`

```ts
const view = h.div(h.h1('Hello world!', { id: 'greeting' }));
const inst = sdom.attach(view, document.body, {});
assert.equal(document.getElementById('greeting').textContent, 'Hello world!');
```

#### element

`function element(name: string, ...rest: Array>>): SDOM;`

Create an html node. Attributes and contents can go in any order

```ts
const view = sdom.element('a', { href: '#link' });
const el = view.create(sdom.store.of({}), msg => {});
assert.instanceOf(el, HTMLAnchorElement);
assert.equal(el.hash, '#link');
```

#### text

`function text(value: string | number | ((m: In) => string | number)): SDOM;`

Create Text node

```ts
const view = sdom.text(n => `You have ${n} unread messages`);
const model = { value: 0 };
const el = view.create(sdom.store.create(model), sdom.noop);
assert.instanceOf(el, Text);
assert.equal(el.nodeValue, 'You have 0 unread messages');
sdom.store.next(model, 5);
assert.equal(el.nodeValue, 'You have 5 unread messages');
```

#### array

`function array(lens: Lens, Array>, name: string, props?: Props): (child: SDOM, X, (idx: number) => Msg, Node>) => SDOM;`

Create an html node which content is a dynamic list of child nodes

```ts
const view = h.array('ul', { class: 'list' })(
m => m.list,
h => h.li(m => m.here),
);
const list = ['One', 'Two', 'Three', 'Four'];
const el = view.create(sdom.store.of({ list }), msg => {});
assert.instanceOf(el, HTMLUListElement);
assert.equal(el.childNodes[3].innerHTML, 'Four');
```

#### discriminate

`function discriminate(discriminator: (m: In) => K, alternatives: Record>): SDOM;`

Generic way to create `SDOM` which content depends on some
condition on `Model`. First parameter checks this condition and
returns the index that points to the current `SDOM` inside
`alternatives`. This is useful for routing, tabs, etc. See also
[variants](/examples/variants/index.ts) example with more
convenient and more typesafe way of displaying union types

```ts
type Tab = 'Details'|'Comments';
type Model = { tab: Tab, details: string; comments: string[] };
const view = h.div(sdom.discriminate(m => m.tab, {
Details: h.p({ id: 'details' }, m => m.details),
Comments: h.p({ id: 'comments' }, m => m.comments.join(', ')),
}));
const model = { value: { tab: 'Details', details: 'This product is awesome', comments: [`No it's not`] } };
const el = view.create(sdom.store.create(model), sdom.noop);
assert.equal(el.childNodes[0].id, 'details');
assert.equal(el.childNodes[0].textContent, 'This product is awesome');
sdom.store.next(model, { ...model.value, tab: 'Comments' });
assert.equal(el.childNodes[0].id, 'comments');
```

#### dimap

`function dimap(coproj: (m: In2) => In1, proj: (m: Out1) => Out2): (s: SDOM) => SDOM;`

Change both type parameters inside `SDOM`.

```ts
type Model1 = { btnTitle: string };
type Msg1 = { tag: 'Clicked' };
type Model2 = string;
type Msg2 = 'Clicked';
let latestMsg: any = void 0;
const view01 = sdom.elem('button', (m: Model2) => m, { onclick: () => 'Clicked'});
const view02 = sdom.dimap(m => m.btnTitle, msg2 => ({ tag: 'Clicked' }))(view01);
const el = view02.create(sdom.observable.of({ btnTitle: 'Click on me' }), msg => (latestMsg = msg));
el.click();
assert.instanceOf(el, HTMLButtonElement);
assert.equal(el.textContent, 'Click on me');
assert.deepEqual(latestMsg, { tag: 'Clicked' });
```