native-document 1.0.172 → 1.0.174

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,27 +11,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## [1.0.173] - 2026-06-18
15
+
16
+ ### Fixed
17
+
18
+ - `ObservableItem` TypeScript definitions - `subscribe()` and `on()` no longer
19
+ typed as returning `Unsubscribe`; they return nothing in the actual
20
+ implementation. Use `.unsubscribe(callback)` / `.off(value, callback)` with
21
+ the same reference to remove a subscription.
22
+
23
+ ### Added
24
+
25
+ - `ObservableItem` type definitions - `interceptMutations`, `clone`, `valueOf`
26
+ - `ObservableItem` comparison helpers typed: `isEqualTo`, `isNotEqualTo`,
27
+ `isGreaterThan`, `isGreaterThanOrEqualTo`, `isLessThan`,
28
+ `isLessThanOrEqualTo`, `isBetween`, `isNull`, `isTruthy`, `isFalsy`,
29
+ `isStartingWith`, `isEndingWith`, `isMatchingPattern`, `isEmpty`,
30
+ `isNotEmpty`, `isIncludes`, `isIncludedIn`, `isOneOf`, `isHaving`
31
+ - `ObservableItem` transform helpers typed: `toUpperCase`, `toLowerCase`,
32
+ `toTrimmed`, `toBoolean`, `toLiteral`, `toFormatted`, `toProperty`,
33
+ `toLength`, `toClamped`, `toPercent`
34
+ - `ObservableArray` type definitions - `swapItems`, `insertAfter`, `sync`,
35
+ `clone`, `isNotEmpty`, `indexOf`, `includes`; `deepSubscribe` and `sync`
36
+ correctly typed to return `Unsubscribe`
37
+ - `ObservableObject` type definitions - `$observables()` and `configs()` typed
38
+ as callable methods; `subscribe()` documented as deep-subscribing nested
39
+ observables and observable arrays
40
+ - `ObservableResource` type definitions (previously entirely missing) -
41
+ `ResourceState`, `ObservableResourceConfig`, `data`, `error`, `state`,
42
+ `loading`, `fetch`, `refetch`, `mutate`, `into`, `apply`, `destroy`,
43
+ `isReady`, `isPending`, `isRefreshing`, `isErrored`, `isUnresolved`,
44
+ `onSuccess`, `onError`
45
+ - `ObservableStatic` type definitions - `resource()`, `setLocale()`,
46
+ `useValueProperty()`
47
+
48
+ ---
49
+
14
50
  ## [1.0.172] - 2026-06-15
15
51
 
16
52
  ### Added
17
53
 
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
54
+ - `Text(string)` - shorthand for `string.toNdChildren()`, parses Observable placeholders in template strings
55
+ - `String.prototype.toNdChildren()` - splits a string containing `{{obs:N}}` placeholders into a mixed array of strings and resolved Observables
56
+ - `ObservableItem.prototype.toString()` - auto-registers the Observable in MemoryManager and returns `{{obs:id}}`, enabling reactive interpolation via standard template literals
57
+ - `NDElement.prototype.className(classes)` - adds static classes, bypasses AttributesWrapper
58
+ - `NDElement.prototype.class(className, value)` - adds a reactive class binding, bypasses AttributesWrapper
59
+ - `__$isNativeNode` flag on DOM prototypes (`Element`, `Text`, `Comment`, `Document`, `DocumentFragment`) - faster alternative to `instanceof Node`
60
+ - `__$isValidNdChild` flag on all valid NdChild prototypes - single property check replaces multiple type guards
61
+ - `defineToNdElement`, `defineValidNdChild`, `defineNativeNode` - non-enumerable prototype helpers in `nd-element-extensions`
62
+ - camelCase variants added to `BOOLEAN_ATTRIBUTES` - removes `toLowerCase()` call on each attribute in the hot path
27
63
 
28
64
  ### Changed
29
65
 
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
66
+ - `ElementCreator.processChildren` and `getChild` - refactored for better performance
67
+ - `nd-element-extensions` - all prototype extensions converted to non-enumerable `defineProperty`
68
+ - `AttributesWrapper` - `options` defaults to `null` instead of `{}`, fixes truthy check issue
69
+ - 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
70
+ - `_prevent`, `_stop`, `_preventStop` - updated with same inline handler strategy
35
71
 
36
72
  ---
37
73
 
@@ -39,34 +75,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
39
75
 
40
76
  ### Added
41
77
 
42
- - Vitest test suite `vitest.config.js`, happy-dom environment, scripts `test`, `test:watch`, `test:ui`, `test:coverage`
43
- - 372 tests passing across 13 files 0 failures, 0 todo
78
+ - Vitest test suite - `vitest.config.js`, happy-dom environment, scripts `test`, `test:watch`, `test:ui`, `test:coverage`
79
+ - 372 tests passing across 13 files - 0 failures, 0 todo
44
80
 
45
- #### Unit tests core
46
- - `observable-item` 48 cases: set, val, subscribe, on/off, once, toggle, reset, transform, equals, clone, cleanup, intercept, assocTrigger
47
- - `observable-array` 48 cases: push, remove, clear, merge, swap, insertAfter, isIncludes, where, sync, clone, reset, deepSubscribe
48
- - `observable-object` 30 cases: constructor, val, get, set, subscribe, keys, observables, reset, clone
49
- - `observable-resource` 29 cases: states, fetch, refetch, mutate, abort, race condition, deps, onSuccess, onError, into, apply, destroy
50
- - `store` 43 cases: create, createResettable, createComposed, use, follow, reset, delete, group, protected, createPersistent
81
+ #### Unit tests - core
82
+ - `observable-item` - 48 cases: set, val, subscribe, on/off, once, toggle, reset, transform, equals, clone, cleanup, intercept, assocTrigger
83
+ - `observable-array` - 48 cases: push, remove, clear, merge, swap, insertAfter, isIncludes, where, sync, clone, reset, deepSubscribe
84
+ - `observable-object` - 30 cases: constructor, val, get, set, subscribe, keys, observables, reset, clone
85
+ - `observable-resource` - 29 cases: states, fetch, refetch, mutate, abort, race condition, deps, onSuccess, onError, into, apply, destroy
86
+ - `store` - 43 cases: create, createResettable, createComposed, use, follow, reset, delete, group, protected, createPersistent
51
87
 
52
- #### Unit tests router
53
- - `route-matching` 26 cases: exact path, params, typed params, custom validators, url(), metadata
54
- - `router-guards` 14 cases: middleware execution, group middlewares, subscribe, resolve, currentState
88
+ #### Unit tests - router
89
+ - `route-matching` - 26 cases: exact path, params, typed params, custom validators, url(), metadata
90
+ - `router-guards` - 14 cases: middleware execution, group middlewares, subscribe, resolve, currentState
55
91
 
56
- #### Unit tests utils
57
- - `cache` 12 cases: singleton, once, memoize
58
- - `filters` 60 cases: equals, comparison, inArray, isEmpty, match, and/or/not, custom, string filters, date/time filters
92
+ #### Unit tests - utils
93
+ - `cache` - 12 cases: singleton, once, memoize
94
+ - `filters` - 60 cases: equals, comparison, inArray, isEmpty, match, and/or/not, custom, string filters, date/time filters
59
95
 
60
- #### Integration tests UI
61
- - `attributes-wrapper` 22 cases: string, class map, style, Observable, boolean, aria attributes
62
- - `show-if` 16 cases: static boolean, observable, factory child, HideIf, HideIfNot
63
- - `for-each-array` 13 cases: static array, push, removeItem, clear, set, swap, index observable, order preservation
64
- - `lifecycle` 11 cases: mounted, unmounted, destroy, destroyOnUnmount
96
+ #### Integration tests - UI
97
+ - `attributes-wrapper` - 22 cases: string, class map, style, Observable, boolean, aria attributes
98
+ - `show-if` - 16 cases: static boolean, observable, factory child, HideIf, HideIfNot
99
+ - `for-each-array` - 13 cases: static array, push, removeItem, clear, set, swap, index observable, order preservation
100
+ - `lifecycle` - 11 cases: mounted, unmounted, destroy, destroyOnUnmount
65
101
 
66
102
  ### Fixed
67
103
 
68
- - `NDElement.prototype.destroy()` guard against double call when `$element` is already null
69
- - `NDElement.prototype.destroy()` disconnect lifecycle observer to prevent callbacks from firing after destroy
104
+ - `NDElement.prototype.destroy()` - guard against double call when `$element` is already null
105
+ - `NDElement.prototype.destroy()` - disconnect lifecycle observer to prevent callbacks from firing after destroy
70
106
 
71
107
  ---
72
108
 
@@ -98,7 +134,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
98
134
 
99
135
  #### UI (renders)
100
136
  - `CardRender` + `card.css` - Card renderer with 5 build slots and `layout(slots)` support
101
- - `ListRender` + `list.css` - `ForEachArray($desc.items)` items self-render
137
+ - `ListRender` + `list.css` - `ForEachArray($desc.items)` - items self-render
102
138
  - `ListItemRender` + `list-item.css` - reactive checkbox, select indicator, swipe gesture with pointer events, auto-close and stopPropagation
103
139
  - `ListGroupRender` + `list-group.css` - collapsible group with reactive chevron via `Switch`
104
140
  - `ListDividerRender` + `list-divider.css`
@@ -240,7 +276,8 @@ Entries use the following categories:
240
276
 
241
277
  ---
242
278
 
243
- [Unreleased]: https://github.com/afrocodeur/native-document/compare/v1.0.172...HEAD
279
+ [Unreleased]: https://github.com/afrocodeur/native-document/compare/v1.0.173...HEAD
280
+ [1.0.173]: https://github.com/afrocodeur/native-document/compare/v1.0.172...v1.0.173
244
281
  [1.0.172]: https://github.com/afrocodeur/native-document/compare/v1.0.170...v1.0.172
245
282
  [1.0.170]: https://github.com/afrocodeur/native-document/compare/v1.0.169...v1.0.170
246
283
  [1.0.169]: https://github.com/afrocodeur/native-document/compare/v1.0.167...v1.0.169
package/index.d.ts CHANGED
@@ -18,4 +18,4 @@ export declare const Observable: ObservableStatic;
18
18
  export declare const $: ObservableStatic;
19
19
  export declare const obs: ObservableStatic;
20
20
  export declare const Store: StoreStatic;
21
- export declare const Router: RouterStatic;
21
+ // export declare const Router: RouterStatic;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "native-document",
3
- "version": "1.0.172",
3
+ "version": "1.0.174",
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;
@@ -33,9 +33,9 @@ export default function ObservableItem(value, configs = null) {
33
33
  this.$listeners = null;
34
34
  this.$watchers = null;
35
35
 
36
- this.$memoryId = null;
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;
@@ -308,7 +308,7 @@ ObservableItem.prototype.cleanup = function() {
308
308
  }
309
309
  this.$cleanupListeners = null;
310
310
  }
311
- MemoryManager.unregister(this.$memoryId);
311
+ MemoryManager.unregister(this.$id);
312
312
  this.disconnectAll();
313
313
  if(process.env.NODE_ENV === 'development') {
314
314
  this.$isCleanedUp = true;
@@ -198,14 +198,13 @@ ObservableObject.prototype.$updateWith = ObservableObject.prototype.set;
198
198
 
199
199
  /**
200
200
  * Returns an array of all internal observable instances (one per property).
201
- * Alias: $observables()
202
201
  *
203
202
  * @returns {ObservableItem[]} Array of observable instances
204
203
  */
205
204
  ObservableObject.prototype.observables = function() {
206
205
  return Object.values(this.$observables);
207
206
  };
208
- ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
207
+ // ObservableObject.prototype.$observables = ObservableObject.prototype.observables;
209
208
 
210
209
  /**
211
210
  * Returns all property names of the observable object.
@@ -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);
@@ -26,7 +26,8 @@ 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
33
  return this;
@@ -34,26 +35,58 @@ EVENTS.forEach(eventSourceName => {
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
 
@@ -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) => {
@@ -15,6 +15,7 @@ export default function NativeFetch($baseUrl) {
15
15
  };
16
16
 
17
17
  this.fetch = async function(method, endpoint, params = {}, options = {}) {
18
+ endpoint = endpoint || '';
18
19
  if(options.formData) {
19
20
  const formData = new FormData();
20
21
  for(const key in params) {
@@ -110,7 +110,7 @@ const buildErrors = ($desc) => {
110
110
  return ShowIf($desc.hasErrors,
111
111
  () => Div({class: 'field-errors', ...($desc.elementsProps.error || {})},
112
112
  ForEachArray($desc.errors, (error) =>
113
- Span({class: 'field-error'}, error),
113
+ Span({class: 'field-error'}, (typeof error === 'string' ? error.toNdChildren() : error)),
114
114
  ),
115
115
  ),
116
116
  );
@@ -32,7 +32,7 @@ interface Interceptors {
32
32
  response: (callback: ResponseInterceptor) => void;
33
33
  }
34
34
 
35
- declare class NativeFetch {
35
+ export declare class NativeFetch {
36
36
  constructor(baseUrl: string);
37
37
 
38
38
  interceptors: Interceptors;
@@ -1,3 +1,3 @@
1
- import { ObservableItem, ObservableChecker, ObservableResourceConfig, ObservableResource } from './observable';
1
+ // import { ObservableResourceConfig, ObservableResource } from './observable';
2
2
 
3
- export type { ObservableResource, ObservableResourceConfig };
3
+ // export type { ObservableResource, ObservableResourceConfig };
@@ -1,4 +1,4 @@
1
- import {FilterResult, PredicateMap} from "./filters";
1
+ import { FilterResult, PredicateMap } from './filters';
2
2
 
3
3
  export type Unsubscribe = () => void;
4
4
 
@@ -41,45 +41,96 @@ export interface FormattersStatic {
41
41
 
42
42
  export declare const Formatters: FormattersStatic;
43
43
 
44
+ // -- Observable system type definitions ---------------------------------------
44
45
 
45
- // Observable system type definitions
46
46
  export interface ObservableItem<T = any> {
47
47
  readonly $currentValue: T;
48
48
  readonly $previousValue: T;
49
49
  readonly $initialValue: T | null;
50
50
  readonly $isCleanedUp: boolean;
51
+ readonly $memoryId: number | null;
52
+ readonly __$Observable: true;
53
+ readonly __$isObservable: true;
51
54
 
52
55
  $value: T;
53
- $memoryId: number | null;
54
56
 
55
57
  val(): T;
56
58
  set(value: T | ((prev: T) => T)): void;
57
59
  trigger(operations?: ObservableOperation): void;
58
60
  cleanup(): void;
61
+ clone(): ObservableItem<T>;
62
+ disconnectAll(): void;
59
63
 
60
- subscribe(callback: (current: T, previous: T, operations?: ObservableOperation) => void): Unsubscribe;
64
+ /**
65
+ * Registers a subscriber. Does NOT return an unsubscribe function —
66
+ * call .unsubscribe(callback) with the same reference to remove it.
67
+ */
68
+ subscribe(callback: (current: T, previous: T, operations?: ObservableOperation) => void): void;
61
69
  unsubscribe(callback: Function): void;
62
- on(value: T, callback: ObservableItem<boolean> | ((isActive: boolean) => void)): () => void;
70
+
71
+ /**
72
+ * Registers a watcher for a specific value. Does NOT return an unsubscribe
73
+ * function — call .off(value, callback) to remove it.
74
+ */
75
+ on(value: T, callback: ObservableItem<boolean> | ((isActive: boolean) => void)): void;
76
+ off(value: T, callback?: Function): void;
77
+ once(predicate: T | ((value: T) => boolean), callback: (value: T) => void): void;
78
+ onCleanup(callback: () => void): void;
79
+
80
+ intercept(callback: (newValue: T, currentValue: T) => T | undefined): this;
81
+ interceptMutations(callback: (operation: ObservableOperation) => void): this;
63
82
 
64
83
  check<U>(callback: (value: T) => U): ObservableChecker<U>;
65
84
  transform<U>(callback: (value: T) => U): ObservableChecker<U>;
66
85
  is<U>(callback: (value: T) => U): ObservableChecker<U>;
67
86
  select<U>(callback: (value: T) => U): ObservableChecker<U>;
68
- pluck<U>(callback: (value: T) => U): ObservableChecker<U>;
87
+ pluck<U>(key: string): ObservableChecker<U>;
69
88
  format(type: FormatType | ((value: T) => string), options?: FormatOptions): ObservableItem<string>;
70
89
  get(key: string | number): any;
71
90
  when(value: T): ObservableWhen<T>;
72
- off(value: T, callback?: Function): void;
73
- once(predicate: T | ((value: T) => boolean), callback: (value: T) => void): void;
74
- onCleanup(callback: () => void): void;
75
- intercept(callback: (newValue: T, currentValue: T) => T | undefined): this;
91
+
76
92
  persist(key: string, options?: {
77
93
  get?: (value: any) => T;
78
94
  set?: (value: T) => any;
79
95
  }): this;
80
- disconnectAll(): void;
96
+
97
+ // -- comparison helpers (is*) -----------------------------------------------
98
+
99
+ isEqualTo(value: T | ObservableItem<T>): ObservableChecker<boolean>;
100
+ isNotEqualTo(value: T | ObservableItem<T>): ObservableChecker<boolean>;
101
+ isGreaterThan(value: T | ObservableItem<T>): ObservableChecker<boolean>;
102
+ isGreaterThanOrEqualTo(value: T | ObservableItem<T>): ObservableChecker<boolean>;
103
+ isLessThan(value: T | ObservableItem<T>): ObservableChecker<boolean>;
104
+ isLessThanOrEqualTo(value: T | ObservableItem<T>): ObservableChecker<boolean>;
105
+ isBetween(min: T | ObservableItem<T>, max: T | ObservableItem<T>): ObservableChecker<boolean>;
106
+ isNull(): ObservableChecker<boolean>;
107
+ isTruthy(): ObservableChecker<boolean>;
108
+ isFalsy(): ObservableChecker<boolean>;
109
+ isStartingWith(value: string | ObservableItem<string>): ObservableChecker<boolean>;
110
+ isEndingWith(value: string | ObservableItem<string>): ObservableChecker<boolean>;
111
+ isMatchingPattern(pattern: RegExp | ObservableItem<RegExp>): ObservableChecker<boolean>;
112
+ isEmpty(): ObservableChecker<boolean>;
113
+ isNotEmpty(): ObservableChecker<boolean>;
114
+ isIncludes(value: any | ObservableItem<any>): ObservableChecker<boolean>;
115
+ isIncludedIn(collection: any[] | ObservableItem<any[]>): ObservableChecker<boolean>;
116
+ isOneOf(collection: any[] | ObservableItem<any[]>): ObservableChecker<boolean>;
117
+ isHaving(key: string | ObservableItem<string>): ObservableChecker<boolean>;
118
+
119
+ // -- transform helpers (to*) -------------------------------------------------
120
+
121
+ toUpperCase(): ObservableChecker<string>;
122
+ toLowerCase(): ObservableChecker<string>;
123
+ toTrimmed(): ObservableChecker<string>;
124
+ toBoolean(): ObservableChecker<boolean>;
125
+ toLiteral(template: string, placeholder?: string): ObservableChecker<string>;
126
+ toFormatted(template: string, placeholder?: string): ObservableChecker<string>;
127
+ toProperty<U = any>(path: string): ObservableChecker<U>;
128
+ toLength(): ObservableChecker<number>;
129
+ toClamped(min: number | ObservableItem<number>, max: number | ObservableItem<number>): ObservableChecker<number>;
130
+ toPercent(total: number | ObservableItem<number>): ObservableChecker<number>;
81
131
 
82
132
  toString(): string;
133
+ valueOf(): T;
83
134
  equals(value: any): boolean;
84
135
  toBool(): boolean;
85
136
  toggle(): void;
@@ -93,7 +144,8 @@ export class ObservableWhen<T = any> {
93
144
 
94
145
  constructor(observer: ObservableItem<T>, value: T);
95
146
 
96
- subscribe(callback: (value: boolean) => void): Unsubscribe;
147
+ /** Does NOT return an unsubscribe function — use .off() on the underlying observer. */
148
+ subscribe(callback: (value: boolean) => void): void;
97
149
  val(): boolean;
98
150
  isMatch(): boolean;
99
151
  isActive(): boolean;
@@ -106,11 +158,14 @@ export interface ObservableOperation {
106
158
  }
107
159
 
108
160
  export interface ObservableChecker<T = any> {
161
+ readonly __$Observable: true;
109
162
  readonly __$isObservableChecker: true;
110
163
 
111
- subscribe(callback: (value: T) => void): Unsubscribe;
164
+ /** Does NOT return an unsubscribe function. */
165
+ subscribe(callback: (value: T) => void): void;
112
166
  check<U>(callback: (value: T) => U): ObservableChecker<U>;
113
167
  val(): T;
168
+ toNdElement(): Text;
114
169
 
115
170
  set(value: any): void;
116
171
  trigger(): void;
@@ -119,6 +174,7 @@ export interface ObservableChecker<T = any> {
119
174
 
120
175
  export interface ObservableArray<T> extends ObservableItem<T[]> {
121
176
  readonly length: number;
177
+ readonly __$isObservableArray: true;
122
178
 
123
179
  push(...items: T[]): number;
124
180
  pop(): T | undefined;
@@ -128,16 +184,19 @@ export interface ObservableArray<T> extends ObservableItem<T[]> {
128
184
  sort(compareFn?: (a: T, b: T) => number): T[];
129
185
  splice(start: number, deleteCount?: number, ...items: T[]): T[];
130
186
 
131
- isEmpty(): boolean;
187
+ empty(): boolean;
132
188
  clear(): boolean;
133
189
  merge(values: T[]): void;
134
190
  removeItem(item: T): T[];
135
191
  remove(index: number): T[];
136
192
  swap(indexA: number, indexB: number): boolean;
137
- count(condition: (item:T, index?:number) => boolean): number;
138
- populateAndRender(iteration: number, callback: (index: number) => T): void;
193
+ swapItems(itemA: T, itemB: T): boolean;
194
+ insertAfter(item: T, afterItem: T): T[];
195
+ count(condition: (item: T, index?: number) => boolean): number;
196
+ populateAndRender(items: T[], factory: any, options: any): void;
139
197
 
140
198
  map<U>(callback: (value: T, index: number, array: T[]) => U): U[];
199
+ forEach(callback: (value: T, index: number, array: T[]) => void): void;
141
200
  filter(callback: (value: T, index: number, array: T[]) => boolean): T[];
142
201
  reduce<U>(callback: (acc: U, value: T, index: number, array: T[]) => U, initial: U): U;
143
202
  some(callback: (value: T, index: number, array: T[]) => boolean): boolean;
@@ -146,12 +205,21 @@ export interface ObservableArray<T> extends ObservableItem<T[]> {
146
205
  at(index: number): T | undefined;
147
206
  findIndex(callback: (value: T, index: number, array: T[]) => boolean): number;
148
207
  concat(...items: (T | T[])[]): T[];
208
+ indexOf(item: T): number;
209
+ includes(item: T): boolean;
149
210
 
150
- where(predicates: PredicateMap<T>): ObservableArray<T>;
211
+ where(predicates: PredicateMap<T> | ((item: T) => boolean)): ObservableArray<T>;
151
212
  whereSome<K extends keyof T>(fields: K[], filter: FilterResult<T[K]>): ObservableArray<T>;
152
213
  whereEvery<K extends keyof T>(fields: K[], filter: FilterResult<T[K]>): ObservableArray<T>;
153
214
 
154
- deepSubscribe(callback: (value: T[]) => void): () => void;
215
+ /** Returns an unsubscribe function that stops all deep observation. */
216
+ deepSubscribe(callback: (value: T[]) => void): Unsubscribe;
217
+
218
+ /** One-way sync — pushes mutations from this array to the target. Returns unsubscribe. */
219
+ sync(target: ObservableArray<T>): Unsubscribe;
220
+
221
+ clone(): ObservableArray<T>;
222
+ isNotEmpty(): ObservableChecker<boolean>;
155
223
  }
156
224
 
157
225
  export type ObservableObject<T extends Record<string, any>> = ObservableItem<T> & {
@@ -175,6 +243,12 @@ export type ObservableObject<T extends Record<string, any>> = ObservableItem<T>
175
243
  keys(): string[];
176
244
  $keys(): string[];
177
245
  observables(): ObservableItem<any>[];
246
+
247
+ /**
248
+ * Subscribing to an ObservableObject also deep-subscribes to all nested
249
+ * observables and observable arrays — triggers on any nested change.
250
+ */
251
+ subscribe(callback: (current: T, previous: T, operations?: ObservableOperation) => void): void;
178
252
  } & {
179
253
  [K in keyof T]: T[K] extends (infer U)[]
180
254
  ? ObservableArray<U>
@@ -183,6 +257,40 @@ export type ObservableObject<T extends Record<string, any>> = ObservableItem<T>
183
257
  : ObservableItem<T[K]>;
184
258
  };
185
259
 
260
+ // -- ObservableResource ---------------------------------------------------------
261
+
262
+ export type ResourceState = 'unresolved' | 'pending' | 'ready' | 'refreshing' | 'errored';
263
+
264
+ export interface ObservableResourceConfig<T = any> {
265
+ auto?: boolean;
266
+ lazy?: boolean;
267
+ debounce?: number;
268
+ into?: ObservableItem<T>;
269
+ apply?: (result: any, data: ObservableItem<T>) => void;
270
+ }
271
+
272
+ export interface ObservableResource<T = any> {
273
+ readonly data: ObservableItem<T>;
274
+ readonly error: ObservableItem<Error | null>;
275
+ readonly state: ObservableItem<ResourceState>;
276
+ readonly loading: ObservableItem<boolean>;
277
+
278
+ fetch(): this;
279
+ refetch(): this;
280
+ mutate(value: T): this;
281
+ into(target: ObservableItem<T>): this;
282
+ apply(callback: (result: any, data: ObservableItem<T>) => void): this;
283
+ destroy(): void;
284
+
285
+ isReady(): ObservableChecker<boolean>;
286
+ isPending(): ObservableChecker<boolean>;
287
+ isRefreshing(): ObservableChecker<boolean>;
288
+ isErrored(): ObservableChecker<boolean>;
289
+ isUnresolved(): ObservableChecker<boolean>;
290
+
291
+ onSuccess(callback: (data: T) => void): this;
292
+ onError(callback: (error: Error) => void): this;
293
+ }
186
294
 
187
295
  export interface BatchFunction<TArgs extends any[] = any[], TReturn = any> {
188
296
  (...args: TArgs): TReturn;
@@ -210,8 +318,14 @@ export interface ObservableStatic {
210
318
  object<T extends Record<string, any>>(value: T, configs?: ObservableConfig | null): ObservableObject<T>;
211
319
  json<T extends Record<string, any>>(value: T, configs?: ObservableConfig | null): ObservableObject<T>;
212
320
 
213
- computed<T>(callback: () => T, dependencies?: ValidComputedDependencies | BatchFunction): ObservableItem<T>;
214
- computed<T>(callback: () => T, batchFunction?: BatchFunction): ObservableItem<T>;
321
+ resource<T = any>(
322
+ fn: (...args: any[]) => Promise<T>,
323
+ dependencies?: ValidComputedDependencies,
324
+ config?: ObservableResourceConfig<T> | boolean,
325
+ ): ObservableResource<T>;
326
+
327
+ computed<T>(callback: (...values: any[]) => T, dependencies?: ValidComputedDependencies | BatchFunction): ObservableItem<T>;
328
+ computed<T>(callback: (...values: any[]) => T, batchFunction?: BatchFunction): ObservableItem<T>;
215
329
 
216
330
  batch<TArgs extends any[], TReturn>(
217
331
  callback: (...args: TArgs) => TReturn
@@ -219,9 +333,11 @@ export interface ObservableStatic {
219
333
 
220
334
  value(data: any): any;
221
335
  update(target: any, data: any): void;
336
+ setLocale(locale: string | ObservableItem<string>): void;
337
+ useValueProperty(propertyName?: string): void;
222
338
 
223
339
  getById(id: number): ObservableItem | null;
224
340
  cleanup(observable: ObservableItem): void;
225
341
  autoCleanup(enable?: boolean, options?: AutoCleanupOptions): void;
226
342
  arrayOfObject<T extends Record<string, any>>(data: T[]): ObservableObject<T>[];
227
- }
343
+ }
@@ -20,4 +20,5 @@ interface ServiceStatic {
20
20
  ): MemoizeProxy<(...args: Args) => T>;
21
21
  }
22
22
 
23
- export const Service: ServiceStatic;
23
+ export const Service: ServiceStatic;
24
+ export const Cache: ServiceStatic;
package/utils.js CHANGED
@@ -1,11 +1,14 @@
1
- import NativeFetch from "./src/fetch/NativeFetch";
2
- import * as Cache from "./src/core/utils/cache";
3
- import * as filters from "./src/core/utils/filters/index";
4
- import {classPropertyAccumulator, cssPropertyAccumulator} from "./src/core/utils/property-accumulator";
1
+ import NativeFetch from './src/fetch/NativeFetch';
2
+ import * as Cache from './src/core/utils/cache';
3
+ import * as filters from './src/core/utils/filters/index';
4
+ import {classPropertyAccumulator, cssPropertyAccumulator} from './src/core/utils/property-accumulator';
5
+
6
+ const Service = Cache;
5
7
 
6
8
  export {
7
9
  NativeFetch,
8
10
  Cache,
11
+ Service,
9
12
  filters,
10
13
  classPropertyAccumulator,
11
14
  cssPropertyAccumulator