@luutanhung/rreact 1.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 +15 -0
- package/src/app.js +45 -0
- package/src/attribute.js +52 -0
- package/src/destroy-dom.js +40 -0
- package/src/dispatcher.js +40 -0
- package/src/event.js +19 -0
- package/src/h.js +44 -0
- package/src/helpers/array.js +6 -0
- package/src/helpers/index.js +1 -0
- package/src/index.js +2 -0
- package/src/mount-dom.js +47 -0
- package/vite.config.js +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@luutanhung/rreact",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"author": "luuanhung",
|
|
6
|
+
"description": "Tiny React Framework",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"vite": "^7.3.1"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { destroyDOM } from "./destroy-dom";
|
|
2
|
+
import { Dispatcher } from "./dispatcher";
|
|
3
|
+
import { mountDOM } from "./mount-dom";
|
|
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
|
+
unmount() {
|
|
40
|
+
destroyDOM(vdom);
|
|
41
|
+
vdom = null;
|
|
42
|
+
subscriptions.forEach((unsubscribe) => unsubscribe());
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
package/src/attribute.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
setAttribute(el, name, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setClass(el, className) {
|
|
20
|
+
el.className = "";
|
|
21
|
+
|
|
22
|
+
if (typeof className === "string") {
|
|
23
|
+
el.className = className;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(className)) {
|
|
27
|
+
el.classList.add(...className);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setStyle(el, name, value) {
|
|
32
|
+
el.style[name] = value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function removeStyle(el, name, value) {
|
|
36
|
+
el.style[name] = value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function removeAttribute(el, name) {
|
|
40
|
+
el[name] = null;
|
|
41
|
+
el.removeAttribute(name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function setAttribute(el, name, value) {
|
|
45
|
+
if (value == null) {
|
|
46
|
+
removeAttribute(el, name);
|
|
47
|
+
} else if (name.startsWith("data-")) {
|
|
48
|
+
el.setAttribute(name, value);
|
|
49
|
+
} else {
|
|
50
|
+
el[name] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { removeEventListeners } from "./event";
|
|
2
|
+
import { DOM_TYPE } from "./h";
|
|
3
|
+
|
|
4
|
+
export function destroyDOM(vdom) {
|
|
5
|
+
const { type } = vdom;
|
|
6
|
+
|
|
7
|
+
if (type === DOM_TYPE.TEXT) {
|
|
8
|
+
removeTextNode(vdom);
|
|
9
|
+
} else if (type === DOM_TYPE.ELEMENT) {
|
|
10
|
+
removeElementNode(vdom);
|
|
11
|
+
} else if (type === DOM_TYPE.FRAGMENT) {
|
|
12
|
+
removeFragmentNode(vdom);
|
|
13
|
+
} else {
|
|
14
|
+
throw new Error(`Can't destroy vdom of type ${type}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
delete vdom.el;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function removeTextNode(vdom) {
|
|
21
|
+
const { el } = vdom;
|
|
22
|
+
el.remove();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function removeElementNode(vdom) {
|
|
26
|
+
const { el, children, listeners } = vdom;
|
|
27
|
+
|
|
28
|
+
el.remove();
|
|
29
|
+
children.forEach(destroyDOM);
|
|
30
|
+
|
|
31
|
+
if (listeners) {
|
|
32
|
+
removeEventListeners(listeners, el);
|
|
33
|
+
delete vdom.listeners;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeFragmentNode(vdom) {
|
|
38
|
+
const { children } = vdom;
|
|
39
|
+
children.forEach((child) => destroyDOM(child));
|
|
40
|
+
}
|
|
@@ -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.include(handler)) {
|
|
12
|
+
return () => {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
handlers.push(handler);
|
|
16
|
+
return () => {
|
|
17
|
+
const idx = handlers.indexOf(handler);
|
|
18
|
+
handlers.splice(idx, 1);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEveryCommand(handler) {
|
|
23
|
+
this.#afterHandlers.push(handler);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
const idx = this.#afterHandlers.indexOf(handler);
|
|
27
|
+
this.#afterHandlers.splice(idx, 1);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dispatch(commandName, payload) {
|
|
32
|
+
if (this.#subs.has(commandName)) {
|
|
33
|
+
this.#subs.get(commandName).forEach((handler) => handler(payload));
|
|
34
|
+
} else {
|
|
35
|
+
console.warn(`No handlers for command: ${commandName}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.#afterHandlers.forEach((handler) => handler());
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/event.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
Object.entries(listeners).forEach(([eventName, handler]) => {
|
|
9
|
+
el.addEventListener(eventName, handler);
|
|
10
|
+
addedListeners[eventName] = handler;
|
|
11
|
+
});
|
|
12
|
+
return addedListeners;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function removeEventListeners(listeners = {}, el) {
|
|
16
|
+
Object.entries(listeners).map(([eventName, handler]) => {
|
|
17
|
+
el.removeEventListener(eventName, handler);
|
|
18
|
+
});
|
|
19
|
+
}
|
package/src/h.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { filterNulls } from "./helpers/array";
|
|
2
|
+
|
|
3
|
+
export const DOM_TYPE = Object.freeze({
|
|
4
|
+
ELEMENT: "element",
|
|
5
|
+
TEXT: "text",
|
|
6
|
+
FRAGMENT: "fragment",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function h(tag, props = {}, children = []) {
|
|
10
|
+
const normalizedChildren = mapToVNodes(children);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
type: DOM_TYPE.ELEMENT,
|
|
14
|
+
tag,
|
|
15
|
+
props,
|
|
16
|
+
children: normalizedChildren,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function normalizeVNode(child) {
|
|
21
|
+
const typeofChild = typeof child;
|
|
22
|
+
if (typeofChild === "string") {
|
|
23
|
+
return hString(child);
|
|
24
|
+
}
|
|
25
|
+
return child;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function mapToVNodes(children = []) {
|
|
29
|
+
return children.map((child) => normalizeVNode(child));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hString(txt = "") {
|
|
33
|
+
return {
|
|
34
|
+
type: DOM_TYPE.TEXT,
|
|
35
|
+
value: txt,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function hFragment(vNodes) {
|
|
40
|
+
return {
|
|
41
|
+
type: DOM_TYPE.FRAGMENT,
|
|
42
|
+
children: mapToVNodes(vNodes),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./array";
|
package/src/index.js
ADDED
package/src/mount-dom.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { setAttributes } from "./attribute";
|
|
2
|
+
import { addEventListeners } from "./event";
|
|
3
|
+
import { DOM_TYPE } from "./h";
|
|
4
|
+
|
|
5
|
+
export function mountDOM(vdom, parentEl) {
|
|
6
|
+
const vdomType = vdom.type;
|
|
7
|
+
|
|
8
|
+
if (vdomType === DOM_TYPE.TEXT) {
|
|
9
|
+
createTextNode(vdom, parentEl);
|
|
10
|
+
} else if (vdomType === DOM_TYPE.ELEMENT) {
|
|
11
|
+
createElementNode(vdom, parentEl);
|
|
12
|
+
} else if (vdomType === DOM_TYPE.FRAGMENT) {
|
|
13
|
+
createFragmentNodes(vdom, parentEl);
|
|
14
|
+
} else {
|
|
15
|
+
throw new Error(`Can't mount vdom of type: ${vdomType}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createTextNode(vdom, parentEl) {
|
|
20
|
+
const { value } = vdom;
|
|
21
|
+
const textNode = document.createTextNode(value);
|
|
22
|
+
vdom.el = textNode;
|
|
23
|
+
parentEl.appendChild(textNode);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createFragmentNodes(vdom, parentEl) {
|
|
27
|
+
const { children } = vdom;
|
|
28
|
+
vdom.el = parentEl;
|
|
29
|
+
children.forEach((child) => mountDOM(child, parentEl));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createElementNode(vdom, parentEl) {
|
|
33
|
+
const { tag, props, children } = vdom;
|
|
34
|
+
|
|
35
|
+
const element = document.createElement(tag);
|
|
36
|
+
addProps(element, props, vdom);
|
|
37
|
+
vdom.el = element;
|
|
38
|
+
children.forEach((child) => mountDOM(child, element));
|
|
39
|
+
parentEl.append(element);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addProps(el, props, vdom) {
|
|
43
|
+
const { on: events, ...attrs } = props;
|
|
44
|
+
|
|
45
|
+
vdom.listeners = addEventListeners(events, el);
|
|
46
|
+
setAttributes(el, attrs);
|
|
47
|
+
}
|