apprun 3.36.0 → 3.36.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +133 -20
  3. package/WHATSNEW.md +20 -12
  4. package/apprun-book.jpg +0 -0
  5. package/apprun.d.ts +5 -2
  6. package/cli/app.js +29 -0
  7. package/cli/index.html +13 -0
  8. package/{apprun-cli.js → cli/index.js} +8 -14
  9. package/dist/apprun-dev-tools.js +1 -2
  10. package/dist/apprun-dev-tools.js.map +1 -1
  11. package/dist/apprun-html.esm.js +7 -7
  12. package/dist/apprun-html.esm.js.map +1 -1
  13. package/dist/apprun-html.js +1 -1
  14. package/dist/apprun-html.js.map +1 -1
  15. package/dist/apprun-play-html.esm.js +7 -7
  16. package/dist/apprun-play-html.esm.js.map +1 -1
  17. package/dist/apprun-play.js +1 -1
  18. package/dist/apprun-play.js.map +1 -1
  19. package/dist/apprun.esm.js +1 -1
  20. package/dist/apprun.esm.js.map +1 -1
  21. package/dist/apprun.js +1 -1
  22. package/dist/apprun.js.map +1 -1
  23. package/esm/app.js +10 -6
  24. package/esm/app.js.map +1 -1
  25. package/esm/apprun-dev-tools.js +1 -7
  26. package/esm/apprun-dev-tools.js.map +1 -1
  27. package/esm/apprun.js +18 -4
  28. package/esm/apprun.js.map +1 -1
  29. package/esm/component.js +4 -0
  30. package/esm/component.js.map +1 -1
  31. package/esm/router.js +241 -18
  32. package/esm/router.js.map +1 -1
  33. package/esm/vdom-my-new.js +329 -0
  34. package/esm/vdom-my-new.js.map +1 -0
  35. package/esm/vdom-my-prop-attr.js +227 -0
  36. package/esm/vdom-my-prop-attr.js.map +1 -0
  37. package/esm/vdom-my.js +109 -143
  38. package/esm/vdom-my.js.map +1 -1
  39. package/esm/version.js +1 -1
  40. package/index.html +1 -1
  41. package/jest.config.js +2 -1
  42. package/jest.setup.js +29 -3
  43. package/jsx-runtime.js +1 -1
  44. package/jsx-runtime.js.map +1 -1
  45. package/package.json +7 -7
  46. package/src/app.ts +11 -6
  47. package/src/apprun-dev-tools.tsx +1 -7
  48. package/src/apprun.ts +22 -4
  49. package/src/component.ts +4 -0
  50. package/src/router.ts +259 -15
  51. package/src/vdom-my-new.ts +311 -0
  52. package/src/vdom-my-prop-attr.ts +241 -0
  53. package/src/vdom-my.ts +109 -134
  54. package/src/version.ts +1 -1
  55. package/cli/export.js +0 -92
  56. package/cli/import.js +0 -68
  57. package/plan/plan-apprun-bugfixes.md +0 -207
package/src/router.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * AppRun Router Implementation
2
+ * AppRun Router Implementation with Hierarchical Matching
3
3
  *
4
4
  * This file provides URL routing capabilities:
5
5
  * 1. Hash-based routing (#/path)
@@ -22,6 +22,7 @@
22
22
  * - Route parameter parsing and injection into events
23
23
  * - URL pattern matching with parameter support
24
24
  * - Global and component-level route handling
25
+ * - **NEW: Hierarchical route matching** - progressively tries parent routes
25
26
  *
26
27
  * Type Safety Improvements (v3.35.1):
27
28
  * - Enhanced route type definitions
@@ -30,7 +31,7 @@
30
31
  *
31
32
  * Usage:
32
33
  * ```ts
33
- * // Handle routes
34
+ * // Basic routing
34
35
  * app.on('#/home', () => // Show home page);
35
36
  * app.on('#/users/:id', (id) => // Show user profile);
36
37
  * app.on('/api/*', (path) => // Handle API routes);
@@ -38,11 +39,264 @@
38
39
  * // Navigate programmatically
39
40
  * app.run('route', '#/home');
40
41
  * route('/users/123'); // Direct routing
42
+ *
43
+ * // Hierarchical matching examples
44
+ * app.on('/api', (operation, id) => // Handle /api/users/123);
45
+ * app.on('#users', (id, action) => // Handle #users/123/edit);
46
+ *
47
+ * // Hierarchical Route Matching (NEW):
48
+ * // For URL: /api/v1/users/123
49
+ * // Router tries: /api/v1/users/123 → /api/v1/users → /api/v1 → /api → 404
50
+ * // If handler found at /api, it receives: ('v1', 'users', '123')
51
+ *
52
+ * // Base Path Support (NEW):
53
+ * app.basePath = '/myapp'; // For sub-directory deployments
54
+ * // Links: <a href="/users/123"> (relative paths)
55
+ * // Navigation: /myapp/users/123 (full path)
56
+ * // Routing: /users/123 (base path stripped)
57
+ *
58
+ * // Empty Path Handling (NEW):
59
+ * // For URL: "" (empty)
60
+ * // Router tries: # → / → #/ → 404 (in priority order)
61
+ *
62
+ * // 404 Behavior (ENHANCED):
63
+ * // Fires only at minimal levels: /a, #a, #/a, a
64
+ * // Never tries root handlers: /, #, #/
41
65
  * ```
42
66
  */
43
67
 
44
68
  import app from './app';
45
69
 
70
+ // Helper functions for hierarchical routing
71
+
72
+ /**
73
+ * Extract clean path segments from URL
74
+ * @param url - The URL to parse
75
+ * @returns Array of path segments
76
+ */
77
+ function parsePathSegments(url: string): string[] {
78
+ if (!url) return [];
79
+
80
+ // Handle different URL types
81
+ if (url.startsWith('#/')) {
82
+ return url.substring(2).split('/');
83
+ } else if (url.startsWith('#')) {
84
+ return url.substring(1).split('/');
85
+ } else if (url.startsWith('/')) {
86
+ return url.substring(1).split('/');
87
+ } else {
88
+ return url.split('/');
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Normalize trailing slash - convert /a/ to /a
94
+ * @param url - The URL to normalize
95
+ * @returns Normalized URL
96
+ */
97
+ function normalizeTrailingSlash(url: string): string {
98
+ if (!url || url === '/' || url === '#' || url === '#/') return url;
99
+ if (url.endsWith('/')) return url.slice(0, -1);
100
+ return url;
101
+ }
102
+
103
+ /**
104
+ * Validate hierarchy depth and warn if too deep
105
+ * @param segments - Path segments to validate
106
+ */
107
+ function validateHierarchyDepth(segments: string[]): void {
108
+ // Count non-empty segments for depth validation
109
+ const nonEmptySegments = segments.filter(Boolean);
110
+ if (nonEmptySegments.length > 11) {
111
+ console.warn(`Deep route hierarchy detected: ${nonEmptySegments.join('/')} (${nonEmptySegments.length} levels)`);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Strip base path from URL
117
+ * @param url - The URL to process
118
+ * @param basePath - The base path to remove
119
+ * @returns URL with base path removed
120
+ */
121
+ function stripBasePath(url: string, basePath: string): string {
122
+ if (!basePath || basePath === '/' || basePath === '') return url;
123
+
124
+ // Normalize base path
125
+ const normalizedBasePath = basePath.startsWith('/') ? basePath : '/' + basePath;
126
+
127
+ if (url.startsWith(normalizedBasePath)) {
128
+ const stripped = url.substring(normalizedBasePath.length);
129
+ return stripped.startsWith('/') ? stripped : '/' + stripped;
130
+ }
131
+
132
+ return url;
133
+ }
134
+
135
+ /**
136
+ * Generate hierarchy of routes to try (stops at minimal level)
137
+ * @param segments - Array of path segments
138
+ * @param routeType - Type of route (path, hash, hash-slash, non-prefixed)
139
+ * @returns Array of route names to try in order
140
+ */
141
+ function generateRouteHierarchy(segments: string[], routeType: 'path' | 'hash' | 'hash-slash' | 'non-prefixed'): string[] {
142
+ const hierarchy: string[] = [];
143
+
144
+ // Build hierarchy from most specific to least specific
145
+ for (let i = segments.length; i > 0; i--) {
146
+ const currentSegments = segments.slice(0, i);
147
+ let routeName = '';
148
+
149
+ switch (routeType) {
150
+ case 'path':
151
+ routeName = '/' + currentSegments.join('/');
152
+ break;
153
+ case 'hash':
154
+ routeName = '#' + currentSegments.join('/');
155
+ break;
156
+ case 'hash-slash':
157
+ routeName = '#/' + currentSegments.join('/');
158
+ break;
159
+ case 'non-prefixed':
160
+ routeName = currentSegments.join('/');
161
+ break;
162
+ }
163
+
164
+ hierarchy.push(routeName);
165
+ }
166
+
167
+ return hierarchy;
168
+ }
169
+
170
+ /**
171
+ * Find handler in hierarchy and return handler info
172
+ * @param hierarchy - Array of route names to try
173
+ * @param originalSegments - Original path segments
174
+ * @returns Handler info if found, null otherwise
175
+ */
176
+ function findHandlerInHierarchy(hierarchy: string[], originalSegments: string[]): { eventName: string; parameters: string[] } | null {
177
+ for (let i = 0; i < hierarchy.length; i++) {
178
+ const routeName = hierarchy[i];
179
+ const subscribers = app.find(routeName);
180
+
181
+ if (subscribers && subscribers.length > 0) {
182
+ // Found handler - calculate remaining parameters
183
+ const handlerDepth = hierarchy.length - i;
184
+ const parameters = originalSegments.slice(handlerDepth);
185
+
186
+ return {
187
+ eventName: routeName,
188
+ parameters: parameters
189
+ };
190
+ }
191
+ }
192
+
193
+ return null;
194
+ }
195
+
196
+ /**
197
+ * Handle empty path with priority order: # → / → #/ → 404
198
+ */
199
+ function handleEmptyPath(): void {
200
+ // Try # first
201
+ const hashSubscribers = app.find('#');
202
+ if (hashSubscribers && hashSubscribers.length > 0) {
203
+ app.run('#');
204
+ app.run(ROUTER_EVENT, '#');
205
+ return;
206
+ }
207
+
208
+ // Try / second
209
+ const pathSubscribers = app.find('/');
210
+ if (pathSubscribers && pathSubscribers.length > 0) {
211
+ app.run('/');
212
+ app.run(ROUTER_EVENT, '/');
213
+ return;
214
+ }
215
+
216
+ // Try #/ third
217
+ const hashSlashSubscribers = app.find('#/');
218
+ if (hashSlashSubscribers && hashSlashSubscribers.length > 0) {
219
+ app.run('#/');
220
+ app.run(ROUTER_EVENT, '#/');
221
+ return;
222
+ }
223
+
224
+ // Fire 404 if no handlers found
225
+ console.warn('No subscribers for event: ');
226
+ app.run(ROUTER_404_EVENT, '');
227
+ app.run(ROUTER_EVENT, '');
228
+ }
229
+
230
+ /**
231
+ * Main hierarchical routing logic
232
+ * @param url - The URL to route
233
+ */
234
+ function routeWithHierarchy(url: string): void {
235
+ // Handle empty path
236
+ if (!url) {
237
+ handleEmptyPath();
238
+ return;
239
+ }
240
+
241
+ // Normalize trailing slash
242
+ url = normalizeTrailingSlash(url);
243
+
244
+ // Strip base path if configured
245
+ const basePath = app['basePath'];
246
+ if (basePath) {
247
+ url = stripBasePath(url, basePath);
248
+ }
249
+
250
+ // Parse segments and validate depth
251
+ const segments = parsePathSegments(url);
252
+ validateHierarchyDepth(segments);
253
+
254
+ // Determine route type
255
+ let routeType: 'path' | 'hash' | 'hash-slash' | 'non-prefixed';
256
+ if (url.startsWith('#/')) {
257
+ routeType = 'hash-slash';
258
+ } else if (url.startsWith('#')) {
259
+ routeType = 'hash';
260
+ } else if (url.startsWith('/')) {
261
+ routeType = 'path';
262
+ } else {
263
+ routeType = 'non-prefixed';
264
+ }
265
+
266
+ // Generate hierarchy
267
+ const hierarchy = generateRouteHierarchy(segments, routeType);
268
+
269
+ // Find handler in hierarchy
270
+ const handlerInfo = findHandlerInHierarchy(hierarchy, segments);
271
+
272
+ if (handlerInfo) {
273
+ // Found handler - publish route with parameters
274
+ publishRoute(handlerInfo.eventName, ...handlerInfo.parameters);
275
+ } else {
276
+ // No handler found - fire 404 with original URL
277
+ if (hierarchy.length > 0) {
278
+ const minimalRoute = hierarchy[hierarchy.length - 1];
279
+ console.warn(`No subscribers for event: ${minimalRoute}`);
280
+ app.run(ROUTER_404_EVENT, url);
281
+ app.run(ROUTER_EVENT, url);
282
+ } else {
283
+ handleEmptyPath();
284
+ }
285
+ }
286
+ }
287
+
288
+ const publishRoute = (name: string, ...args: any[]) => {
289
+ if (!name || name === ROUTER_EVENT || name === ROUTER_404_EVENT) return;
290
+ const subscribers = app.find(name);
291
+ if (!subscribers || subscribers.length === 0) {
292
+ console.warn(`No subscribers for event: ${name}`);
293
+ app.run(ROUTER_404_EVENT, name, ...args);
294
+ } else {
295
+ app.run(name, ...args);
296
+ }
297
+ app.run(ROUTER_EVENT, name, ...args);
298
+ }
299
+
46
300
  export type Route = (url: string, ...args: any[]) => any;
47
301
 
48
302
  export const ROUTER_EVENT: string = '//';
@@ -50,18 +304,8 @@ export const ROUTER_404_EVENT: string = '///';
50
304
  export const route: Route = (url: string) => {
51
305
  if (app['lastUrl'] === url) return; // Prevent duplicate routing
52
306
  app['lastUrl'] = url;
53
- if (!url) url = '#';
54
- if (url.startsWith('#')) {
55
- const [name, ...rest] = url.split('/');
56
- app.run(name, ...rest) || app.run(ROUTER_404_EVENT, name, ...rest);
57
- app.run(ROUTER_EVENT, name, ...rest);
58
- } else if (url.startsWith('/')) {
59
- const [_, name, ...rest] = url.split('/');
60
- app.run('/' + name, ...rest) || app.run(ROUTER_404_EVENT, '/' + name, ...rest);
61
- app.run(ROUTER_EVENT, '/' + name, ...rest);
62
- } else {
63
- app.run(url) || app.run(ROUTER_404_EVENT, url);
64
- app.run(ROUTER_EVENT, url);
65
- }
307
+
308
+ // Use hierarchical routing logic
309
+ routeWithHierarchy(url);
66
310
  }
67
311
  export default route;
@@ -0,0 +1,311 @@
1
+ import { VDOM, VNode } from './types';
2
+ import directive from './directive';
3
+ export type Element = any; //HTMLElement | SVGSVGElement | SVGElement;
4
+
5
+ export function Fragment(props, ...children): any[] {
6
+ return collect(children);
7
+ }
8
+
9
+ const ATTR_PROPS = '_props';
10
+
11
+ function collect(children) {
12
+ const ch = [];
13
+ const push = (c) => {
14
+ if (c !== null && c !== undefined && c !== '' && c !== false) {
15
+ ch.push((typeof c === 'function' || typeof c === 'object') ? c : `${c}`);
16
+ }
17
+ }
18
+ children && children.forEach(c => {
19
+ if (Array.isArray(c)) {
20
+ c.forEach(i => push(i));
21
+ } else {
22
+ push(c);
23
+ }
24
+ });
25
+ return ch;
26
+ }
27
+
28
+ export function createElement(tag: string | Function | [], props?: {}, ...children) {
29
+ const ch = collect(children);
30
+ if (typeof tag === 'string') return { tag, props, children: ch };
31
+ else if (Array.isArray(tag)) return tag; // JSX fragments - babel
32
+ else if (tag === undefined && children) return ch; // JSX fragments - typescript
33
+ else if (Object.getPrototypeOf(tag).__isAppRunComponent) return { tag, props, children: ch } // createComponent(tag, { ...props, children });
34
+ else if (typeof tag === 'function') return tag(props, ch);
35
+ else throw new Error(`Unknown tag in vdom ${tag}`);
36
+ };
37
+
38
+ const keyCache = {};
39
+
40
+ export const updateElement = (element: Element | string, nodes: VDOM, component = {}) => {
41
+ // tslint:disable-next-line
42
+ if (nodes == null || nodes === false) return;
43
+ const el = (typeof element === 'string' && element) ?
44
+ document.getElementById(element) || document.querySelector(element) : element;
45
+ nodes = directive(nodes, component);
46
+ render(el, nodes, component);
47
+ }
48
+
49
+ function render(element: Element, nodes: VDOM, parent = {}) {
50
+ // tslint:disable-next-line
51
+ if (nodes == null || nodes === false) return;
52
+ nodes = createComponent(nodes, parent);
53
+ if (!element) return;
54
+ const isSvg = element.nodeName === "SVG";
55
+ if (Array.isArray(nodes)) {
56
+ updateChildren(element, nodes, isSvg);
57
+ } else {
58
+ updateChildren(element, [nodes], isSvg);
59
+ }
60
+ }
61
+
62
+ function same(el: Element, node: VNode) {
63
+ // if (!el || !node) return false;
64
+ const key1 = el.nodeName;
65
+ const key2 = `${node.tag || ''}`;
66
+ return key1.toUpperCase() === key2.toUpperCase();
67
+ }
68
+
69
+ function update(element: Element, node: VNode, isSvg: boolean) {
70
+ // console.assert(!!element);
71
+ isSvg = isSvg || node.tag === "svg";
72
+ if (!same(element, node)) {
73
+ element.parentNode.replaceChild(create(node, isSvg), element);
74
+ return;
75
+ }
76
+ updateChildren(element, node.children, isSvg);
77
+ updateProps(element, node.props, isSvg);
78
+ }
79
+
80
+ function updateChildren(element: Element, children: any[], isSvg: boolean) {
81
+ const old_len = element.childNodes?.length || 0;
82
+ const new_len = children?.length || 0;
83
+
84
+ // Handle key-based reordering first if any children have keys
85
+ const hasKeysInNewChildren = children?.some(child =>
86
+ child && typeof child === 'object' && child.props && child.props.key !== undefined
87
+ );
88
+
89
+ if (hasKeysInNewChildren) {
90
+ // Create a map of existing keyed elements
91
+ const existingKeyedElements = new Map();
92
+ for (let i = 0; i < old_len; i++) {
93
+ const el = element.childNodes[i];
94
+ if (el && el.key) {
95
+ existingKeyedElements.set(el.key, el);
96
+ }
97
+ }
98
+
99
+ // Build new DOM structure
100
+ const fragment = document.createDocumentFragment();
101
+ for (let i = 0; i < new_len; i++) {
102
+ const child = children[i];
103
+ if (child == null) continue;
104
+
105
+ const key = child.props && child.props['key'];
106
+ if (key && existingKeyedElements.has(key)) {
107
+ // Reuse existing element
108
+ const existingEl = existingKeyedElements.get(key);
109
+ update(existingEl, child as VNode, isSvg);
110
+ fragment.appendChild(existingEl);
111
+ existingKeyedElements.delete(key); // Mark as used
112
+ } else {
113
+ // Create new element
114
+ fragment.appendChild(create(child, isSvg));
115
+ }
116
+ }
117
+
118
+ // Clear current children and append new structure
119
+ while (element.firstChild) {
120
+ element.removeChild(element.firstChild);
121
+ }
122
+ element.appendChild(fragment);
123
+ return;
124
+ }
125
+
126
+ // Original non-keyed logic
127
+ const len = Math.min(old_len, new_len);
128
+ for (let i = 0; i < len; i++) {
129
+ const child = children[i];
130
+ if (child == null) continue;
131
+ const el = element.childNodes[i];
132
+ if (!el) continue; // Safety check for undefined childNodes
133
+ if (typeof child === 'string') {
134
+ if (el.nodeType === 3) {
135
+ if (el.nodeValue !== child) {
136
+ el.nodeValue = child;
137
+ }
138
+ } else {
139
+ element.replaceChild(createText(child), el);
140
+ }
141
+ } else if (child instanceof HTMLElement || child instanceof SVGElement) {
142
+ element.replaceChild(child, el);
143
+ } else if (child && typeof child === 'object') {
144
+ update(element.childNodes[i], child as VNode, isSvg);
145
+ }
146
+ }
147
+
148
+ // Remove extra old nodes
149
+ while (element.childNodes.length > len) {
150
+ element.removeChild(element.lastChild);
151
+ }
152
+
153
+ if (new_len > len) {
154
+ const d = document.createDocumentFragment();
155
+ for (let i = len; i < children.length; i++) {
156
+ const child = children[i];
157
+ if (child != null) {
158
+ d.appendChild(create(child, isSvg));
159
+ }
160
+ }
161
+ element.appendChild(d);
162
+ }
163
+ }
164
+
165
+ export const safeHTML = (html: string) => {
166
+ const div = document.createElement('section');
167
+ div.insertAdjacentHTML('afterbegin', html)
168
+ return Array.from(div.children);
169
+ }
170
+
171
+ function createText(node) {
172
+ if (node?.indexOf('_html:') === 0) { // ?
173
+ const div = document.createElement('div');
174
+ div.insertAdjacentHTML('afterbegin', node.substring(6))
175
+ return div;
176
+ } else {
177
+ return document.createTextNode(node ?? '');
178
+ }
179
+ }
180
+
181
+ function create(node: VNode | string | HTMLElement | SVGElement, isSvg: boolean): Element {
182
+ // console.assert(node !== null && node !== undefined);
183
+ if ((node instanceof HTMLElement) || (node instanceof SVGElement)) return node;
184
+ if (typeof node === "string") return createText(node);
185
+
186
+ // Type guard for VNode objects - handle invalid node types gracefully
187
+ if (!node || typeof node !== 'object' || !node.tag || (typeof node.tag === 'function')) {
188
+ return createText(typeof node === 'object' ? JSON.stringify(node) : String(node ?? ''));
189
+ }
190
+
191
+ isSvg = isSvg || node.tag === "svg";
192
+ const element = isSvg
193
+ ? document.createElementNS("http://www.w3.org/2000/svg", node.tag)
194
+ : document.createElement(node.tag);
195
+
196
+ updateProps(element, node.props, isSvg);
197
+ if (node.children) node.children.forEach(child => element.appendChild(create(child, isSvg)));
198
+ return element
199
+ }
200
+
201
+ function mergeProps(oldProps: {}, newProps: {}): {} {
202
+ newProps['class'] = newProps['class'] || newProps['className'];
203
+ delete newProps['className'];
204
+ const props = {};
205
+ if (oldProps) Object.keys(oldProps).forEach(p => props[p] = null);
206
+ if (newProps) Object.keys(newProps).forEach(p => props[p] = newProps[p]);
207
+ return props;
208
+ }
209
+
210
+ export function updateProps(element: Element, props: {}, isSvg) {
211
+ // console.assert(!!element);
212
+ const cached = element[ATTR_PROPS] || {};
213
+ props = mergeProps(cached, props || {});
214
+ element[ATTR_PROPS] = props;
215
+
216
+ for (const name in props) {
217
+ const value = props[name];
218
+ // if (cached[name] === value) continue;
219
+ // console.log('updateProps', name, value);
220
+ if (name.startsWith('data-')) {
221
+ const dname = name.substring(5);
222
+ const cname = dname.replace(/-(\w)/g, (match) => match[1].toUpperCase());
223
+ if (element.dataset[cname] !== value) {
224
+ if (value || value === "") element.dataset[cname] = value;
225
+ else delete element.dataset[cname];
226
+ }
227
+ } else if (name === 'style') {
228
+ if (element.style.cssText) element.style.cssText = '';
229
+ if (typeof value === 'string') element.style.cssText = value;
230
+ else {
231
+ for (const s in value) {
232
+ if (element.style[s] !== value[s]) element.style[s] = value[s];
233
+ }
234
+ }
235
+ } else if (name.startsWith('xlink')) {
236
+ const xname = name.replace('xlink', '').toLowerCase();
237
+ if (value == null || value === false) {
238
+ element.removeAttributeNS('http://www.w3.org/1999/xlink', xname);
239
+ } else {
240
+ element.setAttributeNS('http://www.w3.org/1999/xlink', xname, value);
241
+ }
242
+ } else if (name.startsWith('on')) {
243
+ if (!value || typeof value === 'function') {
244
+ element[name] = value;
245
+ } else if (typeof value === 'string') {
246
+ if (value) element.setAttribute(name, value);
247
+ else element.removeAttribute(name);
248
+ }
249
+ } else if (/^id$|^class$|^list$|^readonly$|^contenteditable$|^role|-|^for$/g.test(name) || isSvg) {
250
+ if (element.getAttribute(name) !== value) {
251
+ if (value) element.setAttribute(name, value);
252
+ else element.removeAttribute(name);
253
+ }
254
+ } else if (element[name] !== value) {
255
+ element[name] = value;
256
+ }
257
+ if (name === 'key' && value !== undefined) {
258
+ keyCache[value] = element;
259
+ element.key = value; // Set key property on the DOM element
260
+ }
261
+ }
262
+ if (props && typeof props['ref'] === 'function') {
263
+ window.requestAnimationFrame(() => props['ref'](element));
264
+ }
265
+ }
266
+
267
+ function render_component(node, parent, idx) {
268
+ const { tag, props, children } = node;
269
+ let key = `_${idx}`;
270
+ let id = props && props['id'];
271
+ if (!id) id = `_${idx}${Date.now()}`;
272
+ else key = id;
273
+ let asTag = 'section';
274
+ if (props && props['as']) {
275
+ asTag = props['as'];
276
+ delete props['as'];
277
+ }
278
+ if (!parent.__componentCache) parent.__componentCache = {};
279
+ let component = parent.__componentCache[key];
280
+ if (!component || !(component instanceof tag) || !component.element) {
281
+ const element = document.createElement(asTag);
282
+ component = parent.__componentCache[key] = new tag({ ...props, children }).mount(element, { render: true });
283
+ } else {
284
+ component.renderState(component.state);
285
+ }
286
+ if (component.mounted) {
287
+ const new_state = component.mounted(props, children, component.state);
288
+ (typeof new_state !== 'undefined') && component.setState(new_state);
289
+ }
290
+ updateProps(component.element, props, false);
291
+ return component.element;
292
+ }
293
+
294
+ function createComponent(node, parent, idx = 0) {
295
+ if (typeof node === 'string') return node;
296
+ if (Array.isArray(node)) return node.map(child => createComponent(child, parent, idx++));
297
+ let vdom = node;
298
+ if (node && typeof node.tag === 'function' && Object.getPrototypeOf(node.tag).__isAppRunComponent) {
299
+ vdom = render_component(node, parent, idx);
300
+ }
301
+ if (vdom && Array.isArray(vdom.children)) {
302
+ const new_parent = vdom.props?._component;
303
+ if (new_parent) {
304
+ let i = 0;
305
+ vdom.children = vdom.children.map(child => createComponent(child, new_parent, i++));
306
+ } else {
307
+ vdom.children = vdom.children.map(child => createComponent(child, parent, idx++));
308
+ }
309
+ }
310
+ return vdom;
311
+ }