tagu-tagu 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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # tag-tag
2
+
3
+ A lightweight helper for vanilla `HTMLElement`. No config, no jsx — only clean javascript.
4
+
5
+ ## `tag-tag` is
6
+
7
+ just a helper for `HTMLElement`:
8
+
9
+ ```javascript
10
+ import {button} from "tag-tag";
11
+
12
+ const myButton = button("Click Me!"); // `HTMLButtonElement`
13
+ document.body.appendChild(myButton);
14
+ ```
15
+
16
+ with reactivity!
17
+
18
+ ```javascript
19
+ import {button, span, Modify, useState} from "tag-tag";
20
+
21
+ const count = useState(4);
22
+
23
+ function decrementCount() {
24
+ count.set(count.get() - 1);
25
+ }
26
+ function incrementCount() {
27
+ count.set(count.get() + 1);
28
+ }
29
+
30
+ Modify(document.body, [
31
+ button("-", { on: { click: decrementCount } }),// `HTMLButtonElement`
32
+ span(count),// `HTMLSpanElement`
33
+ button("+", { on: { click: incrementCount } }),// `HTMLButtonElement`
34
+ ]);
35
+ ```
@@ -0,0 +1 @@
1
+ var S=class{node2Data=new WeakMap;addCallbacks(t,n){if(!n)return;this.node2DescendantCallbacks.has(t)||this.node2DescendantCallbacks.set(t,{});let o=this.node2DescendantCallbacks.get(t);k(o,n);let i=this.node2Data.get(t);w(o,i)}setDataRecord(t,n){n&&this.node2Data.set(t,n)}resolveCallbacks(t,n){let o=(l,u)=>{if(w(u,this.node2Data.get(l)),!!Object.keys(u).length){if(!l.parentElement){this.node2DescendantCallbacks.has(l)||this.node2DescendantCallbacks.set(l,{});let m=this.node2DescendantCallbacks.get(l);m&&k(m,u);return}o(l.parentElement,u)}},i=this.node2DescendantCallbacks.get(n);i&&o(t,i)}node2DescendantCallbacks=new WeakMap};function w(e,t){for(let n in t)if(n in e){for(let o of e[n])o(t[n]);delete e[n]}}var c=new S;function b(e,t){c.addCallbacks(e,$(t)),c.setDataRecord(e,A(t))}function $(e){return H(e,t=>typeof t=="function",t=>[t])}function A(e){return H(e,t=>typeof t!="function")}function H(e,t,n=o=>o){if(!e)return;let o={};for(let i in e){let l=e[i];t(l)&&(o[i]=n(l))}if(Object.keys(o).length)return o}function k(e,t){for(let n in t)e[n]||(e[n]=[]),e[n].push(...t[n])}var x=class{constructor(t,n){this.key=t;this.map=n}};var d=class{next=null;firstNode=null};var p=class{#e;constructor(t){this.#e=t}get=()=>this.#e;set(t){this.#e=t,this.#n("change")}#n(t){this.#t.dispatchEvent(new Event(t))}#t=new EventTarget;on(t,n){this.#t.addEventListener(t,n)}};function D(e){return new p(e)}function R(e,t){let n=new p(t()),o=()=>{n.set(t())};for(let i of e)i.on("change",o);return n}function z(e,t){let n=V(t);j(n);for(let o of n)O(e,o)}function O(e,t){t instanceof d?t.run(e):(c.resolveCallbacks(e,t),e.appendChild(t))}function V(e){return e.map(t=>{if(typeof t=="string"||t instanceof p){let n=document.createTextNode("");return y(t,o=>{n.textContent=o}),n}return t})}function j(e){for(let t=0;t<e.length;t++){let n=e[t];n instanceof d&&(n.next=e[t+1]??null)}}function B(e){let t=e.next;return t===null?null:t instanceof Node?t:t.firstNode?t.firstNode:B(t)}function T(e){let t=B(e);return t?.parentElement?t:null}function y(e,t){if(typeof e=="string")t(e);else{let n=()=>{t(e.get())};n(),e.on("change",n)}}function g(e,t,n){t instanceof x?b(e,{[t.key]:o=>{let l=o instanceof p?R([o],()=>t.map(o.get())):t.map(o);y(l,n)}}):y(t,n)}function K(e,t){t!==void 0&&g(e,t,n=>{e.innerHTML=n})}function F(e,t){t!==void 0&&g(e,t,n=>{e.textContent=n})}function q(e,t){let n=e.style;if(n instanceof CSSStyleDeclaration)for(let o in t){let i=t[o];g(e,i,l=>n.setProperty(o,l))}}function P(e,t){for(let n in t){let o=t[n];g(e,o,i=>{i?e.setAttribute(n,i):e.removeAttribute(n)})}}function G(e,t){for(let n in t){let o=t[n];g(e,o,i=>{e[n]=i})}}function U(e,t){for(let n in t){let o=e.querySelector(n);o&&C(o,t[n])}}function W(e,t){for(let n in t){let o=e.querySelectorAll(n);for(let i of o)C(i,t[n])}}function Q(e,t){for(let n in t){let i=t[n];i&&(typeof i=="function"?e.addEventListener(n,i):e.addEventListener(n,i.listener,i.options))}}function C(e,t){e&&(typeof t=="string"||t instanceof p?F(e,t):Array.isArray(t)?z(e,t):typeof t=="function"?t(e):(K(e,t.html),F(e,t.text),P(e,t.attr),G(e,t.prop),q(e,t.css),U(e,t.$),W(e,t.$$),Q(e,t.on),b(e,t.data)))}function h(e,...t){let n=typeof e=="string"?document.querySelector(e):e;for(let o of t)C(n,o);return n}function ue(e,...t){let n=document.createElementNS("http://www.w3.org/2000/svg",e);return h(n,...t)}function r(e,...t){let n=document.createElement(e);return h(n,...t),n}function fe(e,...t){let n=L({html:e}).children[0];return h(n,...t)}function Ee(...e){return r("h1",...e)}function Te(...e){return r("h2",...e)}function ge(...e){return r("h3",...e)}function xe(...e){return r("h4",...e)}function ye(...e){return r("h5",...e)}function he(...e){return r("h6",...e)}function ve(...e){return r("p",...e)}function Se(...e){return r("section",...e)}function be(...e){return r("button",...e)}function Ce(...e){return r("span",...e)}function Le(e){function t(n,o){return`${n} {${Object.keys(o).map(i=>`${i}: ${o[i]};`).join("")}}`}return r("style",[Object.keys(e).map(n=>t(n,e[n])).join("")])}function L(...e){return r("div",...e)}function Me(...e){return L({css:{display:"flex"}},...e)}function Ne(...e){return r("input",...e)}function Ie(...e){return r("textarea",...e)}function we(...e){return r("select",...e)}function ke(...e){return r("option",...e)}function He(...e){return r("br",...e)}function De(...e){return r("tr",...e)}function Re(...e){return r("td",...e)}function ze(...e){return r("b",...e)}function Be(...e){return r("label",...e)}function Fe(...e){return r("a",...e)}function $e(...e){return r("blockquote",...e)}function Ae(...e){return r("li",...e)}function Oe(...e){return r("ol",...e)}function Ve(...e){return r("ul",...e)}function je(...e){return r("audio",...e)}function Ke(...e){return r("video",...e)}function qe(...e){return r("img",...e)}function Pe(...e){return r("canvas",...e)}function Ge(...e){return r("iframe",...e)}function Ue(...e){return r("form",...e)}function We(...e){return r("table",...e)}function Qe(...e){return r("tbody",...e)}function Je(...e){return r("hr",...e)}function et(e,t){return new M(e,t)}var M=class extends d{constructor(n,o){super();this.list=n;this.map=o}run(n){let o=new Map,i=new Map,l=()=>{let u=[];for(let a of this.list.get())o.has(a)||u.push(a);let m=[],s=new Set(this.list.get());for(let a of i.keys()){let f=i.get(a);f&&!s.has(f)&&m.push(a)}for(let a of u){let f=this.map(a),v=typeof f=="string"?document.createTextNode(f):f;c.resolveCallbacks(n,v),i.set(v,a),o.set(a,v)}for(let a of m){a.parentNode?.removeChild(a);let f=i.get(a);i.delete(a),f&&o.delete(f)}for(let a of[...i.keys()])a.parentElement?.removeChild(a);let E=T(this);for(let a of this.list.get())n.insertBefore(o.get(a),E);this.firstNode=o.get(this.list.get()[0])??null};l(),this.list.on("change",()=>{l()})}};function rt(e,t,n){return new N(e,t,n)}var N=class extends d{#e;#n;#t;constructor(t,n,o){super(),this.#e=t,this.#n=n,this.#t=o}run(t){let n,o,i=()=>{let l=T(this);this.#e.get()?(n||(n=this.#n()),c.resolveCallbacks(t,n),this.firstNode=n,o?.remove(),t.insertBefore(n,l)):(o||(o=this.#t?.()),o&&c.resolveCallbacks(t,o),this.firstNode=o??null,n?.remove(),o&&t.insertBefore(o,l))};i(),this.#e.on("change",i)}};function dt(e,t,n){return new I(e,t,n)}var I=class extends d{#e;#n;#t;constructor(t,n,o){super(),this.#e=t,this.#n=n,this.#t=o}run(t){let n=new Map,o=new Map;for(let s of this.#n)o.set(s.case,s);let i,l,u=s=>{let E=o.get(s);if(E){if(!n.has(s)){let a=E.show();n.set(s,a)}return n.get(s)}return this.#t&&!l&&(l=this.#t()),l},m=()=>{let s=this.#e.get(),E=T(this),a=u(s);a&&c.resolveCallbacks(t,a),i?.remove(),a&&t.insertBefore(a,E),i=a};m(),this.#e.on("change",m)}};function pt(e,t){let n=D(void 0),o=()=>{let i;for(let l of t)e.get()===l.case&&(i=l);n.set(i)};return e.on("change",o),o(),n}export{Me as FlexDiv,et as For,M as ForMap,R as FromStates,r as Html,rt as If,N as IfFlow,h as Modify,p as State,ue as Svg,dt as Switch,pt as SwitchBlockState,I as SwitchFlow,fe as Tag,Fe as a,y as applyStringOrState,je as audio,ze as b,$e as blockquote,He as br,be as button,Pe as canvas,L as div,Ue as form,Ee as h1,Te as h2,ge as h3,xe as h4,ye as h5,he as h6,Je as hr,Ge as iframe,qe as img,Ne as input,Be as label,Ae as li,Oe as ol,ke as option,ve as p,Se as section,we as select,Ce as span,Le as style,We as table,Qe as tbody,Re as td,Ie as textarea,De as tr,Ve as ul,D as useState,Ke as video};
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "tagu-tagu",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight helper for vanilla `HTMLElement`.",
5
+ "keywords": [
6
+ "vanilla"
7
+ ],
8
+ "homepage": "https://github.com/DoTheSimplest/tag-tag#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/DoTheSimplest/tag-tag/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/DoTheSimplest/tag-tag.git"
15
+ },
16
+ "author": "DoTheSimmplest",
17
+ "type": "module",
18
+ "main": "src/index.ts",
19
+ "files": [
20
+ "src",
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "test:browser": "vitest",
25
+ "build": "tsx scripts/build.ts"
26
+ },
27
+ "devDependencies": {
28
+ "@biomejs/biome": "2.3.8",
29
+ "@vitest/browser-playwright": "^4.0.15",
30
+ "esbuild": "^0.27.2",
31
+ "tsx": "^4.21.0",
32
+ "typescript": "^5.9.3",
33
+ "vitest": "^4.0.15"
34
+ }
35
+ }
@@ -0,0 +1,169 @@
1
+ import type { ElementInitializer } from "./Modify";
2
+ import { Html } from "./Tag";
3
+
4
+ export function h1(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
5
+ return Html("h1", ...initializers);
6
+ }
7
+ export function h2(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
8
+ return Html("h2", ...initializers);
9
+ }
10
+ export function h3(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
11
+ return Html("h3", ...initializers);
12
+ }
13
+ export function h4(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
14
+ return Html("h4", ...initializers);
15
+ }
16
+ export function h5(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
17
+ return Html("h5", ...initializers);
18
+ }
19
+ export function h6(...initializers: ElementInitializer<HTMLHeadingElement>[]) {
20
+ return Html("h6", ...initializers);
21
+ }
22
+ export function p(...initializers: ElementInitializer<HTMLParagraphElement>[]) {
23
+ return Html("p", ...initializers);
24
+ }
25
+
26
+ export function section(...initializers: ElementInitializer<HTMLElement>[]) {
27
+ return Html("section", ...initializers);
28
+ }
29
+
30
+ export function button(
31
+ ...initializers: ElementInitializer<HTMLButtonElement>[]
32
+ ) {
33
+ return Html("button", ...initializers);
34
+ }
35
+ export function span(...initializers: ElementInitializer<HTMLSpanElement>[]) {
36
+ return Html("span", ...initializers);
37
+ }
38
+
39
+ export function style(styles: Record<string, Record<string, string>>) {
40
+ function createStyleText(
41
+ selector: string,
42
+ properties: Record<string, string>,
43
+ ) {
44
+ return `${selector} {${Object.keys(properties)
45
+ .map((property) => `${property}: ${properties[property]};`)
46
+ .join("")}}`;
47
+ }
48
+
49
+ return Html("style", [
50
+ Object.keys(styles)
51
+ .map((selector) => createStyleText(selector, styles[selector]))
52
+ .join(""),
53
+ ]);
54
+ }
55
+
56
+ export function div(...initializers: ElementInitializer<HTMLDivElement>[]) {
57
+ return Html("div", ...initializers);
58
+ }
59
+ export function FlexDiv(...initializers: ElementInitializer<HTMLDivElement>[]) {
60
+ return div({ css: { display: "flex" } }, ...initializers);
61
+ }
62
+
63
+ export function input(...initializers: ElementInitializer<HTMLInputElement>[]) {
64
+ return Html("input", ...initializers);
65
+ }
66
+
67
+ export function textarea(
68
+ ...initializers: ElementInitializer<HTMLTextAreaElement>[]
69
+ ) {
70
+ return Html("textarea", ...initializers);
71
+ }
72
+
73
+ export function select(
74
+ ...initializers: ElementInitializer<HTMLSelectElement>[]
75
+ ) {
76
+ return Html("select", ...initializers);
77
+ }
78
+
79
+ export function option(
80
+ ...initializers: ElementInitializer<HTMLOptionElement>[]
81
+ ) {
82
+ return Html("option", ...initializers);
83
+ }
84
+
85
+ export function br(...initializers: ElementInitializer<HTMLBRElement>[]) {
86
+ return Html("br", ...initializers);
87
+ }
88
+
89
+ export function tr(...initializers: ElementInitializer<HTMLTableRowElement>[]) {
90
+ return Html("tr", ...initializers);
91
+ }
92
+
93
+ export function td(
94
+ ...initializers: ElementInitializer<HTMLTableCellElement>[]
95
+ ) {
96
+ return Html("td", ...initializers);
97
+ }
98
+
99
+ export function b(...initializers: ElementInitializer<HTMLElement>[]) {
100
+ return Html("b", ...initializers);
101
+ }
102
+
103
+ export function label(...initializers: ElementInitializer<HTMLLabelElement>[]) {
104
+ return Html("label", ...initializers);
105
+ }
106
+
107
+ export function a(...initializers: ElementInitializer<HTMLAnchorElement>[]) {
108
+ return Html("a", ...initializers);
109
+ }
110
+
111
+ export function blockquote(
112
+ ...initializers: ElementInitializer<HTMLQuoteElement>[]
113
+ ) {
114
+ return Html("blockquote", ...initializers);
115
+ }
116
+
117
+ export function li(...initializers: ElementInitializer<HTMLLIElement>[]) {
118
+ return Html("li", ...initializers);
119
+ }
120
+
121
+ export function ol(...initializers: ElementInitializer<HTMLOListElement>[]) {
122
+ return Html("ol", ...initializers);
123
+ }
124
+
125
+ export function ul(...initializers: ElementInitializer<HTMLUListElement>[]) {
126
+ return Html("ul", ...initializers);
127
+ }
128
+
129
+ export function audio(...initializers: ElementInitializer<HTMLAudioElement>[]) {
130
+ return Html("audio", ...initializers);
131
+ }
132
+
133
+ export function video(...initializers: ElementInitializer<HTMLVideoElement>[]) {
134
+ return Html("video", ...initializers);
135
+ }
136
+
137
+ export function img(...initializers: ElementInitializer<HTMLImageElement>[]) {
138
+ return Html("img", ...initializers);
139
+ }
140
+
141
+ export function canvas(
142
+ ...initializers: ElementInitializer<HTMLCanvasElement>[]
143
+ ) {
144
+ return Html("canvas", ...initializers);
145
+ }
146
+
147
+ export function iframe(
148
+ ...initializers: ElementInitializer<HTMLIFrameElement>[]
149
+ ) {
150
+ return Html("iframe", ...initializers);
151
+ }
152
+
153
+ export function form(...initializers: ElementInitializer<HTMLFormElement>[]) {
154
+ return Html("form", ...initializers);
155
+ }
156
+
157
+ export function table(...initializers: ElementInitializer<HTMLTableElement>[]) {
158
+ return Html("table", ...initializers);
159
+ }
160
+
161
+ export function tbody(
162
+ ...initializers: ElementInitializer<HTMLTableSectionElement>[]
163
+ ) {
164
+ return Html("tbody", ...initializers);
165
+ }
166
+
167
+ export function hr(...initializers: ElementInitializer<HTMLHRElement>[]) {
168
+ return Html("hr", ...initializers);
169
+ }
package/src/Modify.ts ADDED
@@ -0,0 +1,219 @@
1
+ import { type DataRecord, initializeData } from "./data/data";
2
+ import { Binding } from "./data/useBinding";
3
+ import { type ChildType, initializeChildBlock } from "./initializeChildBlock";
4
+ import { FromStates, State } from "./State";
5
+
6
+ type EventListenerType<
7
+ TEventType2Event,
8
+ TEventType extends keyof TEventType2Event,
9
+ > = (event: TEventType2Event[TEventType]) => any;
10
+
11
+ type EventListenerRecord<TEventType2Event> = {
12
+ [TEventType in keyof TEventType2Event]?:
13
+ | EventListenerType<TEventType2Event, TEventType>
14
+ | {
15
+ listener: EventListenerType<TEventType2Event, TEventType>;
16
+ options: boolean | AddEventListenerOptions;
17
+ };
18
+ };
19
+
20
+ type $Record = Record<
21
+ string,
22
+ ElementInitializer<Element, HTMLElementEventMap | SVGElementEventMap>
23
+ >;
24
+
25
+ type ElementPropertyInitializer<TEventType2Event> = {
26
+ html?: string | State | Binding;
27
+ text?: string | State | Binding;
28
+ attr?: Record<string, string | State | Binding>;
29
+ prop?: Record<string, any | State | Binding>;
30
+ css?: Record<string, string | State | Binding>;
31
+ on?: EventListenerRecord<TEventType2Event>;
32
+ $?: $Record;
33
+ $$?: $Record;
34
+ data?: DataRecord;
35
+ };
36
+
37
+ export type ElementInitializer<
38
+ TElement,
39
+ TEventType2Event = HTMLElementEventMap,
40
+ > =
41
+ | string
42
+ | State
43
+ | ElementPropertyInitializer<TEventType2Event>
44
+ | ChildType[]
45
+ | ((element: TElement) => void);
46
+
47
+ export function applyStringOrState(
48
+ value: string | State,
49
+ initialize: (text: string) => void,
50
+ ) {
51
+ if (typeof value === "string") {
52
+ initialize(value);
53
+ } else {
54
+ const update = () => {
55
+ initialize(value.get());
56
+ };
57
+ update();
58
+ value.on("change", update);
59
+ }
60
+ }
61
+
62
+ function applyStringOrStateOrBinding(
63
+ element: Node,
64
+ value: string | State | Binding,
65
+ initialize: (text: string) => void,
66
+ ) {
67
+ if (value instanceof Binding) {
68
+ initializeData(element, {
69
+ [value.key]: (data: any) => {
70
+ const isState = data instanceof State;
71
+ const stringOrState = isState
72
+ ? FromStates([data], () => value.map(data.get()))
73
+ : value.map(data);
74
+
75
+ applyStringOrState(stringOrState, initialize);
76
+ },
77
+ });
78
+ } else {
79
+ applyStringOrState(value, initialize);
80
+ }
81
+ }
82
+
83
+ function initializeHtml(
84
+ element: Element,
85
+ html: string | State | Binding | undefined,
86
+ ) {
87
+ if (html !== undefined) {
88
+ applyStringOrStateOrBinding(element, html, (text) => {
89
+ element.innerHTML = text;
90
+ });
91
+ }
92
+ }
93
+
94
+ function initializeText(
95
+ node: Node,
96
+ text: string | State | Binding | undefined,
97
+ ) {
98
+ if (text !== undefined) {
99
+ applyStringOrStateOrBinding(node, text, (text) => {
100
+ node.textContent = text;
101
+ });
102
+ }
103
+ }
104
+
105
+ function initializeStyle(
106
+ element: Element,
107
+ css: Record<string, string | State | Binding> | undefined,
108
+ ) {
109
+ const style = (element as any).style;
110
+ if (!(style instanceof CSSStyleDeclaration)) return;
111
+
112
+ for (const propName in css) {
113
+ const value = css[propName];
114
+ applyStringOrStateOrBinding(element, value, (text) =>
115
+ style.setProperty(propName, text),
116
+ );
117
+ }
118
+ }
119
+
120
+ function initializeAttributes(
121
+ element: Element,
122
+ attr: Record<string, string | State | Binding> | undefined,
123
+ ) {
124
+ for (const attrName in attr) {
125
+ const value = attr[attrName];
126
+ applyStringOrStateOrBinding(element, value, (text) => {
127
+ if (!text) element.removeAttribute(attrName);
128
+ else {
129
+ element.setAttribute(attrName, text);
130
+ }
131
+ });
132
+ }
133
+ }
134
+
135
+ function initializeProps(
136
+ element: Node,
137
+ prop: Record<string, any | State | Binding> | undefined,
138
+ ) {
139
+ for (const key in prop) {
140
+ const value = prop[key];
141
+ applyStringOrStateOrBinding(element, value, (resolvedValue) => {
142
+ (element as any)[key] = resolvedValue;
143
+ });
144
+ }
145
+ }
146
+
147
+ function initialize$(element: Element, $: $Record | undefined) {
148
+ for (const selector in $) {
149
+ const selected = element.querySelector(selector);
150
+ if (selected) initialize(selected, $[selector]);
151
+ }
152
+ }
153
+
154
+ function initialize$$(element: Element, $$: $Record | undefined) {
155
+ for (const selector in $$) {
156
+ const selectedItems = element.querySelectorAll(selector);
157
+ for (const selected of selectedItems) {
158
+ initialize(selected, $$[selector]);
159
+ }
160
+ }
161
+ }
162
+
163
+ function initializeEventListeners<TEventType2Event>(
164
+ element: Node,
165
+ on: EventListenerRecord<TEventType2Event> | undefined,
166
+ ) {
167
+ for (const eventName in on) {
168
+ const type = eventName as keyof TEventType2Event;
169
+ const listener = on[type];
170
+ if (!listener) continue;
171
+ if (typeof listener === "function") {
172
+ element.addEventListener(eventName, listener as EventListener);
173
+ } else {
174
+ element.addEventListener(
175
+ eventName,
176
+ listener.listener as EventListener,
177
+ listener.options,
178
+ );
179
+ }
180
+ }
181
+ }
182
+
183
+ function initialize<TElement extends Element, TEventType2Event>(
184
+ element: TElement | null,
185
+ initializer: ElementInitializer<TElement, TEventType2Event>,
186
+ ) {
187
+ if (!element) return;
188
+ if (typeof initializer === "string" || initializer instanceof State) {
189
+ initializeText(element, initializer);
190
+ } else if (Array.isArray(initializer)) {
191
+ initializeChildBlock(element, initializer);
192
+ } else if (typeof initializer === "function") {
193
+ initializer(element);
194
+ } else {
195
+ initializeHtml(element, initializer.html);
196
+ initializeText(element, initializer.text);
197
+ initializeAttributes(element, initializer.attr);
198
+ initializeProps(element, initializer.prop);
199
+ initializeStyle(element, initializer.css);
200
+ initialize$(element, initializer.$);
201
+ initialize$$(element, initializer.$$);
202
+ initializeEventListeners(element, initializer.on);
203
+ initializeData(element, initializer.data);
204
+ }
205
+ }
206
+
207
+ export function Modify<T extends Element>(
208
+ elementOrSelector: T | string,
209
+ ...initializers: ElementInitializer<T>[]
210
+ ) {
211
+ const element =
212
+ typeof elementOrSelector === "string"
213
+ ? (document.querySelector(elementOrSelector) as T)
214
+ : elementOrSelector;
215
+ for (const initializer of initializers) {
216
+ initialize(element, initializer);
217
+ }
218
+ return element;
219
+ }
package/src/State.ts ADDED
@@ -0,0 +1,38 @@
1
+ export class State<T = any> {
2
+ #value: T;
3
+ constructor(value: T) {
4
+ this.#value = value;
5
+ }
6
+
7
+ get = () => this.#value;
8
+ set(value: T) {
9
+ this.#value = value;
10
+
11
+ this.#dispatch("change");
12
+ }
13
+ #dispatch(event: string) {
14
+ this.#listeners.dispatchEvent(new Event(event));
15
+ }
16
+
17
+ #listeners = new EventTarget();
18
+ on(event: StateEventType, listener: () => void) {
19
+ this.#listeners.addEventListener(event, listener);
20
+ }
21
+ }
22
+
23
+ type StateEventType = "change";
24
+
25
+ export function useState<T>(value: T) {
26
+ return new State<T>(value);
27
+ }
28
+
29
+ export function FromStates<T>(states: State[], createValue: () => T) {
30
+ const result = new State<T>(createValue());
31
+ const update = () => {
32
+ result.set(createValue());
33
+ };
34
+ for (const state of states) {
35
+ state.on("change", update);
36
+ }
37
+ return result;
38
+ }
package/src/Tag.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { div } from "./Elements";
2
+ import { type ElementInitializer, Modify } from "./Modify";
3
+
4
+ export type SvgElementInitializer =
5
+ | {
6
+ attr?: Record<string, string>;
7
+ css?: Record<string, string>;
8
+ }
9
+ | Element[];
10
+
11
+ export function Svg<K extends keyof SVGElementTagNameMap>(
12
+ name: K,
13
+ ...initializers: SvgElementInitializer[]
14
+ ): SVGElementTagNameMap[K] {
15
+ const result = document.createElementNS("http://www.w3.org/2000/svg", name);
16
+ return Modify(result, ...initializers);
17
+ }
18
+
19
+ export function Html<K extends keyof HTMLElementTagNameMap>(
20
+ tagName: K,
21
+ ...initializers: ElementInitializer<HTMLElementTagNameMap[K]>[]
22
+ ) {
23
+ const result = document.createElement(tagName);
24
+ Modify(result, ...initializers);
25
+ return result;
26
+ }
27
+
28
+ export function Tag(
29
+ html: string,
30
+ ...initializers: ElementInitializer<Element>[]
31
+ ) {
32
+ const result = div({ html: html }).children[0];
33
+ return Modify(result, ...initializers);
34
+ }
@@ -0,0 +1,160 @@
1
+ export type DataCallback = (data: any) => void;
2
+
3
+ export class NodeData {
4
+ node2Data = new WeakMap<Node, Record<string, any>>();
5
+
6
+ addCallbacks(
7
+ element: Node,
8
+ callbackRecord: Record<string, DataCallback[]> | undefined,
9
+ ) {
10
+ if (!callbackRecord) return;
11
+ if (!this.node2DescendantCallbacks.has(element)) {
12
+ this.node2DescendantCallbacks.set(element, {});
13
+ }
14
+ const originalCallbackRecord = this.node2DescendantCallbacks.get(element)!;
15
+ appendCallbacksRecord(originalCallbackRecord, callbackRecord);
16
+ const dataRecord = this.node2Data.get(element);
17
+ resolveCallbacksByData(originalCallbackRecord, dataRecord);
18
+ }
19
+
20
+ setDataRecord(element: Node, dataRecord: Record<string, any> | undefined) {
21
+ dataRecord && this.node2Data.set(element, dataRecord);
22
+ }
23
+
24
+ resolveCallbacks(element: Node, child: Node) {
25
+ /**
26
+ * Bubble up callbacks record until it reaches root.
27
+ * if it finds data, call callbacks and remove them.
28
+ * if it reaches root, append root callbacks
29
+ */
30
+ const bubbleUp = (
31
+ ancestor: Node,
32
+ callbacksRecord: Record<string, DataCallback[]>,
33
+ ) => {
34
+ resolveCallbacksByData(callbacksRecord, this.node2Data.get(ancestor));
35
+
36
+ // If callbacks are empty (resolved), finish bubbling up
37
+ if (!Object.keys(callbacksRecord).length) return;
38
+
39
+ // If it is root,
40
+ // append callbacks
41
+ if (!ancestor.parentElement) {
42
+ if (!this.node2DescendantCallbacks.has(ancestor)) {
43
+ this.node2DescendantCallbacks.set(ancestor, {});
44
+ }
45
+ const rootCallbacks = this.node2DescendantCallbacks.get(ancestor);
46
+ rootCallbacks && appendCallbacksRecord(rootCallbacks, callbacksRecord);
47
+ return;
48
+ }
49
+
50
+ bubbleUp(ancestor.parentElement, callbacksRecord);
51
+ };
52
+
53
+ const callbacks = this.node2DescendantCallbacks.get(child);
54
+ if (callbacks) {
55
+ bubbleUp(element, callbacks);
56
+ }
57
+ }
58
+
59
+ node2DescendantCallbacks = new WeakMap<
60
+ Node,
61
+ Record<string, DataCallback[]>
62
+ >();
63
+ }
64
+
65
+ /**
66
+ * Resolve callbacks.
67
+ * Resolved keys are deleted
68
+ */
69
+ function resolveCallbacksByData(
70
+ callbacksRecord: Record<string, DataCallback[]>,
71
+ dataRecord: Record<string, any> | undefined,
72
+ ) {
73
+ for (const key in dataRecord) {
74
+ if (key in callbacksRecord) {
75
+ for (const callback of callbacksRecord[key]) {
76
+ callback(dataRecord[key]);
77
+ }
78
+
79
+ delete callbacksRecord[key];
80
+ }
81
+ }
82
+ }
83
+
84
+ export type DataRecord = Record<string, DataCallback | any>;
85
+ export const nodeData = new NodeData();
86
+ export function initializeData(element: Node, data: DataRecord | undefined) {
87
+ nodeData.addCallbacks(element, extractCallbackRecord(data));
88
+ nodeData.setDataRecord(element, extractDataValueRecord(data));
89
+ }
90
+
91
+ export function extractCallbackRecord(
92
+ record: DataRecord | undefined,
93
+ ): Record<string, DataCallback[]> | undefined {
94
+ return extractRecordFromDataRecord(
95
+ record,
96
+ (value) => typeof value === "function",
97
+ (callback) => [callback],
98
+ );
99
+ }
100
+
101
+ export function extractDataValueRecord(
102
+ record: DataRecord | undefined,
103
+ ): Record<string, any> | undefined {
104
+ return extractRecordFromDataRecord(
105
+ record,
106
+ (value) => typeof value !== "function",
107
+ );
108
+ }
109
+
110
+ function extractRecordFromDataRecord(
111
+ record: DataRecord | undefined,
112
+ predicate: (value: any) => boolean,
113
+ map = (value: any) => value,
114
+ ) {
115
+ if (!record) return;
116
+
117
+ const result = {} as Record<string, any>;
118
+ for (const key in record) {
119
+ const value = record[key];
120
+ if (predicate(value)) {
121
+ result[key] = map(value);
122
+ }
123
+ }
124
+
125
+ if (!Object.keys(result).length) return;
126
+
127
+ return result;
128
+ }
129
+
130
+ export function createDescendantCallbacks(
131
+ record: Record<string, DataCallback> | undefined,
132
+ ) {
133
+ return extractRecordFromDataRecord(
134
+ record,
135
+ () => true,
136
+ (value) => [value],
137
+ );
138
+ }
139
+
140
+ export function appendCallbacksRecord(
141
+ record1: Record<string, DataCallback[]>,
142
+ record2: Record<string, DataCallback[]> | undefined,
143
+ ) {
144
+ for (const key in record2) {
145
+ if (!record1[key]) {
146
+ record1[key] = [];
147
+ }
148
+ record1[key].push(...record2[key]);
149
+ }
150
+ }
151
+
152
+ export function findData(node: Node | null, key: string) {
153
+ if (!node) return;
154
+
155
+ const record = nodeData.node2Data.get(node);
156
+ if (record && key in record) {
157
+ return record[key];
158
+ }
159
+ return findData(node.parentElement, key);
160
+ }
@@ -0,0 +1,10 @@
1
+ export function useBinding<T>(key: string, map: (value: T) => string) {
2
+ return new Binding(key, map);
3
+ }
4
+
5
+ export class Binding<T = any> {
6
+ constructor(
7
+ public key: string,
8
+ public map: (value: T) => string,
9
+ ) {}
10
+ }
@@ -0,0 +1,6 @@
1
+ export abstract class ControlFlow {
2
+ next: ControlFlow | Node | null = null;
3
+ firstNode: Node | null = null;
4
+
5
+ abstract run(element: Element): void;
6
+ }
@@ -0,0 +1,82 @@
1
+ import { nodeData } from "../data/data";
2
+ import { getNextNodeSibling } from "../initializeChildBlock";
3
+ import type { State } from "../State";
4
+ import { ControlFlow } from "./ControlFlow";
5
+
6
+ export function For<T>(
7
+ array: State<readonly T[]>,
8
+ map: (value: T) => Node | string,
9
+ ): ControlFlow {
10
+ return new ForMap(array, map);
11
+ }
12
+
13
+ export class ForMap<T> extends ControlFlow {
14
+ constructor(
15
+ private list: State<readonly T[]>,
16
+ private map: (value: T) => Node | string,
17
+ ) {
18
+ super();
19
+ }
20
+
21
+ run(element: Element) {
22
+ const model2View = new Map<T, Node>();
23
+ const view2Model = new Map<Node, T>();
24
+
25
+ // When list is updated, check diffs.
26
+ // Create UIs for added models and delete UIs for deleted models.
27
+ const updateListUI = () => {
28
+ // Added models
29
+ const itemsAdded = [] as T[];
30
+ for (const item of this.list.get()) {
31
+ if (!model2View.has(item)) itemsAdded.push(item);
32
+ }
33
+
34
+ // UIs to remove
35
+ const viewsToRemove = [] as Node[];
36
+ const listSet = new Set<T>(this.list.get());
37
+ for (const view of view2Model.keys()) {
38
+ const oldItem = view2Model.get(view);
39
+ if (oldItem && !listSet.has(oldItem)) viewsToRemove.push(view);
40
+ }
41
+
42
+ // Add UIs for added models
43
+ for (const item of itemsAdded) {
44
+ const mapped = this.map(item);
45
+ const resolved =
46
+ typeof mapped === "string" ? document.createTextNode(mapped) : mapped;
47
+
48
+ // data
49
+ nodeData.resolveCallbacks(element, resolved);
50
+
51
+ // To insert `item`
52
+ view2Model.set(resolved, item);
53
+ model2View.set(item, resolved);
54
+ }
55
+
56
+ // Remove UIs
57
+ for (const view of viewsToRemove) {
58
+ view.parentNode?.removeChild(view);
59
+ const model = view2Model.get(view);
60
+ view2Model.delete(view);
61
+ model && model2View.delete(model);
62
+ }
63
+
64
+ // Sort
65
+ for (const child of [...view2Model.keys()]) {
66
+ // child might not be appended yet. (so we need `?`)
67
+ child.parentElement?.removeChild(child);
68
+ }
69
+
70
+ const next = getNextNodeSibling(this);
71
+ for (const model of this.list.get()) {
72
+ element.insertBefore(model2View.get(model)!, next);
73
+ }
74
+ this.firstNode = model2View.get(this.list.get()[0]) ?? null;
75
+ };
76
+
77
+ updateListUI();
78
+ this.list.on("change", () => {
79
+ updateListUI();
80
+ });
81
+ }
82
+ }
package/src/flow/If.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { nodeData } from "../data/data";
2
+ import { getNextNodeSibling } from "../initializeChildBlock";
3
+ import type { State } from "../State";
4
+ import { ControlFlow } from "./ControlFlow";
5
+
6
+ export function If(
7
+ condition: State<boolean>,
8
+ show: () => Element,
9
+ showElse?: () => Element,
10
+ ): ControlFlow {
11
+ return new IfFlow(condition, show, showElse);
12
+ }
13
+ export class IfFlow extends ControlFlow {
14
+ #condition: State<boolean>;
15
+ #createThen: () => Element;
16
+ #createElse?: () => Element;
17
+
18
+ constructor(
19
+ condition: State<boolean>,
20
+ create: () => Element,
21
+ createElse?: () => Element,
22
+ ) {
23
+ super();
24
+ this.#condition = condition;
25
+ this.#createThen = create;
26
+ this.#createElse = createElse;
27
+ }
28
+
29
+ run(element: Element) {
30
+ let child: Element;
31
+ let elseChild: Element | undefined;
32
+
33
+ const update = () => {
34
+ const next = getNextNodeSibling(this);
35
+
36
+ if (this.#condition.get()) {
37
+ if (!child) child = this.#createThen();
38
+
39
+ // data
40
+ nodeData.resolveCallbacks(element, child);
41
+ // insert `child`
42
+ this.firstNode = child;
43
+ elseChild?.remove();
44
+ element.insertBefore(child, next);
45
+ } else {
46
+ if (!elseChild) elseChild = this.#createElse?.();
47
+
48
+ // data
49
+ elseChild && nodeData.resolveCallbacks(element, elseChild);
50
+ // insert `elseChild`
51
+ this.firstNode = elseChild ?? null;
52
+ child?.remove();
53
+ elseChild && element.insertBefore(elseChild, next);
54
+ }
55
+ };
56
+
57
+ update();
58
+
59
+ this.#condition.on("change", update);
60
+ }
61
+ }
@@ -0,0 +1,76 @@
1
+ import { nodeData } from "../data/data";
2
+ import { getNextNodeSibling } from "../initializeChildBlock";
3
+ import type { State } from "../State";
4
+ import { ControlFlow } from "./ControlFlow";
5
+ import type { SwitchSection } from "./SwitchBlockState";
6
+
7
+ export function Switch<T>(
8
+ value: State<T>,
9
+ sections: SwitchSection<T>[],
10
+ createDefault?: () => Element,
11
+ ): ControlFlow {
12
+ return new SwitchFlow(value, sections, createDefault);
13
+ }
14
+ export class SwitchFlow<T> extends ControlFlow {
15
+ #value: State<T>;
16
+ #sections: SwitchSection<T>[];
17
+ #createDefault?: () => Element;
18
+
19
+ constructor(
20
+ value: State<T>,
21
+ sections: SwitchSection<T>[],
22
+ createDefault?: () => Element,
23
+ ) {
24
+ super();
25
+ this.#value = value;
26
+ this.#sections = sections;
27
+ this.#createDefault = createDefault;
28
+ }
29
+
30
+ run(element: Element) {
31
+ const value2Element = new Map<T, Element>();
32
+ const value2Section = new Map<T, SwitchSection<T>>();
33
+
34
+ for (const section of this.#sections) {
35
+ value2Section.set(section.case, section);
36
+ }
37
+
38
+ let currentElement: Element | undefined;
39
+ let defaultElement: Element | undefined;
40
+
41
+ const getElementFromValue = (value: T) => {
42
+ const section = value2Section.get(value);
43
+ if (section) {
44
+ // If element is not created yet, create and cache
45
+ if (!value2Element.has(value)) {
46
+ const newElement = section.show();
47
+ value2Element.set(value, newElement);
48
+ }
49
+ return value2Element.get(value)!;
50
+ }
51
+
52
+ // ensure default
53
+ if (this.#createDefault && !defaultElement) {
54
+ defaultElement = this.#createDefault();
55
+ }
56
+ return defaultElement;
57
+ };
58
+
59
+ const update = () => {
60
+ const value = this.#value.get();
61
+ const nextNode = getNextNodeSibling(this);
62
+
63
+ const newElement = getElementFromValue(value);
64
+ // data
65
+ newElement && nodeData.resolveCallbacks(element, newElement);
66
+ // insert `newElement`
67
+ currentElement?.remove();
68
+ newElement && element.insertBefore(newElement, nextNode);
69
+ currentElement = newElement;
70
+ };
71
+
72
+ update();
73
+
74
+ this.#value.on("change", update);
75
+ }
76
+ }
@@ -0,0 +1,31 @@
1
+ import { type State, useState } from '../State';
2
+
3
+ export type SwitchSection<TCase, TElement = Element> = {
4
+ case: TCase;
5
+ show: () => TElement;
6
+ };
7
+
8
+ export function SwitchBlockState<TCase, TElement = Element>(
9
+ value: State<TCase>,
10
+ sections: SwitchSection<TCase, TElement>[],
11
+ ) {
12
+ const result = useState<SwitchSection<TCase, TElement> | undefined>(
13
+ undefined,
14
+ );
15
+
16
+ const update = () => {
17
+ let activated: SwitchSection<TCase, TElement> | undefined;
18
+
19
+ for (const section of sections) {
20
+ if (value.get() === section.case) {
21
+ activated = section;
22
+ }
23
+ }
24
+ result.set(activated);
25
+ };
26
+
27
+ value.on('change', update);
28
+
29
+ update();
30
+ return result;
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './Elements';
2
+ export * from './flow/For';
3
+ export * from './flow/If';
4
+ export * from './flow/Switch';
5
+ export * from './flow/SwitchBlockState';
6
+ export * from './Modify';
7
+ export * from './State';
8
+ export * from './Tag';
@@ -0,0 +1,59 @@
1
+ import { nodeData } from "./data/data";
2
+ import { ControlFlow } from "./flow/ControlFlow";
3
+ import { applyStringOrState } from "./Modify";
4
+ import { State } from "./State";
5
+
6
+ export type ChildType = Node | string | State | ControlFlow;
7
+ export function initializeChildBlock(element: Element, children: ChildType[]) {
8
+ const resolvedChildren = resolveTextNode(children);
9
+ connectNeighbours(resolvedChildren);
10
+
11
+ for (const child of resolvedChildren) {
12
+ initializeChild(element, child);
13
+ }
14
+ }
15
+
16
+ function initializeChild(element: Element, child: ControlFlow | Node) {
17
+ if (child instanceof ControlFlow) {
18
+ child.run(element);
19
+ } else {
20
+ nodeData.resolveCallbacks(element, child);
21
+ element.appendChild(child);
22
+ }
23
+ }
24
+
25
+ export function resolveTextNode(children: ChildType[]): (Node | ControlFlow)[] {
26
+ return children.map((c) => {
27
+ if (typeof c === "string" || c instanceof State) {
28
+ const textNode = document.createTextNode("");
29
+ applyStringOrState(c, (text) => {
30
+ textNode.textContent = text;
31
+ });
32
+ return textNode;
33
+ }
34
+ return c;
35
+ });
36
+ }
37
+
38
+ export function connectNeighbours(children: (Node | ControlFlow)[]) {
39
+ for (let i = 0; i < children.length; i++) {
40
+ const child = children[i];
41
+ if (child instanceof ControlFlow) {
42
+ child.next = children[i + 1] ?? null;
43
+ }
44
+ }
45
+ }
46
+
47
+ export function getNextNodeSiblingVirtual(flow: ControlFlow) {
48
+ const next = flow.next;
49
+ if (next === null) return null;
50
+ if (next instanceof Node) return next;
51
+
52
+ if (next.firstNode) return next.firstNode;
53
+ return getNextNodeSiblingVirtual(next);
54
+ }
55
+
56
+ export function getNextNodeSibling(flow: ControlFlow) {
57
+ const virtual = getNextNodeSiblingVirtual(flow);
58
+ return virtual?.parentElement ? virtual : null;
59
+ }