veles 0.0.9 → 1.0.0-alpha.1

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/README.md CHANGED
@@ -4,8 +4,6 @@
4
4
  [![Build Size](https://img.shields.io/bundlephobia/minzip/veles?label=bundle%20size)](https://bundlephobia.com/result?p=veles)
5
5
  [![Version](https://img.shields.io/npm/v/veles)](https://www.npmjs.com/package/veles)
6
6
 
7
- > This library is still in early stages, so the API is not 100% finalized
8
-
9
7
  `Veles` is a component-based performance-focused UI library. The main goal of this library is to provide a composable way to build highly interactive interfaces, which should be performant out of the box, as long as you follow the recommendations.
10
8
 
11
9
  ## Performance
@@ -30,16 +28,16 @@ Types are installed automatically with the same package.
30
28
  import { createState } from "veles";
31
29
 
32
30
  function NameComponent() {
33
- const nameState = createState("");
31
+ const name$ = createState("");
34
32
  return (
35
33
  <div>
36
34
  <input
37
35
  type="text"
38
36
  name="name"
39
- value={nameState.useAttribute()}
40
- onInput={(e) => nameState.setValue(e.target.value)}
37
+ value={name$.attribute()}
38
+ onInput={(e) => name$.set(e.target.value)}
41
39
  />
42
- <p>{nameState.useValue()}</p>
40
+ <p>{name$.render()}</p>
43
41
  </div>
44
42
  );
45
43
  }
@@ -55,7 +53,3 @@ This will render an input and will update the Text node dynamically, without re-
55
53
  - [Differences from other frameworks](https://bloomca.github.io/veles/frameworks-difference.html)
56
54
 
57
55
  There also a companion app ([veles-calendar-app](https://github.com/Bloomca/veles-calendar-app)), which is developed using Veles and is supposed to push it to the limits, identify the issues and ideally improve performance even more.
58
-
59
- ### Features
60
-
61
- The library is under development, so some features are not available yet. Namely the TypeScript type inferring is not the best (although the library does support TypeScript), and Portals are not implemented yet.
@@ -0,0 +1,468 @@
1
+ //#region src/hooks/lifecycle.ts
2
+ const contextStack = [];
3
+ let currentContext = null;
4
+ function addContext(newContext) {
5
+ contextStack.push(newContext);
6
+ currentContext = newContext;
7
+ }
8
+ function popContext() {
9
+ contextStack.pop();
10
+ currentContext = contextStack[contextStack.length - 1];
11
+ }
12
+ function onMount(cb) {
13
+ if (currentContext) currentContext.onMount(cb);
14
+ else console.error("missing current context");
15
+ }
16
+ function onUnmount(cb) {
17
+ if (currentContext) currentContext.onUnmount(cb);
18
+ else console.error("missing current context");
19
+ }
20
+ function hasCurrentLifecycleContext() {
21
+ return Boolean(currentContext);
22
+ }
23
+ //#endregion
24
+ //#region src/create-element/create-text-element.ts
25
+ function createTextElement(text) {
26
+ const mountHandlers = [];
27
+ const unmountHandlers = [];
28
+ return {
29
+ velesStringElement: true,
30
+ html: document.createTextNode(text || ""),
31
+ _privateMethods: {
32
+ _addMountHandler(cb) {
33
+ mountHandlers.push(cb);
34
+ },
35
+ _callMountHandlers() {
36
+ mountHandlers.forEach((cb) => cb());
37
+ },
38
+ _addUnmountHandler: (cb) => {
39
+ unmountHandlers.push(cb);
40
+ },
41
+ _callUnmountHandlers: () => {
42
+ unmountHandlers.forEach((cb) => cb());
43
+ }
44
+ }
45
+ };
46
+ }
47
+ //#endregion
48
+ //#region src/create-element/parse-component.ts
49
+ function parseComponent({ element, props }) {
50
+ const mountCbs = [];
51
+ const unmountCbs = [];
52
+ return {
53
+ velesComponentObject: true,
54
+ element,
55
+ props,
56
+ _privateMethods: {
57
+ _addMountHandler(cb) {
58
+ mountCbs.push(cb);
59
+ },
60
+ _addUnmountHandler: (cb) => {
61
+ unmountCbs.push(cb);
62
+ },
63
+ _callMountHandlers: () => {
64
+ mountCbs.forEach((cb) => cb());
65
+ },
66
+ _callUnmountHandlers: () => {
67
+ unmountCbs.forEach((cb) => cb());
68
+ }
69
+ }
70
+ };
71
+ }
72
+ function executeComponent({ element, props }) {
73
+ let componentUnmountCbs = [];
74
+ let componentMountCbs = [];
75
+ const componentAPI = {
76
+ onMount: (cb) => {
77
+ componentMountCbs.push(cb);
78
+ },
79
+ onUnmount: (cb) => {
80
+ componentUnmountCbs.push(cb);
81
+ }
82
+ };
83
+ addContext(componentAPI);
84
+ const _componentTree = element(props, componentAPI);
85
+ const componentTree = typeof _componentTree === "string" || !_componentTree ? createTextElement(_componentTree) : _componentTree;
86
+ popContext();
87
+ return {
88
+ velesComponent: true,
89
+ tree: componentTree,
90
+ _privateMethods: {
91
+ _addMountHandler(cb) {
92
+ componentMountCbs.push(cb);
93
+ },
94
+ _addUnmountHandler: (cb) => {
95
+ componentAPI.onUnmount(cb);
96
+ },
97
+ _callMountHandlers: () => {
98
+ componentMountCbs.forEach((cb) => {
99
+ const mountCbResult = cb();
100
+ if (typeof mountCbResult === "function") componentAPI.onUnmount(mountCbResult);
101
+ });
102
+ },
103
+ _callUnmountHandlers: () => {
104
+ componentUnmountCbs.forEach((cb) => cb());
105
+ }
106
+ }
107
+ };
108
+ }
109
+ //#endregion
110
+ //#region src/create-element/parse-children.ts
111
+ function parseChildren({ children, htmlElement, velesNode, portal }) {
112
+ const childComponents = [];
113
+ if (children === void 0 || children === null) return childComponents;
114
+ let lastInsertedNode = null;
115
+ (Array.isArray(children) ? children : [children]).forEach((childComponent) => {
116
+ if (typeof childComponent === "string") {
117
+ const textNode = createTextElement(childComponent);
118
+ htmlElement.append(textNode.html);
119
+ lastInsertedNode = textNode.html;
120
+ childComponents.push(textNode);
121
+ } else if (typeof childComponent === "number") {
122
+ const textNode = createTextElement(String(childComponent));
123
+ htmlElement.append(textNode.html);
124
+ lastInsertedNode = textNode.html;
125
+ childComponents.push(textNode);
126
+ } else if (typeof childComponent === "object" && childComponent && "velesNode" in childComponent && childComponent?.velesNode) if (childComponent.phantom) {
127
+ childComponent.childComponents.forEach((childComponentofPhantom) => {
128
+ if ("velesNode" in childComponentofPhantom) {
129
+ htmlElement.append(childComponentofPhantom.html);
130
+ childComponentofPhantom.parentVelesElement = velesNode;
131
+ lastInsertedNode = childComponentofPhantom.html;
132
+ } else if ("velesStringElement" in childComponentofPhantom) {
133
+ const velesElementNode = childComponentofPhantom;
134
+ if (!velesElementNode) console.error("can't find HTML tree in a component chain");
135
+ else {
136
+ htmlElement.append(velesElementNode.html);
137
+ lastInsertedNode = velesElementNode.html;
138
+ velesElementNode.parentVelesElement = velesNode;
139
+ }
140
+ }
141
+ });
142
+ childComponent.parentVelesElement = velesNode;
143
+ childComponents.push(childComponent);
144
+ } else if (childComponent.portal) {
145
+ childComponent.parentVelesElement = velesNode;
146
+ childComponents.push(childComponent);
147
+ } else {
148
+ htmlElement.append(childComponent.html);
149
+ childComponent.parentVelesElement = velesNode;
150
+ childComponents.push(childComponent);
151
+ lastInsertedNode = childComponent.html;
152
+ }
153
+ else if (typeof childComponent === "object" && childComponent && "velesComponentObject" in childComponent) {
154
+ childComponent.parentVelesElement = velesNode;
155
+ childComponents.push(childComponent);
156
+ if (portal) childComponent.portal = portal;
157
+ else {
158
+ childComponent.insertAfter = lastInsertedNode;
159
+ lastInsertedNode = childComponent;
160
+ }
161
+ } else if (typeof childComponent === "object" && childComponent && "velesStringElement" in childComponent && childComponent?.velesStringElement) {
162
+ htmlElement.append(childComponent.html);
163
+ childComponent.parentVelesElement = velesNode;
164
+ childComponents.push(childComponent);
165
+ lastInsertedNode = childComponent.html;
166
+ }
167
+ });
168
+ return childComponents;
169
+ }
170
+ //#endregion
171
+ //#region src/create-element/assign-attributes.ts
172
+ function assignAttributes({ props, htmlElement, velesNode }) {
173
+ Object.entries(props).forEach(([key, value]) => {
174
+ if (typeof value === "function" && value.velesAttribute === true) assignAttribute({
175
+ key,
176
+ value: value(htmlElement, key, velesNode),
177
+ htmlElement
178
+ });
179
+ else assignAttribute({
180
+ key,
181
+ value,
182
+ htmlElement
183
+ });
184
+ });
185
+ }
186
+ function assignAttribute({ key, value, htmlElement }) {
187
+ if (typeof value === "function" && key.startsWith("on")) htmlElement.addEventListener(key.slice(2).toLocaleLowerCase(), value);
188
+ else if (typeof value === "boolean") {
189
+ if (value) htmlElement.setAttribute(key, "");
190
+ } else htmlElement.setAttribute(key, value);
191
+ }
192
+ //#endregion
193
+ //#region src/create-element/create-element.ts
194
+ function createElement(element, props = {}) {
195
+ if (typeof element === "string") {
196
+ const { children, ref, phantom = false, portal = null, ...otherProps } = props;
197
+ const newElement = document.createElement(element);
198
+ const velesNode = {};
199
+ if (ref?.velesRef) ref.current = newElement;
200
+ const childComponents = parseChildren({
201
+ children,
202
+ htmlElement: newElement,
203
+ velesNode,
204
+ portal
205
+ });
206
+ const unmountHandlers = [];
207
+ velesNode.html = newElement;
208
+ velesNode.velesNode = true;
209
+ velesNode.childComponents = childComponents;
210
+ velesNode.phantom = phantom;
211
+ velesNode.portal = portal;
212
+ const mountHandlers = [];
213
+ velesNode._privateMethods = {
214
+ _addMountHandler(cb) {
215
+ mountHandlers.push(cb);
216
+ },
217
+ _callMountHandlers() {
218
+ mountHandlers.forEach((cb) => cb());
219
+ },
220
+ _addUnmountHandler(cb) {
221
+ unmountHandlers.push(cb);
222
+ },
223
+ _callUnmountHandlers() {
224
+ unmountHandlers.forEach((cb) => cb());
225
+ }
226
+ };
227
+ /**
228
+ * Since portal node is already mounted in DOM, we can't just attach our HTML to it
229
+ * imediately. So we attach it only when the component is actually mounted, and detach
230
+ * when it is unmounted. This way we don't need to iterate the tree manually and
231
+ * attach/detach in every case we need to change the tree.
232
+ */
233
+ if (portal) {
234
+ velesNode._privateMethods._addMountHandler(function attachNodeOnMount() {
235
+ velesNode.childComponents.forEach((childComponent) => {
236
+ if ("velesNode" in childComponent) if (childComponent.phantom) childComponent.childComponents.forEach((fragmentChildComponent) => {
237
+ portal.append(fragmentChildComponent.html);
238
+ });
239
+ else portal.append(childComponent.html);
240
+ else if ("velesStringElement" in childComponent) portal.append(childComponent.html);
241
+ else appendComponentToPortal(getExecutedComponentVelesNode(childComponent.executedVersion), portal);
242
+ });
243
+ });
244
+ velesNode._privateMethods._addUnmountHandler(function removeNodeOnUnmount() {
245
+ velesNode.childComponents.forEach((childComponent) => {
246
+ if ("velesNode" in childComponent) childComponent.html.remove();
247
+ else if ("velesStringElement" in childComponent) childComponent.html.remove();
248
+ else cleanupComponentFromPortal(getExecutedComponentVelesNode(childComponent.executedVersion));
249
+ });
250
+ });
251
+ }
252
+ assignAttributes({
253
+ props: otherProps,
254
+ htmlElement: newElement,
255
+ velesNode
256
+ });
257
+ return velesNode;
258
+ } else if (typeof element === "function") return parseComponent({
259
+ element,
260
+ props
261
+ });
262
+ throw new Error("Veles createElement expects a valid DOM string or another component");
263
+ }
264
+ function appendComponentToPortal(componentNode, portal) {
265
+ if ("executedVelesNode" in componentNode && componentNode.phantom) {
266
+ componentNode.childComponents.forEach((fragmentChildComponent) => {
267
+ if ("executedVelesComponent" in fragmentChildComponent) appendComponentToPortal(getExecutedComponentVelesNode(fragmentChildComponent), portal);
268
+ else portal.append(fragmentChildComponent.html);
269
+ });
270
+ componentNode.phantom;
271
+ } else portal.append(componentNode.html);
272
+ }
273
+ function cleanupComponentFromPortal(componentNode) {
274
+ if ("executedVelesNode" in componentNode && componentNode.phantom) {
275
+ componentNode.childComponents.forEach((fragmentChildComponent) => {
276
+ if ("executedVelesComponent" in fragmentChildComponent) cleanupComponentFromPortal(getExecutedComponentVelesNode(fragmentChildComponent));
277
+ else fragmentChildComponent.html.remove();
278
+ });
279
+ componentNode.phantom;
280
+ } else componentNode.html.remove();
281
+ }
282
+ //#endregion
283
+ //#region src/fragment.ts
284
+ function Fragment({ children }) {
285
+ return createElement("div", {
286
+ phantom: true,
287
+ children
288
+ });
289
+ }
290
+ //#endregion
291
+ //#region src/context/index.ts
292
+ const publicContextStack = [];
293
+ let contextIdCounter = 1;
294
+ function createContext() {
295
+ const contextId = contextIdCounter++;
296
+ function addContext(value) {
297
+ const currentContextObject = publicContextStack[publicContextStack.length - 1];
298
+ if (!currentContextObject) console.error("cannot add Context due to missing stack value");
299
+ else publicContextStack[publicContextStack.length - 1] = {
300
+ ...currentContextObject,
301
+ [contextId]: value
302
+ };
303
+ }
304
+ return {
305
+ Provider: ({ value, children }) => {
306
+ addContext(value);
307
+ return createElement(Fragment, { children });
308
+ },
309
+ addContext,
310
+ readContext: () => {
311
+ const currentContext = publicContextStack[publicContextStack.length - 1];
312
+ if (!currentContext) console.error("no Context currently available");
313
+ else return currentContext[contextId];
314
+ }
315
+ };
316
+ }
317
+ function addPublicContext(specificContext) {
318
+ if (specificContext) publicContextStack.push(specificContext);
319
+ else if (publicContextStack.length === 0) publicContextStack.push({});
320
+ else {
321
+ const currentContext = publicContextStack[publicContextStack.length - 1];
322
+ publicContextStack.push(currentContext);
323
+ }
324
+ }
325
+ function popPublicContext() {
326
+ publicContextStack.pop();
327
+ }
328
+ function getCurrentContext() {
329
+ return publicContextStack[publicContextStack.length - 1];
330
+ }
331
+ //#endregion
332
+ //#region src/_utils.ts
333
+ function getExecutedComponentVelesNode(component) {
334
+ if ("executedVelesStringElement" in component) return component;
335
+ let childNode = component;
336
+ while ("executedVelesComponent" in childNode) if ("executedVelesStringElement" in childNode.tree) return childNode.tree;
337
+ else childNode = childNode.tree;
338
+ return childNode;
339
+ }
340
+ function renderTree(component, { parentVelesElement } = {}) {
341
+ if ("velesStringElement" in component) {
342
+ const executedString = {
343
+ executedVelesStringElement: true,
344
+ _privateMethods: component._privateMethods,
345
+ html: component.html,
346
+ parentVelesElement
347
+ };
348
+ if (component.needExecutedVersion) component.executedVersion = executedString;
349
+ return executedString;
350
+ } else if ("velesComponentObject" in component) {
351
+ addPublicContext();
352
+ const componentTree = executeComponent(component);
353
+ const executedComponent = {};
354
+ executedComponent.executedVelesComponent = true;
355
+ executedComponent.tree = renderTree(componentTree.tree);
356
+ popPublicContext();
357
+ executedComponent._privateMethods = {
358
+ ...componentTree._privateMethods,
359
+ _callMountHandlers: () => {
360
+ component._privateMethods._callMountHandlers();
361
+ componentTree._privateMethods._callMountHandlers();
362
+ },
363
+ _callUnmountHandlers: () => {
364
+ component._privateMethods._callUnmountHandlers();
365
+ componentTree._privateMethods._callUnmountHandlers();
366
+ }
367
+ };
368
+ const newNode = getExecutedComponentVelesNode(executedComponent);
369
+ if (component.portal) {
370
+ /**
371
+ * Inserting nodes is handled by the portal parent element.
372
+ * We still need to assign `parentVelesElement`, so that
373
+ * `render` updates correctly
374
+ */
375
+ if (parentVelesElement) newNode.parentVelesElement = parentVelesElement;
376
+ } else if (parentVelesElement) {
377
+ if (component.insertAfter) if ("velesComponentObject" in component.insertAfter) component.html = insertNode({
378
+ velesElement: newNode,
379
+ adjacentNode: component.insertAfter.html,
380
+ parentVelesElement
381
+ });
382
+ else component.html = insertNode({
383
+ velesElement: newNode,
384
+ adjacentNode: component.insertAfter,
385
+ parentVelesElement
386
+ });
387
+ else component.html = insertNode({
388
+ velesElement: newNode,
389
+ adjacentNode: null,
390
+ parentVelesElement
391
+ });
392
+ newNode.parentVelesElement = parentVelesElement;
393
+ }
394
+ if (component.needExecutedVersion || component.portal) component.executedVersion = executedComponent;
395
+ return executedComponent;
396
+ } else if ("velesNode" in component) {
397
+ const executedNode = {};
398
+ executedNode.executedVelesNode = true;
399
+ executedNode._privateMethods = component._privateMethods;
400
+ executedNode.html = component.html;
401
+ if (parentVelesElement) executedNode.parentVelesElement = parentVelesElement;
402
+ if (component.phantom) executedNode.phantom = component.phantom;
403
+ if (component.portal) executedNode.portal = component.portal;
404
+ executedNode.childComponents = component.childComponents.map((childComponent) => renderTree(childComponent, { parentVelesElement: executedNode }));
405
+ if (component.needExecutedVersion) component.executedVersion = executedNode;
406
+ return executedNode;
407
+ }
408
+ }
409
+ function insertNode({ velesElement, adjacentNode, parentVelesElement }) {
410
+ if (velesElement.phantom) {
411
+ let lastInsertedNode = null;
412
+ velesElement.childComponents.forEach((childComponentofPhantom) => {
413
+ if ("executedVelesNode" in childComponentofPhantom) {
414
+ if (lastInsertedNode) lastInsertedNode.after(childComponentofPhantom.html);
415
+ else if (adjacentNode) adjacentNode.after(childComponentofPhantom.html);
416
+ else parentVelesElement.html.prepend(childComponentofPhantom.html);
417
+ childComponentofPhantom.parentVelesElement = parentVelesElement;
418
+ lastInsertedNode = childComponentofPhantom.html;
419
+ } else if ("executedVelesStringElement" in childComponentofPhantom) {
420
+ if (lastInsertedNode) lastInsertedNode.after(childComponentofPhantom.html);
421
+ else if (adjacentNode) adjacentNode.after(childComponentofPhantom.html);
422
+ else parentVelesElement.html.prepend(childComponentofPhantom.html);
423
+ childComponentofPhantom.parentVelesElement = parentVelesElement;
424
+ lastInsertedNode = childComponentofPhantom.html;
425
+ } else {
426
+ const executedNode = getExecutedComponentVelesNode(childComponentofPhantom);
427
+ if (lastInsertedNode) lastInsertedNode.after(executedNode.html);
428
+ else if (adjacentNode) adjacentNode.after(executedNode.html);
429
+ else parentVelesElement.html.prepend(executedNode.html);
430
+ executedNode.parentVelesElement = parentVelesElement;
431
+ lastInsertedNode = executedNode.html;
432
+ }
433
+ });
434
+ velesElement.parentVelesElement = parentVelesElement;
435
+ return lastInsertedNode;
436
+ } else {
437
+ if (adjacentNode) adjacentNode.after(velesElement.html);
438
+ else parentVelesElement.html.prepend(velesElement.html);
439
+ velesElement.parentVelesElement = parentVelesElement;
440
+ return velesElement.html;
441
+ }
442
+ }
443
+ function callMountHandlers(component) {
444
+ component._privateMethods._callMountHandlers();
445
+ if ("executedVelesStringElement" in component) return;
446
+ if ("executedVelesComponent" in component) callMountHandlers(component.tree);
447
+ if ("executedVelesNode" in component) component.childComponents.forEach((childComponent) => callMountHandlers(childComponent));
448
+ }
449
+ function callUnmountHandlers(component) {
450
+ if ("executedVelesStringElement" in component) {} else if ("executedVelesComponent" in component) callUnmountHandlers(component.tree);
451
+ else if ("executedVelesNode" in component) component.childComponents.forEach((childComponent) => callUnmountHandlers(childComponent));
452
+ component._privateMethods._callUnmountHandlers();
453
+ }
454
+ function identity(value1, value2) {
455
+ return value1 === value2;
456
+ }
457
+ function unique(arr) {
458
+ const map = /* @__PURE__ */ new Map();
459
+ const resultArr = [];
460
+ arr.forEach((element) => {
461
+ if (map.has(element)) return;
462
+ map.set(element, true);
463
+ resultArr.push(element);
464
+ });
465
+ return resultArr;
466
+ }
467
+ //#endregion
468
+ export { renderTree as a, createContext as c, Fragment as d, createElement as f, onUnmount as g, onMount as h, identity as i, getCurrentContext as l, hasCurrentLifecycleContext as m, callUnmountHandlers as n, unique as o, createTextElement as p, getExecutedComponentVelesNode as r, addPublicContext as s, callMountHandlers as t, popPublicContext as u };