{"id":26025425,"url":"https://github.com/lagunoff/typescript-sdom","last_synced_at":"2026-02-26T16:36:34.138Z","repository":{"id":39482513,"uuid":"176656777","full_name":"lagunoff/typescript-sdom","owner":"lagunoff","description":"Fast and lightweight VirtualDOM alternative","archived":false,"fork":false,"pushed_at":"2023-01-07T07:25:09.000Z","size":1540,"stargazers_count":3,"open_issues_count":61,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2023-03-08T14:40:38.547Z","etag":null,"topics":["elm","functional-reactive-programming","react","typescript","virtual-dom"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lagunoff.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-03-20T04:57:26.000Z","updated_at":"2022-04-14T14:38:06.000Z","dependencies_parsed_at":"2023-02-06T14:46:40.350Z","dependency_job_id":null,"html_url":"https://github.com/lagunoff/typescript-sdom","commit_stats":null,"previous_names":[],"tags_count":null,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lagunoff%2Ftypescript-sdom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lagunoff%2Ftypescript-sdom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lagunoff%2Ftypescript-sdom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lagunoff%2Ftypescript-sdom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lagunoff","download_url":"https://codeload.github.com/lagunoff/typescript-sdom/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":242220461,"owners_count":20091786,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elm","functional-reactive-programming","react","typescript","virtual-dom"],"created_at":"2025-03-06T13:39:43.900Z","updated_at":"2026-02-26T16:36:29.080Z","avatar_url":"https://github.com/lagunoff.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Generic badge](https://img.shields.io/badge/status-experimental-red.svg)](https://shields.io/)\n## Explanation\nSDOM stands for Static DOM, just like VirtualDOM, SDOM is a\ndeclarative way of describing GUIs. SDOM solves performance problems\nin VirtualDOM by sacrificing some expressiveness. Basically, only the\nattributes and the contents of `Text` nodes can change over time, the\n    overall shape of DOM tree remains constant, thus Static DOM. The\nidea is by [Phil Freeman](https://github.com/paf31) see his\n[post](https://blog.functorial.com/posts/2018-03-12-You-Might-Not-Need-The-Virtual-DOM.html)\nand purescript\n[implementation](https://github.com/paf31/purescript-sdom).\n\nHere is a pseudocode emphasising the difference between VirtualDOM and SDOM approach\n```ts\ntype Model = { text: string; active: boolean };\n\n// The whole tree is recomputed on each render\nconst virtualdom = (model: Model) =\u003e h.div({ class: model.active ? 'active' : '' },\n  h.span(model.text)\n);\n\n// Only the props and the text nodes are recomputed\nconst sdom = h.div({ class: model =\u003e model.active ? 'active' : '' },\n  h.span(model =\u003e model.text)\n);\n```\n\n## Installation\n```sh\n$ yarn add typescript-sdom\n```\n\n## Simplest app\ndemo: [https://lagunoff.github.io/typescript-sdom/simple/](https://lagunoff.github.io/typescript-sdom/simple/)\n```ts\nimport * as sdom from '../../src';\nconst h = sdom.create\u003cDate, never\u003e();\n\nconst view = h.div(\n  { style: `text-align: center` },\n  h.h1('Local time', { style: date =\u003e `color: ` + colors[date.getSeconds() % 6] }),\n  h.p(date =\u003e date.toString()),\n);\n\nconst colors = ['#F44336', '#03A9F4', '#4CAF50', '#3F51B5', '#607D8B', '#FF5722'];\nconst model = { value: new Date() };\nconst el = view.create(sdom.observable.create(model), sdom.noop);\ndocument.body.appendChild(el);\nsetInterval(tick, 1000);\n\nfunction tick() {\n  sdom.observable.next(model, new Date());\n}\n```\n\n## Representation \nSimplified definitions of the main data types:\n```ts\nexport type SDOM\u003cModel, Msg, El\u003e = {\n  create(o: Observable\u003cModel\u003e, sink: Sink\u003cMsg\u003e): El;\n};\nexport type Sink\u003cT\u003e = (x: T) =\u003e void;\nexport type Observable\u003cT\u003e = { subscribe: Subscribe\u003c{ prev: T; next: T; }\u003e\u003e; getValue(): T; }; \nexport type Subscribe\u003cT\u003e = (onNext: (x: T) =\u003e void, onComplete: () =\u003e void) =\u003e Unlisten;\nexport type Unlisten = () =\u003e void;\nexport type Subscription\u003cT\u003e = { onNext: (x: T) =\u003e void; onComplete: () =\u003e void; };\nexport type ObservableValue\u003cT\u003e = { value: T; subscriptions?: Subscription\u003c{ prev: T; next: T }\u003e\u003e[]; };\n```\n\n`SDOM\u003cModel, Msg, El\u003e` describes a piece of UI that consumes data of\ntype `Model` and produces events of type `Msg` also value of type `El`\nis the end-product of running `SDOM` component, in case of browser\napps `El` is a subset of type `Node` (could be Text, HTMLDivElement\netc), but the definition of `SDOM` is supposed to work in other\nsettings as well by changing `El` parameter to the relevant type. The\nmodule [src/observable.ts](src/observable.ts) contains minimal\nimplementation of subscription-based functionality for dealing with\nvalues that can change over time. `Observable\u003cT\u003e` and\n`ObservableValue\u003cT\u003e` are the most important definitions from that\nmodule. `ObservableValue\u003cT\u003e` is the source that contains changing\nvalue and `Observable\u003cT\u003e` provides interface for querying that value\nand also to setup notifications for future changes.\n\n## Examples\n\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eSimple app\u003c/td\u003e\n      \u003ctd\u003e\n\t    \u003ca href=./examples/simple/index.ts target=_blank\u003esource\u003c/a\u003e |\n\t\t\u003ca href=https://lagunoff.github.io/typescript-sdom/simple/ target=_blank\u003edemo\u003ca\u003e\n\t  \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eVariants\u003c/td\u003e\n      \u003ctd\u003e\n\t    \u003ca href=./examples/variants/index.ts target=_blank\u003esource\u003c/a\u003e |\n\t\t\u003ca href=https://lagunoff.github.io/typescript-sdom/variants/ target=_blank\u003edemo\u003ca\u003e\n\t  \u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd\u003eTodoMVC\u003c/td\u003e\n      \u003ctd\u003e\n\t    \u003ca href=./examples/todomvc/src/index.ts target=_blank\u003esource\u003c/a\u003e |\n\t\t\u003ca href=https://lagunoff.github.io/typescript-sdom/todomvc/ target=_blank\u003edemo\u003ca\u003e\n\t  \u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n## Links\n- [https://github.com/paf31/purescript-sdom](https://github.com/paf31/purescript-sdom)\n- [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)\n\n## Todos\n - [ ] Similar approach for non-web GUIs (ReactNative, QTQuick)\n - [ ] Investigate the possibility of using generator-based effects in `update` e.g. [redux-saga](https://github.com/redux-saga/redux-saga), add examples\n - [ ] Better API and docs for `src/observable.ts`\n - [ ] Add benchmarks\n - [ ] Improve performance for large arrays with https://github.com/ashaffer/mini-hamt\n\n## API reference\n#### h\n\n`function h(name: string, ...rest: Array\u003cstring | number | Props\u003cunknown, unknown, unknown, HTMLElement\u003e | SDOM\u003cunknown, unknown, unknown, Node\u003e | ((m: unknown) =\u003e string)\u003e): SDOM\u003cunknown, unknown, unknown, HTMLElement\u003e;`\n\nAn alias for `elem`. Also a namespace for the most [common html\ntags](./src/html.ts) and all public API. All functions exposed by\n`h` have their `Model` and `Msg` parameters bound, see docs for\n`create`, see also [todomvc](examples/todomvc/src/index.ts) for\nusage examples\n\n#### type SDOM\n\n```ts\nexport type SDOM\u003cIn, Out, Msg, Elem extends Node = Node\u003e = {\n  create(model: Store\u003cIn\u003e, sink: Sink\u003cPrimMsg\u003cIn, Out, Msg, Elem\u003e\u003e): Elem;\n};\n```\n\n`SDOM\u003cIn, Out, Msg, Elem\u003e` constructor for dynamic `Elem` node\n\n#### create\n\n`function create\u003cIn, Out, Msg\u003e(): H\u003cIn, Out, Msg\u003e;`\n\nBind type parameters for `h`. This function does nothing at runtime\nand just returns `h` singleton which exposes all API with bound\n`Model` and `Msg` parameters. Without this typescript is not able\nto unify types if you use directly exported functions from the\nlibrary. You dont need this in JS code.\n\n```ts\n type Model = { counter: number };\n type Msg = 'Click';\n const h = sdom.create\u003cIn, Out, Msg\u003e();\n const view = h.div(\n     h.p(m =\u003e `You clicked ${m.counter} times`),\n     h.button('Click here', { onclick: () =\u003e 'Click' }),\n );\n const model = { value: { counter: 0 } };\n const el = view.create(sdom.store.create(model), sdom.noop);\n assert.instanceOf(el.childNodes[0], HTMLParagraphElement);\n assert.instanceOf(el.childNodes[1], HTMLButtonElement);\n```\n\n#### attach\n\n`function attach\u003cModel, Msg, Elem extends Node\u003e(view: SDOM\u003cModel, Model, Msg, Elem\u003e, rootEl: HTMLElement, init: Model, sink: Sink\u003cMsg\u003e): SDOMInstance\u003cModel, Msg, Elem\u003e;`\n\nStart the application and attach it to `rootEl`\n\n```ts\nconst view = h.div(h.h1('Hello world!', { id: 'greeting' }));\nconst inst = sdom.attach(view, document.body, {});\nassert.equal(document.getElementById('greeting').textContent, 'Hello world!');\n```\n\n#### element\n\n`function element\u003cIn, Out, Msg\u003e(name: string, ...rest: Array\u003cMany\u003cElementContent\u003cIn, Out, Msg, HTMLElement\u003e\u003e\u003e): SDOM\u003cIn, Out, Msg, HTMLElement\u003e;`\n\nCreate an html node. Attributes and contents can go in any order\n\n```ts\nconst view = sdom.element('a', { href: '#link' });\nconst el = view.create(sdom.store.of({}), msg =\u003e {});\nassert.instanceOf(el, HTMLAnchorElement);\nassert.equal(el.hash, '#link');\n```\n\n#### text\n\n`function text\u003cIn, Out, Msg\u003e(value: string | number | ((m: In) =\u003e string | number)): SDOM\u003cIn, Out, Msg, Text\u003e;`\n\nCreate Text node\n\n```ts\nconst view = sdom.text(n =\u003e `You have ${n} unread messages`);\nconst model = { value: 0 };\nconst el = view.create(sdom.store.create(model), sdom.noop);\nassert.instanceOf(el, Text);\nassert.equal(el.nodeValue, 'You have 0 unread messages');\nsdom.store.next(model, 5);\nassert.equal(el.nodeValue, 'You have 5 unread messages');\n```\n\n#### array\n\n`function array\u003cIn, Out, Msg, X\u003e(lens: Lens\u003cIn, Out, Array\u003cX\u003e, Array\u003cX\u003e\u003e, name: string, props?: Props\u003cIn, Out, Msg, HTMLElement\u003e): (child: SDOM\u003cNested\u003cIn, X\u003e, X, (idx: number) =\u003e Msg, Node\u003e) =\u003e SDOM\u003cIn, Out, Msg, HTMLElement\u003e;`\n\nCreate an html node which content is a dynamic list of child nodes\n\n```ts\nconst view = h.array('ul', { class: 'list' })(\n  m =\u003e m.list,\n  h =\u003e h.li(m =\u003e m.here),\n);\nconst list = ['One', 'Two', 'Three', 'Four'];\nconst el = view.create(sdom.store.of({ list }), msg =\u003e {});\nassert.instanceOf(el, HTMLUListElement);\nassert.equal(el.childNodes[3].innerHTML, 'Four');\n```\n\n#### discriminate\n\n`function discriminate\u003cIn, Out, Msg, El extends Node, K extends string\u003e(discriminator: (m: In) =\u003e K, alternatives: Record\u003cK, SDOM\u003cIn, Out, Msg, El\u003e\u003e): SDOM\u003cIn, Out, Msg, El\u003e;`\n\nGeneric way to create `SDOM` which content depends on some\ncondition on `Model`. First parameter checks this condition and\nreturns the index that points to the current `SDOM` inside\n`alternatives`. This is useful for routing, tabs, etc. See also\n[variants](/examples/variants/index.ts) example with more\nconvenient and more typesafe way of displaying union types\n\n```ts\ntype Tab = 'Details'|'Comments';\ntype Model = { tab: Tab, details: string; comments: string[] };\nconst view = h.div(sdom.discriminate(m =\u003e m.tab, {\n    Details: h.p({ id: 'details' }, m =\u003e m.details),\n    Comments: h.p({ id: 'comments' }, m =\u003e m.comments.join(', ')),\n}));\nconst model = { value: { tab: 'Details', details: 'This product is awesome', comments: [`No it's not`] } };\nconst el = view.create(sdom.store.create(model), sdom.noop);\nassert.equal(el.childNodes[0].id, 'details'); \nassert.equal(el.childNodes[0].textContent, 'This product is awesome');\nsdom.store.next(model, { ...model.value, tab: 'Comments' });\nassert.equal(el.childNodes[0].id, 'comments');\n```\n\n#### dimap\n\n`function dimap\u003cIn1, Out1, In2, Out2\u003e(coproj: (m: In2) =\u003e In1, proj: (m: Out1) =\u003e Out2): \u003cMsg, Elem extends Node\u003e(s: SDOM\u003cIn1, Out1, Msg, Elem\u003e) =\u003e SDOM\u003cIn2, Out2, Msg, Elem\u003e;`\n\nChange both type parameters inside `SDOM\u003cModel, Msg\u003e`.\n\n```ts\ntype Model1 = { btnTitle: string };\ntype Msg1 = { tag: 'Clicked' };\ntype Model2 = string;\ntype Msg2 = 'Clicked';\nlet latestMsg: any = void 0;\nconst view01 = sdom.elem\u003cModel2, Msg2\u003e('button', (m: Model2) =\u003e m, { onclick: () =\u003e 'Clicked'});\nconst view02 = sdom.dimap\u003cModel1, Msg1, Model2, Msg2\u003e(m =\u003e m.btnTitle, msg2 =\u003e ({ tag: 'Clicked' }))(view01);\nconst el = view02.create(sdom.observable.of({ btnTitle: 'Click on me' }), msg =\u003e (latestMsg = msg));\nel.click();\nassert.instanceOf(el, HTMLButtonElement);\nassert.equal(el.textContent, 'Click on me');\nassert.deepEqual(latestMsg, { tag: 'Clicked' });\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flagunoff%2Ftypescript-sdom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flagunoff%2Ftypescript-sdom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flagunoff%2Ftypescript-sdom/lists"}