native-document 1.0.14 → 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 +1262 -839
  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 +25 -24
  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 +87 -110
  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
@@ -16,101 +16,180 @@ export default function ObservableItem(value) {
16
16
  throw new NativeDocumentError('ObservableItem cannot be an Observable');
17
17
  }
18
18
 
19
- const $initialValue = (typeof value === 'object') ? JSON.parse(JSON.stringify(value)) : value;
20
-
21
- let $previousValue = value;
22
- let $currentValue = value;
23
- let $isCleanedUp = false;
19
+ this.$previousValue = value;
20
+ this.$currentValue = value;
21
+ this.$isCleanedUp = false;
22
+
23
+ this.$listeners = null;
24
+ this.$watchers = null;
25
+
26
+ this.$memoryId = MemoryManager.register(this);
27
+ }
28
+
29
+ Object.defineProperty(ObservableItem.prototype, '$value', {
30
+ get() {
31
+ return this.$currentValue;
32
+ },
33
+ set(value) {
34
+ this.set(value);
35
+ },
36
+ configurable: true,
37
+ });
38
+
39
+ ObservableItem.prototype.triggerListeners = function(operations) {
40
+ const $listeners = this.$listeners;
41
+ const $previousValue = this.$previousValue;
42
+ const $currentValue = this.$currentValue;
43
+
44
+ operations = operations || {};
45
+ if($listeners?.length) {
46
+ for(let i = 0, length = $listeners.length; i < length; i++) {
47
+ $listeners[i]($currentValue, $previousValue, operations);
48
+ }
49
+ }
50
+ };
24
51
 
25
- const $listeners = [];
52
+ ObservableItem.prototype.triggerWatchers = function() {
53
+ if(!this.$watchers) {
54
+ return;
55
+ }
26
56
 
27
- const $memoryId = MemoryManager.register(this, $listeners);
57
+ const $watchers = this.$watchers;
58
+ const $previousValue = this.$previousValue;
59
+ const $currentValue = this.$currentValue;
28
60
 
29
- this.trigger = () => {
30
- $listeners.forEach(listener => {
31
- try {
32
- listener($currentValue, $previousValue);
33
- } catch (error) {
34
- DebugManager.error('Listener Undefined', 'Error in observable listener:', error);
35
- this.unsubscribe(listener);
61
+ if($watchers.has($currentValue)) {
62
+ const watchValueList = $watchers.get($currentValue);
63
+ watchValueList.forEach(itemValue => {
64
+ if(itemValue.ifTrue.called) {
65
+ return;
36
66
  }
67
+ itemValue.ifTrue.callback();
68
+ itemValue.else.called = false;
69
+ })
70
+ }
71
+ if($watchers.has($previousValue)) {
72
+ const watchValueList = $watchers.get($previousValue);
73
+ watchValueList.forEach(itemValue => {
74
+ if(itemValue.else.called) {
75
+ return;
76
+ }
77
+ itemValue.else.callback();
78
+ itemValue.ifTrue.called = false;
37
79
  });
80
+ }
81
+ };
38
82
 
39
- };
40
-
41
- this.originalValue = () => $initialValue;
83
+ ObservableItem.prototype.trigger = function(operations) {
84
+ this.triggerListeners(operations);
85
+ this.triggerWatchers();
86
+ }
42
87
 
43
- /**
44
- * @param {*} data
45
- */
46
- this.set = (data) => {
47
- const newValue = (typeof data === 'function') ? data($currentValue) : data;
48
- if($currentValue === newValue) {
49
- return;
88
+ /**
89
+ * @param {*} data
90
+ */
91
+ ObservableItem.prototype.set = function(data) {
92
+ const newValue = (typeof data === 'function') ? data(this.$currentValue) : data;
93
+ if(this.$currentValue === newValue) {
94
+ return;
95
+ }
96
+ this.$previousValue = this.$currentValue;
97
+ this.$currentValue = newValue;
98
+ this.trigger();
99
+ };
100
+
101
+ ObservableItem.prototype.val = function() {
102
+ return this.$currentValue;
103
+ };
104
+
105
+ ObservableItem.prototype.disconnectAll = function() {
106
+ this.$listeners?.splice(0);
107
+ this.$previousValue = null;
108
+ this.$currentValue = null;
109
+ if(this.$watchers) {
110
+ for (const [_, watchValueList] of this.$watchers) {
111
+ for (const itemValue of watchValueList) {
112
+ itemValue.ifTrue.callback = null;
113
+ itemValue.else.callback = null;
114
+ }
115
+ watchValueList.clear();
50
116
  }
51
- $previousValue = $currentValue;
52
- $currentValue = newValue;
53
- this.trigger();
54
- };
117
+ }
118
+ this.$watchers?.clear();
119
+ this.$listeners = null;
120
+ this.$watchers = null;
121
+ }
122
+ ObservableItem.prototype.cleanup = function() {
123
+ MemoryManager.unregister(this.$memoryId);
124
+ this.disconnectAll();
125
+ this.$isCleanedUp = true;
126
+ delete this.$value;
127
+ }
55
128
 
56
- this.val = () => $currentValue;
129
+ /**
130
+ *
131
+ * @param {Function} callback
132
+ * @returns {(function(): void)}
133
+ */
134
+ ObservableItem.prototype.subscribe = function(callback) {
135
+ this.$listeners = this.$listeners ?? [];
136
+ if (this.$isCleanedUp) {
137
+ DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
138
+ return () => {};
139
+ }
140
+ if (typeof callback !== 'function') {
141
+ throw new NativeDocumentError('Callback must be a function');
142
+ }
57
143
 
58
- this.cleanup = function() {
59
- $listeners.splice(0);
60
- $isCleanedUp = true;
61
- };
144
+ this.$listeners.push(callback);
145
+ return () => this.unsubscribe(callback);
146
+ };
62
147
 
63
- /**
64
- *
65
- * @param {Function} callback
66
- * @returns {(function(): void)}
67
- */
68
- this.subscribe = (callback) => {
69
- if ($isCleanedUp) {
70
- DebugManager.warn('Observable subscription', '⚠️ Attempted to subscribe to a cleaned up observable.');
71
- return () => {};
72
- }
73
- if (typeof callback !== 'function') {
74
- throw new NativeDocumentError('Callback must be a function');
75
- }
148
+ ObservableItem.prototype.on = function(value, callback, elseCallback) {
149
+ this.$watchers = this.$watchers ?? new Map();
76
150
 
77
- $listeners.push(callback);
78
- return () => this.unsubscribe(callback);
79
- };
151
+ let watchValueList = this.$watchers.get(value);
152
+ if(!watchValueList) {
153
+ watchValueList = new Set();
154
+ this.$watchers.set(value, watchValueList);
155
+ }
80
156
 
81
- /**
82
- * Unsubscribe from an observable.
83
- * @param {Function} callback
84
- */
85
- this.unsubscribe = (callback) => {
86
- const index = $listeners.indexOf(callback);
87
- if (index > -1) {
88
- $listeners.splice(index, 1);
89
- }
157
+ let itemValue = {
158
+ ifTrue: { callback, called: false },
159
+ else: { callback: elseCallback, called: false }
90
160
  };
91
-
92
- /**
93
- * Create an Observable checker instance
94
- * @param callback
95
- * @returns {ObservableChecker}
96
- */
97
- this.check = function(callback) {
98
- return new ObservableChecker(this, callback)
161
+ watchValueList.add(itemValue);
162
+ return () => {
163
+ watchValueList?.delete(itemValue);
164
+ if(watchValueList.size === 0) {
165
+ this.$watchers?.delete(value);
166
+ }
167
+ watchValueList = null;
168
+ itemValue = null;
99
169
  };
170
+ };
100
171
 
101
- const $object = this;
102
- Object.defineProperty($object, '$value', {
103
- get() {
104
- return $object.val();
105
- },
106
- set(value) {
107
- $object.set(value);
108
- return $object;
109
- }
110
- })
172
+ /**
173
+ * Unsubscribe from an observable.
174
+ * @param {Function} callback
175
+ */
176
+ ObservableItem.prototype.unsubscribe = function(callback) {
177
+ const index = this.$listeners.indexOf(callback);
178
+ if (index > -1) {
179
+ this.$listeners.splice(index, 1);
180
+ }
181
+ };
111
182
 
112
- this.toString = function() {
113
- return '{{#ObItem::(' +$memoryId+ ')}}';
114
- };
183
+ /**
184
+ * Create an Observable checker instance
185
+ * @param callback
186
+ * @returns {ObservableChecker}
187
+ */
188
+ ObservableItem.prototype.check = function(callback) {
189
+ return new ObservableChecker(this, callback)
190
+ };
191
+ ObservableItem.prototype.get = ObservableItem.prototype.check;
115
192
 
193
+ ObservableItem.prototype.toString = function() {
194
+ return '{{#ObItem::(' +this.$memoryId+ ')}}';
116
195
  }
@@ -0,0 +1,74 @@
1
+ import NativeDocumentError from "../../errors/NativeDocumentError";
2
+ import {Observable} from "../Observable";
3
+ import ObservableItem from "../ObservableItem";
4
+
5
+
6
+ const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'];
7
+
8
+ /**
9
+ *
10
+ * @param {Array} target
11
+ * @returns {ObservableItem}
12
+ */
13
+ Observable.array = function(target) {
14
+ if(!Array.isArray(target)) {
15
+ throw new NativeDocumentError('Observable.array : target must be an array');
16
+ }
17
+ const observer = Observable(target);
18
+
19
+ methods.forEach((method) => {
20
+ observer[method] = function(...values) {
21
+ const result = observer.val()[method](...values);
22
+ observer.trigger({ action: method, args: values, result });
23
+ return result;
24
+ };
25
+ });
26
+
27
+ observer.clear = function() {
28
+ observer.$value.length = 0;
29
+ observer.trigger({ action: 'clear' });
30
+ return true;
31
+ };
32
+
33
+ observer.remove = function(index) {
34
+ const deleted = observer.$value.splice(index, 1);
35
+ if(deleted.length === 0) {
36
+ return [];
37
+ }
38
+ observer.trigger({ action: 'remove', args: [index], result: deleted[0] });
39
+ return deleted;
40
+ };
41
+
42
+ observer.swap = function(indexA, indexB) {
43
+ const value = observer.$value;
44
+ const length = value.length;
45
+ if(length < indexA || length < indexB) {
46
+ return false;
47
+ }
48
+ if(indexB < indexA) {
49
+ const temp = indexA;
50
+ indexA = indexB;
51
+ indexB = temp;
52
+ }
53
+ const elementA = value[indexA];
54
+ const elementB = value[indexB]
55
+
56
+ value[indexA] = elementB;
57
+ value[indexB] = elementA;
58
+ observer.trigger({ action: 'swap', args: [indexA, indexB], result: [elementA, elementB] });
59
+ return true;
60
+ };
61
+
62
+ observer.length = function() {
63
+ return observer.$value.length;
64
+ }
65
+
66
+ const overrideMethods = ['map', 'filter', 'reduce', 'some', 'every', 'find', 'findIndex', 'concat'];
67
+ overrideMethods.forEach((method) => {
68
+ observer[method] = function(...args) {
69
+ return observer.val()[method](...args);
70
+ };
71
+ })
72
+
73
+ return observer;
74
+ };
@@ -0,0 +1,22 @@
1
+ import Validator from "../../utils/validator";
2
+ import {Observable} from "../Observable";
3
+
4
+ /**
5
+ *
6
+ * @param {Function} callback
7
+ * @returns {Function}
8
+ */
9
+ Observable.batch = function(callback) {
10
+ const $observer = Observable(0);
11
+ const batch = function() {
12
+ if(Validator.isAsyncFunction(callback)) {
13
+ return (callback(...arguments)).then(() => {
14
+ $observer.trigger();
15
+ }).catch(error => { throw error; });
16
+ }
17
+ callback(...arguments);
18
+ $observer.trigger();
19
+ };
20
+ batch.$observer = $observer;
21
+ return batch;
22
+ };
@@ -0,0 +1,28 @@
1
+ import ObservableItem from "../ObservableItem";
2
+ import Validator from "../../utils/validator";
3
+ import NativeDocumentError from "../../errors/NativeDocumentError";
4
+ import {Observable} from "../Observable";
5
+
6
+ /**
7
+ *
8
+ * @param {Function} callback
9
+ * @param {Array|Function} dependencies
10
+ * @returns {ObservableItem}
11
+ */
12
+ Observable.computed = function(callback, dependencies = []) {
13
+ const initialValue = callback();
14
+ const observable = new ObservableItem(initialValue);
15
+ const updatedValue = () => observable.set(callback());
16
+
17
+ if(Validator.isFunction(dependencies)) {
18
+ if(!Validator.isObservable(dependencies.$observer)) {
19
+ throw new NativeDocumentError('Observable.computed : dependencies must be valid batch function');
20
+ }
21
+ dependencies.$observer.subscribe(updatedValue);
22
+ return observable;
23
+ }
24
+
25
+ dependencies.forEach(dependency => dependency.subscribe(updatedValue));
26
+
27
+ return observable;
28
+ };
@@ -0,0 +1,111 @@
1
+ import Validator from "../../utils/validator";
2
+ import {Observable} from "../Observable";
3
+
4
+ /**
5
+ *
6
+ * @param {Object} value
7
+ * @returns {Proxy}
8
+ */
9
+ Observable.init = function(value) {
10
+ const data = {};
11
+ for(const key in value) {
12
+ const itemValue = value[key];
13
+ if(Validator.isJson(itemValue)) {
14
+ data[key] = Observable.init(itemValue);
15
+ continue;
16
+ }
17
+ else if(Validator.isArray(itemValue)) {
18
+ data[key] = Observable.array(itemValue);
19
+ continue;
20
+ }
21
+ data[key] = Observable(itemValue);
22
+ }
23
+
24
+ const $val = function() {
25
+ const result = {};
26
+ for(const key in data) {
27
+ const dataItem = data[key];
28
+ if(Validator.isObservable(dataItem)) {
29
+ result[key] = dataItem.val();
30
+ } else if(Validator.isProxy(dataItem)) {
31
+ result[key] = dataItem.$value;
32
+ } else {
33
+ result[key] = dataItem;
34
+ }
35
+ }
36
+ return result;
37
+ };
38
+ const $clone = function() {
39
+
40
+ };
41
+
42
+ return new Proxy(data, {
43
+ get(target, property) {
44
+ if(property === '__isProxy__') {
45
+ return true;
46
+ }
47
+ if(property === '$value') {
48
+ return $val();
49
+ }
50
+ if(property === '$clone') {
51
+ return $clone;
52
+ }
53
+ if(target[property] !== undefined) {
54
+ return target[property];
55
+ }
56
+ return undefined;
57
+ },
58
+ set(target, prop, newValue) {
59
+ if(target[prop] !== undefined) {
60
+ target[prop].set(newValue);
61
+ }
62
+ }
63
+ })
64
+ };
65
+
66
+ /**
67
+ * Get the value of an observable or an object of observables.
68
+ * @param {ObservableItem|Object<ObservableItem>} data
69
+ * @returns {{}|*|null}
70
+ */
71
+ Observable.value = function(data) {
72
+ if(Validator.isObservable(data)) {
73
+ return data.val();
74
+ }
75
+ if(Validator.isProxy(data)) {
76
+ return data.$value;
77
+ }
78
+ if(Validator.isArray(data)) {
79
+ const result = [];
80
+ data.forEach(item => {
81
+ result.push(Observable.value(item));
82
+ });
83
+ return result;
84
+ }
85
+ return data;
86
+ };
87
+
88
+
89
+ Observable.update = function($target, data) {
90
+ for(const key in data) {
91
+ const targetItem = $target[key];
92
+ const newValue = data[key];
93
+
94
+ if(Validator.isObservable(targetItem)) {
95
+ if(Validator.isArray(newValue)) {
96
+ Observable.update(targetItem, newValue);
97
+ continue;
98
+ }
99
+ targetItem.set(newValue);
100
+ continue;
101
+ }
102
+ if(Validator.isProxy(targetItem)) {
103
+ Observable.update(targetItem, newValue);
104
+ continue;
105
+ }
106
+ $target[key] = newValue;
107
+ }
108
+ };
109
+
110
+ Observable.object = Observable.init;
111
+ Observable.json = Observable.init;
@@ -33,6 +33,14 @@ export default function Anchor(name) {
33
33
  parent.insertBefore(getChildAsNode(child), target);
34
34
  };
35
35
 
36
+ element.appendElement = function(child, before = null) {
37
+ if(anchorEnd.parentNode === element) {
38
+ anchorEnd.parentNode.nativeInsertBefore(child, before || anchorEnd);
39
+ return;
40
+ }
41
+ anchorEnd.parentNode?.insertBefore(child, before || anchorEnd);
42
+ };
43
+
36
44
  element.appendChild = function(child, before = null) {
37
45
  const parent = anchorEnd.parentNode;
38
46
  if(!parent) {
@@ -41,28 +49,65 @@ export default function Anchor(name) {
41
49
  }
42
50
  before = before ?? anchorEnd;
43
51
  if(Validator.isArray(child)) {
44
- child.forEach((element) => {
45
- insertBefore(parent, element, before);
46
- });
52
+ const fragment = document.createDocumentFragment();
53
+ for(let i = 0, length = child.length; i < length; i++) {
54
+ fragment.appendChild(getChildAsNode(child[i]));
55
+ }
56
+ insertBefore(parent, fragment, before);
47
57
  return element;
48
58
  }
49
59
  insertBefore(parent, child, before);
50
60
  };
51
61
 
52
- element.remove = function(trueRemove) {
53
- if(anchorEnd.parentNode === element) {
62
+ element.removeChildren = function() {
63
+ const parent = anchorEnd.parentNode;
64
+ if(parent === element) {
65
+ return;
66
+ }
67
+ if(parent.firstChild === anchorStart && parent.lastChild === anchorEnd) {
68
+ parent.replaceChildren(anchorStart, anchorEnd);
69
+ return;
70
+ }
71
+
72
+ let itemToRemove = anchorStart.nextSibling, tempItem;
73
+ const fragment = document.createDocumentFragment();
74
+ while(itemToRemove && itemToRemove !== anchorEnd) {
75
+ tempItem = itemToRemove.nextSibling;
76
+ fragment.append(itemToRemove);
77
+ itemToRemove = tempItem;
78
+ }
79
+ fragment.replaceChildren();
80
+ }
81
+ element.remove = function() {
82
+ const parent = anchorEnd.parentNode;
83
+ if(parent === element) {
54
84
  return;
55
85
  }
56
86
  let itemToRemove = anchorStart.nextSibling, tempItem;
57
87
  while(itemToRemove !== anchorEnd) {
58
88
  tempItem = itemToRemove.nextSibling;
59
- trueRemove ? itemToRemove.remove() : element.nativeAppendChild(itemToRemove);
89
+ element.nativeAppendChild(itemToRemove);
60
90
  itemToRemove = tempItem;
61
91
  }
62
- if(trueRemove) {
63
- anchorEnd.remove();
64
- anchorStart.remove();
92
+ };
93
+
94
+ element.removeWithAnchors = function() {
95
+ element.removeChildren();
96
+ anchorStart.remove();
97
+ anchorEnd.remove();
98
+ };
99
+
100
+ element.replaceContent = function(child) {
101
+ const parent = anchorEnd.parentNode;
102
+ if(!parent) {
103
+ return;
104
+ }
105
+ if(parent.firstChild === anchorStart && parent.lastChild === anchorEnd) {
106
+ parent.replaceChildren(anchorStart, child, anchorEnd);
107
+ return;
65
108
  }
109
+ element.removeChildren();
110
+ parent.insertBefore(child, anchorEnd);
66
111
  };
67
112
 
68
113
  element.insertBefore = function(child, anchor = null) {