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
- Host: GitHub
- URL: https://github.com/lagunoff/typescript-sdom
- Owner: lagunoff
- Created: 2019-03-20T04:57:26.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2023-01-07T07:25:09.000Z (over 2 years ago)
- Last Synced: 2023-03-08T14:40:38.547Z (about 2 years ago)
- Topics: elm, functional-reactive-programming, react, typescript, virtual-dom
- Language: TypeScript
- Homepage:
- Size: 1.47 MB
- Stars: 3
- Watchers: 3
- Forks: 1
- Open Issues: 61
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
[](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' });
```