native-document 1.0.181 → 1.0.183

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-document",
3
- "version": "1.0.181",
3
+ "version": "1.0.183",
4
4
  "description": "A reactive JavaScript framework that preserves native DOM simplicity without sacrificing modern features",
5
5
  "author": "AfroCodeur <https://github.com/afrocodeur>",
6
6
  "license": "MIT",
@@ -1,24 +1,30 @@
1
1
  import type { ValidChild } from '../../../../types/elements';
2
2
  import type { ObservableItem } from '../../../../types/observable';
3
3
  import type { BaseComponent } from '../../BaseComponent';
4
- import type { MenuItemInterface } from './MenuItem';
4
+ import type { MenuItemInterface, MenuItemOptions } from './MenuItem';
5
5
  import type { MenuGroupInterface } from './MenuGroup';
6
- import type { MenuLinkInterface } from './MenuLink';
6
+ import type { MenuLinkInterface, MenuLinkOptions } from './MenuLink';
7
7
  import type { MenuDividerInterface } from './MenuDivider';
8
8
  import type { GlobalAttributes } from '../../../../types/globals';
9
9
 
10
10
  export type MenuDescription = {
11
- items: ObservableItem<Array<MenuItemInterface | MenuGroupInterface | MenuDividerInterface>>;
12
- render: ((desc: MenuDescription, instance: MenuInterface) => ValidChild) | null;
13
- orientation: 'horizontal' | 'vertical' | 'inline';
14
- closeOnSelect: boolean;
15
- keyboardLoop: boolean;
16
- activeItem: ObservableItem<MenuItemInterface | null>;
17
- active: ((item: MenuItemInterface) => boolean) | null;
18
- compactThreshold: number;
19
- clickFirst: boolean;
20
- props: GlobalAttributes;
21
- };
11
+ items: ObservableItem<Array<MenuItemInterface | MenuGroupInterface | MenuDividerInterface>>;
12
+ render: ((desc: MenuDescription, instance: MenuInterface) => ValidChild) | null;
13
+ orientation: 'horizontal' | 'vertical' | 'inline';
14
+ closeOnSelect: boolean;
15
+ keyboardLoop: boolean;
16
+ activeItem: ObservableItem<MenuItemInterface | null>;
17
+ /** Observable tracking which item currently has an open submenu. */
18
+ menuActive: ObservableItem<MenuItemInterface | null>;
19
+ /** True once the user has clicked to activate hover-open behaviour. */
20
+ isMenuActivated: ObservableItem<boolean>;
21
+ /** Null until ResizeObserver fires; then true when below compactThreshold. */
22
+ compact: ObservableItem<boolean | null>;
23
+ active: ((item: MenuItemInterface) => boolean) | null;
24
+ compactThreshold: number;
25
+ clickFirst: boolean;
26
+ props: GlobalAttributes;
27
+ };
22
28
 
23
29
  export interface MenuInterface extends BaseComponent {
24
30
  dataResolver(resolver: (data: unknown) => unknown): this;
@@ -33,28 +39,40 @@ export interface MenuInterface extends BaseComponent {
33
39
  active(callback: (item: MenuItemInterface) => boolean): this;
34
40
  onItemClick(handler: (item: MenuItemInterface, event: MouseEvent) => void): this;
35
41
  onItemSelect(handler: (item: MenuItemInterface) => void): this;
36
- group(label: ValidChild, icon: ValidChild, builder: (group: MenuGroupInterface) => void, props?: Record<string, unknown>): this;
42
+ group(label: ValidChild, icon: ValidChild, builder: (group: MenuGroupInterface) => void, props?: GlobalAttributes): this;
37
43
  getItem(key: string): MenuItemInterface | undefined;
38
44
  compactThreshold(width?: number): this;
39
45
  clickFirst(mode?: boolean): this;
40
- item(label: ValidChild, ...args: unknown[]): this;
41
- link(label: ValidChild, ...args: unknown[]): this;
46
+
47
+ item(label: ValidChild): this;
48
+ item(label: ValidChild, configBuilder: (item: MenuItemInterface) => void): this;
49
+ item(label: ValidChild, options: MenuItemOptions): this;
50
+ item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void): this;
51
+ item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void, props: GlobalAttributes): this;
52
+
53
+ link(label: ValidChild): this;
54
+ link(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
55
+ link(label: ValidChild, options: MenuLinkOptions): this;
56
+ link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
57
+ link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
58
+
59
+ linkTo(label: ValidChild): this;
60
+ linkTo(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
61
+ linkTo(label: ValidChild, options: MenuLinkOptions): this;
62
+ linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
63
+ linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
64
+
42
65
  separator(): this;
43
66
  divider(): this;
44
67
  add(item: MenuItemInterface | MenuGroupInterface | MenuLinkInterface | MenuDividerInterface): this;
45
68
  getDepth(): number;
46
- getRoot(): unknown;
69
+ getRoot(): MenuInterface;
70
+ setParent(parent: MenuInterface | MenuGroupInterface): this;
47
71
  onClicked(): this;
48
72
  onHovered(): this;
49
- render(template: (description: MenuDescription, instance: MenuInterface) => ValidChild): this;
50
73
  }
51
74
 
52
-
53
- export declare function Menu(props?: Record<string, unknown>): MenuInterface;
75
+ export declare function Menu(props?: GlobalAttributes): MenuInterface;
54
76
  export declare namespace Menu {
55
-
56
-
57
77
  function use(template: (description: MenuDescription, instance: MenuInterface) => ValidChild): void;
58
-
59
-
60
- }
78
+ }
@@ -1,44 +1,68 @@
1
1
  import type { ValidChild } from '../../../../types/elements';
2
2
  import type { ObservableItem } from '../../../../types/observable';
3
3
  import type { BaseComponent } from '../../BaseComponent';
4
- import type { MenuItemInterface } from './MenuItem';
4
+ import type { MenuItemInterface, MenuItemOptions } from './MenuItem';
5
+ import type { MenuLinkInterface, MenuLinkOptions } from './MenuLink';
5
6
  import type { MenuDividerInterface } from './MenuDivider';
6
7
  import type { GlobalAttributes } from '../../../../types/globals';
7
8
 
8
9
  export type MenuGroupDescription = {
9
- icon: ValidChild | null;
10
- label: ValidChild;
11
- data: unknown | null;
12
- items: ObservableItem<Array<MenuItemInterface | MenuDividerInterface>>;
13
- render: ((desc: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild) | null;
14
- collapsable: boolean;
15
- collapsed: boolean | null;
16
- visibility: ObservableItem<boolean>;
17
- collapsableOpenedIcon: ValidChild | null;
18
- collapsableClosedIcon: ValidChild | null;
19
- props: GlobalAttributes;
20
- };
10
+ icon: ValidChild | null;
11
+ label: ValidChild;
12
+ data: unknown | null;
13
+ items: ObservableItem<Array<MenuItemInterface | MenuDividerInterface>>;
14
+ render: ((desc: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild) | null;
15
+ collapsable: boolean;
16
+ /**
17
+ * Null until .collapsable() is called — then an ObservableItem<boolean>.
18
+ * The render uses .toggle() and .transform() on this value; it is never
19
+ * a plain boolean at runtime.
20
+ */
21
+ collapsed: ObservableItem<boolean> | null;
22
+ visibility: ObservableItem<boolean>;
23
+ collapsableOpenedIcon: ValidChild | null;
24
+ collapsableClosedIcon: ValidChild | null;
25
+ props: GlobalAttributes;
26
+ };
21
27
 
22
28
  export interface MenuGroupInterface extends BaseComponent {
23
29
  data(data: unknown): this;
24
30
  icon(icon: ValidChild): this;
25
31
  collapsable(mode?: boolean, openedIcon?: ValidChild, closedIcon?: ValidChild): this;
26
32
  collapsed(mode?: boolean): this;
27
- divider(): this;
28
33
  visibility(mode: boolean | ObservableItem<boolean>): this;
29
- item(label: ValidChild, ...args: unknown[]): this;
30
- link(label: ValidChild, ...args: unknown[]): this;
34
+
35
+ item(label: ValidChild): this;
36
+ item(label: ValidChild, configBuilder: (item: MenuItemInterface) => void): this;
37
+ item(label: ValidChild, options: MenuItemOptions): this;
38
+ item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void): this;
39
+ item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void, props: GlobalAttributes): this;
40
+
41
+ link(label: ValidChild): this;
42
+ link(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
43
+ link(label: ValidChild, options: MenuLinkOptions): this;
44
+ link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
45
+ link(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
46
+
47
+ linkTo(label: ValidChild): this;
48
+ linkTo(label: ValidChild, configBuilder: (item: MenuLinkInterface) => void): this;
49
+ linkTo(label: ValidChild, options: MenuLinkOptions): this;
50
+ linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void): this;
51
+ linkTo(label: ValidChild, options: MenuLinkOptions, configBuilder: (item: MenuLinkInterface) => void, props: GlobalAttributes): this;
52
+
53
+ group(label: ValidChild, builder: (group: MenuGroupInterface) => void): this;
31
54
  separator(): this;
32
- add(item: MenuItemInterface | MenuDividerInterface): this;
55
+ divider(): this;
56
+ add(item: MenuItemInterface | MenuGroupInterface | MenuLinkInterface | MenuDividerInterface): this;
57
+ getDepth(): number;
58
+ getRoot(): unknown;
59
+ setParent(parent: unknown): this;
60
+ onClicked(): this;
61
+ onHovered(): this;
33
62
  render(template: (description: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild): this;
34
63
  }
35
64
 
36
-
37
- export declare function MenuGroup(label: ValidChild, props?: Record<string, unknown>): MenuGroupInterface;
65
+ export declare function MenuGroup(label: ValidChild, props?: GlobalAttributes): MenuGroupInterface;
38
66
  export declare namespace MenuGroup {
39
-
40
-
41
67
  function use(template: (description: MenuGroupDescription, instance: MenuGroupInterface) => ValidChild): void;
42
-
43
-
44
- }
68
+ }
@@ -3,21 +3,30 @@ import type { ObservableItem } from '../../../../types/observable';
3
3
  import type { BaseComponent } from '../../BaseComponent';
4
4
  import type { GlobalAttributes } from '../../../../types/globals';
5
5
 
6
+ export type MenuItemOptions = {
7
+ icon?: ValidChild;
8
+ action?: string | Record<string, unknown> | ((data?: unknown) => void);
9
+ shortcut?: ValidChild;
10
+ disabled?: boolean | ObservableItem<boolean>;
11
+ };
12
+
6
13
  export type MenuItemDescription = {
7
- key: string | null;
8
- action: string | Record<string, unknown> | null;
9
- label: ValidChild | null;
10
- icon: ValidChild | null;
11
- shortcut: ValidChild | null;
12
- disabled: ObservableItem<boolean> | boolean | null;
13
- selected: ObservableItem<boolean> | boolean | null;
14
- value: unknown;
15
- data: unknown | null;
16
- render: ((desc: MenuItemDescription, instance: MenuItemInterface) => ValidChild) | null;
17
- trailing: ValidChild | null;
18
- visibility: ObservableItem<boolean> | null;
19
- props: GlobalAttributes;
20
- };
14
+ key: string | null;
15
+ action: string | Record<string, unknown> | ((data?: unknown) => void) | null;
16
+ label: ValidChild | null;
17
+ icon: ValidChild | null;
18
+ shortcut: ValidChild | null;
19
+ disabled: ObservableItem<boolean> | boolean | null;
20
+ selected: ObservableItem<boolean> | boolean | null;
21
+ value: unknown;
22
+ data: unknown | null;
23
+ render: ((desc: MenuItemDescription, instance: MenuItemInterface) => ValidChild) | null;
24
+ trailing: ValidChild | null;
25
+ visibility: ObservableItem<boolean> | null;
26
+ dataResolver: (() => unknown) | null;
27
+ interaction: 'click' | 'hover' | null;
28
+ props: GlobalAttributes;
29
+ };
21
30
 
22
31
  export interface MenuItemInterface extends BaseComponent {
23
32
  label(label: ValidChild): this;
@@ -27,20 +36,43 @@ export interface MenuItemInterface extends BaseComponent {
27
36
  disabled(disabled?: boolean | ObservableItem<boolean>): this;
28
37
  selected(selected?: boolean | ObservableItem<boolean>): this;
29
38
  value(value: unknown): this;
30
- action(action: string | Record<string, unknown>): this;
39
+ action(action: string | Record<string, unknown> | ((data?: unknown) => void)): this;
31
40
  data(data: unknown): this;
32
- divider(): this;
33
41
  key(key: string): this;
34
42
  visibility(mode: boolean | ObservableItem<boolean>): this;
35
- render(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): this;
36
- }
37
43
 
44
+ item(label: ValidChild): this;
45
+ item(label: ValidChild, configBuilder: (item: MenuItemInterface) => void): this;
46
+ item(label: ValidChild, options: MenuItemOptions): this;
47
+ item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void): this;
48
+ item(label: ValidChild, options: MenuItemOptions, configBuilder: (item: MenuItemInterface) => void, props: GlobalAttributes): this;
38
49
 
39
- export declare function MenuItem(props?: Record<string, unknown>): MenuItemInterface;
40
- export declare namespace MenuItem {
41
-
42
-
43
- function use(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): void;
50
+ link(label: ValidChild): this;
51
+ link(label: ValidChild, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
52
+ link(label: ValidChild, options: import('./MenuLink').MenuLinkOptions): this;
53
+ link(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
54
+ link(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void, props: GlobalAttributes): this;
44
55
 
56
+ linkTo(label: ValidChild): this;
57
+ linkTo(label: ValidChild, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
58
+ linkTo(label: ValidChild, options: import('./MenuLink').MenuLinkOptions): this;
59
+ linkTo(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void): this;
60
+ linkTo(label: ValidChild, options: import('./MenuLink').MenuLinkOptions, configBuilder: (item: import('./MenuLink').MenuLinkInterface) => void, props: GlobalAttributes): this;
45
61
 
62
+ group(label: ValidChild, builder: (group: unknown) => void): this;
63
+ separator(): this;
64
+ divider(): this;
65
+ add(item: unknown): this;
66
+ getDepth(): number;
67
+ getRoot(): unknown;
68
+ setParent(parent: unknown): this;
69
+ onClicked(): this;
70
+ onHovered(): this;
71
+ emit(eventName: string, ...args: unknown[]): void;
72
+ render(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): this;
46
73
  }
74
+
75
+ export declare function MenuItem(props?: GlobalAttributes): MenuItemInterface;
76
+ export declare namespace MenuItem {
77
+ function use(template: (description: MenuItemDescription, instance: MenuItemInterface) => ValidChild): void;
78
+ }
@@ -1,16 +1,26 @@
1
1
  import type { ValidChild } from '../../../../types/elements';
2
+ import type { ObservableItem } from '../../../../types/observable';
2
3
  import type { MenuItemInterface, MenuItemDescription } from './MenuItem';
4
+ import type { GlobalAttributes } from '../../../../types/globals';
5
+
6
+ export type MenuLinkOptions = {
7
+ icon?: ValidChild;
8
+ /** URL string or named route name. Used as-is for link(), wrapped in { isRoute: true } for linkTo(). */
9
+ href?: string;
10
+ target?: '_blank' | '_self' | '_parent' | '_top' | string;
11
+ shortcut?: ValidChild;
12
+ disabled?: boolean | ObservableItem<boolean>;
13
+ };
14
+
15
+ export type MenuLinkDescription = MenuItemDescription & {
16
+ target: '_blank' | '_self' | '_parent' | '_top' | string | null;
17
+ };
3
18
 
4
19
  export interface MenuLinkInterface extends MenuItemInterface {
5
20
  target(target: '_blank' | '_self' | '_parent' | '_top' | string): this;
6
21
  }
7
22
 
8
-
9
- export declare function MenuLink(props?: Record<string, unknown>): MenuLinkInterface;
23
+ export declare function MenuLink(props?: GlobalAttributes): MenuLinkInterface;
10
24
  export declare namespace MenuLink {
11
-
12
-
13
- function use(template: (description: MenuItemDescription, instance: MenuLinkInterface) => ValidChild): void;
14
-
15
-
16
- }
25
+ function use(template: (description: MenuLinkDescription, instance: MenuLinkInterface) => ValidChild): void;
26
+ }
@@ -592,6 +592,7 @@ ObservableItem.prototype.valueOf = function() {
592
592
  */
593
593
  ObservableItem.prototype.persist = function(key, options = {}) {
594
594
  let value = $getFromStorage(key, this.$currentValue);
595
+ this.$persistKey = key;
595
596
  if(options.get) {
596
597
  value = options.get(value);
597
598
  }
@@ -608,6 +609,10 @@ ObservableItem.prototype.persist = function(key, options = {}) {
608
609
  return this;
609
610
  };
610
611
 
612
+ ObservableItem.prototype.persistKey = function() {
613
+ return this.$persistKey;
614
+ };
615
+
611
616
  /**
612
617
  * Creates a new ObservableItem with a deep clone of the current value.
613
618
  * For objects implementing a .clone() method, delegates to that method.
@@ -35,7 +35,7 @@ export default function Anchor(name, isUniqueChild = false) {
35
35
  ? () => true: (parent) => (parent.firstChild === anchorStart && parent.lastChild === anchorEnd);
36
36
 
37
37
  const insertBefore = (parent, child, target) => {
38
- const childElement = child.__$isNativeNode ? child : ElementCreator.getChild(child);
38
+ const childElement = ElementCreator.getChild(child);
39
39
  insertBeforeRaw(parent, childElement, target);
40
40
  };
41
41
 
@@ -10,6 +10,8 @@ export default function ForEachArrayCache(isIndexesRequired) {
10
10
  return Array.from(this.$nodes.keys());
11
11
  };
12
12
 
13
+ this.entries = this.$nodes.entries.bind(this.$nodes);
14
+
13
15
  if(isIndexesRequired) {
14
16
  this.delete = function(item) {
15
17
  this.$nodes.get(item)?.nd.destroy();
@@ -35,7 +35,7 @@ export const bindClassAttribute = (element, data) => {
35
35
  }
36
36
 
37
37
  if (value.$hydrate) {
38
- value.$hydrate(element, className);
38
+ value.$hydrate(element, className, 'class');
39
39
  return;
40
40
  }
41
41
 
@@ -85,6 +85,10 @@ export const bindStyleAttribute = (element, data) => {
85
85
  continue;
86
86
  }
87
87
 
88
+ if(value.$hydrate) {
89
+ value.$hydrate(element, styleName, 'style');
90
+ }
91
+
88
92
  element.style[styleName] = value;
89
93
  }
90
94
  };
@@ -167,6 +171,10 @@ const AttributesWrapper = (element, attributes = {}) => {
167
171
  element.setAttribute(attributeName, value);
168
172
  continue;
169
173
  }
174
+ if(value.$hydrate) {
175
+ value.$hydrate(element, attributeName, 'attribute');
176
+ continue;
177
+ }
170
178
  // const attributeName = originalAttributeName.toLowerCase();
171
179
  if(value.__$Observable) {
172
180
  if(BOOLEAN_ATTRIBUTES.has(attributeName)) {
@@ -190,9 +198,10 @@ const AttributesWrapper = (element, attributes = {}) => {
190
198
  bindBooleanAttribute(element, attributeName, value);
191
199
  continue;
192
200
  }
193
- if(value.__$isTemplateBinding) {
194
- value.$hydrate(element, attributeName);
195
- }
201
+
202
+ // if(value.__$isTemplateBinding) {
203
+ // // value.$hydrate(element, attributeName, 'attribute');
204
+ // }
196
205
 
197
206
  element.setAttribute(attributeName, value);
198
207
  }
@@ -41,7 +41,7 @@ export const ElementCreator = {
41
41
  */
42
42
  createHydratableNode: (parent, item) => {
43
43
  const text = ElementCreator.createTextNode();
44
- item.$hydrate(text);
44
+ item.$hydrate(text, null, 'value');
45
45
  return text;
46
46
  },
47
47
 
@@ -28,6 +28,10 @@ EVENTS.forEach(eventSourceName => {
28
28
  const inlineHandler = 'on'+eventName;
29
29
  const fnName = 'on'+eventSourceName;
30
30
  NDElement.prototype[fnName] = function(callback = null, options = null) {
31
+ if(callback.__$isTemplateBinding) {
32
+ this.attach(fnName, callback);
33
+ return this;
34
+ }
31
35
  if(!this.$element[inlineHandler] && !options) {
32
36
  this.$element[inlineHandler] = callback;
33
37
  return this;
@@ -37,37 +41,58 @@ EVENTS.forEach(eventSourceName => {
37
41
  };
38
42
  if(!HTMLElement.prototype[fnName]) {
39
43
  HTMLElement.prototype[fnName] = function(callback, options) {
44
+ if(callback.__$isTemplateBinding) {
45
+ this.nd.attach(fnName, callback);
46
+ return this;
47
+ }
40
48
  if(!this[inlineHandler] && !options) {
41
49
  this[inlineHandler] = callback;
42
50
  return this;
43
51
  }
44
52
  this.addEventListener(eventName, callback, options);
45
53
  return this;
46
- }
54
+ };
47
55
  }
48
56
  });
49
57
 
50
58
  EVENTS_WITH_STOP.forEach(eventSourceName => {
51
59
  const eventName = eventSourceName.toLowerCase();
52
60
  const stopFnName = 'onStop'+eventSourceName;
53
- const preventStopFnName = 'onPreventStop'+eventSourceName;
54
61
 
55
62
  NDElement.prototype[stopFnName] = function(callback = null, options = null) {
63
+ if(callback.__$isTemplateBinding) {
64
+ this.attach(stopFnName, callback);
65
+ return this;
66
+ }
56
67
  _stop(this.$element, eventName, callback, options);
57
68
  return this;
58
69
  };
59
- NDElement.prototype[preventStopFnName] = function(callback = null, options = null) {
60
- _preventStop(this.$element, eventName, callback, options);
61
- return this;
62
- };
63
- if(HTMLElement.prototype[stopFnName]) {
70
+ if(!HTMLElement.prototype[stopFnName]) {
64
71
  HTMLElement.prototype[stopFnName] = function(callback = null, options = null) {
72
+ if(callback.__$isTemplateBinding) {
73
+ this.nd.attach(stopFnName, callback);
74
+ return this;
75
+ }
65
76
  _stop(this, eventName, callback, options);
66
77
  return this;
67
78
  };
68
79
  }
80
+ // ------
81
+ const preventStopFnName = 'onPreventStop'+eventSourceName;
82
+ NDElement.prototype[preventStopFnName] = function(callback = null, options = null) {
83
+ if(callback.__$isTemplateBinding) {
84
+ this.attach(preventStopFnName, callback);
85
+ return this;
86
+ }
87
+ _preventStop(this.$element, eventName, callback, options);
88
+ return this;
89
+ };
69
90
  if(!HTMLElement.prototype[preventStopFnName]) {
70
91
  HTMLElement.prototype[preventStopFnName] = function(callback = null, options = null) {
92
+ if(callback.__$isTemplateBinding) {
93
+ this.nd.attach(preventStopFnName, callback);
94
+ return this;
95
+ }
71
96
  _preventStop(this, eventName, callback, options);
72
97
  return this;
73
98
  };
@@ -78,11 +103,19 @@ EVENTS_WITH_PREVENT.forEach(eventSourceName => {
78
103
  const eventName = eventSourceName.toLowerCase();
79
104
  const preventFnName = 'onPrevent'+eventSourceName;
80
105
  NDElement.prototype[preventFnName] = function(callback = null, options = null) {
106
+ if(callback.__$isTemplateBinding) {
107
+ this.attach(preventFnName, callback);
108
+ return this;
109
+ }
81
110
  _prevent(this.$element, eventName, callback, options);
82
111
  return this;
83
112
  };
84
- if(HTMLElement.prototype[preventFnName]) {
113
+ if(!HTMLElement.prototype[preventFnName]) {
85
114
  HTMLElement.prototype[preventFnName] = function(callback = null, options = null) {
115
+ if(callback.__$isTemplateBinding) {
116
+ this.nd.attach(preventFnName, callback);
117
+ return this;
118
+ }
86
119
  _prevent(this, eventName, callback, options);
87
120
  return this;
88
121
  };
@@ -1,6 +1,7 @@
1
1
  import {ElementCreator} from '../ElementCreator';
2
2
  import {createTextNode} from '../HtmlElementWrapper';
3
3
  import {NDElement} from '../NDElement';
4
+ import {call} from '@babel/traverse/lib/path/context';
4
5
 
5
6
  /**
6
7
  * Stores deferred attribute, class, style, and event bindings for a cloneable element.
@@ -49,11 +50,17 @@ NodeCloner.prototype.__$isNodeCloner = true;
49
50
 
50
51
  const buildProperties = (cache, properties, data) => {
51
52
  for(const key in properties) {
52
- cache[key] = properties[key].apply(null, data);
53
+ const value = properties[key];
54
+ cache[key] = getPropertyValue(value, data);
53
55
  }
54
56
  return cache;
55
57
  };
56
58
 
59
+ const getPropertyValue = (callbackOrProperty, data) => {
60
+ const value = (typeof callbackOrProperty ==='string') ? data[0][callbackOrProperty] : callbackOrProperty;
61
+ return (typeof value === 'function') ? value.apply(this, data) : value;
62
+ };
63
+
57
64
  /**
58
65
  * Pre-compiles all registered bindings into a sequence of optimised steps.
59
66
  * Called once before the first clone operation. Subsequent calls are no-ops.
@@ -71,13 +78,14 @@ NodeCloner.prototype.resolve = function() {
71
78
  const methodName = methods[0];
72
79
  const callback = this.$ndMethods[methodName];
73
80
  steps.push((clonedNode, data) => {
74
- clonedNode.nd[methodName](callback.bind(clonedNode, ...data));
81
+ clonedNode.nd[methodName](callback.length === 0 ? callback : callback.bind(clonedNode, ...data));
75
82
  });
76
83
  } else {
77
84
  steps.push((clonedNode, data) => {
78
85
  const nd = clonedNode.nd;
79
86
  for(const methodName in this.$ndMethods) {
80
- nd[methodName](this.$ndMethods[methodName].bind(clonedNode, ...data));
87
+ const callback = this.$ndMethods[methodName];
88
+ nd[methodName](callback.length === 0 ? callback : this.$ndMethods[methodName].bind(clonedNode, ...data));
81
89
  }
82
90
  });
83
91
  }
@@ -90,7 +98,7 @@ NodeCloner.prototype.resolve = function() {
90
98
  const key = keys[0];
91
99
  const callback = this.$classes[key];
92
100
  steps.push((clonedNode, data) => {
93
- cache[key] = callback.apply(null, data);
101
+ cache[key] = getPropertyValue(callback, data);
94
102
  ElementCreator.processClassAttribute(clonedNode, cache);
95
103
  });
96
104
  } else {
@@ -107,7 +115,7 @@ NodeCloner.prototype.resolve = function() {
107
115
  const key = keys[0];
108
116
  const callback = this.$styles[key];
109
117
  steps.push((clonedNode, data) => {
110
- cache[key] = callback.apply(null, data);
118
+ cache[key] = getPropertyValue(callback, data);
111
119
  ElementCreator.processStyleAttribute(clonedNode, cache);
112
120
  });
113
121
  } else {
@@ -124,7 +132,7 @@ NodeCloner.prototype.resolve = function() {
124
132
  const key = keys[0];
125
133
  const callback = this.$attrs[key];
126
134
  steps.push((clonedNode, data) => {
127
- cache[key] = callback.apply(null, data);
135
+ cache[key] = getPropertyValue(callback, data);
128
136
  ElementCreator.processAttributes(clonedNode, cache);
129
137
  });
130
138
  } else {
@@ -207,7 +215,8 @@ NodeCloner.prototype.attr = function(attrName, value) {
207
215
  this.$styles[value.property] = value.value;
208
216
  return this;
209
217
  }
218
+
210
219
  this.$attrs = this.$attrs || {};
211
- this.$attrs[attrName] = value.value;
220
+ this.$attrs[value.property] = value.value;
212
221
  return this;
213
222
  };
@@ -1,6 +1,8 @@
1
1
  import TemplateBinding from '../TemplateBinding';
2
2
  import { $hydrateFn} from './utils';
3
3
  import NodeCloner from './NodeCloner';
4
+ import {ElementCreator} from '../ElementCreator';
5
+ import NativeDocumentError from '../../errors/NativeDocumentError';
4
6
 
5
7
  /**
6
8
  * Creates a high-performance template cloner for repeated rendering of the same structure.
@@ -22,7 +24,10 @@ import NodeCloner from './NodeCloner';
22
24
  export function TemplateCloner($fn) {
23
25
  let $node = null;
24
26
 
27
+ this.$scopeDataBuilder = null;
28
+
25
29
  const assignClonerToNode = ($node) => {
30
+ $node = ElementCreator.getChild($node);
26
31
  const childNodes = $node.childNodes;
27
32
  let containDynamicNode = $node.nodeCloner?.shouldBeHydrate();
28
33
  const childNodesLength = childNodes.length;
@@ -73,20 +78,45 @@ export function TemplateCloner($fn) {
73
78
  */
74
79
  this.clone = (data) => {
75
80
  const binder = createTemplateCloner(this);
76
- $node = $fn(binder);
81
+ const helpers = {
82
+ useCallback: binder.attach.bind(binder),
83
+ useCallbacks: (callbacks) => {
84
+ for(const key in callbacks) {
85
+ callbacks[key] = binder.attach(callbacks[key]);
86
+ }
87
+ return callbacks;
88
+ },
89
+ useData: (callback) => {
90
+ this.$scopeDataBuilder = callback;
91
+ },
92
+ use: binder.freeProps.bind(binder),
93
+ };
94
+ $node = ElementCreator.getChild($fn(binder, helpers));
77
95
  if(!$node.nodeCloner) {
78
96
  $node.nodeCloner = new NodeCloner($node);
79
97
  }
80
98
  assignClonerToNode($node);
99
+ if(this.$scopeDataBuilder) {
100
+ this.clone = (data) => {
101
+ const scopeData = this.scopeData();
102
+ return $node.dynamicCloneNode([...data, scopeData]);
103
+ };
104
+ return $node.dynamicCloneNode([...data, this.scopeData()]);
105
+ }
106
+
81
107
  this.clone = $node.dynamicCloneNode;
82
108
  return $node.dynamicCloneNode(data);
83
109
  };
84
110
 
111
+ this.scopeData = () => {
112
+ return this.$scopeDataBuilder?.() || {};
113
+ };
114
+
85
115
 
86
116
  const createBinding = (hydrateFunction, targetType) => {
87
117
  return new TemplateBinding((element, property) => {
88
118
  $hydrateFn(hydrateFunction, targetType, element, property);
89
- });
119
+ }, this);
90
120
  };
91
121
 
92
122
  /**
@@ -113,6 +143,18 @@ export function TemplateCloner($fn) {
113
143
  return this.value(propertyName);
114
144
  };
115
145
 
146
+ this.data = (property) => {
147
+ return this.freeProps((...Args) => {
148
+ const data = Args.at(-1);
149
+ if(process.env.NODE_ENV === 'development') {
150
+ if(!data[property]) {
151
+ throw new NativeDocumentError(property + ' is not defined in useData');
152
+ }
153
+ }
154
+ return data[property];
155
+ });
156
+ };
157
+
116
158
  /**
117
159
  * Creates a text/value binding — the result is set as text content or input value.
118
160
  * Alias: .text()
@@ -142,6 +184,16 @@ export function TemplateCloner($fn) {
142
184
  return createBinding(fn, 'attributes');
143
185
  };
144
186
 
187
+ this.freeProps = (callbackOrProperty) => {
188
+ return new TemplateBinding((element, property, type) => {
189
+ let targetType = type;
190
+ if(type === 'attribute') {
191
+ targetType = 'attributes';
192
+ }
193
+ $hydrateFn(callbackOrProperty, targetType, element, property);
194
+ });
195
+ };
196
+
145
197
  /**
146
198
  * Creates an event binding — fn(data) returns the event handler to attach.
147
199
  *
@@ -155,6 +207,13 @@ export function TemplateCloner($fn) {
155
207
  this.callback = this.attach;
156
208
  }
157
209
 
210
+ const createDataProxy = ($binder) => {
211
+ return new Proxy($binder, {
212
+ get(target, key) {
213
+ return target.data(key);
214
+ },
215
+ });
216
+ };
158
217
 
159
218
  const createTemplateCloner = ($binder) => {
160
219
  return new Proxy($binder, {
@@ -162,18 +221,98 @@ const createTemplateCloner = ($binder) => {
162
221
  if(prop in target) {
163
222
  return target[prop];
164
223
  }
165
- if (typeof prop === 'symbol') return target[prop];
166
- return target.value(prop);
224
+ if (typeof prop === 'symbol') {
225
+ return target[prop];
226
+ }
227
+ if(prop === '$data') {
228
+ return createDataProxy($binder);
229
+ }
230
+ return target.freeProps(prop);
167
231
  },
168
232
  });
169
233
  };
170
234
 
235
+ /**
236
+ * Creates a high-performance template factory that compiles once and clones efficiently.
237
+ * The template function is called only on the first render to build and optimise the DOM
238
+ * structure. Subsequent calls clone the compiled result and hydrate it with new data.
239
+ *
240
+ * Execution happens in three distinct phases:
241
+ *
242
+ * **Phase 1 — Compilation** (runs once, on first call)
243
+ * The template function receives `$scope` — a binding proxy. Accessing `$scope.name`,
244
+ * `$scope.color` etc. declares bindings on the template node without reading values yet.
245
+ * `$scope.$data` provides direct bindings to local state properties declared via `useData`.
246
+ * The DOM structure is built and optimised during this phase.
247
+ *
248
+ * **Phase 2 — Hydration** (runs once per clone)
249
+ * `useData(() => ({ ... }))` creates isolated local state for each cloned instance.
250
+ * The factory is called once per item — every clone gets its own independent state object.
251
+ *
252
+ * **Phase 3 — Runtime** (runs on each interaction or reactive update)
253
+ * `useCallback`, `useCallbacks` and `use` callbacks receive the same arguments as the
254
+ * template function, plus the local state object as the last argument:
255
+ * - `item` — the actual item data at the time of execution
256
+ * - `index` — position in the list (when used with ForEachArray)
257
+ * - `data` — the local state for this specific clone (from useData)
258
+ *
259
+ * @template T
260
+ * @param {(
261
+ * $scope: T & { $data: Record<string, any> },
262
+ * helpers: {
263
+ * useData: (factory: () => Record<string, any>) => void,
264
+ * useCallback: (fn: (item: T, index: number, data: Record<string, any>) => EventListener) => TemplateBinding,
265
+ * useCallbacks: (callbacks: Record<string, (item: T, index: number, data: Record<string, any>) => EventListener>) => Record<string, TemplateBinding>,
266
+ * use: (fn: (item: T, index: number, data: Record<string, any>) => any) => TemplateBinding,
267
+ * }
268
+ * ) => HTMLElement} fn - Template builder function called once during compilation
269
+ * @returns {(item: T, index?: number) => HTMLElement} Factory function — pass directly to ForEachArray or call manually
270
+ *
271
+ * @example
272
+ * const UserRow = useCache(($scope, { useData, useCallback, useCallbacks, use }) => {
273
+ *
274
+ * // Phase 1 — declare bindings (runs once)
275
+ * const color = $scope.color;
276
+ * // Bind directly from local state via $scope.$data
277
+ * const selectedClass = $scope.$data.selected;
278
+ *
279
+ * // Phase 2 — local state per clone (runs once per item)
280
+ * useData(() => ({
281
+ * selected: $(false),
282
+ * }));
283
+ *
284
+ * // Phase 3 — runtime callbacks (run on interaction)
285
+ *
286
+ * // Single callback
287
+ * const toggle = useCallback((item, index, data) => {
288
+ * data.selected.toggle();
289
+ * });
290
+ *
291
+ * // Multiple callbacks at once
292
+ * const { select, deselect } = useCallbacks({
293
+ * select: (item, index, data) => data.selected.set(true),
294
+ * deselect: (item, index, data) => data.selected.set(false),
295
+ * });
296
+ *
297
+ * const isSelected = use((item, index, data) => {
298
+ * return data.selected.val() ? 'is-selected' : '';
299
+ * });
300
+ *
301
+ * return Div({ class: isSelected, style: { color } }, Strong($scope.name))
302
+ * .onClick(toggle);
303
+ * });
304
+ *
305
+ * // Pass directly to ForEachArray
306
+ * ForEachArray($users, UserRow)
307
+ *
308
+ * // Or call manually
309
+ * UserRow(item, index)
310
+ */
171
311
  export function useCache(fn) {
172
312
  let $cache = null;
173
313
 
174
314
  let wrapper = (args) => {
175
315
  $cache = new TemplateCloner(fn);
176
-
177
316
  const node = $cache.clone(args);
178
317
  wrapper = $cache.clone;
179
318
  return node;
@@ -32,6 +32,7 @@ export default function NativeFetch($baseUrl) {
32
32
  ...(options.headers || {}),
33
33
  },
34
34
  };
35
+ let parseToString = false;
35
36
  if(params) {
36
37
  if(params instanceof FormData) {
37
38
  configs.body = params;
@@ -39,7 +40,8 @@ export default function NativeFetch($baseUrl) {
39
40
  else {
40
41
  if(method !== 'GET') {
41
42
  configs.headers['Content-Type'] = 'application/json';
42
- configs.body = JSON.stringify(params);
43
+ configs.body = params;
44
+ parseToString = true;
43
45
  } else {
44
46
  const queryString = new URLSearchParams(params).toString();
45
47
  if (queryString) {
@@ -52,6 +54,9 @@ export default function NativeFetch($baseUrl) {
52
54
  for(const interceptor of $interceptors.request) {
53
55
  configs = (await interceptor(configs, endpoint)) || configs;
54
56
  }
57
+ if(parseToString) {
58
+ configs.body = JSON.stringify(configs.body);
59
+ }
55
60
 
56
61
  let response = await fetch(endpoint, configs);
57
62
 
@@ -87,4 +92,28 @@ export default function NativeFetch($baseUrl) {
87
92
  this.get = function (endpoint, params = {}, options = {}) {
88
93
  return this.fetch('GET', endpoint, params, options);
89
94
  };
95
+ };
96
+
97
+ export const resolveData = (data) => {
98
+ if(data.__$Observable) {
99
+ return data.resolve();
100
+ }
101
+ for(const key in data) {
102
+ const value = data[key];
103
+ if(value.__$Observable) {
104
+ data[key] = value.resolve();
105
+ continue;
106
+ }
107
+ if(typeof data[key] === 'object') {
108
+ data[key] = resolveData(data[key]);
109
+ }
110
+ }
111
+ return data;
112
+ };
113
+
114
+ export const resolveObservableInterceptor = (configs) => {
115
+ if(configs.body && !(configs.body instanceof FormData)) {
116
+ configs.body = resolveData(configs.body);
117
+ }
118
+ return configs;
90
119
  };
@@ -57,7 +57,14 @@ function buildContent($desc) {
57
57
  }
58
58
 
59
59
  if($desc.initials || $desc.name) {
60
- return Span({class: 'avatar-initials'}, $desc.initials || $desc.name.split(' ').map(n => n[0]).join(''));
60
+ let initials = $desc.initials;
61
+ if(!initials) {
62
+ initials = $desc.name;
63
+ if($desc.anme.__$Observable) {
64
+ initials = $desc.name.format((name) => name.split(' ').map(n => n[0]).join(''));
65
+ }
66
+ }
67
+ return Span({class: 'avatar-initials'}, initials);
61
68
  }
62
69
 
63
70
  if($desc.icon) {
@@ -120,7 +120,12 @@ const buildSelectedLabel = ($desc) => {
120
120
  return $desc.selectedLabelRender($desc);
121
121
  }
122
122
 
123
- return $desc.value.transform((val) => {
123
+ const dependencies = [$desc.value];
124
+ if(!Array.isArray($desc.options)) {
125
+ dependencies.push($desc.options);
126
+ }
127
+
128
+ return $.computed((val) => {
124
129
  if(!val || (Array.isArray(val) && val.length === 0)) {
125
130
  return $desc.placeholder || 'Select...';
126
131
  }
@@ -156,7 +161,7 @@ const buildSelectedLabel = ($desc) => {
156
161
  }
157
162
 
158
163
  return labels.join(', ');
159
- });
164
+ }, dependencies);
160
165
  };
161
166
 
162
167
  const buildNativeSelect = ($desc) => {
package/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
 
2
2
  export * from './types/native-fetch';
3
3
  export * from './types/service';
4
- export * as filters from './types/filters/index';
4
+ export * as filters from './types/filters/index';
5
+ export * from './types/localStorage'
package/utils.js CHANGED
@@ -1,7 +1,8 @@
1
- import NativeFetch from './src/fetch/NativeFetch';
1
+ import NativeFetch, { resolveObservableInterceptor } from './src/fetch/NativeFetch';
2
2
  import * as Cache from './src/core/utils/cache';
3
3
  import * as filters from './src/core/utils/filters/index';
4
4
  import {classPropertyAccumulator, cssPropertyAccumulator} from './src/core/utils/property-accumulator';
5
+ import {LocalStorage} from './src/core/utils/localstorage';
5
6
 
6
7
  const Service = Cache;
7
8
 
@@ -11,5 +12,7 @@ export {
11
12
  Service,
12
13
  filters,
13
14
  classPropertyAccumulator,
14
- cssPropertyAccumulator
15
+ cssPropertyAccumulator,
16
+ LocalStorage,
17
+ resolveObservableInterceptor,
15
18
  };