jmx-runtime 0.0.27 → 0.0.31

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/jmx.ts CHANGED
@@ -1,183 +1,188 @@
1
- import { rebind } from './base'
2
- import { Expr, FComponent, H, HComp, HCompClass, HElement, HFragment, IClassComponent, Props } from './h'
3
-
4
- // config
5
- export function createElement(tag: string) {
6
- let ns = window.jmx?.getnamespace?.(tag)
7
- return ns ? document.createElementNS(ns, tag) : document.createElement(tag)
8
- }
9
-
10
- const enum NodeType { // vaporizes (but for that must be in this file, otherwise not)
11
- TextNode = 3,
12
- }
13
-
14
- let evaluate = <T>(expr: Expr<T>): T => (expr instanceof Function ? expr() : expr)
15
- let removeexcesschildren = (n: Element, i: number) => {
16
- let c: ChildNode
17
- while ((c = n.childNodes[i])) {
18
- c.remove()
19
- }
20
- }
21
- let iswebcomponent = (h: HElement) => (h.tag as string).includes('-')
22
- let isclasscomponent = (h: HComp): h is HCompClass => (h.tag as any)?.prototype?.view
23
- let iselement = (h: any): h is HElement => typeof h.tag == 'string'
24
- let isfragment = (h: any): h is HFragment => {
25
- return h.tag == undefined && h.cn != undefined
26
- }
27
- let isobject = (o: any): o is object => typeof o === 'object'
28
-
29
- let isproperty = (name: string, value: any) =>
30
- ['value', 'checked', 'disabled', 'className', 'style', 'href', 'src', 'selected', 'readOnly', 'tabIndex'].includes(
31
- name
32
- ) ||
33
- value instanceof Object ||
34
- value instanceof Function
35
-
36
- let setprops = (e: Element, newprops: Props = {}) => {
37
- let oldprops = evaluate(e.h?.p) ?? {}
38
- for (let p in oldprops)
39
- !(p in newprops) && isproperty(p, oldprops[p]) ? ((e as any)[p] = null) : e.removeAttribute(p)
40
- for (let p in newprops) isproperty(p, newprops[p]) ? ((e as any)[p] = newprops[p]) : e.setAttribute(p, newprops[p])
41
- }
42
-
43
- /** syncs at position i of p. returns the number of the element past the last added element.
44
- * if no element was added (eg when h=null) then it returns i
45
- * if a fragment with 5 nodes was added, it returns i + 5
46
- * when a single element or component is added, it is i+1 since they always create exactly 1 node
47
- */
48
- function sync(p: Element, i: number, h: Expr<H | undefined>): number {
49
- // console.log('%csync', "background:orange", p.tagName, i, h)
50
-
51
- h = evaluate(h)
52
- if (h === null || h === undefined) return i // skip this element. not that !!h would forbid to render the number 0 or the boolean value false
53
-
54
- let c = p.childNodes[i] // is often null, eg during fresh creation
55
-
56
- function synctextnode(text: string) {
57
- if (c && c.nodeType == NodeType.TextNode) {
58
- if (c.textContent != text) c.textContent = text // firefox updates even equal text, loosing an existing text selection
59
- } else {
60
- let tn = document.createTextNode(text)
61
- c ? c.replaceWith(tn) : p.appendChild(tn)
62
- }
63
- }
64
-
65
- if (isobject(h)) {
66
- // element nodes
67
-
68
- /** synchronizes children starting at the i-th element. returns the index of the last child synchronized */
69
- function syncchildren(p: Element, h: HElement | HComp | HFragment, i: number): number {
70
- evaluate(h.cn)
71
- ?.flat()
72
- .forEach(hc => (i = sync(p, i, hc)))
73
- return i
74
- }
75
-
76
- if (isfragment(h)) return syncchildren(p, h, i)
77
-
78
- const props = evaluate(h.p)
79
-
80
- if (iselement(h)) {
81
- let n: Element
82
-
83
- if ((<Element>c)?.tagName?.toLowerCase() != h.tag.toLowerCase()) {
84
- n = createElement(h.tag)
85
- c ? c.replaceWith(n) : p.appendChild(n)
86
- setprops(n, props)
87
- props?.mounted?.(n)
88
- } else {
89
- n = c as Element
90
- setprops(n, props)
91
- if (props?.update?.(c, globaluc)) return i + 1
92
- }
93
-
94
- // if only components shall be updateable (advantage: close variables inside component functions are always fresh materialized, avoids surprises), comment this out
95
- n.h = h
96
-
97
- if (!globaluc.patchElementOnly && !iswebcomponent(h as HElement)) {
98
- // tbd: make "island" attribute
99
- const j = syncchildren(n, h, 0)
100
- removeexcesschildren(n, j)
101
- }
102
- return i + 1
103
- }
104
-
105
- switch (typeof h.tag) {
106
- case 'function':
107
- let isupdate = c?.h?.tag == h.tag
108
-
109
- let ci: IClassComponent | undefined
110
-
111
- if (isclasscomponent(h)) {
112
- h.i = ci = (c?.h as HCompClass)?.i ?? rebind(new h.tag(props))
113
- ci.props = props
114
-
115
- // if component instance returns truthy for update(), then syncing is susbstituted by the component
116
- if (isupdate && ci.update(globaluc)) return i + 1
117
- }
118
-
119
- // materialize the component
120
- // we run compoents view() and fun code often, we do not compare properties to avoid their computation
121
- // this means that the inner hr (h resolved) is run often
122
- //
123
- // if ci has a view, return ci.view() even if it is falsy, this is perfectly valid
124
- let hr = ci?.view ? ci?.view() : (h.tag as FComponent)(props, evaluate(h.cn))
125
-
126
- // a component can return undefined or null if it has no elements to show
127
- if (hr === undefined || hr == null) return i
128
-
129
- let j = sync(p, i, hr)
130
-
131
- let cn = p.childNodes[i]!
132
- cn.h = h // attach h onto the materialized component node
133
- // ;(cn as HTMLElement).setAttribute?.('comp', '')
134
-
135
- if (ci) ci.element = cn
136
- if (!isupdate) ci?.mounted?.()
137
-
138
- return j
139
-
140
- case 'object':
141
- return sync(p, i, h.tag) // tbd: type of h is not correct, h.tag == never
142
- }
143
- }
144
- // text nodes
145
- synctextnode(h as string)
146
- return i + 1
147
- }
148
-
149
- let globaluc: IUpdateContext = {}
150
-
151
- export function patch(e: Node | null, h: Expr<H>) {
152
- if (!e) return
153
- if (globaluc.replace) (e as HTMLElement).replaceChildren()
154
- const p = e.parentElement as HTMLElement
155
- const i = [].indexOf.call<any, any, any>(p.childNodes, e)
156
- // always called deferred, because removing elements can trigger events and their handlers (like blur)
157
- sync(p, i, h)
158
- }
159
-
160
- // Overload signatures
161
- type Selector = string | Node | undefined | null
162
- type Selectors = Selector[]
163
-
164
- export function updateviewuc(uc: IUpdateContext, ...sels: Selectors): void {
165
- {
166
- globaluc = uc
167
- updateviewinternal(...sels)
168
- }
169
- }
170
- export function updateview(...sels: Selectors): void {
171
- {
172
- globaluc = {}
173
- updateviewinternal(...sels)
174
- }
175
- }
176
-
177
- function updateviewinternal(...sels: Selector[]): void {
178
- if (!sels.length) sels.push('body')
179
- sels.flatMap(s => (typeof s == 'string' ? [...document.querySelectorAll(s)] : s ? [s] : [])).forEach(e => {
180
- if (!e?.h) throw 'jmx: no h exists on the node'
181
- patch(e, e.h)
182
- })
183
- }
1
+ import { rebind } from './base'
2
+ import { Expr, FComponent, H, HComp, HCompClass, HElement, HFragment, IClassComponent, Props } from './h'
3
+
4
+ // config
5
+ export function createElement(tag: string) {
6
+ let ns = window.jmx?.getnamespace?.(tag)
7
+ return ns ? document.createElementNS(ns, tag) : document.createElement(tag)
8
+ }
9
+
10
+ const enum NodeType { // vaporizes (but for that must be in this file, otherwise not)
11
+ TextNode = 3,
12
+ }
13
+
14
+ let evaluate = <T>(expr: Expr<T>): T => (expr instanceof Function ? expr() : expr)
15
+ let removeexcesschildren = (n: Element, i: number) => {
16
+ let c: ChildNode
17
+ while ((c = n.childNodes[i])) {
18
+ c.remove()
19
+ }
20
+ }
21
+ let iswebcomponent = (h: HElement) => (h.tag as string).includes('-')
22
+ let isclasscomponent = (h: HComp): h is HCompClass => (h.tag as any)?.prototype?.view
23
+ let iselement = (h: any): h is HElement => typeof h.tag == 'string'
24
+ let isfragment = (h: any): h is HFragment => {
25
+ return h.tag == undefined && h.cn != undefined
26
+ }
27
+ let isobject = (o: any): o is object => typeof o === 'object'
28
+
29
+ let isproperty = (name: string, value: any) =>
30
+ ['value', 'checked', 'disabled', 'className', 'style', 'href', 'src', 'selected', 'readOnly', 'tabIndex'].includes(
31
+ name
32
+ ) ||
33
+ value instanceof Object ||
34
+ value instanceof Function
35
+
36
+ let setprops = (e: Element, newprops: Props = {}) => {
37
+ let oldprops = evaluate(e.h?.p) ?? {}
38
+ for (let p in oldprops)
39
+ !(p in newprops) && isproperty(p, oldprops[p]) ? ((e as any)[p] = null) : e.removeAttribute(p)
40
+ for (let p in newprops) isproperty(p, newprops[p]) ? ((e as any)[p] = newprops[p]) : e.setAttribute(p, newprops[p])
41
+ }
42
+
43
+ /** syncs at position i of p. returns the number of the element past the last added element.
44
+ * if no element was added (eg when h=null) then it returns i
45
+ * if a fragment with 5 nodes was added, it returns i + 5
46
+ * when a single element or component is added, it is i+1 since they always create exactly 1 node
47
+ */
48
+ function sync(p: Element, i: number, h: Expr<H | undefined>): number {
49
+ // console.log('%csync', "background:orange", p.tagName, i, h)
50
+
51
+ h = evaluate(h)
52
+ if (h === null || h === undefined) return i // skip this element. not that !!h would forbid to render the number 0 or the boolean value false
53
+
54
+ let c = p.childNodes[i] // is often null, eg during fresh creation
55
+
56
+ function synctextnode(text: string) {
57
+ if (c && c.nodeType == NodeType.TextNode) {
58
+ if (c.textContent != text) c.textContent = text // firefox updates even equal text, loosing an existing text selection
59
+ } else {
60
+ let tn = document.createTextNode(text)
61
+ c ? c.replaceWith(tn) : p.appendChild(tn)
62
+ }
63
+ }
64
+
65
+ if (isobject(h)) {
66
+ // element nodes
67
+
68
+ /** synchronizes children starting at the i-th element. returns the index of the last child synchronized */
69
+ function syncchildren(p: Element, h: HElement | HComp | HFragment, i: number): number {
70
+ evaluate(h.cn)
71
+ ?.flat()
72
+ .forEach(hc => (i = sync(p, i, hc)))
73
+ return i
74
+ }
75
+
76
+ if (isfragment(h)) return syncchildren(p, h, i)
77
+
78
+ const props = evaluate(h.p)
79
+
80
+ if (iselement(h)) {
81
+ let n: Element
82
+
83
+ if ((<Element>c)?.tagName?.toLowerCase() != h.tag.toLowerCase()) {
84
+ n = createElement(h.tag)
85
+ c ? c.replaceWith(n) : p.appendChild(n)
86
+ setprops(n, props)
87
+ props?.mounted?.(n)
88
+ } else {
89
+ n = c as Element
90
+ setprops(n, props)
91
+ if (props?.update?.(c, globaluc)) return i + 1
92
+ }
93
+
94
+ // if only components shall be updateable (advantage: close variables inside component functions are always fresh materialized, avoids surprises), comment this out
95
+ n.h = h
96
+
97
+ if (!globaluc.patchElementOnly && !iswebcomponent(h as HElement)) {
98
+ // tbd: make "island" attribute
99
+ const j = syncchildren(n, h, 0)
100
+ removeexcesschildren(n, j)
101
+ }
102
+ return i + 1
103
+ }
104
+
105
+ switch (typeof h.tag) {
106
+ case 'function':
107
+ let isupdate = c?.h?.tag == h.tag
108
+
109
+ let ci: IClassComponent | undefined
110
+
111
+ if (isclasscomponent(h)) {
112
+ h.i = ci = isupdate ? (c.h as HCompClass)?.i : rebind(new h.tag(props))
113
+ ci.props = props
114
+
115
+ // if component instance returns truthy for update(), then this update substitutes syncing
116
+ if (isupdate && ci.update(globaluc)) return i + 1
117
+ }
118
+
119
+ // materialize the component
120
+ // we run compoents view() and fun code often, we do not compare properties to avoid their computation
121
+ // this means that the inner hr (h resolved) is run often
122
+ //
123
+ // if ci has a view, return ci.view() even if it is falsy, this is perfectly valid
124
+ let hr = ci?.view ? ci?.view() : (h.tag as FComponent)(props, evaluate(h.cn))
125
+
126
+ // a component can return undefined or null if it has no elements to show
127
+ if (hr === undefined || hr == null) return i
128
+
129
+ let j = sync(p, i, hr)
130
+
131
+ // now the dom element exists
132
+
133
+ let cn = p.childNodes[i]!
134
+
135
+ // class component life cycle calls
136
+ if (ci) {
137
+ cn.h = h // attach h onto the materialized component node
138
+ // ;(cn as HTMLElement).setAttribute?.('comp', '')
139
+ ci.element = cn
140
+ }
141
+ if (!isupdate) ci?.mounted?.()
142
+
143
+ return j
144
+
145
+ case 'object':
146
+ return sync(p, i, h.tag) // tbd: type of h is not correct, h.tag == never
147
+ }
148
+ }
149
+ // text nodes
150
+ synctextnode(h as string)
151
+ return i + 1
152
+ }
153
+
154
+ let globaluc: IUpdateContext = {}
155
+
156
+ export function patch(e: Node | null, h: Expr<H>) {
157
+ if (!e) return
158
+ if (globaluc.replace) (e as HTMLElement).replaceChildren()
159
+ const p = e.parentElement as HTMLElement
160
+ const i = [].indexOf.call<any, any, any>(p.childNodes, e)
161
+ // always called deferred, because removing elements can trigger events and their handlers (like blur)
162
+ sync(p, i, h)
163
+ }
164
+
165
+ // Overload signatures
166
+ type Selector = string | Node | undefined | null
167
+ type Selectors = Selector[]
168
+
169
+ export function updateviewuc(uc: IUpdateContext, ...sels: Selectors): void {
170
+ {
171
+ globaluc = uc
172
+ updateviewinternal(...sels)
173
+ }
174
+ }
175
+ export function updateview(...sels: Selectors): void {
176
+ {
177
+ globaluc = {}
178
+ updateviewinternal(...sels)
179
+ }
180
+ }
181
+
182
+ function updateviewinternal(...sels: Selector[]): void {
183
+ if (!sels.length) sels.push('body')
184
+ sels.flatMap(s => (typeof s == 'string' ? [...document.querySelectorAll(s)] : s ? [s] : [])).forEach(e => {
185
+ if (!e?.h) throw 'jmx: no h exists on the node'
186
+ patch(e, e.h)
187
+ })
188
+ }