betal-fe 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/dist/betal-fe.js +242 -0
- package/package.json +32 -0
package/dist/betal-fe.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
function addEventListener(eventName, handler, el) {
|
|
2
|
+
el.addEventListener(eventName, handler);
|
|
3
|
+
return handler;
|
|
4
|
+
}
|
|
5
|
+
function addEventListeners(events = {}, el) {
|
|
6
|
+
const addedEventListeners = {};
|
|
7
|
+
Object.entries(events).forEach(([eventName, handler]) => {
|
|
8
|
+
addedEventListeners[eventName] = addEventListener(eventName, handler, el);
|
|
9
|
+
});
|
|
10
|
+
return addedEventListeners;
|
|
11
|
+
}
|
|
12
|
+
function removeEventListeners(listeners = {}, el) {
|
|
13
|
+
Object.entries(listeners).forEach(([eventName, handler]) => {
|
|
14
|
+
el.removeEventListener(eventName, handler);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function withoutNulls(arr) {
|
|
19
|
+
return arr.filter((item) => item != null);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DOM_TYPES = {
|
|
23
|
+
TEXT: "text",
|
|
24
|
+
ELEMENT: "element",
|
|
25
|
+
FRAGMENT: "fragment",
|
|
26
|
+
};
|
|
27
|
+
function h(tag, props = {}, children = []) {
|
|
28
|
+
return {
|
|
29
|
+
type: DOM_TYPES.ELEMENT,
|
|
30
|
+
tag,
|
|
31
|
+
props,
|
|
32
|
+
children: mapTextNodes(withoutNulls(children)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function mapTextNodes(nodes) {
|
|
36
|
+
return nodes.map((node) =>
|
|
37
|
+
typeof node === "string" ? hString(node) : node
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function hString(str) {
|
|
41
|
+
return { type: DOM_TYPES.TEXT, value: str };
|
|
42
|
+
}
|
|
43
|
+
function hFragment(vNodes) {
|
|
44
|
+
return {
|
|
45
|
+
type: DOM_TYPES.FRAGMENT,
|
|
46
|
+
children: mapTextNodes(withoutNulls(vNodes)),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function destroyDOM(vDom) {
|
|
51
|
+
const { type } = vDom;
|
|
52
|
+
switch (type) {
|
|
53
|
+
case DOM_TYPES.TEXT: {
|
|
54
|
+
removeTextNode(vDom);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case DOM_TYPES.ELEMENT: {
|
|
58
|
+
removeElementNode(vDom);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case DOM_TYPES.FRAGMENT: {
|
|
62
|
+
removeFragmentNodes(vDom);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
throw new Error(`Can't destroy DOM of type: ${type}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
delete vDom.el;
|
|
70
|
+
}
|
|
71
|
+
function removeTextNode(vDom) {
|
|
72
|
+
const { el } = vDom;
|
|
73
|
+
el.remove();
|
|
74
|
+
}
|
|
75
|
+
function removeElementNode(vdom) {
|
|
76
|
+
const { el, children, listeners } = vdom;
|
|
77
|
+
el.remove();
|
|
78
|
+
children.forEach(destroyDOM);
|
|
79
|
+
if (listeners) {
|
|
80
|
+
removeEventListeners(listeners, el);
|
|
81
|
+
delete vdom.listeners;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function removeFragmentNodes(vDom) {
|
|
85
|
+
const { children } = vDom;
|
|
86
|
+
children.forEach(destroyDOM);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class Dispatcher {
|
|
90
|
+
#subs = new Map();
|
|
91
|
+
#afterHandlers = [];
|
|
92
|
+
subscribe(commandName, handler) {
|
|
93
|
+
if (!this.#subs.has(commandName)) {
|
|
94
|
+
this.#subs.set(commandName, []);
|
|
95
|
+
}
|
|
96
|
+
const handlers = this.#subs.get(commandName);
|
|
97
|
+
if (handlers.includes(handler)) {
|
|
98
|
+
return () => {};
|
|
99
|
+
}
|
|
100
|
+
handlers.push(handler);
|
|
101
|
+
return () => {
|
|
102
|
+
const idx = handlers.indexOf(handler);
|
|
103
|
+
handlers.splice(idx, 1);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
afterEveryCommand(handler) {
|
|
107
|
+
this.#afterHandlers.push(handler);
|
|
108
|
+
return () => {
|
|
109
|
+
const idx = this.#afterHandlers.indexOf(handler);
|
|
110
|
+
this.#afterHandlers.splice(idx, 1);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
dispatch(commandName, payload) {
|
|
114
|
+
if (this.#subs.has(commandName)) {
|
|
115
|
+
this.#subs.get(commandName).forEach((handler) => handler(payload));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.warn(`No handlers for command: ${commandName}`);
|
|
119
|
+
}
|
|
120
|
+
this.#afterHandlers.forEach((handler) => handler());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function setAttributes(el, attrs) {
|
|
125
|
+
const { class: className, style, ...otherAttrs } = attrs;
|
|
126
|
+
if (className) {
|
|
127
|
+
setClass(el, className);
|
|
128
|
+
}
|
|
129
|
+
if (style) {
|
|
130
|
+
Object.entries(style).forEach(([prop, value]) => {
|
|
131
|
+
setStyle(el, prop, value);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
for (const [name, value] of Object.entries(otherAttrs)) {
|
|
135
|
+
setAttribute(el, name, value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function setClass(el, className) {
|
|
139
|
+
el.className = '';
|
|
140
|
+
if (typeof className === 'string') {
|
|
141
|
+
el.className = className;
|
|
142
|
+
} else if (Array.isArray(className)) {
|
|
143
|
+
el.classList.add(...className);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function setStyle(el, prop, value) {
|
|
147
|
+
el.style[prop] = value;
|
|
148
|
+
}
|
|
149
|
+
function setAttribute(el, name, value) {
|
|
150
|
+
if (value == null) {
|
|
151
|
+
removeAttribute(el, name);
|
|
152
|
+
} else if (name.startsWith("data-")) {
|
|
153
|
+
el.setAttribute(name, value);
|
|
154
|
+
} else {
|
|
155
|
+
el[name] = value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function removeAttribute(el, name) {
|
|
159
|
+
el[name] = null;
|
|
160
|
+
el.removeAttribute(name);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mountDOM(vDom, parentElement) {
|
|
164
|
+
switch (vDom.type) {
|
|
165
|
+
case DOM_TYPES.TEXT: {
|
|
166
|
+
createTextNode(vDom, parentElement);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case DOM_TYPES.ELEMENT: {
|
|
170
|
+
createElementNode(vDom, parentElement);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case DOM_TYPES.FRAGMENT: {
|
|
174
|
+
createFragmentNode(vDom, parentElement);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default: {
|
|
178
|
+
throw new Error(`Can't mount DOM of type: ${vDom.type}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function createTextNode(vDom, parentElement) {
|
|
183
|
+
const { value } = vDom;
|
|
184
|
+
const textNode = document.createTextNode(value);
|
|
185
|
+
vDom.el = textNode;
|
|
186
|
+
parentElement.appendChild(textNode);
|
|
187
|
+
}
|
|
188
|
+
function createFragmentNode(vDom, parentElement) {
|
|
189
|
+
const { children } = vDom;
|
|
190
|
+
vDom.el = parentElement;
|
|
191
|
+
children.forEach((child) => mountDOM(child, parentElement));
|
|
192
|
+
}
|
|
193
|
+
function createElementNode(vDom, parentElement) {
|
|
194
|
+
const { tag, props, children } = vDom;
|
|
195
|
+
const element = document.createElement(tag);
|
|
196
|
+
addProps(element, props, vDom);
|
|
197
|
+
vDom.el = element;
|
|
198
|
+
children.forEach((child) => mountDOM(child, element));
|
|
199
|
+
parentElement.appendChild(element);
|
|
200
|
+
}
|
|
201
|
+
function addProps(el, props, vdom) {
|
|
202
|
+
const { on: events, ...attrs } = props;
|
|
203
|
+
vdom.listeners = addEventListeners(events, el);
|
|
204
|
+
setAttributes(el, attrs);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function createApp({ state, view, reducers = {} }) {
|
|
208
|
+
let parentEl = null;
|
|
209
|
+
let vdom = null;
|
|
210
|
+
const dispatcher = new Dispatcher();
|
|
211
|
+
const subscriptions = [dispatcher.afterEveryCommand(renderApp)];
|
|
212
|
+
function emit(eventName, payload) {
|
|
213
|
+
dispatcher.dispatch(eventName, payload);
|
|
214
|
+
}
|
|
215
|
+
for (const actionName in reducers) {
|
|
216
|
+
const reducer = reducers[actionName];
|
|
217
|
+
const subs = dispatcher.subscribe(actionName, (payload) => {
|
|
218
|
+
state = reducer(state, payload);
|
|
219
|
+
});
|
|
220
|
+
subscriptions.push(subs);
|
|
221
|
+
}
|
|
222
|
+
function renderApp() {
|
|
223
|
+
if (vdom) {
|
|
224
|
+
destroyDOM(vdom);
|
|
225
|
+
}
|
|
226
|
+
vdom = view(state, emit);
|
|
227
|
+
mountDOM(vdom, parentEl);
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
mount(_parentEl) {
|
|
231
|
+
parentEl = _parentEl;
|
|
232
|
+
renderApp();
|
|
233
|
+
},
|
|
234
|
+
unmount() {
|
|
235
|
+
destroyDOM(vdom);
|
|
236
|
+
vdom = null;
|
|
237
|
+
subscriptions.forEach((unsub) => unsub());
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export { createApp, h, hFragment, hString };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "betal-fe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/betal-fe.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/betal-fe.js"
|
|
8
|
+
],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rollup -c",
|
|
11
|
+
"prepack": "npm run build",
|
|
12
|
+
"lint": "eslint src",
|
|
13
|
+
"lint:fix": "eslint src --fix",
|
|
14
|
+
"test": "vitest",
|
|
15
|
+
"test:run": "vitest --run"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"description": "",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@eslint/js": "^9.38.0",
|
|
23
|
+
"eslint": "^9.38.0",
|
|
24
|
+
"eslint-plugin-import": "^2.32.0",
|
|
25
|
+
"globals": "^16.4.0",
|
|
26
|
+
"jsdom": "^27.0.1",
|
|
27
|
+
"rollup": "^4.52.5",
|
|
28
|
+
"rollup-plugin-cleanup": "^3.2.1",
|
|
29
|
+
"rollup-plugin-filesize": "^10.0.0",
|
|
30
|
+
"vitest": "^3.2.4"
|
|
31
|
+
}
|
|
32
|
+
}
|