@realglebivanov/reactive 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ nodejs 25.4.0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2026 Gleb Ivanov <realglebivanov@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Reactive
2
+
3
+ ## Quick Start
4
+
5
+ ```typescript
6
+ import {
7
+ observable,
8
+ cond,
9
+ template,
10
+ iterable,
11
+ component,
12
+ mapObservable,
13
+ router,
14
+ tags,
15
+ dedupObservable
16
+ } from "@realglebivanov/reactive";
17
+
18
+ import { ReactiveArray } from "@realglebivanov/reactive";
19
+
20
+ const LOREM = `
21
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
22
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
23
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
24
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
25
+
26
+ const { a, p, h1, h2, div, span, button, ul, li, img, input } = tags;
27
+
28
+ const shoppingItems = new ReactiveArray([
29
+ { name: "milk", price$: observable("1.99") },
30
+ { name: "sour cream", price$: observable("2.99") },
31
+ { name: "cheese", price$: observable("0.99") }
32
+ ]);
33
+
34
+ const counter = () => component({
35
+ cache: true,
36
+ props: { counter: "Counter" },
37
+ observables: () => ({
38
+ count$: observable(0),
39
+ hard$: observable(false),
40
+ veryHard$: observable(true)
41
+ }),
42
+ derivedObservables: ({ count$, hard$ }) => ({
43
+ imageSource$: mapObservable(
44
+ (hard) => hard ? "KashaHard.gif" : "Kasha.png",
45
+ dedupObservable(hard$)),
46
+ hexCounter$: mapObservable((x) => x.toString(2), count$)
47
+ }),
48
+ render: function ({ count$, hard$, veryHard$, imageSource$, hexCounter$ }) {
49
+ const onClick = () => {
50
+ count$.update((count) => count + 1);
51
+ hard$.update((hard) => !hard);
52
+ };
53
+
54
+ return div(
55
+ h2(cond({
56
+ if$: mapObservable(
57
+ (hard, veryHard) => hard && veryHard, hard$, veryHard$),
58
+ then: "Rock hard, baby",
59
+ otherwise: "Wood needed"
60
+ })),
61
+ div(span(template`${this.props.counter}: ${hexCounter$}`)),
62
+ div(img("Kasha.png").att$("src", imageSource$).clk(onClick))
63
+ );
64
+ }
65
+ });
66
+
67
+ const shoppingForm = () => component({
68
+ render: () => div(
69
+ div(span('Name: '), input('text').att('id', 'itemName')),
70
+ div(span('Price: '), input('text').att('id', 'itemPrice')),
71
+ button(span('Add')).clk(() => {
72
+ const itemName = document.getElementById('itemName') as HTMLInputElement;
73
+ const itemPrice = document.getElementById('itemPrice') as HTMLInputElement;
74
+
75
+ if (itemName.value == "" || itemPrice.value == "") return;
76
+
77
+ shoppingItems.push({
78
+ name: itemName.value,
79
+ price$: observable(itemPrice.value)
80
+ });
81
+
82
+ itemName.value = "";
83
+ itemPrice.value = "";
84
+ })
85
+ )
86
+ });
87
+
88
+ const shoppingList = () => component({
89
+ observables: () => ({ shoppingItems$: shoppingItems.observable$ }),
90
+ render: ({ shoppingItems$ }) => div(
91
+ h2("Shopping items"),
92
+ ul(
93
+ iterable({
94
+ it$: shoppingItems$,
95
+ buildFn: (_, item) => li(span(template`${item.name} - ${item.price$}`)),
96
+ keyFn: (_, item) => item.name,
97
+ })
98
+ )
99
+ )
100
+ });
101
+
102
+ const exampleRouter = router({
103
+ "/": div(
104
+ h1("Grecha.js"),
105
+ div(a("Foo").att("href", "#/foo")),
106
+ div(a("Bar").att("href", "#/bar")),
107
+ counter(),
108
+ shoppingList(),
109
+ shoppingForm()
110
+ ),
111
+ "/foo": component({
112
+ observables: () => ({ count$: observable(0) }),
113
+ derivedObservables: ({ count$ }) => ({
114
+ paragraphStyle$: mapObservable(
115
+ (count) => `color: ${numberToHexColor(count * 999999)}`, count$)
116
+ }),
117
+ render: ({ count$, paragraphStyle$ }) => div(
118
+ h1("Foo"),
119
+ p(LOREM).att$("style", paragraphStyle$),
120
+ button("Change color").clk(() => count$.update((x) => x + 1)),
121
+ div(a("Home").att("href", "#")),
122
+ )
123
+ }),
124
+ "/bar": div(
125
+ h1("Bar"),
126
+ p(LOREM),
127
+ div(a("Home").att("href", "#"))
128
+ )
129
+ }, { notFoundRoute: "/" });
130
+
131
+ function numberToHexColor(number: number) {
132
+ let hex = (number % 0xffffff).toString(16);
133
+ while (hex.length < 6) hex = "0" + hex;
134
+ return "#" + hex;
135
+ }
136
+
137
+ exampleRouter.mount(document.getElementById('entry')!);
138
+ exampleRouter.activate();
139
+
140
+ ```
141
+ ## Restrictions & Edge Cases
142
+
143
+ ### 1. Component Observables
144
+ - Subscriptions to observables declared outside of a component **will not be automatically cleaned up**. Users must manage them manually to avoid memory leaks.
145
+
146
+ ### 2. ReactiveArray
147
+ - `ReactiveArray` is **primitive** and **not a full replacement for native arrays**.
148
+ - Only the following events are supported:
149
+ - `{ type: "replace", items: T[] }` — replaces the entire array
150
+ - `{ type: "append", items: T[] }` — appends items at the end
151
+ - `{ type: "remove", items: Map<number, T> }` — removes items at given indices
152
+ - `{ type: "replaceKeys", items: Map<number, T> }` — replaces items at specific indices
153
+
154
+ ### 3. Iterable
155
+ - Iterable expects a **reactive source** (`Observable<Event<T>>`) or a normal collection (`Array`/`Map`), which is automatically wrapped in a replace event.
156
+
157
+ ### 5. Performance Considerations
158
+ - For very large arrays or frequent updates, consider using **your own ReactiveArray-like implementation** that emit fine-grained events (`append`, `replaceKeys`, `remove`) instead of full replacements.
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@realglebivanov/reactive",
3
+ "version": "1.0.0",
4
+ "description": "A simple and permissive observable-driven UI toolkit",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "keywords": [
16
+ "reactive",
17
+ "observable",
18
+ "ui",
19
+ "toolkit",
20
+ "framework",
21
+ "typescript",
22
+ "no-deps",
23
+ "no-dependencies"
24
+ ],
25
+ "sideEffects": false,
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "watch": "tsup --watch",
29
+ "serve": "npx serve dist/",
30
+ "test": "echo \"Error: no test specified\" && exit 1"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/realglebivanov/reactive.git"
35
+ },
36
+ "author": "realglebivanov <realglebivanov@gmail.com>",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/realglebivanov/reactive/issues"
40
+ },
41
+ "homepage": "https://github.com/realglebivanov/reactive#readme",
42
+ "devDependencies": {
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^5.9.3"
45
+ }
46
+ }
Binary file
Binary file
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <title>Grecha.js</title>
6
+ </head>
7
+
8
+ <body>
9
+ <div id="entry"></div>
10
+ <script src="./example.js" type="module"></script>
11
+ </body>
12
+
13
+ </html>
package/src/example.ts ADDED
@@ -0,0 +1,133 @@
1
+ import {
2
+ observable,
3
+ cond,
4
+ template,
5
+ iterable,
6
+ component,
7
+ mapObservable,
8
+ router,
9
+ tags,
10
+ dedupObservable
11
+ } from "@realglebivanov/reactive";
12
+
13
+ import { ReactiveArray } from "@realglebivanov/reactive";
14
+
15
+ const LOREM = `
16
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
17
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
18
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
19
+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
20
+
21
+ const { a, p, h1, h2, div, span, button, ul, li, img, input } = tags;
22
+
23
+ const shoppingItems = new ReactiveArray([
24
+ { name: "milk", price$: observable("1.99") },
25
+ { name: "sour cream", price$: observable("2.99") },
26
+ { name: "cheese", price$: observable("0.99") }
27
+ ]);
28
+
29
+ const counter = () => component({
30
+ cache: true,
31
+ props: { counter: "Counter" },
32
+ observables: () => ({
33
+ count$: observable(0),
34
+ hard$: observable(false),
35
+ veryHard$: observable(true)
36
+ }),
37
+ derivedObservables: ({ count$, hard$ }) => ({
38
+ imageSource$: mapObservable(
39
+ (hard) => hard ? "KashaHard.gif" : "Kasha.png",
40
+ dedupObservable(hard$)),
41
+ hexCounter$: mapObservable((x) => x.toString(2), count$)
42
+ }),
43
+ render: function ({ count$, hard$, veryHard$, imageSource$, hexCounter$ }) {
44
+ const onClick = () => {
45
+ count$.update((count) => count + 1);
46
+ hard$.update((hard) => !hard);
47
+ };
48
+
49
+ return div(
50
+ h2(cond({
51
+ if$: mapObservable(
52
+ (hard, veryHard) => hard && veryHard, hard$, veryHard$),
53
+ then: "Rock hard, baby",
54
+ otherwise: "Wood needed"
55
+ })),
56
+ div(span(template`${this.props.counter}: ${hexCounter$}`)),
57
+ div(img("Kasha.png").att$("src", imageSource$).clk(onClick))
58
+ );
59
+ }
60
+ });
61
+
62
+ const shoppingForm = () => component({
63
+ render: () => div(
64
+ div(span('Name: '), input('text').att('id', 'itemName')),
65
+ div(span('Price: '), input('text').att('id', 'itemPrice')),
66
+ button(span('Add')).clk(() => {
67
+ const itemName = document.getElementById('itemName') as HTMLInputElement;
68
+ const itemPrice = document.getElementById('itemPrice') as HTMLInputElement;
69
+
70
+ if (itemName.value == "" || itemPrice.value == "") return;
71
+
72
+ shoppingItems.push({
73
+ name: itemName.value,
74
+ price$: observable(itemPrice.value)
75
+ });
76
+
77
+ itemName.value = "";
78
+ itemPrice.value = "";
79
+ })
80
+ )
81
+ });
82
+
83
+ const shoppingList = () => component({
84
+ observables: () => ({ shoppingItems$: shoppingItems.observable$ }),
85
+ render: ({ shoppingItems$ }) => div(
86
+ h2("Shopping items"),
87
+ ul(
88
+ iterable({
89
+ it$: shoppingItems$,
90
+ buildFn: (_, item) => li(span(template`${item.name} - ${item.price$}`)),
91
+ keyFn: (_, item) => item.name,
92
+ })
93
+ )
94
+ )
95
+ });
96
+
97
+ const exampleRouter = router({
98
+ "/": div(
99
+ h1("Grecha.js"),
100
+ div(a("Foo").att("href", "#/foo")),
101
+ div(a("Bar").att("href", "#/bar")),
102
+ counter(),
103
+ shoppingList(),
104
+ shoppingForm()
105
+ ),
106
+ "/foo": component({
107
+ observables: () => ({ count$: observable(0) }),
108
+ derivedObservables: ({ count$ }) => ({
109
+ paragraphStyle$: mapObservable(
110
+ (count) => `color: ${numberToHexColor(count * 999999)}`, count$)
111
+ }),
112
+ render: ({ count$, paragraphStyle$ }) => div(
113
+ h1("Foo"),
114
+ p(LOREM).att$("style", paragraphStyle$),
115
+ button("Change color").clk(() => count$.update((x) => x + 1)),
116
+ div(a("Home").att("href", "#")),
117
+ )
118
+ }),
119
+ "/bar": div(
120
+ h1("Bar"),
121
+ p(LOREM),
122
+ div(a("Home").att("href", "#"))
123
+ )
124
+ }, { notFoundRoute: "/" });
125
+
126
+ function numberToHexColor(number: number) {
127
+ let hex = (number % 0xffffff).toString(16);
128
+ while (hex.length < 6) hex = "0" + hex;
129
+ return "#" + hex;
130
+ }
131
+
132
+ exampleRouter.mount(document.getElementById('entry')!);
133
+ exampleRouter.activate();
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './tag';
2
+ export * from './observables';
3
+ export * from './nodes';
4
+ export * from './router';
5
+ export * from './task';
6
+ export * from './reactive';
@@ -0,0 +1,44 @@
1
+ export interface Lifecycle {
2
+ mount(parentNode: Node): void,
3
+ activate(): void,
4
+ deactivate(): void,
5
+ unmount(): void
6
+ }
7
+
8
+ export enum ReactiveNodeStatus {
9
+ Active,
10
+ Inactive,
11
+ Mounted,
12
+ Unmounted
13
+ };
14
+
15
+ export const buildLifecycleHooks = (handlers: Lifecycle[]): Lifecycle => {
16
+ let status = ReactiveNodeStatus.Unmounted;
17
+
18
+ return {
19
+ mount: (parentNode: Node) => {
20
+ if (status !== ReactiveNodeStatus.Unmounted)
21
+ return console.warn(`Mounting in status ${ReactiveNodeStatus[status]}`);
22
+ for (const handler of handlers) handler.mount(parentNode);
23
+ status = ReactiveNodeStatus.Mounted;
24
+ },
25
+ activate: () => {
26
+ if (status !== ReactiveNodeStatus.Mounted && status !== ReactiveNodeStatus.Inactive)
27
+ return console.warn(`Activating in status ${ReactiveNodeStatus[status]}`);
28
+ for (const handler of handlers) handler.activate();
29
+ status = ReactiveNodeStatus.Active;
30
+ },
31
+ deactivate: () => {
32
+ if (status !== ReactiveNodeStatus.Active)
33
+ return console.warn(`Deactivating in status ${ReactiveNodeStatus[status]}`);
34
+ for (const handler of handlers) handler.deactivate()
35
+ status = ReactiveNodeStatus.Inactive;
36
+ },
37
+ unmount() {
38
+ if (status !== ReactiveNodeStatus.Inactive)
39
+ return console.warn(`Unmounting in status ${ReactiveNodeStatus[status]}`);
40
+ for (const handler of handlers) handler.unmount();
41
+ status = ReactiveNodeStatus.Unmounted;
42
+ },
43
+ };
44
+ };
@@ -0,0 +1,107 @@
1
+ import {
2
+ scopedObservable,
3
+ ScopedObservable,
4
+ type Observable
5
+ } from "../observables";
6
+ import { toReactiveNode, type ReactiveNode } from "./reactive";
7
+
8
+ type Observables = Record<string, Observable<any>>;
9
+ type ScopedObservables<T extends Observables> = {
10
+ [K in keyof T]: ScopedObservable<T[K]>
11
+ };
12
+
13
+ type RenderFn<O extends Observables, T extends Node, P> =
14
+ (this: Context<T, P>, observables: ScopedObservables<O>) => ReactiveNode<T>;
15
+
16
+ type UserOpts<O1 extends Observables, O2 extends Observables, T extends Node, P> =
17
+ Partial<Opts<O1, O2, T, P>> & { render: RenderFn<O1 & O2, T, P> };
18
+
19
+ type Opts<O1 extends Observables, O2 extends Observables, T extends Node, P> = {
20
+ render: RenderFn<O1 & O2, T, P>,
21
+ observables: () => O1,
22
+ derivedObservables: (observables: O1) => O2,
23
+ cache: boolean,
24
+ props: P
25
+ };
26
+
27
+ const defaultOpts = {
28
+ observables: () => ({}),
29
+ derivedObservables: () => ({}),
30
+ cache: false,
31
+ props: {}
32
+ };
33
+
34
+ export const component = <
35
+ O1 extends Observables,
36
+ O2 extends Observables,
37
+ T extends Node,
38
+ P
39
+ >(opts: UserOpts<O1, O2, T, P>): ReactiveNode<Comment> => new Component<O1, O2, T, P>(
40
+ Object.assign({}, defaultOpts, opts)
41
+ ).toReactiveNode();
42
+
43
+ class Context<T extends Node, P> {
44
+ node: ReactiveNode<T> | undefined;
45
+ constructor(public parent: Node, public props: P) { }
46
+ }
47
+
48
+ class Component<O1 extends Observables, O2 extends Observables, T extends Node, P> {
49
+ private node: ReactiveNode<T> | undefined;
50
+ private observables: ScopedObservables<O1 & O2> | undefined;
51
+ private context: Context<T, P> | undefined;
52
+
53
+ constructor(private opts: Opts<O1, O2, T, P>) { }
54
+
55
+ toReactiveNode() {
56
+ return toReactiveNode(document.createComment('Component'), [{
57
+ mount: (parentNode: Node) => {
58
+ if (this.node === undefined || !this.opts.cache)
59
+ this.setupNode(parentNode);
60
+ this.node?.mount(parentNode);
61
+ },
62
+ activate: () => this.node?.activate(),
63
+ deactivate: () => {
64
+ this.node?.deactivate();
65
+ if (this.observables === undefined) return;
66
+ for (const key in this.observables)
67
+ this.observables[key as keyof O1 & O2].unsubscribeAll();
68
+ },
69
+ unmount: () => {
70
+ this.node?.unmount();
71
+ if (!this.opts.cache) this.cleanUp();
72
+ }
73
+ }]);
74
+ }
75
+
76
+ private setupNode(parentNode: Node) {
77
+ this.observables = this.buildObservables();
78
+ this.context = new Context(parentNode, this.opts.props);
79
+ this.node = this.opts.render.call(this.context, this.observables);
80
+ this.context.node = this.node;
81
+ }
82
+
83
+ private cleanUp() {
84
+ if (this.context !== undefined) this.context.node = undefined;
85
+ this.node = undefined;
86
+ this.observables = undefined;
87
+ }
88
+
89
+ private buildObservables() {
90
+ const coreObservables = this.opts.observables();
91
+ const derivedObservables = this.opts.derivedObservables(coreObservables);
92
+ const observables =
93
+ Object.assign({}, coreObservables, derivedObservables);
94
+ return this.toScoped<O1 & O2>(observables);
95
+ }
96
+
97
+ private toScoped<O extends Observables>(observables: O): ScopedObservables<O> {
98
+ const scopedObservables: Partial<ScopedObservables<O>> = {};
99
+
100
+ for (const key in observables) {
101
+ const k: keyof O = key;
102
+ scopedObservables[k] = scopedObservable(observables[k]);
103
+ }
104
+
105
+ return scopedObservables as ScopedObservables<O>;
106
+ }
107
+ }
@@ -0,0 +1,79 @@
1
+ import { dedupObservable, type Observable } from "../observables";
2
+ import { reactiveTextNode, toReactiveNode, type ReactiveNode } from "./reactive";
3
+
4
+ type ReactiveNodeBuilder<T extends Node> = (() => ReactiveNode<T>);
5
+
6
+ type Params<A extends Node, B extends Node> = {
7
+ if$: Observable<boolean>,
8
+ then: ReactiveNodeBuilder<A> | string,
9
+ otherwise: ReactiveNodeBuilder<B> | string
10
+ };
11
+
12
+ type CurrentNode<A extends Node, B extends Node> =
13
+ ReactiveNode<A> | ReactiveNode<B> | ReactiveNode<Text>;
14
+
15
+ export const cond = <A extends Node, B extends Node>(
16
+ { if$, then, otherwise }: Params<A, B>
17
+ ): ReactiveNode<Comment> => new Cond<A, B>(
18
+ dedupObservable(if$),
19
+ then,
20
+ otherwise
21
+ ).toReactiveNode();
22
+
23
+ class Cond<A extends Node, B extends Node> {
24
+ private id = Symbol('Cond');
25
+ private currentNode: CurrentNode<A, B> | undefined;
26
+
27
+ constructor(
28
+ private if$: Observable<boolean>,
29
+ private then: ReactiveNodeBuilder<A> | string,
30
+ private otherwise: ReactiveNodeBuilder<B> | string
31
+ ) { }
32
+
33
+ toReactiveNode() {
34
+ const anchor = document.createComment('Cond');
35
+ const updateFn = (value: boolean) => this.updateNode(anchor, value);
36
+
37
+ return toReactiveNode(anchor, [{
38
+ mount: (parentNode: Node) => parentNode.appendChild(anchor),
39
+ activate: () => this.if$.subscribeInit(this.id, updateFn),
40
+ deactivate: () => {
41
+ this.if$.unsubscribe(this.id);
42
+ this.currentNode?.deactivate();
43
+ },
44
+ unmount: () => {
45
+ this.currentNode?.unmount();
46
+ this.currentNode = undefined;
47
+ anchor.remove();
48
+ }
49
+ }]);
50
+ }
51
+
52
+ private updateNode(anchor: Node, value: boolean) {
53
+ const newNode = value ?
54
+ this.buildNode<A>(this.then) :
55
+ this.buildNode<B>(this.otherwise);
56
+ try {
57
+ this.switchNode(anchor, newNode);
58
+ } catch (e) {
59
+ console.error(e);
60
+ }
61
+ }
62
+
63
+ private buildNode<T extends Node>(node: string | ReactiveNodeBuilder<T>) {
64
+ if (typeof (node) === 'function')
65
+ return node();
66
+ if (typeof (node) === 'string')
67
+ return reactiveTextNode(node);
68
+
69
+ throw new Error('Then/otherwise should be either string or function');
70
+ }
71
+
72
+ private switchNode(anchor: Node, newNode: CurrentNode<A, B>) {
73
+ this.currentNode?.deactivate();
74
+ this.currentNode?.unmount();
75
+ newNode.mount(anchor.parentNode!);
76
+ newNode.activate();
77
+ this.currentNode = newNode;
78
+ }
79
+ }
@@ -0,0 +1,7 @@
1
+ export * from './component';
2
+ export * from './cond';
3
+ export * from './iterable';
4
+ export * from './iterable/collection';
5
+ export * from './iterable/item';
6
+ export * from './template';
7
+ export * from './reactive';