ks-fwork 1.0.0 → 1.0.2

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,14 +1,19 @@
1
1
  {
2
2
  "name": "ks-fwork",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Demo Frontend Framework",
5
5
  "license": "ISC",
6
6
  "author": "",
7
7
  "type": "module",
8
8
  "main": "src/index.js",
9
9
  "exports": "./src/index.js",
10
- "files": ["src"],
10
+ "files": [
11
+ "src"
12
+ ],
11
13
  "scripts": {
12
14
  "test": "echo \"Error: no test specified\" && exit 1"
15
+ },
16
+ "dependencies": {
17
+ "ks-fwork": "^1.0.1"
13
18
  }
14
19
  }
package/src/app.js ADDED
@@ -0,0 +1,46 @@
1
+ import { destroyDOM } from "./destroy-dom.js";
2
+ import { Dispatcher } from "./dispatcher.js";
3
+ import { mountDOM} from "./mount-dom.js";
4
+
5
+ export function createApp({state, view, reducers = {} }) {
6
+ let parentEl = null;
7
+ let vdom = null;
8
+
9
+ const dispatcher = new Dispatcher();
10
+ const subscriptions = [dispatcher.afterEveryCommand(renderApp)];
11
+
12
+ function emit(eventName, payload) {
13
+ dispatcher.dispatch(eventName, payload);
14
+ }
15
+
16
+ for (const actionName in reducers) {
17
+ const reducer = reducers[actionName];
18
+
19
+ const subs = dispatcher.subscribe(actionName, (payload) => {
20
+ state = reducer(state, payload);
21
+ })
22
+ subscriptions.push(subs);
23
+ }
24
+
25
+ function renderApp() {
26
+ if (vdom) {
27
+ destroyDOM(vdom);
28
+ }
29
+
30
+ vdom = view(state, emit);
31
+ mountDOM(vdom, parentEl);
32
+ }
33
+
34
+ return {
35
+ mount(_parentEl) {
36
+ parentEl = _parentEl;
37
+ renderApp();
38
+ },
39
+
40
+ unmount() {
41
+ destroyDOM(vdom);
42
+ vdom = null;
43
+ subscriptions.forEach((unsubscribe) => unsubscribe());
44
+ },
45
+ }
46
+ }
@@ -0,0 +1,54 @@
1
+ export function setAttributes(el, attrs) {
2
+ const { class: className, style, ...otherAttrs } = attrs;
3
+
4
+ if (className) {
5
+ setClass(el, className);
6
+ }
7
+
8
+ if (style) {
9
+ Object.entries(style).forEach(([prop, value]) => {
10
+ setStyle(el, prop, value);
11
+ })
12
+ }
13
+
14
+ for (const [name, value] of Object.entries(otherAttrs)) {
15
+ setAttributes(el, name, value);
16
+ }
17
+ }
18
+
19
+ function setClass(el, className) {
20
+ el.className = '';
21
+
22
+ // If classes are set as string
23
+ if (typeof className === 'string') {
24
+ el.className = className;
25
+ }
26
+
27
+ // If classes are set as an array
28
+ if (Array.isArray(className)) {
29
+ el.classList.add(className);
30
+ }
31
+ }
32
+
33
+ export function setStyle(el, name, value) {
34
+ el.style[name] = value;
35
+ }
36
+
37
+ export function removeStyle(el, name) {
38
+ el.style[name] = null;
39
+ }
40
+
41
+ export function setAttribute(el, name, value) {
42
+ if (value == null) {
43
+ removeAttribute(el, name);
44
+ } else if (name.startsWith('data-')) {
45
+ el.setAttribute(name, value);
46
+ } else {
47
+ el[name] = value;
48
+ }
49
+ }
50
+
51
+ export function removeAttribute(el, name) {
52
+ el[name] = null;
53
+ el.removeAttribute(name);
54
+ }
@@ -0,0 +1,52 @@
1
+ import { removeEventListeners } from './events.js'
2
+ import { DOM_TYPES } from "./h.js";
3
+
4
+
5
+ export function destroyDOM(vdom) {
6
+ const { type } = vdom;
7
+
8
+ switch (type) {
9
+ case DOM_TYPES.TEXT: {
10
+ removeTextNode(vdom);
11
+ break;
12
+ }
13
+
14
+ case DOM_TYPES.ELEMENT: {
15
+ removeElementNode(vdom);
16
+ break;
17
+ }
18
+
19
+ case DOM_TYPES.FRAGMENT: {
20
+ removeFragmentNode(vdom);
21
+ break;
22
+ }
23
+
24
+ default: {
25
+ throw new Error(`Can't destroy DOM of type: ${vdom.type}`);
26
+ }
27
+ }
28
+
29
+ delete vdom.el;
30
+ }
31
+
32
+ function removeTextNode(vdom) {
33
+ const { el } = vdom;
34
+ el.remove();
35
+ }
36
+
37
+ function removeElementNode(vdom) {
38
+ const { el, children, listeners } = vdom;
39
+
40
+ el.remove();
41
+ children.forEach(destroyDOM)
42
+
43
+ if (listeners) {
44
+ removeEventListeners(listeners, el);
45
+ delete vdom.listeners;
46
+ }
47
+ }
48
+
49
+ function removeFragmentNode() {
50
+ const { children } = vdom;
51
+ children.forEach(destroyDOM);
52
+ }
@@ -0,0 +1,40 @@
1
+ export class Dispatcher {
2
+ #subs = new Map();
3
+ #afterHandlers = [];
4
+
5
+ subscribe(commandName, handler) {
6
+ if (!this.#subs.has(commandName)) {
7
+ this.#subs.set(commandName, []);
8
+ }
9
+
10
+ const handlers = this.#subs.get(commandName);
11
+ if (handlers.includes(handler)) {
12
+ return () => {}
13
+ }
14
+
15
+ handlers.push(handler);
16
+
17
+ return () => {
18
+ const idx = handlers.indexOf(handler);
19
+ handlers.splice(idx, 1);
20
+ }
21
+ }
22
+
23
+ afterEveryCommand(handler) {
24
+ this.#afterHandlers.push(handler);
25
+
26
+ return () => {
27
+ const idx = this.#afterHandlers.indexOf(handler);
28
+ this.#afterHandlers.splice(idx, 1);
29
+ }
30
+ }
31
+
32
+ dispatch(commandName, payload) {
33
+ if (this.#subs.has(commandName)) {
34
+ this.#subs.get(commandName).forEach((handler) => handler(payload))
35
+ } else {
36
+ console.warn(`No handlers for command: ${commandName}`);
37
+ }
38
+ this.#afterHandlers.forEach((handler) => handler());
39
+ }
40
+ }
package/src/events.js ADDED
@@ -0,0 +1,21 @@
1
+ export function addEventListener(eventName, handler, el) {
2
+ el.addEventListener(eventName, handler);
3
+ return handler;
4
+ }
5
+
6
+ export function addEventListeners(listeners = {}, el) {
7
+ const addedListeners = {};
8
+
9
+ Object.entries(listeners).forEach(([eventName, handler]) => {
10
+ const listener = addEventListener(eventName, handler, el);
11
+ addedListeners[eventName] = listener;
12
+ });
13
+
14
+ return addedListeners;
15
+ }
16
+
17
+ export function removeEventListeners(listeners = {}, el) {
18
+ Object.entries(listeners).forEach(([eventName, handler]) => {
19
+ el.removeEventListener(eventName, handler);
20
+ })
21
+ }
package/src/h.js ADDED
@@ -0,0 +1,49 @@
1
+ import { withoutNulls } from './utils/arrays.js';
2
+
3
+ export const DOM_TYPES = {
4
+ TEXT: 'text',
5
+ ELEMENT: 'element',
6
+ FRAGMENT: 'fragment'
7
+ }
8
+
9
+ // Create a virtual DOM element.
10
+ export function h(tag, props = {}, children = []) {
11
+ return {
12
+ tag,
13
+ props,
14
+ children: mapTextNodes(withoutNulls(children)),
15
+ type: DOM_TYPES.ELEMENT,
16
+ }
17
+ }
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
+
26
+ export function hString(str) {
27
+ return { type: DOM_TYPES.TEXT, value: str };
28
+ }
29
+
30
+ export function hFragment(vNodes) {
31
+ return {
32
+ children: mapTextNodes(withoutNulls(vNodes)),
33
+ type: DOM_TYPES.FRAGMENT,
34
+ }
35
+ }
36
+
37
+ // Create an array with specified amount of empty elements and fill them with 'p' virtual node that has text as children.
38
+ export function lipsum(numberOfParagraphs) {
39
+ const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit,
40
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
41
+ enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi
42
+ ut aliquip ex ea commodo consequat.`;
43
+
44
+ return hFragment(
45
+ Array(numberOfParagraphs).fill(
46
+ h('p', {}, [text])
47
+ )
48
+ );
49
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createApp } from './app.js'
2
+ export { h, hFragment, hString } from './h.js'
@@ -0,0 +1,60 @@
1
+ import { DOM_TYPES } from "./h.js";
2
+ import { setAttributes } from './attributes.js'
3
+ import { addEventListeners } from "./events.js";
4
+
5
+ export function mountDOM(vdom, parentEl) {
6
+ switch (vdom.type) {
7
+ case DOM_TYPES.TEXT: {
8
+ createTextNode(vdom, parentEl);
9
+ break;
10
+ }
11
+
12
+ case DOM_TYPES.ELEMENT: {
13
+ createElementNode(vdom, parentEl);
14
+ break;
15
+ }
16
+
17
+ case DOM_TYPES.FRAGMENT: {
18
+ createFragmentNode(vdom, parentEl);
19
+ break;
20
+ }
21
+
22
+ default: {
23
+ throw new Error(`Can't mount DOM of type: ${vdom.type}`);
24
+ }
25
+ }
26
+
27
+ function createTextNode(vdom, parentEl) {
28
+ const { value } = vdom;
29
+
30
+ const textNode = document.createTextNode(value);
31
+ vdom.el = textNode;
32
+
33
+ parentEl.append(textNode);
34
+ }
35
+
36
+ function createElementNode(vdom, parentEl) {
37
+ const { tag, props, children } = vdom;
38
+
39
+ const element = document.createElement(tag);
40
+ addProps(element, props, vdom);
41
+ vdom.el = element;
42
+
43
+ children.forEach((child) => mountDOM(child, element));
44
+ parentEl.append(element);
45
+ }
46
+
47
+ function addProps(el, props, vdom) {
48
+ const { on: events, ...attrs} = props;
49
+
50
+ vdom.listeners = addEventListeners(events, el);
51
+ setAttributes(el, attrs);
52
+ }
53
+
54
+ function createFragmentNode(vdom, parentEl) {
55
+ const { children } = vdom;
56
+ vdom.el = parentEl;
57
+
58
+ children.forEach((child) => mountDOM(child, parentEl));
59
+ }
60
+ }
@@ -0,0 +1,4 @@
1
+ // Filter out null values from the array.
2
+ export function withoutNulls(arr) {
3
+ return arr.filter((item) => item != null);
4
+ }