veles 0.0.1 → 0.0.3

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
@@ -1,8 +1,10 @@
1
1
  # Veles
2
2
 
3
3
  ![Tests status](https://github.com/bloomca/veles/actions/workflows/pull-request-workflow.yaml/badge.svg)
4
+ [![Build Size](https://img.shields.io/bundlephobia/minzip/veles?label=bundle%20size)](https://bundlephobia.com/result?p=veles)
5
+ [![Version](https://img.shields.io/npm/v/veles)](https://www.npmjs.com/package/veles)
4
6
 
5
- > The library is in very early stages and is not published yet as some crucial APIs are still under development
7
+ > The library is in very early stages and some features, like proper TypeScript support, are not fully implemented yet
6
8
 
7
9
  `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.
8
10
 
@@ -22,30 +24,26 @@ Attach Veles tree to a regular DOM Node.
22
24
  > As of right now, this method will wrap the component's HTML into one additional `div`. This will probably go away in the future, but for now it simplifies some things significantly.
23
25
 
24
26
  ```js
25
- import { createElement, attachComponent } from "veles";
27
+ import { attachComponent } from "veles";
28
+
29
+ const App () => <div>App</div>
26
30
 
27
31
  const appContainer = document.getElementById("app");
28
- attachComponent({ htmlElement: appContainer, component: createElement(App) });
32
+ attachComponent({ htmlElement: appContainer, component: <App /> });
29
33
  ```
30
34
 
31
- ### createElement
32
-
33
- Create Veles tree. Accepts strings for regular valid HTML elements (like `div`, `span`, etc) and functions which are expected to return another Veles tree from `createElement`.
35
+ ### JSX support
34
36
 
35
- > [!NOTE]
36
- > JSX should be almost fully supported as long as you specify `Veles.createElement` pragma
37
-
38
- ```js
39
- import { createElement } from "veles";
37
+ Veles supports JSX transformation, so as long as you specify `importSource: "veles"` (this is for Babel, the other JS transpilers should have similar options) it will work as expected.
40
38
 
39
+ ```jsx
41
40
  function App() {
42
- return createElement("div", {
43
- class: "app-container",
44
- children: [
45
- createElement("h1", { children: "Veles app" }),
46
- createElement("p", { children: "Random description" }),
47
- ],
48
- });
41
+ return (
42
+ <div class="app-container">
43
+ <h1>Veles App</h1>
44
+ <p>Random description</p>
45
+ </div>
46
+ );
49
47
  }
50
48
  ```
51
49
 
@@ -57,29 +55,27 @@ function App() {
57
55
 
58
56
  The simplest way to react to state changes in the UI is to use `useValue` method from the state object. Let's build a simple counter to demonstrate:
59
57
 
60
- ```js
61
- import { createElement, createState } from "veles";
58
+ ```jsx
59
+ import { createState } from "veles";
62
60
 
63
61
  function Counter() {
64
62
  const counterState = createState(0);
65
- return createElement("div", {
66
- children: [
67
- createElement("h1", { children: "Counter" }),
68
- createElement("div", {
69
- children: counterState.useValue(
63
+ const increment = () =>
64
+ counterState.setValue((currentValue) => currentValue + 1);
65
+ const decrement = () =>
66
+ counterState.setValue((currentValue) => currentValue - 1);
67
+ return (
68
+ <div>
69
+ <h1>Counter</h1>
70
+ <div>
71
+ {counterState.useValue(
70
72
  (counterValue) => `counter value is: ${counterValue}`
71
- ),
72
- }),
73
- createElement("button", {
74
- onClick: () => {
75
- counterState.setValue(
76
- (currentCounterValue) => currentCounterValue + 1
77
- );
78
- },
79
- children: "+",
80
- }),
81
- ],
82
- });
73
+ )}
74
+ </div>
75
+ <button onClick={increment}>+</button>
76
+ <button onClick={decrement}>-</button>
77
+ </div>
78
+ );
83
79
  }
84
80
  ```
85
81
 
@@ -87,8 +83,8 @@ function Counter() {
87
83
 
88
84
  If you have an object in your store, even atomic updates will be wasteful. Let's say you have an object with several fields, but you are only interested in the `title` property. If you use `useValue`, it will do unnecessary work. To help with that, there is a `useValueSelector` state method, which accepts a selector function as the first parameter. Here is an example:
89
85
 
90
- ```js
91
- import { createElement, createState } from "veles";
86
+ ```jsx
87
+ import { createState } from "veles";
92
88
 
93
89
  function App() {
94
90
  const taskState = createState({
@@ -96,17 +92,17 @@ function App() {
96
92
  title: "title",
97
93
  description: "long description",
98
94
  });
99
- return createElement("div", {
100
- children: [
101
- createElement("h1", { children: "App" }),
102
- createElement("div", {
103
- children: taskState.useSelectorValue(
95
+ return (
96
+ <div>
97
+ <h1>App</h1>
98
+ <div>
99
+ {taskState.useSelectorValue(
104
100
  (task) => task.title,
105
101
  (title) => `task title: ${title}`
106
- ),
107
- }),
108
- ],
109
- });
102
+ )}
103
+ </div>
104
+ </div>
105
+ );
110
106
  }
111
107
  ```
112
108
 
@@ -121,8 +117,8 @@ Lists performance is one of the cornerstones of this library, and to help with t
121
117
 
122
118
  Let's build a simple list component:
123
119
 
124
- ```js
125
- import { createState, createElement } from "veles";
120
+ ```jsx
121
+ import { createState } from "veles";
126
122
 
127
123
  function List() {
128
124
  const listState = createState([
@@ -131,23 +127,18 @@ function List() {
131
127
  { id: 3, name: "third task" },
132
128
  ]);
133
129
 
134
- return createElement("div", {
135
- children: [
136
- createElement("h1", { children: "list" }),
137
- listState.useValueIterator<{ id: number; name: string }>(
130
+ return <div>
131
+ <h1>List</h1>
132
+ {listState.useValueIterator<{ id: number; name: string }>(
138
133
  { key: "id" },
139
- ({ elementState }) => {
140
- return createElement("div", {
141
- children: [
142
- elementState.useValueSelector((element) => element.name, (name) =>
143
- createElement("div", { children: name })
144
- ),
145
- ],
146
- });
147
- }
148
- ),
149
- ],
150
- });
134
+ ({ elementState }) => <div>
135
+ {elementState.useValueSelector(
136
+ (element) => element.name,
137
+ (name) => <div>{name}</div>
138
+ )}
139
+ </div>
140
+ )}
141
+ </div>
151
142
  }
152
143
  ```
153
144
 
@@ -155,41 +146,38 @@ function List() {
155
146
 
156
147
  In order to avoid unnecessary re-renders when you to change properties on a wrapper element/component, there is a special state method `useAttribute`. It allows to recalculate value for an attribute every time the state changes, but instead of re-rendering, it calls the `htmlNode.setAttribute` method. Here is an example:
157
148
 
158
- ```js
159
- import { createElement, createState } from "veles";
149
+ ```jsx
150
+ import { createState } from "veles";
160
151
 
161
152
  function Counter() {
162
153
  const counterState = createState(0);
163
- return createElement("div", {
164
- children: [
165
- createElement("h1", { children: "Counter" }),
166
- counterState.useValue((counterValue) =>
167
- createElement("div", { children: `counter value is: ${counterValue}` })
168
- ),
169
- createElement("button", {
170
- onClick: () => {
171
- counterState.setValue(
172
- (currentCounterValue) => currentCounterValue + 1
173
- );
174
- },
175
- style: counterState.useAttribute(
154
+ const increment = () =>
155
+ counterState.setValue((currentValue) => currentValue + 1);
156
+ return (
157
+ <div>
158
+ <h1>Counter</h1>
159
+ <div>{counterState.useValue((value) => `counter value is ${value}`)}</div>
160
+ <button
161
+ onClick={increment}
162
+ style={counterState.useAttribute(
176
163
  (currentValue) => `width: ${50 + currentValue}px;`
177
- ),
178
- children: "+",
179
- }),
180
- ],
181
- });
164
+ )}
165
+ >
166
+ +
167
+ </button>
168
+ </div>
169
+ );
182
170
  }
183
171
  ```
184
172
 
185
- You can see that we dynamically change the width of the button with every press.
173
+ You can see that we dynamically change the width of the button with every press. This will be the only change, the rest of the application will not be re-rendered or any code will not be executed. You still need to be mindful what are you changing, as with some CSS changes you can force reflows/repaints and that can still cause some performance issues.
186
174
 
187
175
  ### createState and subscribing to updates
188
176
 
189
177
  What if you don't want to render anything when the value changes, but you want to call your code? The state provides `trackValue` method, which does exactly that. Here is an example:
190
178
 
191
- ```js
192
- import { createElement, createState } from "veles";
179
+ ```jsx
180
+ import { createState } from "veles";
193
181
 
194
182
  function Counter() {
195
183
  const counterState = createState(0);
@@ -197,23 +185,23 @@ function Counter() {
197
185
  counterState.trackValue((counterValue) => {
198
186
  console.log(`new counter value is ${counterValue}`);
199
187
  });
200
-
201
- return createElement("div", {
202
- children: [
203
- createElement("h1", { children: "Counter" }),
204
- counterState.useValue((counterValue) =>
205
- createElement("div", { children: `counter value is: ${counterValue}` })
206
- ),
207
- createElement("button", {
208
- onClick: () => {
209
- counterState.setValue(
210
- (currentCounterValue) => currentCounterValue + 1
211
- );
212
- },
213
- children: "+",
214
- }),
215
- ],
216
- });
188
+ const increment = () =>
189
+ counterState.setValue((currentValue) => currentValue + 1);
190
+ const decrement = () =>
191
+ counterState.setValue((currentValue) => currentValue - 1);
192
+
193
+ return (
194
+ <div>
195
+ <h1>Counter</h1>
196
+ <div>
197
+ {counterState.useValue(
198
+ (counterValue) => `counter value is: ${counterValue}`
199
+ )}
200
+ </div>
201
+ <button onClick={increment}>+</button>
202
+ <button onClick={decrement}>-</button>
203
+ </div>
204
+ );
217
205
  }
218
206
  ```
219
207
 
@@ -227,8 +215,8 @@ In case you don't want to subscribe to the whole state, you have 2 options. You
227
215
 
228
216
  Since `createState` is the only way to add dynamic behaviour to the application, sooner or later you'll need to build UI which depends on several states. To do so, you can use `combineState` function which accepts any amount of state objects, and returns an array with all combined values in it.
229
217
 
230
- ```js
231
- import { createElement, createState } from "veles";
218
+ ```jsx
219
+ import { createState } from "veles";
232
220
 
233
221
  function Counter() {
234
222
  const firstcounterState = createState(0);
@@ -237,44 +225,37 @@ function Counter() {
237
225
  firstcounterState,
238
226
  secondCounterState
239
227
  );
240
- return createElement("div", {
241
- children: [
242
- createElement("h1", { children: "Counter" }),
243
- firstcounterState.useValue((counterValue) =>
244
- createElement("div", {
245
- children: `first counter value is: ${counterValue}`,
246
- })
247
- ),
248
- secondCounterState.useValue((counterValue) =>
249
- createElement("div", {
250
- children: `second counter value is: ${counterValue}`,
251
- })
252
- ),
253
- combinedCounterState.useValueSelector(
254
- ([firstValue, secondValue]) => firstValue + secondValue,
255
- (counterValue) =>
256
- createElement("div", {
257
- children: `combined counter value is: ${counterValue}`,
258
- })
259
- ),
260
- createElement("button", {
261
- onClick: () => {
262
- firstcounterState.setValue(
263
- (currentCounterValue) => currentCounterValue + 1
264
- );
265
- },
266
- children: "add to the first counter",
267
- }),
268
- createElement("button", {
269
- onClick: () => {
270
- secondCounterState.setValue(
271
- (currentCounterValue) => currentCounterValue + 1
272
- );
273
- },
274
- children: "add to the second counter",
275
- }),
276
- ],
277
- });
228
+ const incrementFirstCounter = () =>
229
+ firstcounterState.setValue(
230
+ (currentCounterValue) => currentCounterValue + 1
231
+ );
232
+ const incrementSecondCounter = () =>
233
+ secondCounterState.setValue(
234
+ (currentCounterValue) => currentCounterValue + 1
235
+ );
236
+ return (
237
+ <div>
238
+ <h1>Counters</h1>
239
+ <div>
240
+ {firstcounterState.useValue(
241
+ (value) => `first counter value is: ${value}`
242
+ )}
243
+ </div>
244
+ <div>
245
+ {secondCounterState.useValue(
246
+ (value) => `second counter value is: ${value}`
247
+ )}
248
+ </div>
249
+ <div>
250
+ {combinedCounterState.useValueSelector(
251
+ ([firstValue, secondValue]) => firstValue + secondValue,
252
+ (counterValue) => `combined counter value is: ${counterValue}`
253
+ )}
254
+ </div>
255
+ <button onClick={incrementFirstCounter}>Increment first counter</button>
256
+ <button onClick={incrementSecondCounter}>Increment second counter</button>
257
+ </div>
258
+ );
278
259
  }
279
260
  ```
280
261
 
@@ -285,8 +266,8 @@ Right now there are `onMount` and `onUnmount` lifecycle hooks. In your component
285
266
  > [!NOTE]
286
267
  > You can only use them during the original initialization of the component. If you want to add some callbacks later, use the same version passed as a second argument to your component
287
268
 
288
- ```js
289
- import { createElement, onMount, onUnmount } from "veles";
269
+ ```jsx
270
+ import { onMount, onUnmount } from "veles";
290
271
 
291
272
  function App(_props, componentAPI) {
292
273
  // could be used as `componentAPI.onMount()`
@@ -297,8 +278,6 @@ function App(_props, componentAPI) {
297
278
  onUnmount(() => {
298
279
  console.log("called when the component unmounts");
299
280
  });
300
- return createElement("div", {
301
- children: "Application",
302
- });
281
+ return <div>Application<div>
303
282
  }
304
283
  ```
@@ -47,6 +47,20 @@ function getComponentVelesNode(component) {
47
47
  }
48
48
  return { velesElementNode: childNode, componentsTree };
49
49
  }
50
+ function callMountHandlers(component) {
51
+ if ("velesStringElement" in component) {
52
+ return;
53
+ }
54
+ if ("velesComponent" in component) {
55
+ component._privateMethods._callMountHandlers();
56
+ callMountHandlers(component.tree);
57
+ }
58
+ if ("velesNode" in component) {
59
+ component.childComponents.forEach(
60
+ (childComponent) => callMountHandlers(childComponent)
61
+ );
62
+ }
63
+ }
50
64
  function identity(value1, value2) {
51
65
  return value1 === value2;
52
66
  }
@@ -66,6 +80,9 @@ function parseChildren({
66
80
  if (typeof childComponent === "string") {
67
81
  const text = document.createTextNode(childComponent);
68
82
  htmlElement.append(text);
83
+ } else if (typeof childComponent === "number") {
84
+ const text = document.createTextNode(String(childComponent));
85
+ htmlElement.append(text);
69
86
  } else if (typeof childComponent === "object" && childComponent && "velesNode" in childComponent && (childComponent == null ? void 0 : childComponent.velesNode)) {
70
87
  if (childComponent.phantom) {
71
88
  childComponent.childComponents.forEach((childComponentofPhantom) => {
@@ -73,14 +90,13 @@ function parseChildren({
73
90
  htmlElement.append(childComponentofPhantom.html);
74
91
  childComponentofPhantom.parentVelesElement = velesNode;
75
92
  } else {
76
- const { componentsTree, velesElementNode } = getComponentVelesNode(childComponentofPhantom);
93
+ const { velesElementNode } = getComponentVelesNode(
94
+ childComponentofPhantom
95
+ );
77
96
  if (!velesElementNode) {
78
97
  console.error("can't find HTML tree in a component chain");
79
98
  } else {
80
99
  htmlElement.append(velesElementNode.html);
81
- componentsTree.forEach((component) => {
82
- component._privateMethods._callMountHandlers();
83
- });
84
100
  velesElementNode.parentVelesElement = velesNode;
85
101
  }
86
102
  }
@@ -109,11 +125,6 @@ function parseChildren({
109
125
  console.error("can't find HTML tree in a component chain");
110
126
  } else {
111
127
  htmlElement.append(velesElementNode2.html);
112
- setTimeout(() => {
113
- componentsTree2.forEach((component) => {
114
- component._privateMethods._callMountHandlers();
115
- });
116
- }, 0);
117
128
  velesElementNode2.parentVelesElement = velesNode;
118
129
  }
119
130
  }
@@ -122,11 +133,6 @@ function parseChildren({
122
133
  } else {
123
134
  htmlElement.append(velesElementNode.html);
124
135
  }
125
- setTimeout(() => {
126
- componentsTree.forEach((component) => {
127
- component._privateMethods._callMountHandlers();
128
- });
129
- }, 0);
130
136
  velesElementNode.parentVelesElement = velesNode;
131
137
  childComponents.push(childComponent);
132
138
  }
@@ -197,7 +203,12 @@ function parseComponent({
197
203
  componentAPI.onUnmount(cb);
198
204
  },
199
205
  _callMountHandlers: () => {
200
- componentMountCbs.forEach((cb) => cb());
206
+ componentMountCbs.forEach((cb) => {
207
+ const mountCbResult = cb();
208
+ if (typeof mountCbResult === "function") {
209
+ componentAPI.onUnmount(mountCbResult);
210
+ }
211
+ });
201
212
  },
202
213
  _callUnmountHandlers: () => {
203
214
  componentUnmountCbs.forEach((cb) => cb());
@@ -262,10 +273,10 @@ function Fragment({ children }) {
262
273
 
263
274
  export {
264
275
  getComponentVelesNode,
276
+ callMountHandlers,
265
277
  identity,
266
278
  onMount,
267
279
  onUnmount,
268
280
  createElement,
269
281
  Fragment
270
282
  };
271
- //# sourceMappingURL=chunk-EVX3ZDYT.js.map