native-document 1.0.294 → 1.0.296

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.294",
3
+ "version": "1.0.296",
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",
@@ -301,6 +301,10 @@ ObservableItem.prototype.toBoolean = function () {
301
301
  return $checker(this, x => !!x);
302
302
  };
303
303
 
304
+ ObservableItem.prototype.toNumber = function () {
305
+ return $checker(this, Number);
306
+ };
307
+
304
308
  /**
305
309
  * Returns a derived observable that emits a string with the placeholder replaced by the current value.
306
310
  * Alias: toFormatted
@@ -64,7 +64,7 @@ ObservableItem.prototype.is = function(callbackOrValue) {
64
64
  if(typeof callbackOrValue === 'function') {
65
65
  return new ObservableChecker(this, callbackOrValue);
66
66
  }
67
- return new ObservableChecker(this, (value) => value === callbackOrValue);
67
+ return this.isEqualTo(callbackOrValue);
68
68
  };
69
69
  ObservableItem.prototype.select = ObservableItem.prototype.check;
70
70
 
@@ -47,7 +47,7 @@ export function Route($path, $component, $options = {}) {
47
47
  const $shouldRebuild = $options.shouldRebuild || false;
48
48
  const $paramsValidators = $options.with || {};
49
49
  const $layout = $options.layout || null;
50
- const $layouts = $options.layouts || null;
50
+ const $layouts = $options.layouts || [];
51
51
 
52
52
  const $params = {};
53
53
  const $paramsNames = [];
@@ -58,8 +58,14 @@ export const RouteGroupHelper = {
58
58
  return null;
59
59
  },
60
60
  layouts($groupTree) {
61
- return $groupTree
62
- .map(g => g.options?.layout)
63
- .filter(Boolean);
61
+ return $groupTree.map(g => g.options?.layout).filter(Boolean);
62
+ },
63
+ watches($groupTree) {
64
+ return $groupTree.mpa((g) => {
65
+ if(!g.options?.layout || !g.options?.watch) {
66
+ return null;
67
+ }
68
+ return { layout: g.options.layout, watch: g.options.watch };
69
+ }).filter(Boolean);
64
70
  },
65
71
  };
@@ -25,6 +25,7 @@ export default function Router($options = {}) {
25
25
  const $groupTree = [];
26
26
  const $listeners = [];
27
27
  const $currentState = { route: null, params: null, query: null, path: null, hash: null };
28
+ const $layoutWatches = new Map();
28
29
 
29
30
  if($options.mode === 'hash') {
30
31
  HashRouter.apply(this, []);
@@ -54,10 +55,14 @@ export default function Router($options = {}) {
54
55
  *
55
56
  * @param {string} path
56
57
  * @param {Function} component
57
- * @param {{name:?string, middlewares:Function[], shouldRebuild:Boolean, with: Object, layout: Function }} options
58
+ * @param {{name:?string, middlewares:Function[], shouldRebuild:Boolean, with: Object, layout: Function, watch?: string[] }} options
58
59
  * @returns {this}
59
60
  */
60
61
  this.add = function(path, component, options) {
62
+ if(options?.watch && !options?.layout) {
63
+ DebugManager.warn('Router', `'watch' ignored on route '${path}': requires a 'layout' defined at a group() level`);
64
+ }
65
+
61
66
  const route = new Route(RouteGroupHelper.fullPath($groupTree, path), component, {
62
67
  ...options,
63
68
  middlewares: RouteGroupHelper.fullMiddlewares($groupTree, options?.middlewares || []),
@@ -68,6 +73,7 @@ export default function Router($options = {}) {
68
73
  ...(options?.layout ? [options.layout] : []),
69
74
  ],
70
75
  });
76
+
71
77
  $routes.push(route);
72
78
  if(route.name()) {
73
79
  $routesByName[route.name()] = route;
@@ -84,6 +90,10 @@ export default function Router($options = {}) {
84
90
  });
85
91
  };
86
92
 
93
+ this.getLayoutWatches = function(layout) {
94
+ return $layoutWatches.get(layout);
95
+ };
96
+
87
97
  /**
88
98
  * Groups routes under a common path prefix with shared options.
89
99
  *
@@ -104,7 +114,10 @@ export default function Router($options = {}) {
104
114
  if(!Validator.isFunction(callback)) {
105
115
  throw new RouterError('Callback must be a function');
106
116
  }
107
- $groupTree.push({suffix, options});
117
+ $groupTree.push({ suffix, options });
118
+ if(options.layout && options.watch) {
119
+ $layoutWatches.set(options.layout, options.watch);
120
+ }
108
121
  callback();
109
122
  $groupTree.pop();
110
123
  return this;
@@ -1,6 +1,7 @@
1
1
  import Validator from '../core/utils/validator';
2
2
  import {Anchor} from '../../elements';
3
3
  import {ElementCreator} from '../core/wrappers/ElementCreator';
4
+ import {Observable} from 'native-document';
4
5
 
5
6
  /**
6
7
  *
@@ -12,7 +13,6 @@ export function RouterComponent(router, container) {
12
13
  const $cache = new Map();
13
14
  const $layoutCache = new WeakMap();
14
15
  const $routeInstanceAnchors = new WeakMap();
15
- let $currentLayout = null;
16
16
 
17
17
  let $lastNodeInserted = null;
18
18
 
@@ -42,10 +42,6 @@ export function RouterComponent(router, container) {
42
42
  const cleanContainer = () => {
43
43
  container.nodeValue = '';
44
44
  removeLastNodeInserted();
45
-
46
- if($currentLayout) {
47
- $currentLayout.remove();
48
- }
49
45
  };
50
46
 
51
47
  const getNodeToInsert = (node) => {
@@ -56,7 +52,7 @@ export function RouterComponent(router, container) {
56
52
  return nodeToInsert;
57
53
  };
58
54
 
59
- const updateContainerByLayout = (layouts, node, route, path) => {
55
+ const updateContainerByLayout = (layouts, node, route, path, routeContext) => {
60
56
 
61
57
  const nodeToInsert = getNodeToInsert(node);
62
58
  let matchDepth = 0;
@@ -69,6 +65,7 @@ export function RouterComponent(router, container) {
69
65
  removeLastNodeInserted();
70
66
  $lastNodeInserted = nodeToInsert;
71
67
  $mountedChain[matchDepth - 1]?.anchor?.replaceContent(nodeToInsert);
68
+ updateLayoutParams(routeContext);
72
69
  return;
73
70
  }
74
71
 
@@ -85,21 +82,32 @@ export function RouterComponent(router, container) {
85
82
  for(let i = layouts.length - 1; i >= matchDepth; i -= 1) {
86
83
  const layout = layouts[i];
87
84
  let cached = $layoutCache.get(layout);
85
+ let $watches = null;
88
86
  if(!cached) {
87
+ const watches = router.getLayoutWatches(layout);
88
+ if(watches?.length) {
89
+ const watchesSource = {};
90
+ for(const name of watches) {
91
+ watchesSource[name] = routeContext.params[name];
92
+ }
93
+ $watches = Observable.object(watchesSource)
94
+
95
+ }
89
96
  const anchor = getNodeAnchorForLayout(slot, `${path}:layout:${i}`);
90
- const element = ElementCreator.getChild(layout(anchor));
91
- cached = { element, anchor };
97
+ const element = ElementCreator.getChild(layout(anchor, { params: $watches }));
98
+ cached = { element, anchor, $watches };
92
99
  $layoutCache.set(layout, cached);
93
100
  }
94
101
  else {
95
102
  cached.anchor.replaceContent(slot);
96
103
  }
97
104
 
98
- newLayers.unshift({ layoutFn: layout, anchor: cached.anchor, element: cached.element });
105
+ newLayers.unshift({ layoutFn: layout, anchor: cached.anchor, element: cached.element, $watches: cached.$watches });
99
106
  slot = cached.element;
100
107
  }
101
108
 
102
109
  $mountedChain = [...$mountedChainKeep, ...newLayers];
110
+ updateLayoutParams(routeContext);
103
111
 
104
112
  if (matchDepth === 0) {
105
113
  container.appendChild(newLayers[0].element);
@@ -109,12 +117,25 @@ export function RouterComponent(router, container) {
109
117
 
110
118
  };
111
119
 
112
- const updateContainer = function(node, route, path) {
113
- const layouts = route.layouts();
120
+ const updateLayoutParams = function(routeContext) {
121
+ for(let i = 0; i < $mountedChain.length; i++) {
122
+ const layer = $mountedChain[i];
123
+ if(!layer.$watches) {
124
+ continue;
125
+ }
126
+ const watches = router.getLayoutWatches(layer.layoutFn);
127
+
128
+ for(const name of watches) {
129
+ layer.$watches[name]?.set(routeContext.params[name]);
130
+ }
131
+ }
132
+ };
114
133
 
134
+ const updateContainer = function(node, route, path, routeContext) {
135
+ const layouts = route.layouts();
115
136
 
116
137
  if(layouts.length) {
117
- updateContainerByLayout(layouts, node, route, path);
138
+ updateContainerByLayout(layouts, node, route, path, routeContext);
118
139
  return;
119
140
  }
120
141
  const nodeToInsert = getNodeToInsert(node);
@@ -135,9 +156,11 @@ export function RouterComponent(router, container) {
135
156
  $lifecycles.get($currentPath)?.onLeave?.();
136
157
  }
137
158
 
159
+
160
+
138
161
  if($cache.has(path)) {
139
162
  const cacheNode = $cache.get(path);
140
- updateContainer(cacheNode, route);
163
+ updateContainer(cacheNode, route, path, { query, params });
141
164
 
142
165
  $lifecycles.get(path)?.onEnter?.(params, query);
143
166
  $currentPath = path;
@@ -145,19 +168,20 @@ export function RouterComponent(router, container) {
145
168
  return;
146
169
  }
147
170
  const pathLifecycles = {};
171
+ const routeContext = {
172
+ params,
173
+ query,
174
+ onEnter: (cb) => { pathLifecycles.onEnter = cb; },
175
+ onLeave: (cb) => { pathLifecycles.onLeave = cb; },
176
+ };
148
177
  $lifecycles.set(path, pathLifecycles);
149
178
 
150
179
 
151
180
 
152
181
  const Component = route.component();
153
- const node = Component({
154
- params,
155
- query,
156
- onEnter: (cb) => { pathLifecycles.onEnter = cb; },
157
- onLeave: (cb) => { pathLifecycles.onLeave = cb; },
158
- });
182
+ const node = Component(routeContext);
159
183
  $cache.set(path, node);
160
- updateContainer(node, route, path);
184
+ updateContainer(node, route, path, routeContext);
161
185
 
162
186
  pathLifecycles.onEnter?.(params, query);
163
187
  $currentPath = path;
@@ -25,15 +25,35 @@ export default function ListItemRender($desc, instance) {
25
25
 
26
26
  const $list = instance.$parent;
27
27
 
28
- // Checkbox — réactif via ShowIf
29
28
  const selectedValues = $list.$description.selectedValues;
30
29
  const value = $desc.value ?? instance;
31
30
  const isChecked = selectedValues ? selectedValues.isIncludes(value) : null;
32
31
 
32
+
33
33
  if($list.$description?.selectable || $desc.selected) {
34
34
  const selectedSource = $desc.selected ?? isChecked;
35
35
  props.class.add('is-selected', selectedSource);
36
36
  props['aria-selected'] = selectedSource;
37
+
38
+ if(selectedSource.__$Observable) {
39
+ selectedSource.subscribe((isSelected) => {
40
+ const $values = $list.$description.selectedValues;
41
+ if (!$values) return;
42
+ const value = $desc.value ?? instance;
43
+
44
+ if(isSelected) {
45
+ if(!$values.includes(value)) {
46
+ if (!$list.$description.multiSelect.val()) {
47
+ $values.clear();
48
+ }
49
+ $values.push(value);
50
+ }
51
+ } else {
52
+ $values.removeItem(value);
53
+ }
54
+ });
55
+ }
56
+
37
57
  }
38
58
 
39
59
  if ($list?.$description?.selectByCheckbox) {
@@ -48,26 +68,21 @@ export default function ListItemRender($desc, instance) {
48
68
  );
49
69
  }
50
70
 
71
+
72
+
51
73
  if ($desc.icon) {
52
74
  frontContent.push(Span({ class: 'list-item-icon' }, $desc.icon));
53
75
  }
54
76
 
55
77
  frontContent.push(buildBody($desc));
56
78
 
57
- // Select indicator — réactif via isIncludes
58
79
  if ($list?.$description?.selectByClick) {
59
- const selectedValues = $list.$description.selectedValues;
60
- const value = $desc.value ?? instance;
61
- const isSelected = selectedValues ? selectedValues.isIncludes(value) : null;
62
-
63
- if (isSelected) {
64
- frontContent.push(
65
- Span({
66
- class: isSelected.transform((s) => 'list-item-indicator' + (s ? ' is-visible' : '')),
67
- 'aria-hidden': 'true',
68
- }, $desc.isSelectedIcon)
69
- );
70
- }
80
+ frontContent.push(
81
+ Span({
82
+ class: isChecked.transform((s) => 'list-item-indicator' + (s ? ' is-visible' : '')),
83
+ 'aria-hidden': 'true',
84
+ }, $desc.isSelectedIcon)
85
+ );
71
86
  }
72
87
 
73
88
  if ($desc.trailing) {
@@ -138,6 +138,7 @@ export interface ObservableItem<T = any> {
138
138
  toTrimmed(): ObservableChecker<string>;
139
139
  toBoolean(): ObservableChecker<boolean>;
140
140
  toLiteral(template: string, placeholder?: string): ObservableChecker<string>;
141
+ toNumber(): ObservableChecker<number>;
141
142
  toFormatted(template: string, placeholder?: string): ObservableChecker<string>;
142
143
  toProperty<U = any>(path: string): ObservableChecker<U>;
143
144
  toLength(): ObservableChecker<number>;
package/types/router.d.ts CHANGED
@@ -44,7 +44,8 @@ export interface Router {
44
44
  middlewares?: Function[];
45
45
  shouldRebuild?: boolean;
46
46
  with?: Record<string, string>;
47
- layout?: (children: ValidChild) => ValidChild ;
47
+ layout?: (children: ValidChild) => ValidChild;
48
+ watch?: string[]
48
49
  }): this;
49
50
  notFound(page: (context: RouteContext) => HTMLElement | DocumentFragment): void;
50
51