native-document 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/native-document.dev.js +1297 -804
  2. package/dist/native-document.min.js +1 -1
  3. package/docs/anchor.md +216 -53
  4. package/docs/conditional-rendering.md +25 -24
  5. package/docs/core-concepts.md +20 -19
  6. package/docs/elements.md +21 -20
  7. package/docs/getting-started.md +28 -27
  8. package/docs/lifecycle-events.md +2 -2
  9. package/docs/list-rendering.md +607 -0
  10. package/docs/memory-management.md +1 -1
  11. package/docs/observables.md +15 -14
  12. package/docs/routing.md +22 -22
  13. package/docs/state-management.md +8 -8
  14. package/docs/validation.md +0 -2
  15. package/index.js +6 -1
  16. package/package.json +1 -1
  17. package/readme.md +5 -4
  18. package/src/data/MemoryManager.js +8 -20
  19. package/src/data/Observable.js +2 -180
  20. package/src/data/ObservableChecker.js +26 -21
  21. package/src/data/ObservableItem.js +158 -79
  22. package/src/data/observable-helpers/array.js +74 -0
  23. package/src/data/observable-helpers/batch.js +22 -0
  24. package/src/data/observable-helpers/computed.js +28 -0
  25. package/src/data/observable-helpers/object.js +111 -0
  26. package/src/elements/anchor.js +54 -9
  27. package/src/elements/control/for-each-array.js +280 -0
  28. package/src/elements/control/for-each.js +100 -56
  29. package/src/elements/index.js +1 -0
  30. package/src/elements/list.js +4 -0
  31. package/src/utils/helpers.js +44 -21
  32. package/src/wrappers/AttributesWrapper.js +5 -18
  33. package/src/wrappers/DocumentObserver.js +58 -29
  34. package/src/wrappers/ElementCreator.js +114 -0
  35. package/src/wrappers/HtmlElementEventsWrapper.js +52 -65
  36. package/src/wrappers/HtmlElementWrapper.js +11 -167
  37. package/src/wrappers/NdPrototype.js +109 -0
@@ -0,0 +1,280 @@
1
+ import Anchor from "../anchor";
2
+ import {Observable} from "../../data/Observable";
3
+ import Validator from "../../utils/validator";
4
+ import {createTextNode} from "../../wrappers/HtmlElementWrapper";
5
+ import DebugManager from "../../utils/debug-manager";
6
+ import {getKey} from "../../utils/helpers";
7
+
8
+ export function ForEachArray(data, callback, key, configs = {}) {
9
+ const element = new Anchor('ForEach Array');
10
+ const blockEnd = element.endElement();
11
+ const blockStart = element.startElement();
12
+
13
+ let cache = new Map();
14
+ let nodeCacheByElement = new WeakMap();
15
+ let lastNumberOfItems = 0;
16
+
17
+ const keysCache = new WeakMap();
18
+
19
+ const clear = () => {
20
+ element.removeChildren();
21
+ cleanCache();
22
+ lastNumberOfItems = 0;
23
+ };
24
+ const getItemKey = (item, indexKey) => {
25
+ if(keysCache.has(item)) {
26
+ return keysCache.get(item);
27
+ }
28
+ return getKey(item, indexKey, key);
29
+ }
30
+
31
+ const updateIndexObservers = (items, startFrom = 0) => {
32
+ if(callback.length < 2) {
33
+ return;
34
+ }
35
+ let index = startFrom;
36
+ for(let i = startFrom, length = items?.length; i < length; i++) {
37
+ const cacheItem = cache.get(getItemKey(items[i], i));
38
+ if(!cacheItem) {
39
+ continue;
40
+ }
41
+ cacheItem.indexObserver?.deref()?.set(index);
42
+ index++;
43
+ }
44
+ };
45
+
46
+ const removeCacheItem = (cacheItem, removeChild = true) => {
47
+ if(!cacheItem) {
48
+ return;
49
+ }
50
+ const child = cacheItem.child?.deref();
51
+ cacheItem.indexObserver?.deref()?.cleanup();
52
+ cacheItem.child = null;
53
+ cacheItem.indexObserver = null;
54
+ nodeCacheByElement.delete(cacheItem.item);
55
+ keysCache.delete(cacheItem.item);
56
+ cacheItem.item = null;
57
+ if(removeChild) {
58
+ child?.remove();
59
+ cache.delete(cacheItem.keyId);
60
+ }
61
+ }
62
+
63
+ const removeCacheItemByKey = (keyId, removeChild = true) => {
64
+ removeCacheItem(cache.get(keyId), removeChild);
65
+ };
66
+
67
+ const cleanCache = () => {
68
+ for (const [keyId, cacheItem] of cache.entries()) {
69
+ removeCacheItem(cacheItem, false);
70
+ }
71
+ cache.clear();
72
+ }
73
+
74
+ const buildItem = (item, indexKey) => {
75
+ const keyId = getItemKey(item, indexKey);
76
+
77
+ if(cache.has(keyId)) {
78
+ const cacheItem = cache.get(keyId);
79
+ cacheItem.indexObserver?.deref()?.set(indexKey);
80
+ cacheItem.isNew = false;
81
+ const child = cacheItem.child?.deref();
82
+ if(child) {
83
+ return child;
84
+ }
85
+ cache.delete(keyId);
86
+ }
87
+
88
+ try {
89
+ const indexObserver = callback.length >= 2 ? Observable(indexKey) : null;
90
+ let child = callback(item, indexObserver);
91
+ if(Validator.isStringOrObservable(child)) {
92
+ child = createTextNode(child);
93
+ }
94
+ cache.set(keyId, {
95
+ keyId,
96
+ isNew: true,
97
+ item,
98
+ child: new WeakRef(child),
99
+ indexObserver: (indexObserver ? new WeakRef(indexObserver) : null)
100
+ });
101
+ keysCache.set(item, keyId);
102
+ if(Validator.isObject(item)) {
103
+ nodeCacheByElement.set(item, child);
104
+ }
105
+ return child;
106
+ } catch (e) {
107
+ DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
108
+ throw e;
109
+ }
110
+ };
111
+ const getChildByKey = function(keyId, fragment) {
112
+ const cacheItem = cache.get(keyId);
113
+ if(!cacheItem) {
114
+ return null;
115
+ }
116
+ const child = cacheItem.child?.deref();
117
+ if(!child) {
118
+ removeCacheItem(cacheItem, false);
119
+ return null;
120
+ }
121
+ return child;
122
+ };
123
+
124
+ const removeByKey = function(keyId, fragment) {
125
+ const cacheItem = cache.get(keyId);
126
+ if(!cacheItem) {
127
+ return null;
128
+ }
129
+ const child = cacheItem.child?.deref();
130
+ if(!child) {
131
+ return null;
132
+ }
133
+
134
+ if(fragment) {
135
+ fragment.appendChild(child);
136
+ return;
137
+ }
138
+ child.remove();
139
+ }
140
+
141
+ const Actions = {
142
+ toFragment(items, startIndexFrom = 0){
143
+ const fragment = document.createDocumentFragment();
144
+ for(let i = 0, length = items.length; i < length; i++) {
145
+ fragment.append(buildItem(items[i], lastNumberOfItems));
146
+ lastNumberOfItems++;
147
+ }
148
+ return fragment;
149
+ },
150
+ add(items, delay = 0) {
151
+ setTimeout(() => {
152
+ element.appendElement(Actions.toFragment(items))
153
+ }, delay);
154
+ },
155
+ replace(items) {
156
+ clear();
157
+ Actions.add(items);
158
+ },
159
+ reOrder(items) {
160
+ let child = null;
161
+ const fragment = document.createDocumentFragment();
162
+ for(const item of items) {
163
+ child = nodeCacheByElement.get(item);
164
+ if(child) {
165
+ fragment.appendChild(child);
166
+ }
167
+ }
168
+ child = null;
169
+ element.appendElement(fragment, blockEnd);
170
+ },
171
+ removeOne(element, index) {
172
+ let child = nodeCacheByElement.get(element);
173
+ if(child) {
174
+ child.remove();
175
+ nodeCacheByElement.delete(element);
176
+ removeCacheItemByKey(getItemKey(element, index));
177
+ }
178
+ child = null;
179
+ },
180
+ clear,
181
+ push(items) {
182
+ let delay = 0;
183
+ if(configs.pushDelay) {
184
+ delay = configs.pushDelay(items) ?? 0;
185
+ } else {
186
+ delay = (items.length >= 1000) ? 10 : 0;
187
+ }
188
+ Actions.add(items, delay);
189
+ },
190
+ unshift(values){
191
+ element.insertBefore(Actions.toFragment(values), blockStart.nextSibling);
192
+ },
193
+ splice(args, deleted) {
194
+ const [start, deleteCount, ...values] = args;
195
+ let elementBeforeFirst = null;
196
+ const garbageFragment = document.createDocumentFragment();
197
+
198
+ if(deleted.length > 0) {
199
+ let firstKey = getItemKey(deleted[0], start);
200
+ if(deleted.length === 1) {
201
+ removeByKey(firstKey, garbageFragment);
202
+ } else if(deleted.length > 1) {
203
+ const firstChildRemoved = getChildByKey(firstKey);
204
+ elementBeforeFirst = firstChildRemoved?.previousSibling;
205
+
206
+ for(let i = 0; i < deleted.length; i++) {
207
+ const keyId = getItemKey(deleted[i], start + i, key);
208
+ removeByKey(keyId, garbageFragment);
209
+ }
210
+ }
211
+ } else {
212
+ elementBeforeFirst = blockEnd;
213
+ }
214
+ garbageFragment.replaceChildren();
215
+
216
+ if(values && values.length && elementBeforeFirst) {
217
+ element.insertBefore(Actions.toFragment(values), elementBeforeFirst.nextSibling);
218
+ }
219
+
220
+ },
221
+ reverse(_, reversed) {
222
+ Actions.reOrder(reversed);
223
+ },
224
+ sort(_, sorted) {
225
+ Actions.reOrder(sorted);
226
+ },
227
+ remove(_, deleted) {
228
+ Actions.removeOne(deleted);
229
+ },
230
+ pop(_, deleted) {
231
+ Actions.removeOne(deleted);
232
+ },
233
+ shift(_, deleted) {
234
+ Actions.removeOne(deleted);
235
+ },
236
+ swap(args, elements) {
237
+ const parent = blockEnd.parentNode;
238
+
239
+ let childA = nodeCacheByElement.get(elements[0]);
240
+ let childB = nodeCacheByElement.get(elements[1]);
241
+ if(!childA || !childB) {
242
+ return;
243
+ }
244
+
245
+ const childBNext = childB.nextSibling;
246
+ parent.insertBefore(childB, childA);
247
+ parent.insertBefore(childA, childBNext);
248
+ childA = null;
249
+ childB = null;
250
+ }
251
+ };
252
+
253
+ const buildContent = (items, _, operations) => {
254
+ if(operations.action === 'clear' || !items.length) {
255
+ if(lastNumberOfItems === 0) {
256
+ return;
257
+ }
258
+ clear();
259
+ }
260
+
261
+ if(!operations?.action) {
262
+ if(lastNumberOfItems === 0) {
263
+ Actions.add(items);
264
+ return;
265
+ }
266
+ Actions.replace(items);
267
+ }
268
+ else if(Actions[operations.action]) {
269
+ Actions[operations.action](operations.args, operations.result);
270
+ }
271
+ updateIndexObservers(items, 0);
272
+ };
273
+
274
+ buildContent(data.val(), null, {action: null});
275
+ if(Validator.isObservable(data)) {
276
+ data.subscribe(buildContent);
277
+ }
278
+
279
+ return element;
280
+ }
@@ -2,39 +2,9 @@ import ObservableItem from "../../data/ObservableItem";
2
2
  import {Observable} from "../../data/Observable";
3
3
  import {createTextNode} from "../../wrappers/HtmlElementWrapper";
4
4
  import Validator from "../../utils/validator";
5
- import {throttle} from "../../utils/helpers.js";
6
5
  import Anchor from "../anchor";
7
-
8
-
9
- /**
10
- *
11
- * @param {*} item
12
- * @param {string|null} defaultKey
13
- * @param {?Function} key
14
- * @returns {*}
15
- */
16
- const getKey = (item, defaultKey, key) => {
17
- if(Validator.isFunction(key)) return key(item, defaultKey);
18
- if(Validator.isObservable(item)) {
19
- const val = item.val();
20
- return (val && key) ? val[key] : defaultKey;
21
- }
22
- return item[key] ?? defaultKey;
23
- }
24
-
25
- /**
26
- *
27
- * @param {Map} cache
28
- * @param {Set} keyIds
29
- */
30
- const cleanBlockByCache = (cache, keyIds) => {
31
- for(const [key, {child}] of cache.entries()) {
32
- if(keyIds.has(key)) {
33
- continue;
34
- }
35
- child.remove();
36
- }
37
- }
6
+ import DebugManager from "../../utils/debug-manager";
7
+ import {getKey} from "../../utils/helpers";
38
8
 
39
9
  /**
40
10
  *
@@ -46,62 +16,136 @@ const cleanBlockByCache = (cache, keyIds) => {
46
16
  export function ForEach(data, callback, key) {
47
17
  const element = new Anchor('ForEach');
48
18
  const blockEnd = element.endElement();
19
+ const blockStart = element.startElement();
49
20
 
50
21
  let cache = new Map();
22
+ let lastKeyOrder = null;
23
+ const keyIds = new Set();
24
+
25
+ const clear = () => {
26
+ element.removeChildren();
27
+ cleanCache();
28
+ };
29
+
30
+ const cleanCache = (parent) => {
31
+ for(const [keyId, cacheItem] of cache.entries()) {
32
+ if(keyIds.has(keyId)) {
33
+ continue;
34
+ }
35
+ const child = cacheItem.child?.deref();
36
+ if(parent && child) {
37
+ parent.removeChild(child);
38
+ }
39
+ cacheItem.indexObserver?.cleanup();
40
+ cacheItem.child = null;
41
+ cacheItem.indexObserver = null;
42
+ cache.delete(cacheItem.keyId);
43
+ lastKeyOrder && lastKeyOrder.delete(cacheItem.keyId);
44
+ }
45
+ };
51
46
 
52
47
  const handleContentItem = (item, indexKey) => {
53
48
  const keyId = getKey(item, indexKey, key);
54
49
 
55
50
  if(cache.has(keyId)) {
56
- cache.get(keyId).indexObserver.set(indexKey);
51
+ const cacheItem = cache.get(keyId);
52
+ cacheItem.indexObserver?.set(indexKey);
53
+ cacheItem.isNew = false;
54
+ if(cacheItem.child?.deref()) {
55
+ return keyId;
56
+ }
57
+ cache.delete(keyId);
57
58
  }
58
- else {
59
- const indexObserver = Observable(indexKey);
59
+
60
+ try {
61
+ const indexObserver = callback.length >= 2 ? Observable(indexKey) : null;
60
62
  let child = callback(item, indexObserver);
61
63
  if(Validator.isStringOrObservable(child)) {
62
64
  child = createTextNode(child);
63
65
  }
64
- cache.set(keyId, { child, indexObserver});
66
+ cache.set(keyId, { keyId, isNew: true, child: new WeakRef(child), indexObserver});
67
+ } catch (e) {
68
+ DebugManager.error('ForEach', `Error creating element for key ${keyId}` , e);
69
+ throw e;
65
70
  }
66
71
  return keyId;
72
+ };
73
+
74
+ const batchDOMUpdates = (parent) => {
75
+ const fragment = document.createDocumentFragment();
76
+ for(const itemKey of keyIds) {
77
+ const cacheItem = cache.get(itemKey);
78
+ if(!cacheItem) {
79
+ continue;
80
+ }
81
+ const child = cacheItem.child?.deref();
82
+ child && fragment.appendChild(child);
83
+ }
84
+ parent.insertBefore(fragment, blockEnd);
67
85
  }
68
- const keyIds = new Set();
86
+
87
+ const diffingDOMUpdates = (parent) => {
88
+ const operations = [];
89
+ let fragment = document.createDocumentFragment();
90
+ const newKeys = Array.from(keyIds);
91
+ const oldKeys = Array.from(lastKeyOrder);
92
+
93
+ let currentPosition = blockStart;
94
+
95
+ for(const index in newKeys) {
96
+ const itemKey = newKeys[index];
97
+ const cacheItem = cache.get(itemKey);
98
+ if(!cacheItem) {
99
+ continue;
100
+ }
101
+ const child = cacheItem.child.deref();
102
+ if(!child) {
103
+ continue;
104
+ }
105
+ fragment.appendChild(child);
106
+ }
107
+ element.replaceContent(fragment);
108
+ };
69
109
 
70
110
  const buildContent = () => {
71
- const items = (Validator.isObservable(data)) ? data.val() : data;
72
111
  const parent = blockEnd.parentNode;
73
112
  if(!parent) {
74
113
  return;
75
114
  }
115
+
116
+ const items = (Validator.isObservable(data)) ? data.val() : data;
76
117
  keyIds.clear();
77
118
  if(Array.isArray(items)) {
78
- items.forEach((item, index) => keyIds.add(handleContentItem(item, index)));
119
+ for(let i = 0, length = items.length; i < length; i++) {
120
+ const keyId= handleContentItem(items[i], i);
121
+ keyIds.add(keyId);
122
+ }
79
123
  } else {
80
124
  for(const indexKey in items) {
81
- keyIds.add(handleContentItem(items[indexKey], indexKey));
125
+ const keyId = handleContentItem(items[indexKey], indexKey);
126
+ keyIds.add(keyId);
82
127
  }
83
128
  }
84
129
 
85
- cleanBlockByCache(cache, keyIds);
86
- let nextElementSibling = blockEnd;
87
- for(const item of [...keyIds].reverse()) {
88
- const { child } = cache.get(item);
89
- if(child) {
90
- if(nextElementSibling && nextElementSibling.previousSibling === child) {
91
- nextElementSibling = child;
92
- continue;
93
- }
94
- parent.insertBefore(child, nextElementSibling);
95
- nextElementSibling = child;
96
- }
130
+ if(keyIds.size === 0) {
131
+ clear();
132
+ lastKeyOrder?.clear();
133
+ return;
97
134
  }
135
+
136
+ cleanCache(parent);
137
+ if(!lastKeyOrder || lastKeyOrder.size === 0) {
138
+ batchDOMUpdates(parent);
139
+ } else {
140
+ diffingDOMUpdates(parent);
141
+ }
142
+ lastKeyOrder?.clear();
143
+ lastKeyOrder = new Set([...keyIds]);
98
144
  };
99
145
 
100
146
  buildContent();
101
147
  if(Validator.isObservable(data)) {
102
- data.subscribe(throttle((newValue, oldValue) => {
103
- buildContent(newValue, oldValue);
104
- }, 50, { debounce: true }))
148
+ data.subscribe(buildContent)
105
149
  }
106
150
  return element;
107
- }
151
+ }
@@ -1,6 +1,7 @@
1
1
  import HtmlElementWrapper from "../wrappers/HtmlElementWrapper";
2
2
 
3
3
  export * from './control/for-each';
4
+ export * from './control/for-each-array';
4
5
  export * from './control/show-if';
5
6
  export * from './control/switch';
6
7
  export * from './content-formatter';
@@ -4,3 +4,7 @@ export const OrderedList = HtmlElementWrapper('ol');
4
4
  export const UnorderedList = HtmlElementWrapper('ul');
5
5
  export const ListItem = HtmlElementWrapper('li');
6
6
 
7
+ export const Li = ListItem;
8
+ export const Ol = OrderedList;
9
+ export const Ul = UnorderedList;
10
+
@@ -1,35 +1,58 @@
1
+ import Validator from "./validator";
2
+
3
+ const invoke = function(fn, args, context) {
4
+ if(context) {
5
+ fn.apply(context, args);
6
+ } else {
7
+ fn(...args);
8
+ }
9
+ };
1
10
  /**
2
11
  *
3
12
  * @param {Function} fn
4
13
  * @param {number} delay
5
- * @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean}}options
14
+ * @param {{leading?:Boolean, trailing?:Boolean, debounce?:Boolean, check: Function}}options
6
15
  * @returns {(function(...[*]): void)|*}
7
16
  */
8
- export const throttle = function(fn, delay, options = {}) {
17
+ export const debounce = function(fn, delay, options = {}) {
9
18
  let timer = null;
10
- let lastExecTime = 0;
11
- const { leading = true, trailing = true, debounce = false } = options;
19
+ let lastArgs = null;
12
20
 
13
21
  return function(...args) {
14
- const now = Date.now();
15
- if (debounce) {
16
- // debounce mode: reset the timer for each call
17
- clearTimeout(timer);
18
- timer = setTimeout(() => fn.apply(this, args), delay);
19
- return;
20
- }
21
- if (leading && now - lastExecTime >= delay) {
22
- fn.apply(this, args);
23
- lastExecTime = now;
24
- }
25
- if (trailing && !timer) {
26
- timer = setTimeout(() => {
27
- fn.apply(this, args);
28
- lastExecTime = Date.now();
29
- timer = null;
30
- }, delay - (now - lastExecTime));
22
+ const context = options.context === true ? this : null;
23
+ let scopeDelay = delay;
24
+ if(options.check) {
25
+ const response = options.check(...args);
26
+ if(typeof response === 'number') {
27
+ scopeDelay = response;
28
+ }
31
29
  }
30
+ lastArgs = args;
31
+
32
+ // debounce mode: reset the timer for each call
33
+ clearTimeout(timer);
34
+ timer = setTimeout(() => invoke(fn, lastArgs, context), delay);
35
+ }
36
+ };
37
+
38
+
39
+ /**
40
+ *
41
+ * @param {*} item
42
+ * @param {string|null} defaultKey
43
+ * @param {?Function} key
44
+ * @returns {*}
45
+ */
46
+ export const getKey = (item, defaultKey, key) => {
47
+ if(Validator.isFunction(key)) return key(item, defaultKey);
48
+ if(Validator.isObservable(item)) {
49
+ const val = item.val();
50
+ return (val && key) ? val[key] : defaultKey;
51
+ }
52
+ if(!Validator.isObject(item)) {
53
+ return item;
32
54
  }
55
+ return item[key] ?? defaultKey;
33
56
  };
34
57
 
35
58
  export const trim = function(str, char) {
@@ -3,20 +3,6 @@ import NativeDocumentError from "../errors/NativeDocumentError";
3
3
  import {BOOLEAN_ATTRIBUTES} from "./constants.js";
4
4
  import {Observable} from "../data/Observable";
5
5
 
6
- /**
7
- *
8
- * @param {HTMLElement} element
9
- * @param {string} className
10
- * @param {string} value
11
- */
12
- const toggleClassItem = function(element, className, value) {
13
- if(value) {
14
- element.classList.add(className);
15
- } else {
16
- element.classList.remove(className);
17
- }
18
- }
19
-
20
6
  /**
21
7
  *
22
8
  * @param {HTMLElement} element
@@ -26,11 +12,11 @@ function bindClassAttribute(element, data) {
26
12
  for(let className in data) {
27
13
  const value = data[className];
28
14
  if(Validator.isObservable(value)) {
29
- toggleClassItem(element, className, value.val());
30
- value.subscribe(newValue => toggleClassItem(element, className, newValue));
15
+ element.classList.toggle(className, value.val());
16
+ value.subscribe(newValue => element.classList.toggle(className, newValue));
31
17
  continue;
32
18
  }
33
- toggleClassItem(element, className, value);
19
+ element.classList.toggle(className, value)
34
20
  }
35
21
  }
36
22
 
@@ -102,8 +88,8 @@ function bindAttributeWithObservable(element, attributeName, value) {
102
88
  }
103
89
  element.setAttribute(attributeName, newValue);
104
90
  };
91
+ applyValue(value.val());
105
92
  value.subscribe(applyValue);
106
- applyValue(value.val())
107
93
 
108
94
  if(attributeName === 'value') {
109
95
  element.addEventListener('input', () => value.set(element.value));
@@ -152,6 +138,7 @@ export default function AttributesWrapper(element, attributes) {
152
138
  continue;
153
139
  }
154
140
  element.setAttribute(attributeName, value);
141
+
155
142
  }
156
143
  return element;
157
144
  }