native-document 1.0.17-1.8 → 1.0.17-2.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.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.0.172] - 2026-06-15
15
+
16
+ ### Added
17
+
18
+ - `Text(string)` — shorthand for `string.toNdChildren()`, parses Observable placeholders in template strings
19
+ - `String.prototype.toNdChildren()` — splits a string containing `{{obs:N}}` placeholders into a mixed array of strings and resolved Observables
20
+ - `ObservableItem.prototype.toString()` — auto-registers the Observable in MemoryManager and returns `{{obs:id}}`, enabling reactive interpolation via standard template literals
21
+ - `NDElement.prototype.className(classes)` — adds static classes, bypasses AttributesWrapper
22
+ - `NDElement.prototype.class(className, value)` — adds a reactive class binding, bypasses AttributesWrapper
23
+ - `__$isNativeNode` flag on DOM prototypes (`Element`, `Text`, `Comment`, `Document`, `DocumentFragment`) — faster alternative to `instanceof Node`
24
+ - `__$isValidNdChild` flag on all valid NdChild prototypes — single property check replaces multiple type guards
25
+ - `defineToNdElement`, `defineValidNdChild`, `defineNativeNode` — non-enumerable prototype helpers in `nd-element-extensions`
26
+ - camelCase variants added to `BOOLEAN_ATTRIBUTES` — removes `toLowerCase()` call on each attribute in the hot path
27
+
28
+ ### Changed
29
+
30
+ - `ElementCreator.processChildren` and `getChild` — refactored for better performance
31
+ - `nd-element-extensions` — all prototype extensions converted to non-enumerable `defineProperty`
32
+ - `AttributesWrapper` — `options` defaults to `null` instead of `{}`, fixes truthy check issue
33
+ - NDElement events — inline handler (`on*`) used when no existing handler and no options; falls back to `addEventListener` for subsequent handlers or when options are provided
34
+ - `_prevent`, `_stop`, `_preventStop` — updated with same inline handler strategy
35
+
36
+ ---
37
+
14
38
  ## [1.0.170] - 2026-06-10
15
39
 
16
40
  ### Added
@@ -216,7 +240,8 @@ Entries use the following categories:
216
240
 
217
241
  ---
218
242
 
219
- [Unreleased]: https://github.com/afrocodeur/native-document/compare/v1.0.170...HEAD
243
+ [Unreleased]: https://github.com/afrocodeur/native-document/compare/v1.0.172...HEAD
244
+ [1.0.172]: https://github.com/afrocodeur/native-document/compare/v1.0.170...v1.0.172
220
245
  [1.0.170]: https://github.com/afrocodeur/native-document/compare/v1.0.169...v1.0.170
221
246
  [1.0.169]: https://github.com/afrocodeur/native-document/compare/v1.0.167...v1.0.169
222
247
  [1.0.167]: https://github.com/afrocodeur/native-document/compare/v1.0.166...v1.0.167
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-document",
3
- "version": "1.0.171.8",
3
+ "version": "1.0.172.1",
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",
@@ -18,10 +18,10 @@ import {$getFromStorage, $saveToStorage} from '../utils/localstorage';
18
18
  * @param {boolean} [configs.propagation] - Controls whether changes propagate to parent observables
19
19
  * @example
20
20
  * const count = new ObservableItem(0);
21
- * const name = new ObservableItem('John', { reset: true });
21
+ * const name = new ObservableItem('John', { reset: true });
22
22
  */
23
23
  export default function ObservableItem(value, configs = null) {
24
- value = Validator.isObservable(value) ? value.val() : value;
24
+ value = value?.__$Observable ? value.val() : value;
25
25
 
26
26
  this.$previousValue = null;
27
27
  this.$currentValue = value;
@@ -29,13 +29,13 @@ export default function ObservableItem(value, configs = null) {
29
29
  this.$isCleanedUp = false;
30
30
  }
31
31
 
32
- this.$firstListener = null;
33
- this.$listeners = null;
34
- this.$watchers = null;
35
-
36
- this.$memoryId = null;
32
+ // this.$firstListener = null;
33
+ // this.$listeners = null;
34
+ // this.$watchers = null;
35
+ //
36
+ // this.$id = null;
37
37
 
38
- if(configs) {
38
+ if(configs !== null) {
39
39
  this.configs = configs;
40
40
  if(configs.reset) {
41
41
  this.$initialValue = Validator.isObject(value) ? deepClone(value) : value;
@@ -1,38 +1,9 @@
1
1
  import Validator from '../utils/validator';
2
- import NativeDocumentError from '../errors/NativeDocumentError';
3
2
  import {BOOL_ATTRIBUTES_NAME, BOOLEAN_ATTRIBUTES} from './constants.js';
4
3
  import {Observable} from '../data/Observable';
5
4
 
6
5
 
7
6
 
8
- export const handleClassAttributeItem = (element, className, value) => {
9
- if (value.__$isObservableChecker) {
10
- let lastClass = value.val();
11
- if (typeof lastClass === 'string') {
12
- element.classes.toggle(lastClass, true);
13
- value.subscribe((currentValue) => {
14
- element.classes.remove(lastClass);
15
- element.classes.toggle(currentValue, true);
16
- lastClass = currentValue;
17
- });
18
- return;
19
- }
20
- }
21
-
22
- if (value.__$Observable) {
23
- element.classes.toggle(className, value.val());
24
- value.subscribe((shouldAdd) => element.classes.toggle(className, shouldAdd));
25
- return;
26
- }
27
-
28
- if (value.$hydrate) {
29
- value.$hydrate(element, className);
30
- return;
31
- }
32
-
33
- element.classes.toggle(className, value);
34
- };
35
-
36
7
  /**
37
8
  * Applies a reactive class map to an HTMLElement.
38
9
  * Each key is a CSS class name; each value is a boolean or ObservableItem<boolean>.
@@ -44,7 +15,31 @@ export const handleClassAttributeItem = (element, className, value) => {
44
15
  export const bindClassAttribute = (element, data) => {
45
16
  for(const className in data) {
46
17
  const value = data[className];
47
- handleClassAttributeItem(element, className, value);
18
+ if (value.__$isObservableChecker) {
19
+ let lastClass = value.val();
20
+ if (typeof lastClass === 'string') {
21
+ element.classes.toggle(lastClass, true);
22
+ value.subscribe((currentValue) => {
23
+ element.classes.remove(lastClass);
24
+ element.classes.toggle(currentValue, true);
25
+ lastClass = currentValue;
26
+ });
27
+ return;
28
+ }
29
+ }
30
+
31
+ if (value.__$Observable) {
32
+ element.classes.toggle(className, value.val());
33
+ value.subscribe((shouldAdd) => element.classes.toggle(className, shouldAdd));
34
+ return;
35
+ }
36
+
37
+ if (value.$hydrate) {
38
+ value.$hydrate(element, className);
39
+ return;
40
+ }
41
+
42
+ element.classes.toggle(className, value);
48
43
  }
49
44
  };
50
45
 
@@ -90,15 +90,8 @@ export const ElementCreator = {
90
90
  }
91
91
  if(Array.isArray(children)) {
92
92
  for(let i = 0, length = children.length; i < length; i++) {
93
- let child = children[i];
94
- if(child == null) {
95
- continue;
96
- }
97
- if(!child.__$isNativeNode) {
98
- child = ElementCreator.getChild(children[i]);
99
- if(child) {
100
- parent.appendChild(child);
101
- }
93
+ const child = ElementCreator.getChild(children[i]);
94
+ if(child === null) {
102
95
  continue;
103
96
  }
104
97
  parent.appendChild(child);
@@ -120,11 +113,11 @@ export const ElementCreator = {
120
113
  }
121
114
 
122
115
  child = child.toNdElement();
123
- if (child && child.__$isNativeNode) {
116
+ if (child && (child instanceof Node)) {
124
117
  return child;
125
118
  }
126
119
 
127
- while (child != null && !child.__$isNativeNode) {
120
+ while (child != null && !(child instanceof Node)) {
128
121
  child = child.toNdElement?.();
129
122
  }
130
123
 
@@ -1,6 +1,5 @@
1
1
  import Anchor from '../elements/anchor/anchor';
2
2
  import {ElementCreator} from './ElementCreator';
3
- import {normalizeComponentArgs} from '../utils/args-types';
4
3
  import './NdPrototype';
5
4
 
6
5
  import '../../core/utils/prototypes.js';
@@ -19,7 +18,7 @@ import '../wrappers/prototypes/attributes-extensions';
19
18
  * @returns {Text} Text node, reactive if value is an ObservableItem
20
19
  */
21
20
  export const createTextNode = (value) => {
22
- if(value) {
21
+ if(value != null) {
23
22
  return value.toNdElement();
24
23
  }
25
24
  return ElementCreator.createTextNode();
@@ -58,11 +57,12 @@ export const createHtmlElement = (element, _attributes, _children = null) => {
58
57
  attributes = _children;
59
58
  children = _attributes;
60
59
  }
60
+ element = element.cloneNode();
61
61
 
62
- if(attributes) {
62
+ if(attributes != null) {
63
63
  ElementCreator.processAttributes(element, attributes);
64
64
  }
65
- if(children) {
65
+ if(children != null) {
66
66
  ElementCreator.processChildren(children, element);
67
67
  }
68
68
  return element;
@@ -75,6 +75,7 @@ export const createHtmlElement = (element, _attributes, _children = null) => {
75
75
  *
76
76
  * @param {string} name - HTML tag name (e.g. 'div', 'button', 'input'). Pass empty string to create a Fragment.
77
77
  * @param {((element: HTMLElement) => HTMLElement)|null} [customWrapper=null] - Optional function to augment the element before returning
78
+ * @param {boolean} [isVoid=false] - If true, the element is a void element (no children allowed, e.g. input, img)
78
79
  * @returns {(attr?: Object, children?: ValidChild) => HTMLElement} Element factory function
79
80
  * @example
80
81
  * const Div = HtmlElementWrapper('div');
@@ -89,14 +90,12 @@ export const createHtmlElement = (element, _attributes, _children = null) => {
89
90
  export default function HtmlElementWrapper(name, customWrapper = null, isVoid = false) {
90
91
  const elementCreator = isVoid ? createVoidHtmlElement : createHtmlElement;
91
92
  if(name) {
92
- if(customWrapper) {
93
+ if(customWrapper == null) {
93
94
  let node = null;
94
95
  let createElement = (attr, children) => {
95
96
  node = document.createElement(name);
96
- createElement = (attr, children) => {
97
- return elementCreator(customWrapper(node.cloneNode()), attr, children);
98
- };
99
- return elementCreator(customWrapper(node.cloneNode()), attr, children);;
97
+ createElement = elementCreator.bind(null, node);
98
+ return elementCreator(node.cloneNode(), attr, children);
100
99
  };
101
100
 
102
101
  return (attr, children) => createElement(attr, children);
@@ -106,9 +105,9 @@ export default function HtmlElementWrapper(name, customWrapper = null, isVoid =
106
105
  let createElement = (attr, children) => {
107
106
  node = document.createElement(name);
108
107
  createElement = (attr, children) => {
109
- return elementCreator(node.cloneNode(), attr, children);
108
+ return elementCreator(customWrapper(node.cloneNode()), attr, children);
110
109
  };
111
- return elementCreator(node.cloneNode(), attr, children);
110
+ return elementCreator(customWrapper(node.cloneNode()), attr, children);;
112
111
  };
113
112
 
114
113
  return (attr, children) => createElement(attr, children);
@@ -5,7 +5,7 @@ import DebugManager from '../utils/debug-manager.js';
5
5
  import attributesWrapper, {
6
6
  bindAttributeWithObservable,
7
7
  bindClassAttribute,
8
- bindStyleAttribute, handleClassAttributeItem,
8
+ bindStyleAttribute,
9
9
  } from './AttributesWrapper';
10
10
 
11
11
  /**
@@ -434,17 +434,6 @@ NDElement.prototype.style = function(style) {
434
434
  return this;
435
435
  };
436
436
 
437
- NDElement.prototype.className = function(classes) {
438
- this.$element.classes.add(classes);
439
- return this;
440
- };
441
-
442
- NDElement.prototype.class = function(className, value) {
443
- handleClassAttributeItem(this.$element, className, value);
444
- return this;
445
- };
446
-
447
-
448
437
  /**
449
438
  * Extends the NDElement prototype with new methods available to all NDElement instances.
450
439
  * Use this to add global methods to all NDElements.
@@ -26,34 +26,67 @@ Object.defineProperty(NDElement.prototype, 'nd', {
26
26
  EVENTS.forEach(eventSourceName => {
27
27
  const eventName = eventSourceName.toLowerCase();
28
28
  const inlineHandler = 'on'+eventName;
29
- NDElement.prototype['on'+eventSourceName] = function(callback = null, options = null) {
29
+ const fnName = 'on'+eventSourceName;
30
+ NDElement.prototype[fnName] = function(callback = null, options = null) {
30
31
  if(!this.$element[inlineHandler] && !options) {
31
32
  this.$element[inlineHandler] = callback;
32
- } else {
33
- this.$element.addEventListener(eventName, callback, options);
33
+ return this;
34
34
  }
35
+ this.$element.addEventListener(eventName, callback, options);
35
36
  return this;
36
37
  };
38
+ if(!HTMLElement.prototype[fnName]) {
39
+ HTMLElement.prototype[fnName] = function(callback, options) {
40
+ if(!this[inlineHandler] && !options) {
41
+ this[inlineHandler] = callback;
42
+ return this;
43
+ }
44
+ this.addEventListener(eventName, callback, options);
45
+ return this;
46
+ }
47
+ }
37
48
  });
38
49
 
39
50
  EVENTS_WITH_STOP.forEach(eventSourceName => {
40
51
  const eventName = eventSourceName.toLowerCase();
41
- NDElement.prototype['onStop'+eventSourceName] = function(callback = null, options = null) {
52
+ const stopFnName = 'onStop'+eventSourceName;
53
+ const preventStopFnName = 'onPreventStop'+eventSourceName;
54
+
55
+ NDElement.prototype[stopFnName] = function(callback = null, options = null) {
42
56
  _stop(this.$element, eventName, callback, options);
43
57
  return this;
44
58
  };
45
- NDElement.prototype['onPreventStop'+eventSourceName] = function(callback = null, options = null) {
59
+ NDElement.prototype[preventStopFnName] = function(callback = null, options = null) {
46
60
  _preventStop(this.$element, eventName, callback, options);
47
61
  return this;
48
62
  };
63
+ if(HTMLElement.prototype[stopFnName]) {
64
+ HTMLElement.prototype[stopFnName] = function(callback = null, options = null) {
65
+ _stop(this, eventName, callback, options);
66
+ return this;
67
+ };
68
+ }
69
+ if(!HTMLElement.prototype[preventStopFnName]) {
70
+ HTMLElement.prototype[preventStopFnName] = function(callback = null, options = null) {
71
+ _preventStop(this, eventName, callback, options);
72
+ return this;
73
+ };
74
+ }
49
75
  });
50
76
 
51
77
  EVENTS_WITH_PREVENT.forEach(eventSourceName => {
52
78
  const eventName = eventSourceName.toLowerCase();
53
- NDElement.prototype['onPrevent'+eventSourceName] = function(callback = null, options = null) {
79
+ const preventFnName = 'onPrevent'+eventSourceName;
80
+ NDElement.prototype[preventFnName] = function(callback = null, options = null) {
54
81
  _prevent(this.$element, eventName, callback, options);
55
82
  return this;
56
83
  };
84
+ if(HTMLElement.prototype[preventFnName]) {
85
+ HTMLElement.prototype[preventFnName] = function(callback = null, options = null) {
86
+ _prevent(this, eventName, callback, options);
87
+ return this;
88
+ };
89
+ }
57
90
  });
58
91
 
59
92
 
@@ -160,6 +193,7 @@ const _preventStop = function(element, eventName, callback, options) {
160
193
  const inlineHandler = 'on' + eventName;
161
194
  if(!element[inlineHandler] && !options) {
162
195
  element[inlineHandler] = handler;
196
+ return;
163
197
  }
164
198
  element.addEventListener(eventName, handler, options);
165
199
  return this;
@@ -17,8 +17,12 @@ export default function NodeCloner($element) {
17
17
  this.$styles = null;
18
18
  this.$attrs = null;
19
19
  this.$ndMethods = null;
20
+ this.$content = null;
20
21
  }
21
22
 
23
+ NodeCloner.prototype.shouldBeHydrate = function() {
24
+ return this.$attrs !== null || this.$classes !== null || this.$styles !== null || this.$ndMethods !== null || this.$content !== null;
25
+ };
22
26
 
23
27
  /**
24
28
  * Attaches a template binding to the element by hydrating it with the specified method.
@@ -171,16 +175,16 @@ NodeCloner.prototype.attach = function(methodName, callback) {
171
175
  * Registers a reactive text content binding for the element.
172
176
  *
173
177
  * @internal
174
- * @param {Function} valueorProperty - Function receiving data and returning the text content
178
+ * @param {Function} valueOrProperty - Function receiving data and returning the text content
175
179
  * @returns {NodeCloner} this
176
180
  */
177
- NodeCloner.prototype.text = function(valueorProperty) {
178
- this.$content = valueorProperty;
179
- if(typeof valueorProperty === 'function') {
180
- this.cloneNode = (data) => createTextNode(valueorProperty.apply(null, data));
181
+ NodeCloner.prototype.text = function(valueOrProperty) {
182
+ this.$content = valueOrProperty;
183
+ if(typeof valueOrProperty === 'function') {
184
+ this.cloneNode = (data) => createTextNode(valueOrProperty.apply(null, data));
181
185
  return this;
182
186
  }
183
- this.cloneNode = (data) => createTextNode(data[0][valueorProperty]);
187
+ this.cloneNode = (data) => createTextNode(data[0][valueOrProperty]);
184
188
  return this;
185
189
  };
186
190
 
@@ -24,11 +24,11 @@ export function TemplateCloner($fn) {
24
24
 
25
25
  const assignClonerToNode = ($node) => {
26
26
  const childNodes = $node.childNodes;
27
- let containDynamicNode = !!$node.nodeCloner;
27
+ let containDynamicNode = $node.nodeCloner?.shouldBeHydrate();
28
28
  const childNodesLength = childNodes.length;
29
29
  for(let i = 0; i < childNodesLength; i++) {
30
30
  const child = childNodes[i];
31
- if(child.nodeCloner) {
31
+ if(child.nodeCloner && child.nodeCloner.shouldBeHydrate()) {
32
32
  containDynamicNode = true;
33
33
  }
34
34
  const localContainDynamicNode = assignClonerToNode(child);
@@ -179,9 +179,12 @@ export function useCache(fn) {
179
179
  return node;
180
180
  };
181
181
 
182
+ if(fn.length === 0) {
183
+ return () => wrapper();
184
+ }
182
185
  if(fn.length < 2) {
183
- return (...args) => {
184
- return wrapper(args);
186
+ return (arg) => {
187
+ return wrapper([arg]);
185
188
  };
186
189
  }
187
190
  return (_, __, ...args) => {