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 +13 -2
- package/src/app.js +12 -27
- package/src/component.js +149 -0
- package/src/destroy-dom.js +5 -0
- package/src/events.js +13 -8
- package/src/h.js +6 -2
- package/src/index.js +1 -0
- package/src/mount-dom.js +32 -14
- package/src/nodes-equal.js +13 -1
- package/src/patch-dom.js +48 -15
- package/src/utils/arrays.js +51 -50
- package/src/utils/objects.js +4 -0
- package/src/utils/props.js +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ks-fwork",
|
|
3
|
-
"version": "
|
|
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(
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
37
|
+
destroyDOM(rootNode);
|
|
38
|
+
reset();
|
|
54
39
|
},
|
|
55
40
|
}
|
|
56
41
|
}
|
package/src/component.js
ADDED
|
@@ -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
|
+
}
|
package/src/destroy-dom.js
CHANGED
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
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
|
-
|
|
41
|
-
|
|
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,
|
|
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,
|
|
64
|
-
const {
|
|
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
|
}
|
package/src/nodes-equal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/arrays.js
CHANGED
|
@@ -40,81 +40,51 @@ class ArrayWithOriginalIndices {
|
|
|
40
40
|
return this.#originalIndices[index];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
+
addItem(item, index) {
|
|
55
49
|
const operation = {
|
|
56
|
-
op: ARRAY_DIFF_OP.
|
|
50
|
+
op: ARRAY_DIFF_OP.ADD,
|
|
57
51
|
index,
|
|
58
|
-
item
|
|
52
|
+
item
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
this.#oldArray.splice(index,
|
|
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
|
-
//
|
|
68
|
-
|
|
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
|
|
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
|
-
|
|
72
|
+
removeItem(index) {
|
|
103
73
|
const operation = {
|
|
104
|
-
op: ARRAY_DIFF_OP.
|
|
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,
|
|
110
|
-
this.#originalIndices.splice(index,
|
|
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.
|
|
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
|
-
//
|
|
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) {
|
package/src/utils/objects.js
CHANGED