ks-fwork 1.0.4 → 1.0.6
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 +1 -1
- package/src/app.js +21 -15
- package/src/attributes.js +8 -0
- package/src/destroy-dom.js +21 -16
- package/src/dispatcher.js +26 -10
- package/src/events.js +4 -2
- package/src/h.js +14 -9
- package/src/mount-dom.js +36 -30
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -4,43 +4,49 @@ import { mountDOM} from "./mount-dom.js";
|
|
|
4
4
|
|
|
5
5
|
export function createApp({state, view, reducers = {} }) {
|
|
6
6
|
let parentEl = null;
|
|
7
|
-
let
|
|
7
|
+
let rootNode = null;
|
|
8
8
|
|
|
9
9
|
const dispatcher = new Dispatcher();
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
function emit(eventName, payload) {
|
|
13
|
-
dispatcher.dispatch(eventName, payload);
|
|
14
|
-
}
|
|
10
|
+
const unsubscribers = [dispatcher.afterEveryCommand(renderApp)];
|
|
15
11
|
|
|
12
|
+
// Wire each reducer to its action: when the action is emitted, replace state with the reducer's result.
|
|
16
13
|
for (const actionName in reducers) {
|
|
17
14
|
const reducer = reducers[actionName];
|
|
18
15
|
|
|
19
|
-
const
|
|
16
|
+
const unsubscribe = dispatcher.subscribe(actionName, (payload) => {
|
|
20
17
|
state = reducer(state, payload);
|
|
21
18
|
})
|
|
22
|
-
|
|
19
|
+
unsubscribers.push(unsubscribe);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Passed into the view so it can trigger actions without touching the dispatcher directly.
|
|
23
|
+
function emit(actionName, payload) {
|
|
24
|
+
dispatcher.dispatch(actionName, payload);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function renderApp() {
|
|
26
|
-
if (
|
|
27
|
-
destroyDOM(
|
|
28
|
+
if (rootNode) {
|
|
29
|
+
destroyDOM(rootNode);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
mountDOM(
|
|
32
|
+
rootNode = view(state, emit);
|
|
33
|
+
mountDOM(rootNode, parentEl);
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
return {
|
|
37
|
+
// Attach the app to a real DOM element and do the first render.
|
|
35
38
|
mount(_parentEl) {
|
|
36
39
|
parentEl = _parentEl;
|
|
37
40
|
renderApp();
|
|
38
41
|
},
|
|
39
42
|
|
|
43
|
+
// Detach the app: remove its DOM and cancel every subscription.
|
|
40
44
|
unmount() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
if (rootNode) {
|
|
46
|
+
destroyDOM(rootNode);
|
|
47
|
+
}
|
|
48
|
+
rootNode = null;
|
|
49
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
44
50
|
},
|
|
45
51
|
}
|
|
46
52
|
}
|
package/src/attributes.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Set attributes to an element
|
|
1
2
|
export function setAttributes(el, attrs) {
|
|
2
3
|
const { class: className, style, ...otherAttrs } = attrs;
|
|
3
4
|
|
|
@@ -16,6 +17,7 @@ export function setAttributes(el, attrs) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
// Set class or array of classes to an element
|
|
19
21
|
function setClass(el, className) {
|
|
20
22
|
el.className = '';
|
|
21
23
|
|
|
@@ -30,17 +32,22 @@ function setClass(el, className) {
|
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
// Set style to an element
|
|
33
36
|
export function setStyle(el, name, value) {
|
|
34
37
|
el.style[name] = value;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// Remove style from an element
|
|
37
41
|
export function removeStyle(el, name) {
|
|
38
42
|
el.style[name] = null;
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
// Set attribute to an element
|
|
41
46
|
export function setAttribute(el, name, value) {
|
|
47
|
+
// Remove attributes with no values
|
|
42
48
|
if (value == null) {
|
|
43
49
|
removeAttribute(el, name);
|
|
50
|
+
// Set 'data-*' attributes separately
|
|
44
51
|
} else if (name.startsWith('data-')) {
|
|
45
52
|
el.setAttribute(name, value);
|
|
46
53
|
} else {
|
|
@@ -48,6 +55,7 @@ export function setAttribute(el, name, value) {
|
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
// Remove attribute from an element
|
|
51
59
|
export function removeAttribute(el, name) {
|
|
52
60
|
el[name] = null;
|
|
53
61
|
el.removeAttribute(name);
|
package/src/destroy-dom.js
CHANGED
|
@@ -1,52 +1,57 @@
|
|
|
1
1
|
import { removeEventListeners } from './events.js'
|
|
2
2
|
import { DOM_TYPES } from "./h.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
export function destroyDOM(
|
|
6
|
-
const { type } =
|
|
4
|
+
// Recursively destroy a virtual node (and its subtree).
|
|
5
|
+
export function destroyDOM(vNode) {
|
|
6
|
+
const { type } = vNode;
|
|
7
7
|
|
|
8
8
|
switch (type) {
|
|
9
9
|
case DOM_TYPES.TEXT: {
|
|
10
|
-
removeTextNode(
|
|
10
|
+
removeTextNode(vNode);
|
|
11
11
|
break;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
case DOM_TYPES.ELEMENT: {
|
|
15
|
-
removeElementNode(
|
|
15
|
+
removeElementNode(vNode);
|
|
16
16
|
break;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
case DOM_TYPES.FRAGMENT: {
|
|
20
|
-
removeFragmentNode(
|
|
20
|
+
removeFragmentNode(vNode);
|
|
21
21
|
break;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
default: {
|
|
25
|
-
throw new Error(`Can't destroy
|
|
25
|
+
throw new Error(`Can't destroy virtual node of type: ${vNode.type}`);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Drop the reference to the DOM element
|
|
30
|
+
delete vNode.el;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// Detach the text node from the DOM.
|
|
34
|
+
function removeTextNode(vNode) {
|
|
35
|
+
const { el } = vNode;
|
|
34
36
|
el.remove();
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
// Recursively destroy the element's children, remove its event listeners, then detach the element.
|
|
40
|
+
function removeElementNode(vNode) {
|
|
41
|
+
const { el, children, listeners } = vNode;
|
|
39
42
|
|
|
40
|
-
el.remove();
|
|
41
43
|
children.forEach(destroyDOM)
|
|
42
44
|
|
|
43
45
|
if (listeners) {
|
|
44
46
|
removeEventListeners(listeners, el);
|
|
45
|
-
delete
|
|
47
|
+
delete vNode.listeners;
|
|
46
48
|
}
|
|
49
|
+
|
|
50
|
+
el.remove();
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
// Fragment has no DOM element. Destroy fragment node's children recursively.
|
|
54
|
+
function removeFragmentNode(vNode) {
|
|
55
|
+
const { children } = vNode;
|
|
51
56
|
children.forEach(destroyDOM);
|
|
52
57
|
}
|
package/src/dispatcher.js
CHANGED
|
@@ -1,40 +1,56 @@
|
|
|
1
1
|
export class Dispatcher {
|
|
2
|
-
|
|
2
|
+
// All registered commands mapped to their handlers (callback functions)
|
|
3
|
+
#handlersByCommand = new Map();
|
|
4
|
+
|
|
5
|
+
// Handlers that run after every dispatch, regardless of command
|
|
3
6
|
#afterHandlers = [];
|
|
4
7
|
|
|
8
|
+
// Subscribe to a command
|
|
5
9
|
subscribe(commandName, handler) {
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
// Create an empty handler array for this command if none exists yet
|
|
11
|
+
if (!this.#handlersByCommand.has(commandName)) {
|
|
12
|
+
this.#handlersByCommand.set(commandName, []);
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
// Get the handler array for this command
|
|
16
|
+
const handlers = this.#handlersByCommand.get(commandName);
|
|
17
|
+
|
|
18
|
+
// If this exact handler reference is already registered, skip it and return a no-op unsubscribe
|
|
11
19
|
if (handlers.includes(handler)) {
|
|
12
20
|
return () => {}
|
|
13
21
|
}
|
|
14
22
|
|
|
23
|
+
// Register the handler
|
|
15
24
|
handlers.push(handler);
|
|
16
25
|
|
|
26
|
+
// Return unsubscribe function
|
|
17
27
|
return () => {
|
|
18
|
-
const
|
|
19
|
-
handlers.splice(
|
|
28
|
+
const index = handlers.indexOf(handler);
|
|
29
|
+
handlers.splice(index, 1);
|
|
20
30
|
}
|
|
21
31
|
}
|
|
22
32
|
|
|
33
|
+
// Register a handler that will be called after every dispatch
|
|
23
34
|
afterEveryCommand(handler) {
|
|
24
35
|
this.#afterHandlers.push(handler);
|
|
25
36
|
|
|
37
|
+
// Return unsubscribe function
|
|
26
38
|
return () => {
|
|
27
|
-
const
|
|
28
|
-
this.#afterHandlers.splice(
|
|
39
|
+
const index = this.#afterHandlers.indexOf(handler);
|
|
40
|
+
this.#afterHandlers.splice(index, 1);
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
// Dispatch a command: run its handlers with the payload
|
|
32
45
|
dispatch(commandName, payload) {
|
|
33
|
-
if (this.#
|
|
34
|
-
|
|
46
|
+
if (this.#handlersByCommand.has(commandName)) {
|
|
47
|
+
// Call every handler subscribed to this command
|
|
48
|
+
this.#handlersByCommand.get(commandName).forEach((handler) => handler(payload))
|
|
35
49
|
} else {
|
|
36
50
|
console.warn(`No handlers for command: ${commandName}`);
|
|
37
51
|
}
|
|
52
|
+
|
|
53
|
+
// Call all 'afterEveryCommand' handlers (no payload)
|
|
38
54
|
this.#afterHandlers.forEach((handler) => handler());
|
|
39
55
|
}
|
|
40
56
|
}
|
package/src/events.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
// Add an event listener to an element
|
|
1
2
|
export function addEventListener(eventName, handler, el) {
|
|
2
3
|
el.addEventListener(eventName, handler);
|
|
3
4
|
return handler;
|
|
4
5
|
}
|
|
5
6
|
|
|
7
|
+
// Add an object of event listeners to an element
|
|
6
8
|
export function addEventListeners(listeners = {}, el) {
|
|
7
9
|
const addedListeners = {};
|
|
8
10
|
|
|
9
11
|
Object.entries(listeners).forEach(([eventName, handler]) => {
|
|
10
|
-
|
|
11
|
-
addedListeners[eventName] = listener;
|
|
12
|
+
addedListeners[eventName] = addEventListener(eventName, handler, el);
|
|
12
13
|
});
|
|
13
14
|
|
|
14
15
|
return addedListeners;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// Remove an object of event listeners from an element
|
|
17
19
|
export function removeEventListeners(listeners = {}, el) {
|
|
18
20
|
Object.entries(listeners).forEach(([eventName, handler]) => {
|
|
19
21
|
el.removeEventListener(eventName, handler);
|
package/src/h.js
CHANGED
|
@@ -6,7 +6,7 @@ export const DOM_TYPES = {
|
|
|
6
6
|
FRAGMENT: 'fragment'
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
// Create a virtual DOM element
|
|
9
|
+
// Create a virtual DOM element
|
|
10
10
|
export function h(tag, props = {}, children = []) {
|
|
11
11
|
return {
|
|
12
12
|
tag,
|
|
@@ -16,17 +16,15 @@ export function h(tag, props = {}, children = []) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
function mapTextNodes(children) {
|
|
21
|
-
return children.map((child) =>
|
|
22
|
-
typeof child === 'string' ? hString(child) : child
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
19
|
+
// Create a virtual text node
|
|
26
20
|
export function hString(str) {
|
|
27
|
-
return {
|
|
21
|
+
return {
|
|
22
|
+
type: DOM_TYPES.TEXT,
|
|
23
|
+
value: str
|
|
24
|
+
};
|
|
28
25
|
}
|
|
29
26
|
|
|
27
|
+
// Create a fragment node
|
|
30
28
|
export function hFragment(vNodes) {
|
|
31
29
|
return {
|
|
32
30
|
children: mapTextNodes(withoutNulls(vNodes)),
|
|
@@ -34,6 +32,13 @@ export function hFragment(vNodes) {
|
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
35
|
+
// Transform strings into text virtual nodes
|
|
36
|
+
function mapTextNodes(children) {
|
|
37
|
+
return children.map((child) =>
|
|
38
|
+
typeof child === 'string' ? hString(child) : child
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
37
42
|
// Create an array with specified amount of empty elements and fill them with 'p' virtual node that has text as children.
|
|
38
43
|
export function lipsum(numberOfParagraphs) {
|
|
39
44
|
const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
package/src/mount-dom.js
CHANGED
|
@@ -2,59 +2,65 @@ import { DOM_TYPES } from "./h.js";
|
|
|
2
2
|
import { setAttributes } from './attributes.js'
|
|
3
3
|
import { addEventListeners } from "./events.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Recursively mount a virtual node (and its subtree) into a real parent DOM element.
|
|
6
|
+
export function mountDOM(vNode, parentEl) {
|
|
7
|
+
switch (vNode.type) {
|
|
7
8
|
case DOM_TYPES.TEXT: {
|
|
8
|
-
createTextNode(
|
|
9
|
+
createTextNode(vNode, parentEl);
|
|
9
10
|
break;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
case DOM_TYPES.ELEMENT: {
|
|
13
|
-
createElementNode(
|
|
14
|
+
createElementNode(vNode, parentEl);
|
|
14
15
|
break;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
case DOM_TYPES.FRAGMENT: {
|
|
18
|
-
createFragmentNode(
|
|
19
|
+
createFragmentNode(vNode, parentEl);
|
|
19
20
|
break;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
default: {
|
|
23
|
-
throw new Error(`Can't mount
|
|
24
|
+
throw new Error(`Can't mount virtual node of type: ${vNode.type}`);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
27
|
+
}
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
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) {
|
|
31
|
+
const { value } = vNode;
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const textNode = document.createTextNode(value);
|
|
34
|
+
vNode.el = textNode;
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
parentEl.append(textNode);
|
|
37
|
+
}
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
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) {
|
|
42
|
+
const { tag, props, children } = vNode;
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
const el = document.createElement(tag);
|
|
45
|
+
addProps(el, props, vNode);
|
|
46
|
+
vNode.el = el;
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
children.forEach((child) => mountDOM(child, el));
|
|
49
|
+
parentEl.append(el);
|
|
50
|
+
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
// Fragment has no DOM element. Mount its children directly into the parent.
|
|
53
|
+
function createFragmentNode(vNode, parentEl) {
|
|
54
|
+
const { children } = vNode;
|
|
55
|
+
vNode.el = parentEl;
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
57
|
+
children.forEach((child) => mountDOM(child, parentEl));
|
|
58
|
+
}
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
// Split props into event listeners ('on') and attributes and add them to DOM element.
|
|
61
|
+
function addProps(el, props, vNode) {
|
|
62
|
+
const { on: events, ...attrs} = props;
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
vNode.listeners = addEventListeners(events, el);
|
|
65
|
+
setAttributes(el, attrs);
|
|
60
66
|
}
|