ks-fwork 1.0.6 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ks-fwork",
3
- "version": "1.0.6",
3
+ "version": "2.0.1",
4
4
  "description": "Demo Frontend Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -12,8 +12,5 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "test": "echo \"Error: no test specified\" && exit 1"
15
- },
16
- "dependencies": {
17
- "ks-fwork": "^1.0.1"
18
15
  }
19
16
  }
package/src/app.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { destroyDOM } from "./destroy-dom.js";
2
2
  import { Dispatcher } from "./dispatcher.js";
3
- import { mountDOM} from "./mount-dom.js";
3
+ import { mountDOM } from "./mount-dom.js";
4
+ import { patchDOM } from "./patch-dom.js";
4
5
 
5
6
  export function createApp({state, view, reducers = {} }) {
6
7
  let parentEl = null;
7
8
  let rootNode = null;
9
+ let isMounted = false;
8
10
 
9
11
  const dispatcher = new Dispatcher();
10
12
  const unsubscribers = [dispatcher.afterEveryCommand(renderApp)];
@@ -25,19 +27,19 @@ export function createApp({state, view, reducers = {} }) {
25
27
  }
26
28
 
27
29
  function renderApp() {
28
- if (rootNode) {
29
- destroyDOM(rootNode);
30
- }
30
+ const newRootNode = view(state, emit);
31
31
 
32
- rootNode = view(state, emit);
33
- mountDOM(rootNode, parentEl);
32
+ rootNode = patchDOM(rootNode, newRootNode, parentEl);
34
33
  }
35
34
 
36
35
  return {
37
36
  // Attach the app to a real DOM element and do the first render.
38
37
  mount(_parentEl) {
38
+ if (isMounted) throw new Error('The application is already mounted');
39
+
39
40
  parentEl = _parentEl;
40
- renderApp();
41
+ rootNode = view(state, emit);
42
+ mountDOM(rootNode, parentEl);
41
43
  },
42
44
 
43
45
  // Detach the app: remove its DOM and cancel every subscription.
@@ -47,6 +49,8 @@ export function createApp({state, view, reducers = {} }) {
47
49
  }
48
50
  rootNode = null;
49
51
  unsubscribers.forEach((unsubscribe) => unsubscribe());
52
+
53
+ isMounted = false;
50
54
  },
51
55
  }
52
56
  }
package/src/h.js CHANGED
@@ -51,4 +51,20 @@ export function lipsum(numberOfParagraphs) {
51
51
  h('p', {}, [text])
52
52
  )
53
53
  );
54
+ }
55
+
56
+ export function extractChildren(vNode) {
57
+ if (vNode.children == null) return [];
58
+
59
+ const children = [];
60
+
61
+ for (const child of vNode.children) {
62
+ if (child.type === DOM_TYPES.FRAGMENT) {
63
+ children.push(...extractChildren(child))
64
+ } else {
65
+ children.push(child);
66
+ }
67
+ }
68
+
69
+ return children;
54
70
  }
package/src/mount-dom.js CHANGED
@@ -3,20 +3,20 @@ import { setAttributes } from './attributes.js'
3
3
  import { addEventListeners } from "./events.js";
4
4
 
5
5
  // Recursively mount a virtual node (and its subtree) into a real parent DOM element.
6
- export function mountDOM(vNode, parentEl) {
6
+ export function mountDOM(vNode, parentEl, index) {
7
7
  switch (vNode.type) {
8
8
  case DOM_TYPES.TEXT: {
9
- createTextNode(vNode, parentEl);
9
+ createTextNode(vNode, parentEl, index);
10
10
  break;
11
11
  }
12
12
 
13
13
  case DOM_TYPES.ELEMENT: {
14
- createElementNode(vNode, parentEl);
14
+ createElementNode(vNode, parentEl, index);
15
15
  break;
16
16
  }
17
17
 
18
18
  case DOM_TYPES.FRAGMENT: {
19
- createFragmentNode(vNode, parentEl);
19
+ createFragmentNode(vNode, parentEl, index);
20
20
  break;
21
21
  }
22
22
 
@@ -27,18 +27,18 @@ export function mountDOM(vNode, parentEl) {
27
27
  }
28
28
 
29
29
  // Create a real text node, save a reference to it on the vNode, and append it to the parent.
30
- function createTextNode(vNode, parentEl) {
30
+ function createTextNode(vNode, parentEl, index) {
31
31
  const { value } = vNode;
32
32
 
33
33
  const textNode = document.createTextNode(value);
34
34
  vNode.el = textNode;
35
35
 
36
- parentEl.append(textNode);
36
+ insert(textNode, parentEl, index);
37
37
  }
38
38
 
39
39
  // Create a real element, apply its props, recursively mount its children into it,
40
40
  // then append the finished element to the parent.
41
- function createElementNode(vNode, parentEl) {
41
+ function createElementNode(vNode, parentEl, index) {
42
42
  const { tag, props, children } = vNode;
43
43
 
44
44
  const el = document.createElement(tag);
@@ -46,15 +46,17 @@ function createElementNode(vNode, parentEl) {
46
46
  vNode.el = el;
47
47
 
48
48
  children.forEach((child) => mountDOM(child, el));
49
- parentEl.append(el);
49
+ insert(el, parentEl, index);
50
50
  }
51
51
 
52
52
  // Fragment has no DOM element. Mount its children directly into the parent.
53
- function createFragmentNode(vNode, parentEl) {
53
+ function createFragmentNode(vNode, parentEl, index) {
54
54
  const { children } = vNode;
55
55
  vNode.el = parentEl;
56
56
 
57
- children.forEach((child) => mountDOM(child, parentEl));
57
+ children.forEach((child, i) => {
58
+ mountDOM(child, parentEl, index ? index + i : null)
59
+ });
58
60
  }
59
61
 
60
62
  // Split props into event listeners ('on') and attributes and add them to DOM element.
@@ -63,4 +65,23 @@ function addProps(el, props, vNode) {
63
65
 
64
66
  vNode.listeners = addEventListeners(events, el);
65
67
  setAttributes(el, attrs);
68
+ }
69
+
70
+ function insert(el, parentEl, index) {
71
+ if (index == null) {
72
+ parentEl.append(el);
73
+ return;
74
+ }
75
+
76
+ if (index < 0) {
77
+ throw new Error(`Index must be a positive number, got ${index}`);
78
+ }
79
+
80
+ const children = parentEl.childNodes;
81
+
82
+ if (index >= children.length) {
83
+ parentEl.append(el);
84
+ } else {
85
+ parentEl.insertBefore(el, children[index]);
86
+ }
66
87
  }
@@ -0,0 +1,11 @@
1
+ import { DOM_TYPES } from "./h.js";
2
+
3
+ export function areNodesEqual(nodeOne, nodeTwo) {
4
+ if (nodeOne.type !== nodeTwo.type) return false;
5
+
6
+ if (nodeOne.type === DOM_TYPES.ELEMENT) {
7
+ return nodeOne.tag === nodeTwo.tag;
8
+ }
9
+
10
+ return true;
11
+ }
@@ -0,0 +1,179 @@
1
+ import { areNodesEqual } from "./nodes-equal.js";
2
+ import { destroyDOM } from "./destroy-dom.js";
3
+ import { mountDOM } from "./mount-dom.js";
4
+ import { DOM_TYPES, extractChildren } from "./h.js";
5
+ import { objectsDiff } from "./utils/objects.js";
6
+ import { removeAttribute, removeStyle, setAttribute, setStyle } from "./attributes.js";
7
+ import { ARRAY_DIFF_OP, arraysDiff, arraysDiffSequence } from "./utils/arrays.js";
8
+ import { isNotBlankOrEmptyString } from "./utils/strings.js";
9
+ import { addEventListener } from "./events.js";
10
+
11
+ export function patchDOM(oldVNode, newVNode, parentEl) {
12
+ if (!areNodesEqual(oldVNode, newVNode)) {
13
+ const index = findIndexInParent(parentEl, oldVNode.el);
14
+ destroyDOM(oldVNode);
15
+ mountDOM(newVNode, parentEl, index);
16
+
17
+ return newVNode;
18
+ }
19
+
20
+ newVNode.el = oldVNode.el;
21
+
22
+ switch (newVNode.type) {
23
+ case DOM_TYPES.TEXT: {
24
+ patchText(oldVNode, newVNode);
25
+ return newVNode;
26
+ }
27
+
28
+ case DOM_TYPES.ELEMENT: {
29
+ patchElement(oldVNode, newVNode);
30
+ break;
31
+ }
32
+ }
33
+
34
+ patchChildren(oldVNode, newVNode);
35
+
36
+ return newVNode;
37
+ }
38
+
39
+ function findIndexInParent(parentEl, el) {
40
+ const index = Array.from(parentEl.childNodes).indexOf(el);
41
+ if (index < 0) return null;
42
+
43
+ return index;
44
+ }
45
+
46
+ function patchText(oldVNode, newVNode) {
47
+ const el = oldVNode.el;
48
+ const { value: oldText } = oldVNode;
49
+ const { value: newText } = newVNode;
50
+
51
+ if (oldText !== newText) {
52
+ el.nodeValue = newText;
53
+ }
54
+ }
55
+
56
+ function patchElement(oldVNode, newVNode) {
57
+ const el = oldVNode.el;
58
+
59
+ const {
60
+ class: oldClass,
61
+ style: oldStyle,
62
+ on: oldEvents,
63
+ ...oldAttrs
64
+ } = oldVNode.props;
65
+
66
+ const {
67
+ class: newClass,
68
+ style: newStyle,
69
+ on: newEvents,
70
+ ...newAttrs
71
+ } = newVNode.props;
72
+
73
+ const { listeners: oldListeners } = oldVNode;
74
+
75
+ patchAttrs(el, oldAttrs, newAttrs);
76
+ patchClasses(el, oldClass, newClass);
77
+ patchStyles(el, oldStyle, newStyle);
78
+ newVNode.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
79
+ }
80
+
81
+ function patchAttrs(el, oldAttrs, newAttrs) {
82
+ const { added, removed, updated } = objectsDiff(oldAttrs, newAttrs);
83
+
84
+ for (const attr of removed) {
85
+ removeAttribute(el, attr);
86
+ }
87
+
88
+ // Concatenate added and updated attributes and set them to element.
89
+ for (const attr of added.concat(updated)) {
90
+ setAttribute(el, attr, newAttrs[attr]);
91
+ }
92
+ }
93
+
94
+ function patchClasses(el, oldClass, newClass) {
95
+ const oldClasses = toClassList(oldClass);
96
+ const newClasses = toClassList(newClass);
97
+
98
+ const { added, removed } = arraysDiff(oldClasses, newClasses);
99
+
100
+ if (removed.length > 0) {
101
+ el.classList.remove(...removed);
102
+ }
103
+
104
+ if (added.length > 0) {
105
+ el.classList.add(...added);
106
+ }
107
+ }
108
+
109
+ function toClassList(classes = '') {
110
+ return Array.isArray(classes)
111
+ ? classes.filter(isNotBlankOrEmptyString)
112
+ : classes.split(/(\s+)/).filter(isNotBlankOrEmptyString);
113
+ }
114
+
115
+ function patchStyles(el, oldStyle = {}, newStyle = {}) {
116
+ const { added, removed, updated} = objectsDiff(oldStyle, newStyle);
117
+
118
+ for (const style of removed) {
119
+ removeStyle(el, style);
120
+ }
121
+
122
+ for (const style of added.concat(updated)) {
123
+ setStyle(el, style, newStyle[style]);
124
+ }
125
+ }
126
+
127
+ function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
128
+ const { removed, added, updated} = objectsDiff(oldEvents, newEvents);
129
+
130
+ for (const eventName of removed.concat(updated)) {
131
+ el.removeEventListener(eventName, oldListeners[eventName]);
132
+ }
133
+
134
+ const addedListeners = {};
135
+
136
+ for (const eventName of added.concat(updated)) {
137
+ addedListeners[eventName] = addEventListener(eventName, newEvents[eventName], el);
138
+ }
139
+
140
+ return addedListeners;
141
+ }
142
+
143
+ function patchChildren(oldVNode, newVNode) {
144
+ const oldChildren = extractChildren(oldVNode);
145
+ const newChildren = extractChildren(newVNode);
146
+ const parentEl = oldVNode.el;
147
+
148
+ const diffSeq = arraysDiffSequence(oldChildren, newChildren, areNodesEqual);
149
+
150
+ for (const operation of diffSeq) {
151
+ const { originalIndex, from, index, item } = operation;
152
+
153
+ switch (operation.op) {
154
+ case ARRAY_DIFF_OP.ADD: {
155
+ mountDOM(item, parentEl, index)
156
+ break;
157
+ }
158
+
159
+ case ARRAY_DIFF_OP.REMOVE: {
160
+ destroyDOM(item);
161
+ break;
162
+ }
163
+
164
+ case ARRAY_DIFF_OP.MOVE: {
165
+ const el = oldChildren[from].el;
166
+ const elAtTargetIndex = parentEl.childNodes[index];
167
+
168
+ parentEl.insertBefore(el, elAtTargetIndex);
169
+ patchDOM(oldChildren[from], newChildren[index], parentEl);
170
+ break;
171
+ }
172
+
173
+ case ARRAY_DIFF_OP.NOOP: {
174
+ patchDOM(oldChildren[originalIndex], newChildren[index], parentEl)
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ }
@@ -1,4 +1,202 @@
1
1
  // Filter out null values from the array.
2
2
  export function withoutNulls(arr) {
3
3
  return arr.filter((item) => item != null);
4
+ }
5
+
6
+ export function arraysDiff(oldArray, newArray) {
7
+ return {
8
+ added: newArray.filter(
9
+ (newItem) => !oldArray.includes(newItem)
10
+ ),
11
+ removed: oldArray.filter(
12
+ (oldItem) => !newArray.includes(oldItem)
13
+ ),
14
+ }
15
+ }
16
+
17
+ export const ARRAY_DIFF_OP = {
18
+ ADD: 'add',
19
+ REMOVE: 'remove',
20
+ MOVE: 'move',
21
+ NOOP: 'noop'
22
+ }
23
+
24
+ class ArrayWithOriginalIndices {
25
+ #oldArray = [];
26
+ #originalIndices = [];
27
+ #equalsFn;
28
+
29
+ constructor(array, equalsFn) {
30
+ this.#oldArray = [...array];
31
+ this.#originalIndices = array.map((value, index) => index);
32
+ this.#equalsFn = equalsFn;
33
+ }
34
+
35
+ get length() {
36
+ return this.#oldArray.length;
37
+ }
38
+
39
+ originalIndexAt(index) {
40
+ return this.#originalIndices[index];
41
+ }
42
+
43
+ // REMOVE
44
+ isRemoval(index, newArray) {
45
+ // Bail out if index is past the end of the old array.
46
+ if (index >= this.length) { return false; }
47
+
48
+ const item = this.#oldArray[index];
49
+ const indexInNewArray = newArray.findIndex((newArrayItem) => this.#equalsFn(item, newArrayItem))
50
+
51
+ return indexInNewArray === -1;
52
+ }
53
+
54
+ removeItem(index) {
55
+ const operation = {
56
+ op: ARRAY_DIFF_OP.REMOVE,
57
+ index,
58
+ item: this.#oldArray[index],
59
+ }
60
+
61
+ this.#oldArray.splice(index, 1);
62
+ this.#originalIndices.splice(index, 1);
63
+
64
+ return operation;
65
+ }
66
+
67
+ // NOOP
68
+ isNoop(index, newArray) {
69
+ // Bail out if index is past the end of the old array.
70
+ if (index >= this.length) {return false; }
71
+
72
+ const item = this.#oldArray[index];
73
+ const newArrayItem = newArray[index];
74
+
75
+ return this.#equalsFn(item, newArrayItem);
76
+ }
77
+
78
+ noopItem(index) {
79
+ return {
80
+ op: ARRAY_DIFF_OP.NOOP,
81
+ originalIndex: this.originalIndexAt(index),
82
+ index,
83
+ item: this.#oldArray[index],
84
+ }
85
+ }
86
+
87
+ // ADD
88
+ isAddition(item, fromIndex) {
89
+ return this.findFromIndex(item, fromIndex) === -1;
90
+ }
91
+
92
+ findFromIndex(item, fromIndex) {
93
+ for (let i = fromIndex; i < this.length; i++) {
94
+ if (this.#equalsFn(item, this.#oldArray[i])) {
95
+ return i;
96
+ }
97
+ }
98
+
99
+ return -1;
100
+ }
101
+
102
+ addItem(item, index) {
103
+ const operation = {
104
+ op: ARRAY_DIFF_OP.ADD,
105
+ index,
106
+ item
107
+ }
108
+
109
+ this.#oldArray.splice(index, 0, item);
110
+ this.#originalIndices.splice(index, 0, -1);
111
+
112
+ return operation;
113
+ }
114
+
115
+ // MOVE
116
+ moveItem(item, toIndex) {
117
+ const fromIndex = this.findFromIndex(item, toIndex);
118
+
119
+ const operation = {
120
+ op: ARRAY_DIFF_OP.MOVE,
121
+ originalIndex: this.originalIndexAt(fromIndex),
122
+ from: fromIndex,
123
+ index: toIndex,
124
+ item: this.#oldArray[fromIndex],
125
+ }
126
+
127
+ const [itemToMove] = this.#oldArray.splice(fromIndex, 1);
128
+ this.#oldArray.splice(toIndex, 0, itemToMove);
129
+
130
+ const [originalIndex] = this.#originalIndices.splice(fromIndex, 1);
131
+ this.#originalIndices.splice(toIndex, 0, originalIndex);
132
+
133
+ return operation;
134
+ }
135
+
136
+ // REMOVE ITEMS AFTER
137
+ removeItemsAfter(index) {
138
+ const operations = [];
139
+
140
+ while (this.length > index) {
141
+ operations.push(this.removeItem(index))
142
+ }
143
+
144
+ return operations;
145
+ }
146
+ }
147
+
148
+ export function arraysDiffSequence(oldArray, newArray, equalsFn = (a, b) => a === b) {
149
+ const sequence = [];
150
+
151
+ // Store old array with original indices preserved.
152
+ const array = new ArrayWithOriginalIndices(oldArray, equalsFn);
153
+
154
+ for (let index = 0; index < newArray.length; index++) {
155
+ if (array.isRemoval(index, newArray)) {
156
+ sequence.push(array.removeItem(index));
157
+ index--
158
+ continue;
159
+ }
160
+
161
+ if (array.isNoop(index, newArray)) {
162
+ sequence.push(array.noopItem(index));
163
+ continue;
164
+ }
165
+
166
+ const item = newArray[index];
167
+
168
+ if (array.isAddition(item, index)) {
169
+ sequence.push(array.addItem(item, index));
170
+ continue;
171
+ }
172
+
173
+ sequence.push(array.moveItem(item, index));
174
+ }
175
+
176
+ sequence.push(...array.removeItemsAfter(newArray.length));
177
+
178
+ return sequence;
179
+ }
180
+
181
+ export function applyArraysDiffSequence(oldArray, diffSequence) {
182
+ return diffSequence.reduce((array, { op, item, index, from }) => {
183
+ switch (op) {
184
+ // Insert an item at the specified index.
185
+ case 'add':
186
+ array.splice(index, 0, item)
187
+ break
188
+
189
+ // Remove an element at the specified index.
190
+ case 'remove':
191
+ array.splice(index, 1)
192
+ break
193
+
194
+ // Remove element at index 'from' and move it to specified index.
195
+ case 'move':
196
+ array.splice(index, 0, array.splice(from, 1)[0])
197
+ break
198
+ }
199
+
200
+ return array
201
+ }, oldArray)
4
202
  }
@@ -0,0 +1,12 @@
1
+ export function objectsDiff(oldObj, newObj) {
2
+ const oldKeys = Object.keys(oldObj);
3
+ const newKeys = Object.keys(newObj);
4
+
5
+ return {
6
+ added: newKeys.filter((newKey) => !(newKey in oldObj)),
7
+ removed: oldKeys.filter((oldKey) => !(oldKey in newObj)),
8
+ updated: newKeys.filter(
9
+ (key) => key in oldObj && oldObj[key] !== newObj[key]
10
+ ),
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ export function isNotEmptyString(str) {
2
+ return str !== '';
3
+ }
4
+
5
+ export function isNotBlankOrEmptyString(str) {
6
+ return isNotEmptyString(str.trim());
7
+ }