ks-fwork 1.0.3 → 1.0.5

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.3",
3
+ "version": "1.0.5",
4
4
  "description": "Demo Frontend Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
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 vdom = null;
7
+ let rootNode = null;
8
8
 
9
9
  const dispatcher = new Dispatcher();
10
- const subscriptions = [dispatcher.afterEveryCommand(renderApp)];
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 fires, replace state with the reducer's result.
16
13
  for (const actionName in reducers) {
17
14
  const reducer = reducers[actionName];
18
15
 
19
- const subs = dispatcher.subscribe(actionName, (payload) => {
16
+ const unsubscribe = dispatcher.subscribe(actionName, (payload) => {
20
17
  state = reducer(state, payload);
21
18
  })
22
- subscriptions.push(subs);
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 (vdom) {
27
- destroyDOM(vdom);
28
+ if (rootNode) {
29
+ destroyDOM(rootNode);
28
30
  }
29
31
 
30
- vdom = view(state, emit);
31
- mountDOM(vdom, parentEl);
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
- destroyDOM(vdom);
42
- vdom = null;
43
- subscriptions.forEach((unsubscribe) => unsubscribe());
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);
@@ -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(vdom) {
6
- const { type } = vdom;
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(vdom);
10
+ removeTextNode(vNode);
11
11
  break;
12
12
  }
13
13
 
14
14
  case DOM_TYPES.ELEMENT: {
15
- removeElementNode(vdom);
15
+ removeElementNode(vNode);
16
16
  break;
17
17
  }
18
18
 
19
19
  case DOM_TYPES.FRAGMENT: {
20
- removeFragmentNode(vdom);
20
+ removeFragmentNode(vNode);
21
21
  break;
22
22
  }
23
23
 
24
24
  default: {
25
- throw new Error(`Can't destroy DOM of type: ${vdom.type}`);
25
+ throw new Error(`Can't destroy virtual node of type: ${vNode.type}`);
26
26
  }
27
27
  }
28
28
 
29
- delete vdom.el;
29
+ // Drop the reference to the DOM element
30
+ delete vNode.el;
30
31
  }
31
32
 
32
- function removeTextNode(vdom) {
33
- const { el } = vdom;
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
- function removeElementNode(vdom) {
38
- const { el, children, listeners } = vdom;
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 vdom.listeners;
47
+ delete vNode.listeners;
46
48
  }
49
+
50
+ el.remove();
47
51
  }
48
52
 
49
- function removeFragmentNode() {
50
- const { children } = vdom;
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
- #subs = new Map();
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
- if (!this.#subs.has(commandName)) {
7
- this.#subs.set(commandName, []);
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
- const handlers = this.#subs.get(commandName);
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 idx = handlers.indexOf(handler);
19
- handlers.splice(idx, 1);
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 idx = this.#afterHandlers.indexOf(handler);
28
- this.#afterHandlers.splice(idx, 1);
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.#subs.has(commandName)) {
34
- this.#subs.get(commandName).forEach((handler) => handler(payload))
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
- const listener = addEventListener(eventName, handler, el);
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
- // Transform strings into text virtual nodes.
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 { type: DOM_TYPES.TEXT, value: str };
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
- export function mountDOM(vdom, parentEl) {
6
- switch (vdom.type) {
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(vdom, parentEl);
9
+ createTextNode(vNode, parentEl);
9
10
  break;
10
11
  }
11
12
 
12
13
  case DOM_TYPES.ELEMENT: {
13
- createElementNode(vdom, parentEl);
14
+ createElementNode(vNode, parentEl);
14
15
  break;
15
16
  }
16
17
 
17
18
  case DOM_TYPES.FRAGMENT: {
18
- createFragmentNode(vdom, parentEl);
19
+ createFragmentNode(vNode, parentEl);
19
20
  break;
20
21
  }
21
22
 
22
23
  default: {
23
- throw new Error(`Can't mount DOM of type: ${vdom.type}`);
24
+ throw new Error(`Can't mount virtual node of type: ${vNode.type}`);
24
25
  }
25
26
  }
27
+ }
26
28
 
27
- function createTextNode(vdom, parentEl) {
28
- const { value } = vdom;
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
- const textNode = document.createTextNode(value);
31
- vdom.el = textNode;
33
+ const textNode = document.createTextNode(value);
34
+ vNode.el = textNode;
32
35
 
33
- parentEl.append(textNode);
34
- }
36
+ parentEl.append(textNode);
37
+ }
35
38
 
36
- function createElementNode(vdom, parentEl) {
37
- const { tag, props, children } = vdom;
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
- const element = document.createElement(tag);
40
- addProps(element, props, vdom);
41
- vdom.el = element;
44
+ const el = document.createElement(tag);
45
+ addProps(el, props, vNode);
46
+ vNode.el = el;
42
47
 
43
- children.forEach((child) => mountDOM(child, element));
44
- parentEl.append(element);
45
- }
48
+ children.forEach((child) => mountDOM(child, el));
49
+ parentEl.append(el);
50
+ }
46
51
 
47
- function addProps(el, props, vdom) {
48
- const { on: events, ...attrs} = props;
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
- vdom.listeners = addEventListeners(events, el);
51
- setAttributes(el, attrs);
52
- }
57
+ children.forEach((child) => mountDOM(child, parentEl));
58
+ }
53
59
 
54
- function createFragmentNode(vdom, parentEl) {
55
- const { children } = vdom;
56
- vdom.el = parentEl;
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
- children.forEach((child) => mountDOM(child, parentEl));
59
- }
64
+ vNode.listeners = addEventListeners(events, el);
65
+ setAttributes(el, attrs);
60
66
  }