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