apprun 3.33.10 → 3.36.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.
Files changed (59) hide show
  1. package/README.md +5 -0
  2. package/WHATSNEW.md +58 -3
  3. package/apprun.d.ts +6 -6
  4. package/dist/apprun-dev-tools.js +1 -1
  5. package/dist/apprun-dev-tools.js.map +1 -1
  6. package/dist/apprun-html.esm.js +7 -7
  7. package/dist/apprun-html.esm.js.map +1 -1
  8. package/dist/apprun-html.js +1 -1
  9. package/dist/apprun-html.js.map +1 -1
  10. package/dist/apprun-play-html.esm.js +7 -7
  11. package/dist/apprun-play-html.esm.js.map +1 -1
  12. package/dist/apprun-play.js +1 -1
  13. package/dist/apprun-play.js.map +1 -1
  14. package/dist/apprun.esm.js +1 -1
  15. package/dist/apprun.esm.js.map +1 -1
  16. package/dist/apprun.js +1 -1
  17. package/dist/apprun.js.map +1 -1
  18. package/esm/app.js +55 -11
  19. package/esm/app.js.map +1 -1
  20. package/esm/apprun-html.js +8 -4
  21. package/esm/apprun-html.js.map +1 -1
  22. package/esm/apprun.js +82 -28
  23. package/esm/apprun.js.map +1 -1
  24. package/esm/component.js +56 -19
  25. package/esm/component.js.map +1 -1
  26. package/esm/decorator.js +22 -5
  27. package/esm/decorator.js.map +1 -1
  28. package/esm/directive.js +64 -13
  29. package/esm/directive.js.map +1 -1
  30. package/esm/router.js +25 -4
  31. package/esm/router.js.map +1 -1
  32. package/esm/type-utils.js +91 -0
  33. package/esm/type-utils.js.map +1 -0
  34. package/esm/types.js +32 -11
  35. package/esm/types.js.map +1 -1
  36. package/esm/vdom-my.js +66 -43
  37. package/esm/vdom-my.js.map +1 -1
  38. package/esm/version.js +15 -0
  39. package/esm/version.js.map +1 -0
  40. package/esm/web-component.js +30 -10
  41. package/esm/web-component.js.map +1 -1
  42. package/index.html +1 -0
  43. package/jest.config.js +1 -7
  44. package/jsx-runtime.js +1 -1
  45. package/jsx-runtime.js.map +1 -1
  46. package/package.json +1 -1
  47. package/plan/plan-apprun-bugfixes.md +207 -0
  48. package/src/app.ts +57 -15
  49. package/src/apprun-html.ts +8 -4
  50. package/src/apprun.ts +98 -33
  51. package/src/component.ts +58 -20
  52. package/src/decorator.ts +23 -6
  53. package/src/directive.ts +64 -13
  54. package/src/router.ts +25 -6
  55. package/src/type-utils.ts +130 -0
  56. package/src/types.ts +33 -12
  57. package/src/vdom-my.ts +75 -39
  58. package/src/version.ts +16 -0
  59. package/src/web-component.ts +31 -11
package/src/apprun.ts CHANGED
@@ -7,8 +7,9 @@
7
7
  * 3. Initializes global app instance with:
8
8
  * - Virtual DOM rendering
9
9
  * - Component system
10
- * - Router
10
+ * - Router with improved null safety
11
11
  * - Web component support
12
+ * - Type-safe React integration
12
13
  *
13
14
  * Key exports:
14
15
  * - app: Global event system instance
@@ -17,6 +18,22 @@
17
18
  * - Router events and configuration
18
19
  * - Web component registration
19
20
  *
21
+ * Features:
22
+ * - Event-driven architecture with pub/sub pattern
23
+ * - Virtual DOM rendering with multiple renderer support
24
+ * - Component lifecycle management
25
+ * - Client-side routing with hash/path support
26
+ * - Web Components integration
27
+ * - React compatibility layer
28
+ * - TypeScript support with strong typing
29
+ *
30
+ * Type Safety Improvements (v3.35.1):
31
+ * - Added null checks for DOM event targets
32
+ * - Improved global window object assignments with proper typing
33
+ * - Enhanced React integration parameter validation
34
+ * - Better error handling for invalid event handlers
35
+ * - Safer element access with proper type assertions
36
+ *
20
37
  * Usage:
21
38
  * ```ts
22
39
  * import { app, Component } from 'apprun';
@@ -39,6 +56,7 @@ import { VNode, View, Action, Update, EventOptions, ActionOptions, MountOptions,
39
56
  import { on, update, customElement } from './decorator';
40
57
  import webComponent, { CustomElementOptions } from './web-component';
41
58
  import { Route, route, ROUTER_EVENT, ROUTER_404_EVENT } from './router';
59
+ import { APPRUN_VERSION } from './version';
42
60
 
43
61
  export type StatelessComponent<T = {}> = (args: T) => string | VNode | void;
44
62
  type OnDecorator = {
@@ -70,9 +88,12 @@ export interface IApp {
70
88
  start<T, E = any>(element?: Element | string, model?: T, view?: View<T>, update?: Update<T, E>,
71
89
  options?: AppStartOptions<T>): Component<T, E>;
72
90
  on(name: string, fn: (...args: any[]) => void, options?: any): void;
91
+ once(name: string, fn: (...args: any[]) => void, options?: any): void;
73
92
  off(name: string, fn: (...args: any[]) => void): void;
74
93
  run(name: string, ...args: any[]): number;
75
94
  find(name: string): any | any[];
95
+ query(name: string, ...args: any[]): Promise<any[]>;
96
+ runAsync(name: string, ...args: any[]): Promise<any[]>;
76
97
  h(tag: string | Function, props, ...children): VNode | VNode[];
77
98
  createElement(tag: string | Function, props, ...children): VNode | VNode[];
78
99
  render(element: Element | string, node: VNode): void;
@@ -81,10 +102,14 @@ export interface IApp {
81
102
  webComponent(name: string, componentClass, options?: CustomElementOptions): void;
82
103
  safeHTML(html: string): any[];
83
104
  use_render(render, mode?: 0 | 1);
84
- use_react(createRoot);
105
+ use_react(React, ReactDOM);
106
+ version: string;
85
107
  }
86
108
 
87
109
  if (!app.start) {
110
+
111
+ app.version = APPRUN_VERSION;
112
+
88
113
  app.h = app.createElement = createElement;
89
114
  app.render = render;
90
115
  app.Fragment = Fragment;
@@ -101,19 +126,46 @@ if (!app.start) {
101
126
  return component;
102
127
  };
103
128
 
129
+ app.once = app.once || ((name: string, fn: (...args: any[]) => void, options: any = {}) => {
130
+ app.on(name, fn, { ...options, once: true });
131
+ });
132
+
133
+ app.query = app.query || app.runAsync;
134
+
104
135
  const NOOP = _ => {/* Intentionally empty */ }
105
- app.on('$', NOOP);
136
+ // app.on('$', NOOP);
106
137
  app.on('debug', _ => NOOP);
107
138
  app.on(ROUTER_EVENT, NOOP);
108
- app.on('#', NOOP);
109
- app['route'] = route;
139
+ app.on(ROUTER_404_EVENT, NOOP);
140
+ app.route = route;
110
141
  app.on('route', url => app['route'] && app['route'](url));
111
142
 
112
143
  if (typeof document === 'object') {
113
144
  document.addEventListener("DOMContentLoaded", () => {
114
- if (app['route'] === route) {
115
- window.onpopstate = () => route(location.hash);
116
- document.body.hasAttribute('apprun-no-init') || app['no-init-route'] || route(location.hash);
145
+ const no_init_route = document.body.hasAttribute('apprun-no-init') || app['no-init-route'] || false;
146
+ const use_hash = app.find('#') || app.find('#/') || false;
147
+
148
+ // console.log(`AppRun ${app.version} started with ${use_hash ? 'hash' : 'path'} routing. Initial load: ${init_load ? 'disabled' : 'enabled'}.`);
149
+ window.addEventListener('hashchange', () => route(location.hash));
150
+ window.addEventListener('popstate', () => route(location.pathname));
151
+
152
+ if (use_hash) {
153
+ !no_init_route && route(location.hash);
154
+ } else {
155
+ !no_init_route && route(location.pathname);
156
+ document.body.addEventListener('click', e => {
157
+ const element = e.target as HTMLElement;
158
+ if (!element) return;
159
+
160
+ const menu = (element.tagName === 'A' ? element : element.closest('a')) as HTMLAnchorElement;
161
+ if (menu &&
162
+ menu.origin === location.origin &&
163
+ menu.pathname) {
164
+ e.preventDefault();
165
+ history.pushState(null, '', menu.pathname);
166
+ route(menu.pathname);
167
+ }
168
+ });
117
169
  }
118
170
  });
119
171
  }
@@ -123,12 +175,13 @@ if (!app.start) {
123
175
  };
124
176
 
125
177
  if (typeof window === 'object') {
126
- window['Component'] = Component as ComponentType;
127
- window['_React'] = window['React'];
128
- window['React'] = app;
129
- window['on'] = on as OnDecorator;
130
- window['customElement'] = customElement;
131
- window['safeHTML'] = safeHTML;
178
+ const globalWindow = window as any;
179
+ globalWindow['Component'] = Component as ComponentType;
180
+ globalWindow['_React'] = globalWindow['React'];
181
+ globalWindow['React'] = app;
182
+ globalWindow['on'] = on as OnDecorator;
183
+ globalWindow['customElement'] = customElement;
184
+ globalWindow['safeHTML'] = safeHTML;
132
185
  }
133
186
 
134
187
  app.use_render = (render, mode = 0) => {
@@ -140,30 +193,42 @@ if (!app.start) {
140
193
  };
141
194
 
142
195
  app.use_react = (React, ReactDOM) => {
196
+ if (!React || !ReactDOM) {
197
+ console.error('AppRun use_react: React and ReactDOM parameters are required');
198
+ return;
199
+ }
200
+
201
+ if (typeof React.createElement !== 'function') {
202
+ console.error('AppRun use_react: Invalid React object - createElement method not found');
203
+ return;
204
+ }
205
+
206
+ if (!React.Fragment) {
207
+ console.error('AppRun use_react: Invalid React object - Fragment not found');
208
+ return;
209
+ }
210
+
143
211
  app.h = app.createElement = React.createElement;
144
212
  app.Fragment = React.Fragment;
145
- app.render = (el, vdom) => ReactDOM.render(vdom, el);
213
+
214
+ // React 18+ uses createRoot API
146
215
  if (React.version && React.version.startsWith('18')) {
216
+ if (!ReactDOM.createRoot || typeof ReactDOM.createRoot !== 'function') {
217
+ console.error('AppRun use_react: ReactDOM.createRoot not found in React 18+');
218
+ return;
219
+ }
147
220
  app.render = (el, vdom) => {
148
- if (!el || !vdom) return;
149
- if (!el._root) el._root = ReactDOM.createRoot(el);
150
- el._root.render(vdom);
221
+ if (!el || vdom === undefined) return;
222
+ if (!(el as any)._root) (el as any)._root = ReactDOM.createRoot(el);
223
+ (el as any)._root.render(vdom);
151
224
  }
152
- }
153
- }
154
-
155
- app.use_prettyLink = () => {
156
- window.onload = () => app.route(location.pathname);
157
- window.addEventListener("popstate", () => app.route(location.pathname));
158
- document.body.addEventListener('click', e => {
159
- const element = e.target as HTMLElement;
160
- const menu = (element.tagName === 'A' ? element : element.closest('a')) as HTMLAnchorElement;
161
- if (menu &&
162
- menu.origin === location.origin) {
163
- e.preventDefault();
164
- history.pushState(null, '', menu.pathname);
165
- app.route(menu.pathname);
225
+ } else {
226
+ // Legacy React versions
227
+ if (!ReactDOM.render || typeof ReactDOM.render !== 'function') {
228
+ console.error('AppRun use_react: ReactDOM.render not found in legacy React');
229
+ return;
166
230
  }
167
- });
231
+ app.render = (el, vdom) => ReactDOM.render(vdom, el);
232
+ }
168
233
  }
169
234
  }
package/src/component.ts CHANGED
@@ -3,19 +3,38 @@
3
3
  *
4
4
  * This file provides the Component class which is the foundation for:
5
5
  * 1. State Management
6
- * - Maintains component state
7
- * - Handles state updates
8
- * - Supports state history
6
+ * - Maintains component state with history support
7
+ * - Handles state updates with async/iterator support
8
+ * - Supports state history navigation (prev/next)
9
+ * - Promise and async iterator state handling
9
10
  *
10
11
  * 2. View Rendering
11
- * - Renders virtual DOM to real DOM
12
+ * - Renders virtual DOM to real DOM with directives
12
13
  * - Handles component lifecycle (mounted, rendered, unload)
13
14
  * - Supports shadow DOM and web components
15
+ * - DOM change tracking with MutationObserver
16
+ * - View transition API support
14
17
  *
15
18
  * 3. Event Handling
16
- * - Local and global event subscription
19
+ * - Local and global event subscription management
17
20
  * - Event handler registration via decorators
18
- * - Action to state updates
21
+ * - Action to state updates with error handling
22
+ * - Support for event options (delay, once, global)
23
+ *
24
+ * Features:
25
+ * - Component caching for debugging
26
+ * - Element tracking for cleanup
27
+ * - History navigation support
28
+ * - Global vs local event routing
29
+ * - Async state handling
30
+ * - Memory leak prevention
31
+ * - Component unmounting with cleanup
32
+ *
33
+ * Type Safety Improvements (v3.35.1):
34
+ * - Enhanced element access with null checks and warnings
35
+ * - Improved action validation and error handling
36
+ * - Better error reporting in component actions
37
+ * - Safer DOM element queries with fallback warnings
19
38
  *
20
39
  * Usage:
21
40
  * ```ts
@@ -36,6 +55,7 @@ import app, { App } from './app';
36
55
  import { Reflect } from './decorator'
37
56
  import { View, Update, ActionDef, ActionOptions, MountOptions, EventOptions } from './types';
38
57
  import directive from './directive';
58
+ import { safeQuerySelector, safeGetElementById } from './type-utils';
39
59
 
40
60
  const componentCache = new Map();
41
61
  if (!app.find('get-components')) app.on('get-components', o => o.components = componentCache);
@@ -74,9 +94,12 @@ export class Component<T = any, E = any> {
74
94
  if (typeof document !== 'object') return;
75
95
 
76
96
  const el = (typeof this.element === 'string' && this.element) ?
77
- document.getElementById(this.element) || document.querySelector(this.element) : this.element;
97
+ safeGetElementById(this.element) || safeQuerySelector(this.element) : this.element;
78
98
 
79
- if (!el) return;
99
+ if (!el) {
100
+ console.warn(`Component element not found: ${this.element}`);
101
+ return;
102
+ }
80
103
  const tracking_attr = '_c';
81
104
  if (!this.unload) {
82
105
  el.removeAttribute && el.removeAttribute(tracking_attr);
@@ -236,7 +259,10 @@ export class Component<T = any, E = any> {
236
259
  }
237
260
 
238
261
  add_action(name: string, action, options: ActionOptions = {}) {
239
- if (!action || typeof action !== 'function') return;
262
+ if (!action || typeof action !== 'function') {
263
+ console.warn(`Component action for '${name}' is not a valid function:`, action);
264
+ return;
265
+ }
240
266
  if (options.global) this._global_events.push(name);
241
267
  this.on(name as any, (...p) => {
242
268
 
@@ -248,18 +274,30 @@ export class Component<T = any, E = any> {
248
274
  options
249
275
  });
250
276
 
251
- const newState = action(this.state, ...p);
252
-
253
- app['debug'] && app.run('debug', {
254
- component: this,
255
- _: '<',
256
- event: name, p,
257
- newState,
258
- state: this.state,
259
- options
260
- });
277
+ try {
278
+ const newState = action(this.state, ...p);
279
+
280
+ app['debug'] && app.run('debug', {
281
+ component: this,
282
+ _: '<',
283
+ event: name, p,
284
+ newState,
285
+ state: this.state,
286
+ options
287
+ });
261
288
 
262
- this.setState(newState, options)
289
+ this.setState(newState, options);
290
+ } catch (error) {
291
+ console.error(`Error in component action '${name}':`, error);
292
+ app['debug'] && app.run('debug', {
293
+ component: this,
294
+ _: '!',
295
+ event: name, p,
296
+ error,
297
+ state: this.state,
298
+ options
299
+ });
300
+ }
263
301
  }, options);
264
302
  }
265
303
 
package/src/decorator.ts CHANGED
@@ -5,30 +5,47 @@ import webComponent, { CustomElementOptions } from './web-component';
5
5
  *
6
6
  * This file provides decorators that enable:
7
7
  * 1. Event Handler Registration
8
- * - @on(): Subscribe to events with options
9
- * - @update(): Define state updates with metadata
8
+ * - @on(): Subscribe to events with options (global, once, delay)
9
+ * - @update(): Define state updates with metadata and options
10
+ * - Supports method and class decoration patterns
10
11
  *
11
12
  * 2. Web Component Integration
12
13
  * - @customElement(): Register as custom element with options
13
14
  * - Handles shadow DOM and attribute observation
15
+ * - Automatic lifecycle management for web components
14
16
  *
15
17
  * 3. Metadata Management
16
18
  * - Custom Reflect implementation for decorator metadata
17
- * - Stores event handler and update metadata
19
+ * - Stores event handler and update metadata for runtime use
18
20
  * - Supports runtime reflection for dynamic behavior
21
+ * - Metadata keys for event bindings and updates
19
22
  *
23
+ * Features:
24
+ * - Event handler decoration with options
25
+ * - State update method decoration
26
+ * - Web component registration
27
+ * - Metadata-driven event binding
28
+ * - Flexible decorator patterns
29
+ * - Runtime metadata access
30
+ *
31
+ * Type Safety Improvements (v3.35.1):
32
+ * - Enhanced decorator typing for better IDE support
33
+ * - Improved metadata key management
34
+ * - Better type inference for decorated methods
35
+ *
20
36
  * Usage:
21
37
  * ```ts
22
38
  * @customElement('my-element')
23
39
  * class MyComponent extends Component {
24
- * @on('event')
40
+ * @on('event', { global: true })
25
41
  * handler(state, ...args) {
26
42
  * // Handle event
27
43
  * }
28
44
  *
29
- * @update('event')
45
+ * @update('event', { render: true })
30
46
  * updater(state, ...args) {
31
47
  * // Update state
48
+ * return newState;
32
49
  * }
33
50
  * }
34
51
  * ```
@@ -55,7 +72,7 @@ export const Reflect = {
55
72
  }
56
73
  }
57
74
 
58
- export function update<E=string>(events?: E, options: any = {}) {
75
+ export function update<E = string>(events?: E, options: any = {}) {
59
76
  return (target: any, key: string, descriptor: any) => {
60
77
  const name = events ? events.toString() : key;
61
78
  Reflect.defineMetadata(`apprun-update:${name}`,
package/src/directive.ts CHANGED
@@ -2,22 +2,38 @@
2
2
  * Component Directives Implementation
3
3
  *
4
4
  * This file provides built-in directives for components:
5
- * 1. Event Binding
5
+ * 1. Event Binding ($on directives)
6
6
  * - $on: Bind DOM events to component events
7
7
  * - Supports event delegation and parameters
8
8
  * - Handles function, string, and array event handlers
9
+ * - Type-safe event target handling
9
10
  *
10
- * 2. Two-way Data Binding
11
+ * 2. Two-way Data Binding ($bind directive)
11
12
  * - $bind: Sync form elements with component state
12
- * - Handles input types: text, checkbox, radio, select
13
- * - Automatic value type conversion
13
+ * - Handles input types: text, checkbox, radio, number, range
14
+ * - Supports select (single/multiple) and textarea elements
15
+ * - Automatic value type conversion for numbers
14
16
  * - Support for complex property paths
15
17
  *
16
18
  * 3. Custom Directives
17
- * - Extensible directive system
19
+ * - Extensible directive system via '$' events
18
20
  * - Processes virtual DOM during rendering
19
21
  * - Supports custom attribute directives
20
22
  *
23
+ * Features:
24
+ * - Automatic state synchronization
25
+ * - Type conversion for form inputs
26
+ * - Event delegation support
27
+ * - Multiple event handler formats
28
+ * - Nested property binding
29
+ * - Custom directive extensibility
30
+ *
31
+ * Type Safety Improvements (v3.35.1):
32
+ * - Added null checks for event targets before type assertions
33
+ * - Proper typing for different HTML element types
34
+ * - Enhanced error handling for invalid event targets
35
+ * - Safer DOM element property access
36
+ *
21
37
  * Usage:
22
38
  * ```tsx
23
39
  * // Event binding
@@ -27,10 +43,14 @@
27
43
  * // Two-way binding
28
44
  * <input $bind="state.property" />
29
45
  * <select $bind="selected">...</select>
46
+ *
47
+ * // Array handlers
48
+ * <button $onclick={['handler', param1, param2]}>Click</button>
30
49
  * ```
31
50
  */
32
51
 
33
52
  import app from './app';
53
+ import { safeEventTarget } from './type-utils';
34
54
 
35
55
  const getStateValue = (component, name) => {
36
56
  return (name ? component['state'][name] : component['state']) || '';
@@ -72,34 +92,65 @@ const apply_directive = (key: string, props: {}, tag, component) => {
72
92
  switch (type) {
73
93
  case 'checkbox':
74
94
  props['checked'] = getStateValue(component, name);
75
- props['onclick'] = e => setStateValue(component, name || e.target.name, e.target.checked);
95
+ props['onclick'] = e => {
96
+ const target = safeEventTarget<HTMLInputElement>(e);
97
+ if (target) {
98
+ setStateValue(component, name || target.name, target.checked);
99
+ }
100
+ };
76
101
  break;
77
102
  case 'radio':
78
103
  props['checked'] = getStateValue(component, name) === props['value'];
79
- props['onclick'] = e => setStateValue(component, name || e.target.name, e.target.value);
104
+ props['onclick'] = e => {
105
+ const target = safeEventTarget<HTMLInputElement>(e);
106
+ if (target) {
107
+ setStateValue(component, name || target.name, target.value);
108
+ }
109
+ };
80
110
  break;
81
111
  case 'number':
82
112
  case 'range':
83
113
  props['value'] = getStateValue(component, name);
84
- props['oninput'] = e => setStateValue(component, name || e.target.name, Number(e.target.value));
114
+ props['oninput'] = e => {
115
+ const target = safeEventTarget<HTMLInputElement>(e);
116
+ if (target) {
117
+ setStateValue(component, name || target.name, Number(target.value));
118
+ }
119
+ };
85
120
  break;
86
121
  default:
87
122
  props['value'] = getStateValue(component, name);
88
- props['oninput'] = e => setStateValue(component, name || e.target.name, e.target.value);
123
+ props['oninput'] = e => {
124
+ const target = safeEventTarget<HTMLInputElement>(e);
125
+ if (target) {
126
+ setStateValue(component, name || target.name, target.value);
127
+ }
128
+ };
89
129
  }
90
130
  } else if (tag === 'select') {
91
131
  props['value'] = getStateValue(component, name);
92
132
  props['onchange'] = e => {
93
- if (!e.target.multiple) { // multiple selection use $bind on option
94
- setStateValue(component, name || e.target.name, e.target.value);
133
+ const target = safeEventTarget<HTMLSelectElement>(e);
134
+ if (target && !target.multiple) { // multiple selection use $bind on option
135
+ setStateValue(component, name || target.name, target.value);
95
136
  }
96
137
  }
97
138
  } else if (tag === 'option') {
98
139
  props['selected'] = getStateValue(component, name);
99
- props['onclick'] = e => setStateValue(component, name || e.target.name, e.target.selected);
140
+ props['onclick'] = e => {
141
+ const target = safeEventTarget<HTMLOptionElement>(e);
142
+ if (target) {
143
+ setStateValue(component, name || (target as any).name, target.selected);
144
+ }
145
+ };
100
146
  } else if (tag === 'textarea') {
101
147
  props['innerHTML'] = getStateValue(component, name);
102
- props['oninput'] = e => setStateValue(component, name || e.target.name, e.target.value);
148
+ props['oninput'] = e => {
149
+ const target = safeEventTarget<HTMLTextAreaElement>(e);
150
+ if (target) {
151
+ setStateValue(component, name || target.name, target.value);
152
+ }
153
+ };
103
154
  }
104
155
  } else {
105
156
  app.run('$', { key, tag, props, component });
package/src/router.ts CHANGED
@@ -3,23 +3,41 @@
3
3
  *
4
4
  * This file provides URL routing capabilities:
5
5
  * 1. Hash-based routing (#/path)
6
+ * - Works with SPA mode using hash fragments
7
+ * - Handles hashchange events automatically
8
+ * - No server configuration required
6
9
  * 2. Path-based routing (/path)
10
+ * - Works with browser history API
11
+ * - Requires server configuration for SPA routing
12
+ * - Handles popstate events automatically
7
13
  * 3. Event-based navigation
14
+ * - Routes trigger corresponding component events
15
+ * - Automatic route parameter extraction
16
+ * - Fallback to 404 handling for unknown routes
8
17
  *
9
18
  * Features:
10
- * - Automatic route event firing
11
- * - 404 handling via ROUTER_404_EVENT
12
- * - History API integration
13
- * - Route parameter parsing
19
+ * - Automatic route event firing with ROUTER_EVENT
20
+ * - 404 handling via ROUTER_404_EVENT for unmatched routes
21
+ * - History API integration for seamless navigation
22
+ * - Route parameter parsing and injection into events
23
+ * - URL pattern matching with parameter support
24
+ * - Global and component-level route handling
25
+ *
26
+ * Type Safety Improvements (v3.35.1):
27
+ * - Enhanced route type definitions
28
+ * - Better parameter type inference
29
+ * - Improved URL validation and error handling
14
30
  *
15
31
  * Usage:
16
32
  * ```ts
17
33
  * // Handle routes
18
34
  * app.on('#/home', () => // Show home page);
19
35
  * app.on('#/users/:id', (id) => // Show user profile);
36
+ * app.on('/api/*', (path) => // Handle API routes);
20
37
  *
21
- * // Navigate programmatically
38
+ * // Navigate programmatically
22
39
  * app.run('route', '#/home');
40
+ * route('/users/123'); // Direct routing
23
41
  * ```
24
42
  */
25
43
 
@@ -29,8 +47,9 @@ export type Route = (url: string, ...args: any[]) => any;
29
47
 
30
48
  export const ROUTER_EVENT: string = '//';
31
49
  export const ROUTER_404_EVENT: string = '///';
32
-
33
50
  export const route: Route = (url: string) => {
51
+ if (app['lastUrl'] === url) return; // Prevent duplicate routing
52
+ app['lastUrl'] = url;
34
53
  if (!url) url = '#';
35
54
  if (url.startsWith('#')) {
36
55
  const [name, ...rest] = url.split('/');