ks-fwork 2.0.1 → 3.0.0

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": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "Demo Frontend Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -11,6 +11,17 @@
11
11
  "src"
12
12
  ],
13
13
  "scripts": {
14
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "test": "echo \"Error: no test specified\" && exit 1",
15
+ "build": "rollup -c"
16
+ },
17
+ "dependencies": {
18
+ "fast-deep-equal": "^3.1.3"
19
+ },
20
+ "devDependencies": {
21
+ "@rollup/plugin-commonjs": "^29.0.3",
22
+ "@rollup/plugin-node-resolve": "^16.0.3",
23
+ "rollup": "^4.24.0",
24
+ "rollup-plugin-cleanup": "^3.2.1",
25
+ "rollup-plugin-filesize": "^10.0.0"
15
26
  }
16
27
  }
package/src/app.js CHANGED
@@ -2,8 +2,9 @@ import { destroyDOM } from "./destroy-dom.js";
2
2
  import { Dispatcher } from "./dispatcher.js";
3
3
  import { mountDOM } from "./mount-dom.js";
4
4
  import { patchDOM } from "./patch-dom.js";
5
+ import { h } from "./h.js";
5
6
 
6
- export function createApp({state, view, reducers = {} }) {
7
+ export function createApp(RootComponent, props = {}) {
7
8
  let parentEl = null;
8
9
  let rootNode = null;
9
10
  let isMounted = false;
@@ -11,25 +12,10 @@ export function createApp({state, view, reducers = {} }) {
11
12
  const dispatcher = new Dispatcher();
12
13
  const unsubscribers = [dispatcher.afterEveryCommand(renderApp)];
13
14
 
14
- // Wire each reducer to its action: when the action is emitted, replace state with the reducer's result.
15
- for (const actionName in reducers) {
16
- const reducer = reducers[actionName];
17
-
18
- const unsubscribe = dispatcher.subscribe(actionName, (payload) => {
19
- state = reducer(state, payload);
20
- })
21
- unsubscribers.push(unsubscribe);
22
- }
23
-
24
- // Passed into the view so it can trigger actions without touching the dispatcher directly.
25
- function emit(actionName, payload) {
26
- dispatcher.dispatch(actionName, payload);
27
- }
28
-
29
- function renderApp() {
30
- const newRootNode = view(state, emit);
31
-
32
- rootNode = patchDOM(rootNode, newRootNode, parentEl);
15
+ function reset() {
16
+ parentEl = null;
17
+ rootNode = null;
18
+ isMounted = false;
33
19
  }
34
20
 
35
21
  return {
@@ -38,19 +24,18 @@ export function createApp({state, view, reducers = {} }) {
38
24
  if (isMounted) throw new Error('The application is already mounted');
39
25
 
40
26
  parentEl = _parentEl;
41
- rootNode = view(state, emit);
27
+ rootNode = h(RootComponent, props);
42
28
  mountDOM(rootNode, parentEl);
29
+
30
+ isMounted = true;
43
31
  },
44
32
 
45
33
  // Detach the app: remove its DOM and cancel every subscription.
46
34
  unmount() {
47
- if (rootNode) {
48
- destroyDOM(rootNode);
49
- }
50
- rootNode = null;
51
- unsubscribers.forEach((unsubscribe) => unsubscribe());
35
+ if (!isMounted) throw new Error('The application is not mounted');
52
36
 
53
- isMounted = false;
37
+ destroyDOM(rootNode);
38
+ reset();
54
39
  },
55
40
  }
56
41
  }
@@ -0,0 +1,149 @@
1
+ import { mountDOM } from "./mount-dom.js";
2
+ import { destroyDOM } from "./destroy-dom.js";
3
+ import { patchDOM } from "./patch-dom.js";
4
+ import { DOM_TYPES, extractChildren } from "./h.js";
5
+ import { hasOwnProperty } from "./utils/objects.js";
6
+
7
+ import equal from 'fast-deep-equal';
8
+ import { Dispatcher } from "./dispatcher.js";
9
+
10
+ export function defineComponent({ render, state, ...methods }) {
11
+ class Component {
12
+ #isMounted = false;
13
+ #vNode = null;
14
+ #hostEl = null;
15
+ #eventHandlers = null;
16
+ #parentComponent = null;
17
+ #dispatcher = new Dispatcher();
18
+ #subscriptions = [];
19
+
20
+ constructor(props = {}, eventHandlers = {}, parentComponent = null) {
21
+ this.props = props;
22
+ this.state = state ? state(props) : {};
23
+ this.#eventHandlers = eventHandlers;
24
+ this.#parentComponent = parentComponent;
25
+ }
26
+
27
+ get elements() {
28
+ // If vNode is null, return an empty array
29
+ if (this.#vNode == null) {
30
+ return [];
31
+ }
32
+
33
+ // If vNode's top element is fragment, return the elements inside it
34
+ if (this.#vNode.type === DOM_TYPES.FRAGMENT) {
35
+ return extractChildren(this.#vNode).flatMap((child) => {
36
+ if (child.type === DOM_TYPES.COMPONENT) {
37
+ return child.component.elements;
38
+ }
39
+
40
+ return [child.el];
41
+ });
42
+ }
43
+
44
+ // If vNode's top element is a single node, return its element
45
+ return [this.#vNode.el];
46
+ }
47
+
48
+ // Return component's first element
49
+ get firstElement() {
50
+ return this.elements[0];
51
+ }
52
+
53
+ // Return component's first element offset inside the parent element. If is not fragment, return offset 0
54
+ get offset() {
55
+ if(this.#vNode.type === DOM_TYPES.FRAGMENT) {
56
+ return Array.from(this.#hostEl.children).indexOf(this.firstElement);
57
+ }
58
+
59
+ return 0;
60
+ }
61
+
62
+ updateProps(props) {
63
+ const newProps = { ...this.props, ...props };
64
+
65
+ if (equal(this.props, newProps)) {
66
+ return;
67
+ }
68
+
69
+ this.props = newProps;
70
+ this.#patch();
71
+ }
72
+
73
+ updateState(state) {
74
+ this.state = { ...this.state, ...state };
75
+ this.#patch();
76
+ }
77
+
78
+ render() {
79
+ return render.call(this);
80
+ }
81
+
82
+ emit(eventName, payload) {
83
+ this.#dispatcher.dispatch(eventName, payload);
84
+ }
85
+
86
+ mount(hostEl, index = null) {
87
+ if (this.#isMounted) {
88
+ throw new Error('Component is already mounted');
89
+ }
90
+
91
+ this.#vNode = this.render();
92
+ mountDOM(this.#vNode, hostEl, index, this);
93
+ this.#wireEventHandlers();
94
+
95
+ this.#hostEl = hostEl;
96
+ this.#isMounted = true;
97
+ }
98
+
99
+ unmount() {
100
+ if (!this.#isMounted) {
101
+ throw new Error('Component is not mounted');
102
+ }
103
+
104
+ destroyDOM(this.#vNode);
105
+ this.#subscriptions.forEach((unsubscribe) => unsubscribe());
106
+
107
+ this.#vNode = null;
108
+ this.#hostEl = null;
109
+ this.#isMounted = false;
110
+ this.#subscriptions = [];
111
+ }
112
+
113
+ #patch() {
114
+ if (!this.#isMounted) {
115
+ throw new Error('Component is not mounted');
116
+ }
117
+
118
+ const vNode = this.render();
119
+ this.#vNode = patchDOM(this.#vNode, vNode, this.#hostEl, this);
120
+ }
121
+
122
+ #wireEventHandlers() {
123
+ this.#subscriptions = Object.entries(this.#eventHandlers).map(
124
+ ([eventName, handler]) =>
125
+ this.#wireEventHandler(eventName, handler)
126
+ )
127
+ }
128
+
129
+ #wireEventHandler(eventName, handler) {
130
+ return this.#dispatcher.subscribe(eventName, (payload) => {
131
+ if (this.#parentComponent) {
132
+ handler.call(this.#parentComponent, payload)
133
+ } else {
134
+ handler(payload)
135
+ }
136
+ })
137
+ }
138
+ }
139
+
140
+ for (const methodName in methods) {
141
+ if (hasOwnProperty(Component, methodName)) {
142
+ throw new Error(`Method "${methodName}()" already exists in the component.`);
143
+ }
144
+
145
+ Component.prototype[methodName] = methods[methodName];
146
+ }
147
+
148
+ return Component;
149
+ }
@@ -21,6 +21,11 @@ export function destroyDOM(vNode) {
21
21
  break;
22
22
  }
23
23
 
24
+ case DOM_TYPES.COMPONENT: {
25
+ vNode.component.unmount();
26
+ break;
27
+ }
28
+
24
29
  default: {
25
30
  throw new Error(`Can't destroy virtual node of type: ${vNode.type}`);
26
31
  }
package/src/events.js CHANGED
@@ -1,20 +1,25 @@
1
- // Add an event listener to an element
2
- export function addEventListener(eventName, handler, el) {
3
- el.addEventListener(eventName, handler);
4
- return handler;
5
- }
6
-
7
1
  // Add an object of event listeners to an element
8
- export function addEventListeners(listeners = {}, el) {
2
+ export function addEventListeners(listeners = {}, el, hostComponent = null) {
9
3
  const addedListeners = {};
10
4
 
11
5
  Object.entries(listeners).forEach(([eventName, handler]) => {
12
- addedListeners[eventName] = addEventListener(eventName, handler, el);
6
+ addedListeners[eventName] = addEventListener(eventName, handler, el, hostComponent);
13
7
  });
14
8
 
15
9
  return addedListeners;
16
10
  }
17
11
 
12
+ // Add an event listener to an element
13
+ export function addEventListener(eventName, handler, el, hostComponent = null) {
14
+ function boundHandler() {
15
+ hostComponent ? handler.apply(hostComponent, arguments) : handler(...arguments);
16
+ }
17
+
18
+ el.addEventListener(eventName, boundHandler);
19
+
20
+ return boundHandler;
21
+ }
22
+
18
23
  // Remove an object of event listeners from an element
19
24
  export function removeEventListeners(listeners = {}, el) {
20
25
  Object.entries(listeners).forEach(([eventName, handler]) => {
package/src/h.js CHANGED
@@ -3,16 +3,19 @@ import { withoutNulls } from './utils/arrays.js';
3
3
  export const DOM_TYPES = {
4
4
  TEXT: 'text',
5
5
  ELEMENT: 'element',
6
- FRAGMENT: 'fragment'
6
+ FRAGMENT: 'fragment',
7
+ COMPONENT: 'component'
7
8
  }
8
9
 
9
10
  // Create a virtual DOM element
10
11
  export function h(tag, props = {}, children = []) {
12
+ const type = typeof tag === 'string' ? DOM_TYPES.ELEMENT : DOM_TYPES.COMPONENT;
13
+
11
14
  return {
12
15
  tag,
13
16
  props,
17
+ type,
14
18
  children: mapTextNodes(withoutNulls(children)),
15
- type: DOM_TYPES.ELEMENT,
16
19
  }
17
20
  }
18
21
 
@@ -53,6 +56,7 @@ export function lipsum(numberOfParagraphs) {
53
56
  );
54
57
  }
55
58
 
59
+ // Recursively extract a node's children into a flat list, dissolving any nested fragments into their child nodes
56
60
  export function extractChildren(vNode) {
57
61
  if (vNode.children == null) return [];
58
62
 
package/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { createApp } from './app.js'
2
+ export { defineComponent } from './component.js'
2
3
  export { h, hFragment, hString } from './h.js'
package/src/mount-dom.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { DOM_TYPES } from "./h.js";
2
2
  import { setAttributes } from './attributes.js'
3
3
  import { addEventListeners } from "./events.js";
4
+ import { extractPropsAndEvents } from "./utils/props.js";
4
5
 
5
6
  // Recursively mount a virtual node (and its subtree) into a real parent DOM element.
6
- export function mountDOM(vNode, parentEl, index) {
7
+ export function mountDOM(vNode, parentEl, index, hostComponent = null) {
7
8
  switch (vNode.type) {
8
9
  case DOM_TYPES.TEXT: {
9
10
  createTextNode(vNode, parentEl, index);
@@ -11,15 +12,19 @@ export function mountDOM(vNode, parentEl, index) {
11
12
  }
12
13
 
13
14
  case DOM_TYPES.ELEMENT: {
14
- createElementNode(vNode, parentEl, index);
15
+ createElementNode(vNode, parentEl, index, hostComponent);
15
16
  break;
16
17
  }
17
18
 
18
19
  case DOM_TYPES.FRAGMENT: {
19
- createFragmentNode(vNode, parentEl, index);
20
+ createFragmentNode(vNode, parentEl, index, hostComponent);
20
21
  break;
21
22
  }
22
23
 
24
+ case DOM_TYPES.COMPONENT: {
25
+ createComponentNode(vNode, parentEl, index, hostComponent);
26
+ }
27
+
23
28
  default: {
24
29
  throw new Error(`Can't mount virtual node of type: ${vNode.type}`);
25
30
  }
@@ -36,38 +41,49 @@ function createTextNode(vNode, parentEl, index) {
36
41
  insert(textNode, parentEl, index);
37
42
  }
38
43
 
39
- // Create a real element, apply its props, recursively mount its children into it,
40
- // then append the finished element to the parent.
41
- function createElementNode(vNode, parentEl, index) {
42
- const { tag, props, children } = vNode;
44
+ // Create a real element, apply its props, recursively mount its children into it, then append the finished element to the parent.
45
+ function createElementNode(vNode, parentEl, index, hostComponent) {
46
+ const { tag, children } = vNode;
43
47
 
44
48
  const el = document.createElement(tag);
45
- addProps(el, props, vNode);
49
+ addProps(el, vNode, hostComponent);
46
50
  vNode.el = el;
47
51
 
48
- children.forEach((child) => mountDOM(child, el));
52
+ children.forEach((child) => mountDOM(child, el, null, hostComponent));
49
53
  insert(el, parentEl, index);
50
54
  }
51
55
 
52
56
  // Fragment has no DOM element. Mount its children directly into the parent.
53
- function createFragmentNode(vNode, parentEl, index) {
57
+ function createFragmentNode(vNode, parentEl, index, hostComponent) {
54
58
  const { children } = vNode;
55
59
  vNode.el = parentEl;
56
60
 
57
61
  children.forEach((child, i) => {
58
- mountDOM(child, parentEl, index ? index + i : null)
62
+ mountDOM(child, parentEl, index ? index + i : null, hostComponent)
59
63
  });
60
64
  }
61
65
 
66
+ function createComponentNode(vNode, parentEl, index, hostComponent) {
67
+ const Component = vNode.tag;
68
+ const { props, events } = extractPropsAndEvents(vNode);
69
+ const component = new Component(props, events, hostComponent);
70
+
71
+ component.mount(parentEl, index);
72
+ vNode.component = component;
73
+ vNode.el = component.firstElement;
74
+ }
75
+
62
76
  // Split props into event listeners ('on') and attributes and add them to DOM element.
63
- function addProps(el, props, vNode) {
64
- const { on: events, ...attrs} = props;
77
+ function addProps(el, vNode, hostComponent) {
78
+ const { props: attrs, events} = extractPropsAndEvents(vNode);
65
79
 
66
- vNode.listeners = addEventListeners(events, el);
80
+ vNode.listeners = addEventListeners(events, el, hostComponent);
67
81
  setAttributes(el, attrs);
68
82
  }
69
83
 
84
+ // Insert element into parent element at the specified index (or append if no index given)
70
85
  function insert(el, parentEl, index) {
86
+ // Append if no index specified (null or undefined)
71
87
  if (index == null) {
72
88
  parentEl.append(el);
73
89
  return;
@@ -80,8 +96,10 @@ function insert(el, parentEl, index) {
80
96
  const children = parentEl.childNodes;
81
97
 
82
98
  if (index >= children.length) {
99
+ // Append if index is greater than or equal to the parent element's length of children
83
100
  parentEl.append(el);
84
101
  } else {
102
+ // Insert before the element at the specified index
85
103
  parentEl.insertBefore(el, children[index]);
86
104
  }
87
105
  }
@@ -1,11 +1,23 @@
1
1
  import { DOM_TYPES } from "./h.js";
2
2
 
3
3
  export function areNodesEqual(nodeOne, nodeTwo) {
4
+ // Nodes are different type
4
5
  if (nodeOne.type !== nodeTwo.type) return false;
5
6
 
6
7
  if (nodeOne.type === DOM_TYPES.ELEMENT) {
7
- return nodeOne.tag === nodeTwo.tag;
8
+ const { tag: tagOne, props: { key: keyOne } } = nodeOne;
9
+ const { tag: tagTwo, props: { key: keyTwo } } = nodeTwo;
10
+
11
+ return tagOne === tagTwo && keyOne === keyTwo;
12
+ }
13
+
14
+ if (nodeOne.type === DOM_TYPES.COMPONENT) {
15
+ const { tag: componentOne, props: { key: keyOne } } = nodeOne;
16
+ const { tag: componentTwo, props: { key: keyTwo } } = nodeTwo;
17
+
18
+ return componentOne === componentTwo && keyOne === keyTwo;
8
19
  }
9
20
 
21
+ // Text and fragment nodes are always equal
10
22
  return true;
11
23
  }
package/src/patch-dom.js CHANGED
@@ -7,16 +7,24 @@ import { removeAttribute, removeStyle, setAttribute, setStyle } from "./attribut
7
7
  import { ARRAY_DIFF_OP, arraysDiff, arraysDiffSequence } from "./utils/arrays.js";
8
8
  import { isNotBlankOrEmptyString } from "./utils/strings.js";
9
9
  import { addEventListener } from "./events.js";
10
+ import { extractPropsAndEvents } from "./utils/props.js";
10
11
 
11
- export function patchDOM(oldVNode, newVNode, parentEl) {
12
+ export function patchDOM(oldVNode, newVNode, parentEl, hostComponent = null) {
13
+ // Nodes are too different to reuse/patch.
12
14
  if (!areNodesEqual(oldVNode, newVNode)) {
15
+ // Remember where the old node was located inside the parent so the replacement lands in the same spot.
13
16
  const index = findIndexInParent(parentEl, oldVNode.el);
17
+
18
+ // Destroy the old node and its whole subtree.
14
19
  destroyDOM(oldVNode);
15
- mountDOM(newVNode, parentEl, index);
20
+
21
+ // Mount the new node and its whole subtree at the index of the old node.
22
+ mountDOM(newVNode, parentEl, index, hostComponent);
16
23
 
17
24
  return newVNode;
18
25
  }
19
26
 
27
+ // Copy over the DOM element reference
20
28
  newVNode.el = oldVNode.el;
21
29
 
22
30
  switch (newVNode.type) {
@@ -26,16 +34,22 @@ export function patchDOM(oldVNode, newVNode, parentEl) {
26
34
  }
27
35
 
28
36
  case DOM_TYPES.ELEMENT: {
29
- patchElement(oldVNode, newVNode);
37
+ patchElement(oldVNode, newVNode, hostComponent);
38
+ break;
39
+ }
40
+
41
+ case DOM_TYPES.COMPONENT: {
42
+ patchComponent(oldVNode, newVNode);
30
43
  break;
31
44
  }
32
45
  }
33
46
 
34
- patchChildren(oldVNode, newVNode);
47
+ patchChildren(oldVNode, newVNode, hostComponent);
35
48
 
36
49
  return newVNode;
37
50
  }
38
51
 
52
+ // Get index of the element inside the parent element
39
53
  function findIndexInParent(parentEl, el) {
40
54
  const index = Array.from(parentEl.childNodes).indexOf(el);
41
55
  if (index < 0) return null;
@@ -53,7 +67,7 @@ function patchText(oldVNode, newVNode) {
53
67
  }
54
68
  }
55
69
 
56
- function patchElement(oldVNode, newVNode) {
70
+ function patchElement(oldVNode, newVNode, hostComponent) {
57
71
  const el = oldVNode.el;
58
72
 
59
73
  const {
@@ -75,7 +89,17 @@ function patchElement(oldVNode, newVNode) {
75
89
  patchAttrs(el, oldAttrs, newAttrs);
76
90
  patchClasses(el, oldClass, newClass);
77
91
  patchStyles(el, oldStyle, newStyle);
78
- newVNode.listeners = patchEvents(el, oldListeners, oldEvents, newEvents);
92
+ newVNode.listeners = patchEvents(el, oldListeners, oldEvents, newEvents, hostComponent);
93
+ }
94
+
95
+ function patchComponent(oldVNode, newVNode) {
96
+ const { component } = oldVNode;
97
+ const { props } = extractPropsAndEvents(newVNode);
98
+
99
+ component.updateProps(props);
100
+
101
+ newVNode.component = component;
102
+ newVNode.el = component.firstElement;
79
103
  }
80
104
 
81
105
  function patchAttrs(el, oldAttrs, newAttrs) {
@@ -124,7 +148,7 @@ function patchStyles(el, oldStyle = {}, newStyle = {}) {
124
148
  }
125
149
  }
126
150
 
127
- function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
151
+ function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}, hostComponent) {
128
152
  const { removed, added, updated} = objectsDiff(oldEvents, newEvents);
129
153
 
130
154
  for (const eventName of removed.concat(updated)) {
@@ -134,46 +158,55 @@ function patchEvents(el, oldListeners = {}, oldEvents = {}, newEvents = {}) {
134
158
  const addedListeners = {};
135
159
 
136
160
  for (const eventName of added.concat(updated)) {
137
- addedListeners[eventName] = addEventListener(eventName, newEvents[eventName], el);
161
+ addedListeners[eventName] = addEventListener(eventName, newEvents[eventName], el, hostComponent);
138
162
  }
139
163
 
140
164
  return addedListeners;
141
165
  }
142
166
 
143
- function patchChildren(oldVNode, newVNode) {
167
+ // Reconcile a node's child lists.
168
+ function patchChildren(oldVNode, newVNode, hostComponent) {
144
169
  const oldChildren = extractChildren(oldVNode);
145
170
  const newChildren = extractChildren(newVNode);
146
171
  const parentEl = oldVNode.el;
147
172
 
173
+ // Create the sequence of operations to match the note's child lists.
148
174
  const diffSeq = arraysDiffSequence(oldChildren, newChildren, areNodesEqual);
149
175
 
176
+ // Loop over the operations and perform the operations on the DOM.
150
177
  for (const operation of diffSeq) {
151
178
  const { originalIndex, from, index, item } = operation;
179
+ // Get the host component’s offset, if there is one; offset is 0 otherwise
180
+ const offset = hostComponent?.offset ?? 0;
152
181
 
153
182
  switch (operation.op) {
183
+ // New node with no old counterpart: build its subtree and insert it at `index`
154
184
  case ARRAY_DIFF_OP.ADD: {
155
- mountDOM(item, parentEl, index)
185
+ mountDOM(item, parentEl, index, hostComponent)
156
186
  break;
157
187
  }
158
188
 
189
+ // Old node gone from the new list: destroy it and its subtree
159
190
  case ARRAY_DIFF_OP.REMOVE: {
160
191
  destroyDOM(item);
161
192
  break;
162
193
  }
163
194
 
195
+ // Node still present but at a different position: move the existing node to its new spot, then patch it in place for any inner changes
164
196
  case ARRAY_DIFF_OP.MOVE: {
165
197
  const el = oldChildren[from].el;
166
- const elAtTargetIndex = parentEl.childNodes[index];
167
-
198
+ const elAtTargetIndex = parentEl.childNodes[index + offset];
168
199
  parentEl.insertBefore(el, elAtTargetIndex);
169
- patchDOM(oldChildren[from], newChildren[index], parentEl);
200
+
201
+ patchDOM(oldChildren[from], newChildren[index], parentEl, hostComponent);
170
202
  break;
171
203
  }
172
204
 
205
+ // Same node, same position, but its contents may still differ, so recurse
173
206
  case ARRAY_DIFF_OP.NOOP: {
174
- patchDOM(oldChildren[originalIndex], newChildren[index], parentEl)
207
+ patchDOM(oldChildren[originalIndex], newChildren[index], parentEl, hostComponent);
175
208
  break;
176
209
  }
177
210
  }
178
211
  }
179
- }
212
+ }
@@ -40,81 +40,51 @@ class ArrayWithOriginalIndices {
40
40
  return this.#originalIndices[index];
41
41
  }
42
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;
43
+ // ADD
44
+ isAddition(item, startIndex) {
45
+ return this.findIndexFrom(item, startIndex) === -1;
52
46
  }
53
47
 
54
- removeItem(index) {
48
+ addItem(item, index) {
55
49
  const operation = {
56
- op: ARRAY_DIFF_OP.REMOVE,
50
+ op: ARRAY_DIFF_OP.ADD,
57
51
  index,
58
- item: this.#oldArray[index],
52
+ item
59
53
  }
60
54
 
61
- this.#oldArray.splice(index, 1);
62
- this.#originalIndices.splice(index, 1);
55
+ this.#oldArray.splice(index, 0, item);
56
+ this.#originalIndices.splice(index, 0, -1);
63
57
 
64
58
  return operation;
65
59
  }
66
60
 
67
- // NOOP
68
- isNoop(index, newArray) {
61
+ // REMOVE
62
+ isRemoval(index, newArray) {
69
63
  // Bail out if index is past the end of the old array.
70
- if (index >= this.length) {return false; }
64
+ if (index >= this.length) { return false; }
71
65
 
72
66
  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
- }
67
+ const indexInNewArray = newArray.findIndex((newArrayItem) => this.#equalsFn(item, newArrayItem))
98
68
 
99
- return -1;
69
+ return indexInNewArray === -1;
100
70
  }
101
71
 
102
- addItem(item, index) {
72
+ removeItem(index) {
103
73
  const operation = {
104
- op: ARRAY_DIFF_OP.ADD,
74
+ op: ARRAY_DIFF_OP.REMOVE,
105
75
  index,
106
- item
76
+ item: this.#oldArray[index],
107
77
  }
108
78
 
109
- this.#oldArray.splice(index, 0, item);
110
- this.#originalIndices.splice(index, 0, -1);
79
+ this.#oldArray.splice(index, 1);
80
+ this.#originalIndices.splice(index, 1);
111
81
 
112
82
  return operation;
113
83
  }
114
84
 
115
85
  // MOVE
116
86
  moveItem(item, toIndex) {
117
- const fromIndex = this.findFromIndex(item, toIndex);
87
+ const fromIndex = this.findIndexFrom(item, toIndex);
118
88
 
119
89
  const operation = {
120
90
  op: ARRAY_DIFF_OP.MOVE,
@@ -133,7 +103,27 @@ class ArrayWithOriginalIndices {
133
103
  return operation;
134
104
  }
135
105
 
136
- // REMOVE ITEMS AFTER
106
+ // NOOP
107
+ isNoop(index, newArray) {
108
+ // Bail out if index is past the end of the old array.
109
+ if (index >= this.length) {return false; }
110
+
111
+ const item = this.#oldArray[index];
112
+ const newArrayItem = newArray[index];
113
+
114
+ return this.#equalsFn(item, newArrayItem);
115
+ }
116
+
117
+ noopItem(index) {
118
+ return {
119
+ op: ARRAY_DIFF_OP.NOOP,
120
+ originalIndex: this.originalIndexAt(index),
121
+ index,
122
+ item: this.#oldArray[index],
123
+ }
124
+ }
125
+
126
+ // Remove all items after specified index
137
127
  removeItemsAfter(index) {
138
128
  const operations = [];
139
129
 
@@ -143,6 +133,17 @@ class ArrayWithOriginalIndices {
143
133
 
144
134
  return operations;
145
135
  }
136
+
137
+ // Return the first index at or after startIndex where the item matches, or -1 if item is not found.
138
+ findIndexFrom(item, startIndex) {
139
+ for (let i = startIndex; i < this.length; i++) {
140
+ if (this.#equalsFn(item, this.#oldArray[i])) {
141
+ return i;
142
+ }
143
+ }
144
+
145
+ return -1;
146
+ }
146
147
  }
147
148
 
148
149
  export function arraysDiffSequence(oldArray, newArray, equalsFn = (a, b) => a === b) {
@@ -9,4 +9,8 @@ export function objectsDiff(oldObj, newObj) {
9
9
  (key) => key in oldObj && oldObj[key] !== newObj[key]
10
10
  ),
11
11
  }
12
+ }
13
+
14
+ export function hasOwnProperty(obj, prop) {
15
+ return Object.prototype.hasOwnProperty.call(obj, prop);
12
16
  }
@@ -0,0 +1,6 @@
1
+ export function extractPropsAndEvents(vNode) {
2
+ const { on: events = {}, ...props } = vNode.props;
3
+ delete props.key;
4
+
5
+ return { props, events }
6
+ }