├── .gitignore
├── .npmignore
├── README.md
├── assets
├── counter.png
└── logo.png
├── index.d.ts
├── lang
└── es
│ └── README.md
├── package.json
├── src
├── component.js
├── constants.js
├── context.js
├── customHooks.js
├── diff.js
├── dom.js
├── index.js
├── options.js
├── utils.js
└── vtag.js
├── test
├── attrs.test.js
├── children.test.js
├── component.test.js
├── createContext.test.js
├── hook.test.js
├── jsdom.test.js
├── keys.test.js
├── lifecycle.test.js
├── svg.test.js
└── util.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | dev
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dev
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Orby is a small and minimalist library to create modern interfaces based on JSX, Virtual-Dom and Functions.
4 |
5 | [](https://codesandbox.io/s/20k8jm0x0r)
6 |
7 | ## Index
8 |
9 | 1. [Motivation](#motivation)
10 | 2. [JSX](#jsx)
11 | 3. [Component](#component)
12 | 1. [Properties of the component](#properties-of-the-component)
13 | 2. [Control of component status](#control-of-component-status)
14 | 3. [Control of the context of the component](#control-of-the-context-of-the-component)
15 | 4. [Lifecycle](#lifecycle)
16 | 1. [onCreate](#onCreate)
17 | 2. [onCreated](#onCreated)
18 | 3. [onRemove](#onRemove)
19 | 4. [onRemoved](#onRemoved)
20 | 5. [onUpdate](#onUpdate)
21 | 6. [onUpdated](#onUpdated)
22 | 5. [Hooks](#hooks)
23 | 1. [useState](#useState)
24 | 2. [useEffect](#useEffect)
25 | 3. [useReducer](#useReducer)
26 | 4. [useContext](#useContext)
27 | 5. [useMemo](#useMemo)
28 | 6. [Special properties](#special-properties)
29 | 1. [key](#key)
30 | 2. [scoped](#scoped)
31 | 3. [context](#context)
32 | 7. [createContext](#createContext)
33 | 8. [Examples](#examples)
34 | 9. [Utils](#utils)
35 |
36 | ## Motivation
37 |
38 | Simplify the creation and maintenance of components, limiting its scope only functions, avoiding the use of classes, with the aim of simplifying the learning curve centered only on the use of functions, life cycle associated with nodes, hooks and Jsx.
39 |
40 | Another motivation is the use of shadow-dom, as part of the process of detecting changes. Example of this is the creation of a component with style scope thanks to the use of **shadow-dom**, please see the following example and note the definition of the property [scoped](#scoped).
41 |
42 | ```jsx
43 | function Button(props){
44 | return
45 |
53 | {props.children}
54 |
55 | }
56 | ```
57 |
58 | The `` component will be isolated in the Dom tree, this will define a closed scope of css styles.
59 |
60 | ## JSX
61 |
62 | [JSX](https://reactjs.org/docs/introducing-jsx.html) is defined as an extension of the JavaScript syntax, this allows to maintain a readable union between HTML and JS, and then be used for example in the manipulation of nodes, assignment of events, mutation of attributes and more.
63 |
64 | When working with Orby, please consider the following differences with other libraries, such as React, in:
65 |
66 | **Without fragment support**, Orby's components are more attached to the definition of a tree always maintaining a root node, this is how it is expressed in the [Lifecycle](#lifecycle).
67 |
68 |
69 | ## Component
70 |
71 | The functional Orbison components and you can manipulate the state of the nodes either through the [Lifecycle](#lifecycle) associated with the virtual-dom or through the use of [hooks](#hooks).
72 |
73 | ### Properties of the component
74 |
75 | Like any functional component, the first argument of the component will always be its properties.
76 |
77 | ```jsx
78 | export function Button(props){
79 | return
80 | }
81 | ```
82 |
83 | The design pattern of purely functional components does not change if you were only limited to the use of Props.
84 |
85 | ### Control of the context of the component
86 |
87 | The context allows you to share a defined object at a higher level, it will be very useful if you look for interaction between 2 components.
88 |
89 | ```jsx
90 | export function Button(props,context){
91 | return
92 | }
93 | ```
94 |
95 | Another important point is that context management can be defined by using the `context` property external to the component.
96 |
97 | ```jsx
98 | import {h,render} from "@orby/core";
99 | import App from "./app";
100 |
101 | render(
102 | ,
103 | document.querySelector("#app")
104 | );
105 | ```
106 |
107 | ## Lifecycle
108 |
109 | The life cycle manifests itself on the virtual-dom in the creation, updating and elimination of the nodes, this is similar to how it operates in [Hyperapp](https://github.com/jorgebucaran/hyperapp).
110 |
111 | ```jsx
112 | export function Button(){
113 | return
114 | }
115 | ```
116 |
117 | The DIFF process will invoke the `onCreate` properties only when the ``node is created in the dom tree. You can add the life cycle properties to the nodes you deem convenient.
118 |
119 | ### onCreate
120 |
121 | The `onCreate` property is invoked when the node is added in the dom tree.
122 |
123 | ```jsx
124 | export function Button(){
125 | return
128 | }
129 | ```
130 |
131 | ### onCreated
132 |
133 | The `onCreated` property is invoked after the node was added to the dom tree and propagated the changes to its children.
134 |
135 | ```jsx
136 | export function Button(){
137 | return
140 | }
141 | ```
142 |
143 | ### onRemove
144 |
145 | The `onRemove` property is invoked when removing the node from the dom tree.
146 |
147 | ```jsx
148 | export function Button(){
149 | return
152 | }
153 | ```
154 |
155 | ### onRemoved
156 |
157 | The `onRemoved` property is invoked after removing the node from the dom tree and propagating the changes to its children.
158 |
159 | ```jsx
160 | export function Button(){
161 | return
164 | }
165 | ```
166 |
167 | ### onUpdate
168 |
169 | The `onUpdate` property is invoked before propagating from the node of the dom tree. **return`false` to avoid such propagation**
170 |
171 | ```jsx
172 | export function Button(){
173 | return
176 | }
177 | ```
178 |
179 | ### onUpdated
180 |
181 | The `onUpdated` property is invoked after propagating from the node of the dom tree.
182 |
183 | ```jsx
184 | export function Button(){
185 | return
188 | }
189 | ```
190 |
191 |
192 | ## Hooks
193 |
194 | Hooks are a powerful way to extend the behavior of a functional component created with **Orby**, this is a small implementation based on the [React Hooks](https://reactjs.org/docs/hooks-intro.html), consider also knowing the benefits of this pattern and [rules associated with the use of Hooks](https://reactjs.org/docs/hooks-rules.html)
195 |
196 | ### ¿Why hooks?
197 |
198 | Hooks are a powerful way to separate logic from the functional component, you can create custom effects that are linked to the component only with the invocation, it is such a link that these effects manage to control the state of the component without the need to know the same component.
199 |
200 | ### useState
201 |
202 | It allows using a state and associating it with the component, by default the components in Orby do not have status since they are functions, if you require a component that can manipulate changes based on a state you can use `useState` within the component as many times as you deem appropriate. `useState` append the status control only when it is invoked within the component
203 |
204 | > Unlike `useState` of React, this returns in the array a 3 argument, this one has the purpose of obtaining the state in asynchronous behaviors, its use is optional.
205 |
206 | ```jsx
207 | import {h,useState} from "@orby/core";
208 | export function Button(){
209 | let [state,useState] = useState();
210 | }
211 | ```
212 |
213 | Note that `useState` returns an array, which you can use with [Destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) to associate A variable, `useState` also supports a first argument that defines the initial state.
214 |
215 | ```jsx
216 | import {h,useState} from "@orby/core";
217 |
218 | export function Button(){
219 | let [count,setCount] = useState(0);
220 | }
221 | ```
222 |
223 | if this first argument is a function, it is executed only when initializing the state of the component.
224 |
225 | ```jsx
226 | import {h,useState} from "@orby/core";
227 | function createState(){
228 | return {data:[]};
229 | }
230 | export function Button(){
231 | let [state,useState] = useState(createState);
232 | }
233 | ```
234 |
235 | ### useEffect
236 |
237 | It allows the execution of a function so many times the component is executed, this function is executed after the rendering process associated with patching the changes of the node.
238 |
239 | It is easier to understand the execution of `useEffect` by associating it with the life cycle methods of virtual-dom [onCreated](#onCreated) and [onUpdated](#onUpdated) and [onRemove](#onRemove).
240 |
241 | ```jsx
242 | import {h,useEffect} from "@orby/core";
243 |
244 | export function Button(){
245 | const [count, setCount] = useState(0);
246 | useEffect(()=>{
247 | document.title = `clicked ${count}`;
248 | });
249 | return ;
250 | }
251 | ```
252 |
253 | If you try to assimilate the execution of the event [onRemove](#onRemove) of the virtual-dom within `useEffect`, the function associated with `useEffect` must return a function.
254 |
255 | ```jsx
256 | export function Button(props,context){
257 | const [count, setCount] = useState(0);
258 | useEffect(()=>{
259 | document.title = `clicked ${count}`;
260 | return ()=>{
261 | document.title = `component remove`;
262 | }
263 | });
264 | return ;
265 | }
266 | ```
267 |
268 | `useEffect` also receives a second argument, this gives the ability to limit the execution of the effect only to the changes associated with the second argument. The following example shows how to limit the execution of the effect only to a first instance.
269 |
270 | ```jsx
271 | export function Button(props,context){
272 | useEffect(()=>{
273 | console.log("component created")
274 | return ()=>{
275 | console.log("component remove")
276 | }
277 | },[true]);
278 | return ;
279 | }
280 | ```
281 |
282 | ### useReducer
283 |
284 | small implementation [use React](https://reactjs.org/docs/hooks-reference.html)
285 |
286 | ```jsx
287 | const initialState = {count: 0};
288 |
289 | function reducer(state, action) {
290 | switch (action.type) {
291 | case 'reset':
292 | return {count: action.payload};
293 | case 'increment':
294 | return {count: state.count + 1};
295 | case 'decrement':
296 | return {count: state.count - 1};
297 | default:
298 | // A reducer must always return a valid state.
299 | // Alternatively you can throw an error if an invalid action is dispatched.
300 | return state;
301 | }
302 | }
303 |
304 | function Counter({initialCount}) {
305 | const [state, dispatch] = useReducer(
306 | reducer,
307 | initialState,
308 | {type: 'reset', payload: initialCount},
309 | );
310 |
311 | return (
312 |
321 | );
322 | }
323 | ```
324 |
325 |
326 |
327 | ### useContext
328 |
329 | It allows to recover the context of the component, unlike React's `useContext`, it returns the whole context if it does not have an argument.
330 |
331 | ```jsx
332 |
333 | const context = useContext(Context);
334 | ```
335 |
336 | Context is the return of the `createContext` instance, this homologous behavior of `React.createContext`.
337 |
338 | ### useMemo
339 |
340 | allows to memorize the return of a callback, each time the second argument changes.
341 |
342 | ```jsx
343 | const list = useMemo(()=>{
344 | let list =[];
345 | for(let i=0;i<1000;i++){
346 | list.push(i);
347 | }
348 | return list;
349 | },[1000]);
350 | ```
351 |
352 | this function is executed in second instance only if the values given in the second argument of useMemo are disintos to the previous one.
353 |
354 |
355 | ## Special properties
356 |
357 | ### key
358 |
359 | It allows to define the identifier on the virtual-dom, to link it to a previous state, regardless of its order. The use of keys allows for example:
360 |
361 | 1. Maintain an associative state of virtual-dom and a node indifferent to its order.
362 | 2. Reduce the amount of manipulations associated with sun.
363 |
364 | ### scoped
365 |
366 | the `scoped` property allows to enable the use of `shadow-dom` on the node, when defining scoped as true, the DIFF process will understand that the nodes will be mounted in the `shadowRoot` of the node.
367 |
368 | ```jsx
369 | export function Button(props){
370 | return
371 |
372 | {props.children}
373 |
374 | }
375 | ```
376 |
377 | ### context
378 |
379 | The `context` property allows you to add new properties to the context.
380 |
381 | ```jsx
382 |
383 |
384 |
385 | ```
386 |
387 | The example component `ChildComponent` can make use of the context defined in a superior way. Note that it is not necessary to enter the component to create contexts.
388 |
389 | ## createContext
390 |
391 | This function allows you to create contexts that already reserve a namespace.
392 |
393 | ### Default contexts in Orby
394 |
395 | Orby by default allows to generate contexts in a simple way but this forces the child node to know the name of the property to access it and this can generate conflict.
396 |
397 | ```jsx
398 | import {h,render} from "@orby/core";
399 |
400 | function Title(props,context){
401 | return
{context.myTitleContext}
402 | }
403 |
404 | render(
405 | ,
406 | document.body
407 | )
408 | ```
409 |
410 | ### context with createContext
411 |
412 | Through createContext, you ensure that the name of the property is stored only within the createContext instance, reducing the possibility of name conflict.
413 |
414 | ```jsx
415 | import {h, createContext} from "@orby/core";
416 |
417 | let Context = createContext({title:"hello"});
418 |
419 | function Title(props,context){
420 | return
421 |
422 | {(data)=>data.title}
423 |
424 |
425 | }
426 |
427 | render(
428 |
429 |
430 | ,
431 | document.body
432 | )
433 | ```
434 |
435 | ### consume context with useContext
436 |
437 | By giving useContext the context instance this returns the value of the property associated with the reservation and name of the context
438 |
439 |
440 | ```jsx
441 | import {h,useContext,createContext} from "@orby/core";
442 |
443 | let Context = createContext({title:"hello"});
444 |
445 | function Title(props,context){
446 | let data = useContext(Context);
447 | return
448 | {data.title}
449 |
450 | }
451 |
452 | render(
453 |
454 |
455 | ,
456 | document.body
457 | )
458 | ```
459 |
460 | ## Examples
461 |
462 | | Title | Description | link |
463 | |---------|------------------------------|------------------------------------------------|
464 | | Counter | shows the use of `useState` | [🔗 link](https://codesandbox.io/s/x308nz8mrp) |
465 | | Counter with Reducer | shows the use of `useState`| [🔗 link](https://codesandbox.io/s/2ww3ylj5wp) |
466 | | Hooks Router | show how to use `useRouter` and `useRedirect`| [🔗 link](https://codesandbox.io/s/p7vzn8xx57) |
467 |
468 | ## Utilities
469 |
470 | | Title | Description | Repo |
471 | |---------|-----------------------------------------------------|------------------------------------------------|
472 | | Router | Manage your routes in a simple and declarative way | [🔗 link](https://github.com/orbyjs/router) |
--------------------------------------------------------------------------------
/assets/counter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orbyjs/core/673637926c528a4d007e1dff457c6e01928c21f0/assets/counter.png
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/orbyjs/core/673637926c528a4d007e1dff457c6e01928c21f0/assets/logo.png
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Vtag } from "./src/vtag";
2 | import { Component } from "./src/component";
3 |
4 | interface Options{
5 | document:?object,
6 | delay:Number
7 | }
8 |
9 | interface Reducer{
10 | (state:any,action:object):any
11 | }
12 |
13 | interface Action{
14 | type : any,
15 | }
16 |
17 | interface Dispatch{
18 | (action:Action):void
19 | }
20 |
21 | interface ProviderProps{
22 | value:?any,
23 | children:Array
24 | }
25 |
26 | interface Context{
27 | Provider(props:ProviderProps,context:object):Vtag,
28 | Consumer(handler:Function):Vtag|any
29 | }
30 |
31 | declare module "@orby/core"{
32 | export function h(tag:string,props?:object,...children):Vtag;
33 | export function render(next:Vtag,parent:HTMLElement,child:?HTMLElement,context:?object,isSvg:?boolean):HTMLElement;
34 | export function useState(initialState:any):[any,Function,Function];
35 | export function useEffect(handler:Function,list:any[]):void;
36 | export function useContext(Context?:object):any;
37 | export function useReducer(state:any,reducer:Reducer,actionInit:?Action):[any,Dispatch];
38 | export function useRef(current:?any):object;
39 | export function useMemo(handler:Function):any;
40 | export function createContext(value:?any):Context;
41 | export function getCurrentComponent():Component;
42 | export let option:Option;
43 | }
--------------------------------------------------------------------------------
/lang/es/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Orby es una pequeña y minimalista librería para crear interfaces modernas a base de JSX, Virtual-Dom y Funciones.
4 |
5 | [](https://codesandbox.io/s/20k8jm0x0r)
6 |
7 | ## Índice
8 |
9 | 1. [Motivacion](#motivación)
10 | 2. [JSX](#jsx)
11 | 3. [Componente](#componente)
12 | 1. [Propiedades del componente](#propiedades-del-componente)
13 | 2. [Estado del componente](#estado-del-componente)
14 | 3. [Contexto en componente](#contexto-en-componente)
15 | 4. [Ciclo de vida](#ciclo-de-vida)
16 | 1. [onCreate](#onCreate)
17 | 2. [onCreated](#onCreated)
18 | 3. [onRemove](#onRemove)
19 | 4. [onRemoved](#onRemoved)
20 | 5. [onUpdate](#onUpdate)
21 | 6. [onUpdated](#onUpdated)
22 | 5. [Hooks](#hooks)
23 | 1. [useState](#useState)
24 | 2. [useEffect](#useEffect)
25 | 6. [Propiedades especiales](#propiedades-especiales)
26 | 1. [key](#key)
27 | 2. [scoped](#scoped)
28 | 3. [context](#context)
29 | 7. [Ejemplos](#ejemplos)
30 |
31 | ## Motivación
32 |
33 | Simplificar la creación y mantención de componentes, limitando su alcance solo funciones, evitando la utilización de clases, con el objetivo de simplificar la curva de aprendizaje centrada solo en el uso de funciones , ciclo de vida asociado a nodos, hooks y Jsx.
34 |
35 | Otra motivación es el aprovechamiento del shadow-dom, como parte del proceso de detección de cambios. Ejemplo de esto es la creación de un componente con alcance de estilo gracias al uso del **shadow-dom**, favor vea el siguiente ejemplo y note la definición de la propiedad [scoped](#scoped).
36 |
37 | ```jsx
38 | function Button(props){
39 | return
40 |
48 | {props.children}
49 |
50 | }
51 | ```
52 |
53 | El componente `` quedara asilado en el árbol de Dom, esto definirá un alcance cerrado de estilos css.
54 |
55 | ## JSX
56 |
57 | [JSX](https://reactjs.org/docs/introducing-jsx.html) se define como una extensión de la sintaxis de JavaScript, esta permite mantener una unión legible entre el HTML y JS, para luego ser usada por ejemplo en la manipulación de nodos, asignación de eventos, mutación de atributos y más.
58 |
59 | Al momento de trabajar con Orby, por favor considere las siguientes diferencias con otras bibliotecas como React, en :
60 |
61 | **Sin soporte a fragmentos**, los componentes de Orby se apegan mas a la definición de un árbol manteniendo siempre un nodo de raiz, esto es por como se expresa el [ciclo de vida](#ciclo-de-vida) del mismo arbol.
62 |
63 |
64 |
65 | ## Componente
66 |
67 | Los componentes en Orby son funcionales y ud puede manipular el estado de los nodos sea mediante el [ciclo de vida](#ciclo-de-vida) asociado al virtual-dom o mediante el uso de [hooks](#hooks)
68 |
69 | ### Propiedades del componente
70 |
71 | Al igual que cualquier componente funcional, el primer argumento del componente siempre serán sus propiedades
72 |
73 | ```jsx
74 | export function Button(props){
75 | return
76 | }
77 | ```
78 |
79 | El patrón de diseño de componentes puramente funcionales no cambia si ud solo se limitara al uso de Props
80 |
81 | ### Contexto en componente
82 |
83 | El contexto permite compartir un objeto definido a nivel superior, le resultara sumamente útil si busca interacción entre 2 componentes.
84 |
85 | ```jsx
86 | export function Button(props,context){
87 | return
88 | }
89 | ```
90 |
91 | Otro punto importante es que el manejo del contexto puede ser definido mediante el uso de la propiedad `context` de forma externa al componente.
92 |
93 | ```jsx
94 | import {h,render} from "@orby/core";
95 | import App from "./app";
96 |
97 | render(
98 | ,
99 | document.querySelector("#app")
100 | );
101 | ```
102 |
103 | ## Ciclo de vida
104 |
105 | El ciclo de vida se manifiesta sobre el virtual-dom en la creación, actualización y eliminación de los nodos, esto es similar a como opera en [Hyperapp](https://github.com/jorgebucaran/hyperapp).
106 |
107 | ```jsx
108 | export function Button(){
109 | return
110 | }
111 | ```
112 |
113 | El proceso de DIFF invocara la propiedades `onCreate` solo cuando el nodo `` se cree en el árbol de dom. Ud puede añadir las propiedad de ciclo de vida a los nodos que estime conveniente.
114 |
115 | ### onCreate
116 |
117 | La propiedad `onCreate` se invoca cuando el nodo se añade en el árbol de dom.
118 |
119 | ```jsx
120 | export function Button(){
121 | return
124 | }
125 | ```
126 |
127 | ### onCreated
128 |
129 | La propiedad `onCreated` se invoca luego de que el nodo se añadió al árbol de dom y propago los cambios hacia sus hijos.
130 |
131 | ```jsx
132 | export function Button(){
133 | return
136 | }
137 | ```
138 |
139 | ### onRemove
140 |
141 | La propiedad `onRemove` se invoca al eliminar el nodo del arbol de dom.
142 |
143 | ```jsx
144 | export function Button(){
145 | return
148 | }
149 | ```
150 |
151 | ### onRemoved
152 |
153 | La propiedad `onRemoved` se invoca luego de eliminar el nodo del arbol de dom y propagar los cambios hacia sus hijos.
154 |
155 | ```jsx
156 | export function Button(){
157 | return
160 | }
161 | ```
162 |
163 | ### onUpdate
164 |
165 | La propiedad `onUpdate` se invoca antes de propagar del nodo del arbol de dom. **retorne`false` para evitar tal propagación**
166 |
167 | ```jsx
168 | export function Button(){
169 | return
172 | }
173 | ```
174 |
175 | ### onUpdated
176 |
177 | La propiedad `onUpdated` se invoca luego de propagar del nodo del arbol de dom.
178 |
179 | ```jsx
180 | export function Button(){
181 | return
184 | }
185 | ```
186 |
187 | ## Hooks
188 |
189 | Los hooks son una poderosa forma de extender el comportamiento de un componente funcional creado con **Orby**, esta es una pequeña implementación basada en los [Hooks de React](https://reactjs.org/docs/hooks-intro.html), considere también conocer los beneficios de este patrón y [reglas asociadas al uso de Hooks](https://reactjs.org/docs/hooks-rules.html)
190 |
191 | ### ¿Por que hooks?
192 |
193 | Los hooks son una poderosa forma de independizar lógica del componente funcional, ud puede crear efectos personalizados que se vinculen al componente solo con la invocación, es tal la vinculación que dichos efectos logran controlar el estado del componente sin la necesidad de conocer al mismo componente.
194 |
195 | ### useState
196 |
197 | Permite usar un estado y asociarlo al componente, por defecto los componentes en Orby no poseen estado ya que son funciones, si ud requiere un componente que pueda manipular cambios a base de un estado puede usar `useState` dentro del componente tantas veces estime conveniente. `useState` anexara el control de estado solo cuando este se invoque dentro del componente
198 |
199 | > A diferencia de `useState` de React, este retorna en el arreglo un 3 argumento, este posee la finalidad de obtener el estado en comportamientos asíncronos, su uso es opcional.
200 |
201 | ```jsx
202 | import {h,useState} from "@orby/core";
203 | export function Button(){
204 | let [state,useState,getState] = useState();
205 | }
206 | ```
207 |
208 | Note que `useState` retorna un arreglo, el que ud mediante el uso de [Destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) puede asociar a una variable, `useState` también admite un primer argumento el que define el estado inicial.
209 |
210 | ```jsx
211 | import {h,useState} from "@orby/core";
212 |
213 | export function Button(){
214 | let [count,setCount] = useState(0);
215 | }
216 | ```
217 |
218 | si este primer argumento es una función, se ejecutara solo al inicializar el estado del componente.
219 |
220 | ```jsx
221 | import {h,useState} from "@orby/core";
222 | function createState(){
223 | return {data:[]};
224 | }
225 | export function Button(){
226 | let [state,useState,getState] = useState(createState);
227 | }
228 | ```
229 |
230 |
231 |
232 | ### useEffect
233 |
234 | Permite la ejecución de una función tantas veces se ejecute el componente, esta función se ejecuta luego del proceso de render asociado a parchar los cambios del nodo.
235 |
236 | Es mas fácil entender a la ejecución de `useEffect` asociándola a los métodos de ciclo de vida del virtual-dom [onCreated](#created) y [onUpdated](#onUpdated) y [onRemove](#onRemove).
237 |
238 | ```jsx
239 | import {h,useEffect} from "@orby/core";
240 |
241 | export function Button(){
242 | const [count, setCount] = useState(0);
243 | useEffect(()=>{
244 | document.title = `clicked ${count}`;
245 | });
246 | return ;
247 | }
248 | ```
249 |
250 | si ud busca asimilar la ejecución del evento [onRemove](#remove) del virtual-dom dentro de `useEffect`, la función asociada a `useEffect` deberá retornar una función.
251 |
252 | ```jsx
253 | export function Button(props,context){
254 | const [count, setCount] = useState(0);
255 | useEffect(()=>{
256 | document.title = `clicked ${count}`;
257 | return ()=>{
258 | document.title = `component remove`;
259 | }
260 | });
261 | return ;
262 | }
263 | ```
264 |
265 | `useEffect` también recibe un segundo argumentó, este le entrega la capacidad de limitar la ejecución del efecto solo ante los cambios asociados al segundo argumento. el siguiente ejemplo enseña como limitar la ejecución del efecto solo a una primera instancia.
266 |
267 | ```jsx
268 | export function Button(props,context){
269 | useEffect(()=>{
270 | console.log("component created")
271 | return ()=>{
272 | console.log("component remove")
273 | }
274 | },[true]);
275 | return ;
276 | }
277 | ```
278 |
279 | ### useReducer
280 |
281 | pequeña implementación [useReducer de React](https://reactjs.org/docs/hooks-reference.html)
282 |
283 | ```jsx
284 | const initialState = {count: 0};
285 |
286 | function reducer(state, action) {
287 | switch (action.type) {
288 | case 'reset':
289 | return {count: action.payload};
290 | case 'increment':
291 | return {count: state.count + 1};
292 | case 'decrement':
293 | return {count: state.count - 1};
294 | default:
295 | // A reducer must always return a valid state.
296 | // Alternatively you can throw an error if an invalid action is dispatched.
297 | return state;
298 | }
299 | }
300 |
301 | function Counter({initialCount}) {
302 | const [state, dispatch] = useReducer(
303 | reducer,
304 | initialState,
305 | {type: 'reset', payload: initialCount},
306 | );
307 |
308 | return (
309 |
318 | );
319 | }
320 | ```
321 |
322 |
323 |
324 | ### useContext
325 |
326 | Permite recuperar el contexto del componente, a diferencia de `useContext` de React, este retorna todo el contexto si no posee argumento.
327 |
328 | ```jsx
329 | const context = useContext(Context);
330 | ```
331 |
332 | Contexto es el retorno de la instancia de `createContext` de la librería [@orby/context](https://github.com/orbyjs/context), esta homologa el comportamiento de `React.createContext`.
333 |
334 |
335 |
336 | ## Propiedades especiales
337 |
338 | ### key
339 |
340 | Permite definir identificador sobre el virtual-dom, para vincularlo a un estado anterior, indiferente a su orden. El uso de keys permite por ejemplo :
341 |
342 | 1. Mantener un estado asociativo del virtual-dom y un nodo indiferente a su orden.
343 | 2. Reducir la cantidad de manipulaciones asociadas al dom.
344 |
345 | ### scoped
346 |
347 | la propiedad `scoped` permite habilitar el uso de `shadow-dom` sobre el nodo, al definir scoped como verdadero, el proceso de DIFF, entenderá que los nodos se montaran en el `shadowRoot` del nodo.
348 |
349 | ```jsx
350 | export function Button(props){
351 | return
352 |
353 | {props.children}
354 |
355 | }
356 | ```
357 |
358 | ### context
359 |
360 | la propiedad `context`, permite añadir nuevas propiedades al contexto.
361 |
362 | ```jsx
363 |
364 |
365 |
366 | ```
367 |
368 | El componente de ejemplo `ChildComponent`, puede hacer uso del contexto definido de forma superior. Note que no es necesario ingresar al componente para crear contextos.
369 |
370 | ## Ejemplos
371 |
372 | | Titulo | link |
373 | |--------|------|
374 | | Counter | [🔗 link](https://codesandbox.io/s/20k8jm0x0r) |
375 |
376 | ## Utilidades
377 |
378 | | Titulo | Detalle | Repo |
379 | |---------|-----------------------------------------------------|------------------------------------------------|
380 | | Router | Gestiona tus rutas de forma simple y declarativa | [🔗 link](https://github.com/orbyjs/router) |
381 | | Context | Una pequeña implementación de `React.createContext` | [🔗 link](https://github.com/orbyjs/context) |
382 |
383 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@orby/core",
3 | "description": "Orby is a small experiment of functional components based on virtual-dom.",
4 | "version": "0.1.6",
5 | "main": "dist/orby.js",
6 | "module": "dist/orby.mjs",
7 | "umd:main": "dist/orby.umd.js",
8 | "source": "src/index.js",
9 | "author": "Matias Trujillo Olivares",
10 | "license": "ISC",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/orbyjs/core.git"
14 | },
15 | "scripts": {
16 | "watch": "microbundle -w --name @orby/core",
17 | "test": "jest",
18 | "build": "microbundle --name @orby/core",
19 | "build:uncompress": "microbundle --name @orby/core --compress ",
20 | "prepare": "npm run build && npm test",
21 | "upload": "npm publish --access public"
22 | },
23 | "devDependencies": {
24 | "@babel/core": "^7.1.5",
25 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0",
26 | "@babel/plugin-transform-react-jsx": "^7.0.0",
27 | "@babel/preset-env": "^7.2.3",
28 | "babel-core": "7.0.0-bridge.0",
29 | "babel-jest": "^23.6.0",
30 | "jest": "^23.6.0",
31 | "microbundle": "^0.7.0",
32 | "regenerator-runtime": "^0.12.1"
33 | },
34 | "babel": {
35 | "presets":[
36 | [
37 | "@babel/preset-env",
38 | {
39 | "useBuiltIns": "entry"
40 | }
41 | ]
42 | ],
43 | "plugins": [
44 | [
45 | "@babel/plugin-transform-react-jsx",
46 | {
47 | "pragma": "h"
48 | }
49 | ]
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/component.js:
--------------------------------------------------------------------------------
1 | import { isArray, isEqualArray } from "./utils";
2 | import { options } from "./options";
3 | import { updateElement } from "./diff";
4 | import { REMOVE } from "./constants";
5 |
6 | let CURRENT_COMPONENT;
7 | let CURRENT_KEY_STATE;
8 | /**
9 | * @param {Array} components List of components to clean the effects
10 | * @param {boolean} [remove] If true, component.update will not continue with the update process since the component is given as removed.
11 | */
12 | export function clearComponentEffects(components, remove) {
13 | let length = components.length;
14 | for (let i = 0; i < length; i++) {
15 | let component = components[i];
16 | component.clearEffects(false);
17 | if (remove && component) component.remove = true;
18 | }
19 | }
20 |
21 | /**
22 | * obtains the component in execution, for the connection with the hooks
23 | * @returns {Vtag}
24 | */
25 | export function getCurrentComponent() {
26 | if (CURRENT_COMPONENT) {
27 | return CURRENT_COMPONENT;
28 | }
29 | throw new Error(
30 | "the hooks can only be called from an existing functional component in the diff queue"
31 | );
32 | }
33 | /**
34 | * Allows you to add an observer status of changes to the functional component
35 | * @param {*} initialState - Initial state to register
36 | */
37 | export function useState(initialState) {
38 | let key = CURRENT_KEY_STATE++,
39 | use = getCurrentComponent();
40 |
41 | if (!(key in use.states)) {
42 | use.states.push(
43 | typeof initialState === "function" ? initialState() : initialState
44 | );
45 | }
46 | return [
47 | use.states[key],
48 | nextState => {
49 | use.states[key] = nextState;
50 | if (use.prevent) return;
51 | setTimeout(() => {
52 | use.update(true);
53 | use.prevent = false;
54 | }, options.delay);
55 | use.prevent = true;
56 | }
57 | ];
58 | }
59 |
60 | /**
61 | * allows to add an observer effect before the changes of the component
62 | * note the use of `clearComponentEffects`, this function allows to clean the
63 | * effects associated with the elimination of the component.
64 | * @param {Function} handler
65 | * @param {array} args - allows to issue the handler only when one of the properties is different from the previous one
66 | */
67 | export function useEffect(handler, args) {
68 | let setup,
69 | use = getCurrentComponent(),
70 | isValidArgs = isArray(args);
71 |
72 | let [state] = useState(() => {
73 | setup = true;
74 | return { args: isValidArgs ? args : [] };
75 | });
76 |
77 | if (!setup) {
78 | if (isValidArgs) {
79 | if (isEqualArray(state.args, args)) {
80 | use.effectsToPrevent[use.effectsToUpdated.length] = true;
81 | }
82 | state.args = args;
83 | }
84 | }
85 | use.effectsToUpdated.push(handler);
86 | }
87 |
88 | /**
89 | * generates a routing point by associating it with an instance stored in the current node
90 | * @class
91 | * @param {Function} tag - function to associate with the component instance
92 | * @param {number} deep - depth index of components
93 | * @param {Array} components - group of components associated with the node
94 | * @property {HTMLElement} parent - parentElement where the node to be updated is hosted by the component
95 | * @property {HTMLElement} base - node already hosted within the parentElement, to update or replace
96 | * @property {boolean} isSvg - if true, the component will create svg nodes
97 | * @property {boolean} deep - depth level of the component, in the high order list.
98 | * @property {boolean} remove - Bootstrap, defines if the component is instantiated, to force executions
99 | * @property {boolean} boot - Bootstrap, defines if the component is instantiated, to force executions
100 | * @property {Array} components - List of components in high order
101 | * @property {object} props - component properties
102 | * @property {states} states - state store for useState
103 | * @property {Array} effectsToRemove - Efectos a limpiar antes de cada render o eliminación.
104 | * @property {Array} effectsToUpdated - Effects to call after each render
105 | * @property {Object} effectsToPrevent - effects to prevent execution, either by comparison of parameters
106 | * @property {Object} context - inherited context to the component
107 | * @property {boolean} prevent - the microtask, blocks the execution of render based on options.delay,
108 | * prevent allows to respect this blocking
109 | */
110 | export class Component {
111 | constructor(tag, deep, components) {
112 | this.parent;
113 | this.base;
114 | this.isSvg;
115 | this.remove;
116 | this.deep = deep;
117 | this.boot = true;
118 | this.components = components;
119 | this.tag = tag;
120 | this.props = {};
121 | this.states = [];
122 | this.effectsToRemove = [];
123 | this.effectsToUpdated = [];
124 | this.effectsToPrevent = {};
125 | this.context = {};
126 | this.prevent = false;
127 | }
128 | /**
129 | * cleans the effects associated with the component
130 | * @param {boolean} withPrevent - being true uses the effectsToPrevent property to skip execution
131 | * this option is given in a cleaning without elimination of the node
132 | */
133 | clearEffects(withPrevent) {
134 | let length = this.effectsToRemove.length;
135 | for (let i = 0; i < length; i++) {
136 | let remove = this.effectsToRemove[i];
137 | if (remove && (withPrevent ? !this.effectsToPrevent[i] : true))
138 | remove();
139 | }
140 | }
141 | /**
142 | * creates a new deletion effects queue, being true within effectsToPrevent,
143 | * the previous handler is retrieved
144 | */
145 | recollectEffects() {
146 | let remove = [],
147 | length = this.effectsToUpdated.length;
148 |
149 | for (let i = 0; i < length; i++) {
150 | let handler = this.effectsToUpdated[i];
151 | remove[i] = this.effectsToPrevent[i]
152 | ? this.effectsToRemove[i]
153 | : handler();
154 | }
155 | this.effectsToRemove = remove;
156 | }
157 | update() {
158 | //if (this.prevent) return this.base;
159 | if (this.base[REMOVE] || this.remove) return;
160 |
161 | CURRENT_KEY_STATE = 0;
162 | CURRENT_COMPONENT = this;
163 |
164 | this.effectsToUpdated = [];
165 | this.effectsToPrevent = {};
166 |
167 | let nextState = this.tag(this.props, this.context);
168 |
169 | CURRENT_COMPONENT = false;
170 |
171 | this.clearEffects(true);
172 |
173 | let base = updateElement(
174 | this.parent,
175 | this.base,
176 | false,
177 | nextState,
178 | this.context,
179 | this.isSvg,
180 | this.boot,
181 | this.deep + 1,
182 | this.components
183 | );
184 | /**
185 | * updates the parents of the presence of the new node
186 | */
187 | if (base !== this.base) {
188 | for (let i = 0; i < this.deep; i++) {
189 | this.components[i].base = base;
190 | }
191 | }
192 | this.base = base;
193 | this.recollectEffects();
194 | this.boot = false;
195 | return this.base;
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * constant for the list of components associated with the node
3 | */
4 | export const COMPONENTS = "__COMPONENTS__";
5 |
6 | /**
7 | * constant to store the previous vtag
8 | */
9 | export const PREVIOUS = "__PREVIOUS__";
10 | /**
11 | * CSS does not change the style based on an iteration since
12 | * each mutation is a mutation in the sun. instead of this
13 | * it creates a string style, to generate a single mutation,
14 | * this string is stored in the node associating it with this constant
15 | */
16 | export const PREVIOUS_CSS_TEXT = "__PREVIOUS_CSS__";
17 | /**
18 | * constant to mark the deletion of a node
19 | */
20 | export const REMOVE = "__REMOVE__";
21 | /**
22 | * Constant to mark events within a node
23 | */
24 | export const HANDLERS = "__HANDLERS__";
25 |
26 | /**
27 | * Special properties of virtual dom,
28 | * these are ignored from the updateProperties process,
29 | * since it is part of the component's life cycle
30 | */
31 |
32 | export const IGNORE = /^(context|children|(on){1}(Create|Update|Remove)(d){0,1}|xmlns|key|ref)$/;
33 | /**
34 | *The createClass function uses this constant to define the context prefix
35 | */
36 | export const CONTEXT = "__CONTEXT__";
37 |
--------------------------------------------------------------------------------
/src/context.js:
--------------------------------------------------------------------------------
1 | import { h } from "./vtag";
2 | import { CONTEXT } from "./constants";
3 |
4 | let counter = 0;
5 | /**
6 | * create a consumable context by useContext,
7 | * using counter the context name is
8 | * incremental insuring a namespace for each
9 | * invocation of createContext
10 | * @param {*} valueDefault
11 | */
12 | export function createContext(valueDefault) {
13 | let space = CONTEXT + counter++,
14 | Context = ({ children }) => children[0];
15 | return {
16 | /**
17 | * usState obtains the context through the use of space
18 | * `useState(Context)`
19 | */
20 | toString() {
21 | return space;
22 | },
23 | /**
24 | * adds to the diff process the new context
25 | * @param {*} props
26 | */
27 | Provider(props) {
28 | return (
29 |
30 | {props.children}
31 |
32 | );
33 | },
34 | /**
35 | * obtains from the current context the value associated with the instance
36 | * @param {object} props
37 | * @param {object} context
38 | */
39 | Consumer(props, context) {
40 | return props.children[0](context[space]);
41 | }
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/customHooks.js:
--------------------------------------------------------------------------------
1 | import { useState, getCurrentComponent } from "./component";
2 | import { isArray, isEqualArray } from "./utils";
3 |
4 | export function useReducer(reducer, firstAction = { type: "setup" }) {
5 | let [state, setState] = useState(() => ({
6 | current: reducer(undefined, firstAction)
7 | }));
8 | return [
9 | state.current,
10 | action => {
11 | state.current = reducer(state.current, action);
12 | setState(state);
13 | }
14 | ];
15 | }
16 |
17 | /**
18 | * returns the current context of the component in execution
19 | * @param {string} [space]
20 | */
21 | export function useContext(space) {
22 | let context = getCurrentComponent().context;
23 | return space ? context[space] : context;
24 | }
25 |
26 | export function useMemo(callback, args) {
27 | let [state] = useState({ args: [] }),
28 | isValidArgs = isArray(args);
29 |
30 | if (isValidArgs && !isEqualArray(state.args, args)) {
31 | state.memo = callback();
32 | state.args = args;
33 | }
34 |
35 | return state.memo;
36 | }
37 |
38 | export function useRef(current) {
39 | let [state] = useState({ current });
40 | return state;
41 | }
42 |
--------------------------------------------------------------------------------
/src/diff.js:
--------------------------------------------------------------------------------
1 | import { Vtag } from "./vtag";
2 | import { create, remove, append, replace, root, before } from "./dom";
3 | import { options } from "./options";
4 |
5 | import {
6 | COMPONENTS,
7 | PREVIOUS,
8 | REMOVE,
9 | HANDLERS,
10 | IGNORE,
11 | PREVIOUS_CSS_TEXT
12 | } from "./constants";
13 |
14 | import {
15 | Component,
16 | clearComponentEffects,
17 | getCurrentComponent
18 | } from "./component";
19 |
20 | let MEMO_PROPERTIES_CSS = {};
21 | let MEMO_PROPERTIES_HANDLERS = {};
22 | /**
23 | * It allows to print the status of virtual dom on the planned configuration
24 | * @param {Vtag} next - the next state of the node
25 | * @param {HTMLElement} parent - the container of the node
26 | * @param {HTMLElement} [child] - the ancestor of the node
27 | * @param {object} [context] - the context of the node
28 | * @param {boolean} [isSvg] - check if the node belongs to a svg unit, to control it as such
29 | * @returns {HTMLElement} - The current node
30 | */
31 | export function render(next, parent, child) {
32 | return updateElement(root(parent), child, false, next);
33 | }
34 | /**
35 | * allows to issue a property of the object
36 | * @param {Vtag} Vtag
37 | * @param {string} prop
38 | * @param {...any} args
39 | * @returns {boolean|undefined}
40 | */
41 | export function emit(Vtag, prop, ...args) {
42 | if (Vtag.removed) return;
43 | if (Vtag.remove && prop !== "onRemoved") return;
44 | if (prop === "onRemove") Vtag.remove = true;
45 | if (prop === "onRemoved") Vtag.removed = true;
46 | if (Vtag.props[prop]) return Vtag.props[prop](...args);
47 | }
48 | /**
49 | * obtains the previous state of the node, if it does not exist it creates an empty one
50 | * @param {HTMLElement|SVGElement|Text|undefined} node -node to extract the previous state by using the constate PREVIOUS
51 | * @param {boolean} create - being false this prevents the creation of an empty state
52 | * @returns {Vtag|boolean}
53 | */
54 | export function getPrevious(node, create = true) {
55 | if (node) {
56 | if (node[PREVIOUS]) {
57 | return node[PREVIOUS];
58 | } else {
59 | /**static_render
60 | // STATIC RENDER
61 | // it is a way to homologate the behavior of server render,
62 | // it allows transoforming existing nodes in the document, in valid vtag for the diff process.
63 | // `render(vtag,elementContainer,elementStaticRender)`
64 |
65 | let tag = "",
66 | props = {},
67 | children = [];
68 | if (node instanceof Text) {
69 | children = [node.textContent];
70 | } else {
71 | let isScoped,
72 | attrs = node.attributes,
73 | childrenReal = node.childNodes,
74 | childrenRealLength = childrenReal.length,
75 | childrenCountRemove = 0,
76 | attrsLength = attrs.length,
77 | supportAttachShadow = "attachShadow" in node;
78 |
79 | tag = node.tagName.toLowerCase();
80 |
81 | for (let i = 0; i < attrsLength; i++) {
82 | let { name, value } = attrs[i];
83 | props[name] = name === "value" ? value : value || true;
84 | if (name === "scoped") isScoped = true;
85 | }
86 | if (isScoped && supportAttachShadow) {
87 | if (!node.shadowRoot) node.attachShadow({ mode: "open" });
88 | }
89 |
90 | for (let i = 0; i < childrenRealLength; i++) {
91 | let childReal = childrenReal[i - childrenCountRemove];
92 | if (isScoped && supportAttachShadow) {
93 | node.shadowRoot.appendChild(childReal);
94 | childrenCountRemove++;
95 | }
96 | children.push(getPrevious(childReal));
97 | }
98 | }
99 | node[STATIC_RENDER] = true;
100 | node[COMPONENTS] = [];
101 | return (node[PREVIOUS] = new Vtag(tag, props, children));
102 | */
103 | }
104 | }
105 | return create ? new Vtag() : false;
106 | }
107 | /**
108 | * the components associated with the node return
109 | * @param {HTMLElement|SVGElement|undefined} node
110 | * @returns {Array|undefined}
111 | */
112 | export function getComponents(node) {
113 | return node && node[COMPONENTS];
114 | }
115 | /**
116 | * allows to create, change or update
117 | * @param {HTMLElement} parent - the container of the node
118 | * @param {HTMLElement} [node] - the ancestor of the node
119 | * @param {HTMLElement} [nodeSibling] - allows using the before method in replacement of append, if the node is created
120 | * @param {Vtag} next - the next state of the node
121 | * @param {Object} [context] - the context of the node
122 | * @param {boolean} [isSvg] - check if the node belongs to a svg unit, to control it as such
123 | * @param {number} [deep] - this is a depth marker used to generate an index to store the state of the component
124 | * @param {object} [currentComponents] - the functional components are stored in an object created by the first component
125 | * @returns {HTMLElement} - The current node
126 | */
127 |
128 | export function updateElement(
129 | parent,
130 | node,
131 | nodeSibling,
132 | next,
133 | context = {},
134 | isSvg,
135 | isCreate,
136 | deep = 0,
137 | components = []
138 | ) {
139 | let prev = getPrevious(node),
140 | base = node,
141 | component,
142 | withUpdate = true;
143 |
144 | components = getComponents(node) || components;
145 | //
146 | if (prev === next) return base;
147 |
148 | if (!(next instanceof Vtag)) {
149 | let nextType = typeof next;
150 | next = new Vtag("", {}, [
151 | nextType === "string" || nextType === "number" ? next : ""
152 | ]);
153 | }
154 |
155 | let addContext = next.props.context;
156 |
157 | context = addContext ? { ...context, ...addContext } : context;
158 |
159 | isSvg = next.tag === "svg" || isSvg;
160 | /**
161 | * being a component compares the index with the current node,
162 | * being different proceeds to its cleaning.
163 | * this happens with the removal of a node
164 | */
165 | if (components[deep] && components[deep].tag !== next.tag) {
166 | clearComponentEffects(components.splice(deep), true);
167 | }
168 | /**
169 | * if the current tag is a function, a state memorization is created as a component
170 | */
171 | if (typeof next.tag === "function") {
172 | if ((components[deep] || {}).tag !== next.tag) {
173 | components[deep] = new Component(next.tag, deep, components);
174 | }
175 | component = components[deep];
176 | next = next.clone(prev.tag || "");
177 | }
178 | if (prev.tag !== next.tag) {
179 | base = create(next.tag, isSvg);
180 | if (node) {
181 | if (!component && prev.tag) {
182 | recollectNodeTree(node, deep);
183 | }
184 | replace(parent, base, node);
185 | } else {
186 | /**
187 | * if there is no Sibling, the use of insertNode is assumed, replacing appendChild.
188 | */
189 | (nodeSibling ? before : append)(parent, base, nodeSibling);
190 | }
191 | isCreate = true;
192 | } else {
193 | if (next.static) return base;
194 | }
195 | if (next.ref) next.ref.current = base;
196 | /**static_render
197 | if (base[STATIC_RENDER]) {
198 | isCreate = true;
199 | base[STATIC_RENDER] = false;
200 | }
201 | */
202 | if (isCreate && !component) {
203 | base[REMOVE] = false;
204 | emit(next, "onCreate", base);
205 | }
206 |
207 | if (component) {
208 | component.isSvg = isSvg;
209 | component.base = base;
210 | component.parent = parent;
211 | component.props = next.props;
212 | component.context = context;
213 |
214 | if (component.prevent) {
215 | return component.base;
216 | }
217 |
218 | return component.update();
219 | } else if (next.tag) {
220 | withUpdate =
221 | emit(next, "onUpdate", base, prev.props, next.props) !== false;
222 | if (isCreate || withUpdate) {
223 | updateProperties(
224 | base,
225 | prev.tag === next.tag ? prev.props : {},
226 | next.props,
227 | isSvg
228 | );
229 | let childrenVtag = next.children,
230 | childrenVtagKeys = next.keys,
231 | nextParent = next.props.scoped ? root(base) : base,
232 | childrenReal = nextParent.childNodes,
233 | childrenVtagLength = childrenVtag.length,
234 | childrenRealLength = childrenReal.length,
235 | childrenByKeys = {},
236 | childrenCountRemove = 0;
237 |
238 | for (let index = 0; index < childrenRealLength; index++) {
239 | let node = childrenReal[index - childrenCountRemove],
240 | prev = getPrevious(node),
241 | useKey = prev && prev.useKey,
242 | key = useKey ? prev.key : index;
243 |
244 | if (childrenVtagKeys[key]) {
245 | childrenByKeys[key] = [node, useKey];
246 | } else {
247 | recollectNodeTree(node);
248 | remove(nextParent, node);
249 | childrenCountRemove++;
250 | }
251 | }
252 |
253 | for (let i = 0; i < childrenVtagLength; i++) {
254 | let childVtag = childrenVtag[i],
255 | childReal = childrenReal[i],
256 | [childFromKey, useKey] =
257 | childrenByKeys[
258 | childVtag instanceof Vtag
259 | ? childVtag.useKey
260 | ? childVtag.key
261 | : i
262 | : i
263 | ] || [];
264 |
265 | if (useKey && childFromKey !== childReal) {
266 | before(nextParent, childFromKey, childReal);
267 | }
268 |
269 | updateElement(
270 | nextParent,
271 | childFromKey,
272 | childReal,
273 | childVtag,
274 | context,
275 | isSvg,
276 | isCreate
277 | );
278 | }
279 | }
280 | } else {
281 | if (prev.children[0] !== next.children[0]) {
282 | base.textContent = next.children[0];
283 | }
284 | }
285 |
286 | base[PREVIOUS] = withUpdate ? next : prev;
287 | base[COMPONENTS] = components;
288 | emit(next, isCreate ? "onCreated" : "onUpdated", base);
289 |
290 | return base;
291 | }
292 | /**
293 | * Update or delete the attributes and events of a node
294 | * @param {HTMLElement} node - Node to assign changes
295 | * @param {Object} prev - Previous status of attributes
296 | * @param {Object} next - next status of attributes
297 | * @param {Boolean} [isSvg] - If it belongs to svg tree
298 | */
299 | export function updateProperties(node, prev, next, isSvg) {
300 | let prevKeys = Object.keys(prev),
301 | nextKeys = Object.keys(next),
302 | keys = prevKeys.concat(nextKeys),
303 | length = keys.length,
304 | ignore = {};
305 | for (let i = 0; i < length; i++) {
306 | let prop = keys[i],
307 | inNext = prop in next,
308 | prevValue = prev[prop],
309 | nextValue = next[prop];
310 |
311 | if (ignore[prop] || prevValue === nextValue || IGNORE.test(prop))
312 | continue;
313 |
314 | ignore[prop] = true;
315 |
316 | if (prop === "class" && !isSvg) {
317 | prop = "className";
318 | nextValue = nextValue || "";
319 | prevValue = prevValue || "";
320 | }
321 |
322 | if ("scoped" === prop && "attachShadow" in node) {
323 | if (
324 | (node.shadowRoot && !nextValue) ||
325 | (!node.shadowRoot && nextValue)
326 | ) {
327 | node.attachShadow({ mode: nextValue ? "open" : "closed" });
328 | }
329 | continue;
330 | }
331 |
332 | let isFnPrev = typeof prevValue === "function",
333 | isFnNext = typeof nextValue === "function";
334 | if (isFnPrev || isFnNext) {
335 | if (prop[0] !== "o" && props[1] !== "n") continue;
336 | if (!MEMO_PROPERTIES_HANDLERS[prop]) {
337 | MEMO_PROPERTIES_HANDLERS[prop] = prop.toLowerCase().slice(2);
338 | }
339 | prop = MEMO_PROPERTIES_HANDLERS[prop];
340 | if (!isFnNext && isFnPrev) {
341 | node.removeEventListener(prop, node[HANDLERS][prop][0]);
342 | }
343 | if (isFnNext) {
344 | if (!isFnPrev) {
345 | node[HANDLERS] = node[HANDLERS] || {};
346 | if (!node[HANDLERS][prop]) {
347 | node[HANDLERS][prop] = [
348 | event => {
349 | node[HANDLERS][prop][1](event);
350 | }
351 | ];
352 | }
353 | node.addEventListener(prop, node[HANDLERS][prop][0]);
354 | }
355 | node[HANDLERS][prop][1] = nextValue;
356 | }
357 | } else if (inNext) {
358 | if (
359 | (prop in node &&
360 | prop !== "list" &&
361 | prop !== "type" &&
362 | !isSvg) ||
363 | (isSvg && prop === "style")
364 | ) {
365 | if (prop === "style") {
366 | let prevCss = node[PREVIOUS_CSS_TEXT],
367 | nextCss = nextValue;
368 | if (typeof nextValue === "object") {
369 | nextCss = "";
370 | for (let prop in nextValue) {
371 | if (nextValue) {
372 | if (!MEMO_PROPERTIES_CSS[prop]) {
373 | MEMO_PROPERTIES_CSS[prop] = prop.replace(
374 | /([^A-Z])([A-Z])/g,
375 | (all, letterBefore, letterAfter) =>
376 | letterBefore +
377 | "-" +
378 | letterAfter.toLowerCase()
379 | );
380 | }
381 | nextCss += `${MEMO_PROPERTIES_CSS[prop]}:${
382 | nextValue[prop]
383 | };`;
384 | }
385 | }
386 | }
387 | if (nextCss !== prevCss) {
388 | node.style.cssText = nextCss;
389 | }
390 | node[PREVIOUS_CSS_TEXT] = nextCss;
391 | } else {
392 | node[prop] = nextValue;
393 | }
394 | } else {
395 | isSvg
396 | ? node.setAttributeNS(
397 | isSvg && prop === "xlink"
398 | ? "http://www.w3.org/1999/xlink"
399 | : null,
400 | prop === "xlink" ? "xlink:href" : prop,
401 | nextValue
402 | )
403 | : node.setAttribute(prop, nextValue);
404 | }
405 | } else {
406 | node.removeAttribute(
407 | isSvg && prop === "xlink" ? "xlink:href" : prop
408 | );
409 | }
410 | }
411 | }
412 | /**
413 | * Issues the deletion of node and its children
414 | * @param {HTMLElement} node
415 | * @param {boolean} ignoreEffects - allows to ignore the collection of effects, this is ignored in case the component changes the root node
416 | */
417 | function recollectNodeTree(node, ignoreEffects) {
418 | let prev = getPrevious(node, false),
419 | components = getComponents(node),
420 | children = node.childNodes,
421 | length;
422 |
423 | if (!prev) return;
424 |
425 | node[REMOVE] = true;
426 |
427 | emit(prev, "onRemove", node);
428 |
429 | if (!ignoreEffects) clearComponentEffects(components, true);
430 |
431 | length = children.length;
432 |
433 | for (let i = 0; i < length; i++) {
434 | recollectNodeTree(children[i]);
435 | }
436 |
437 | emit(prev, "onRemoved", node);
438 | }
439 |
--------------------------------------------------------------------------------
/src/dom.js:
--------------------------------------------------------------------------------
1 | import { options } from "./options";
2 |
3 | export function create(tag, isSvg) {
4 | let doc = options.document || document;
5 | if (tag) {
6 | return isSvg
7 | ? doc.createElementNS("http://www.w3.org/2000/svg", tag)
8 | : doc.createElement(tag);
9 | } else {
10 | return doc.createTextNode("");
11 | }
12 | }
13 |
14 | export function root(parent) {
15 | return parent.shadowRoot || parent;
16 | }
17 | export function remove(parent, child) {
18 | parent.removeChild(child);
19 | }
20 |
21 | export function append(parent, child) {
22 | parent.appendChild(child);
23 | }
24 |
25 | export function replace(parent, newChild, oldChild) {
26 | parent.replaceChild(newChild, oldChild);
27 | }
28 |
29 | export function before(parent, newChild, oldChild) {
30 | parent.insertBefore(newChild, oldChild);
31 | }
32 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { useReducer, useContext, useMemo, useRef } from "./customHooks";
2 | export { useState, useEffect, getCurrentComponent } from "./component";
3 | export { createContext } from "./context";
4 | export { options } from "./options";
5 | export { render } from "./diff";
6 | export { h } from "./vtag";
7 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @property {Object} document - allows to replace the document to be used by default within orby
3 | * @property {number} delay - controls the microtask waiting time
4 | */
5 | export let options = {
6 | delay: 0
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function isArray(list) {
2 | return Array.isArray(list);
3 | }
4 |
5 | export function isEqualArray(before, after) {
6 | let length = before.length;
7 | if (length !== after.length) return false;
8 | for (let i = 0; i < length; i++) {
9 | if (before[i] !== after[i]) return false;
10 | }
11 | return true;
12 | }
13 |
--------------------------------------------------------------------------------
/src/vtag.js:
--------------------------------------------------------------------------------
1 | import { isArray } from "./utils";
2 | /**
3 | * @property {Function|string} tag - being string will create a node, since it is a
4 | * function that will isolate its execution as a component
5 | * @property {object} keys - to avoid the index map within the diff process the vtag will
6 | * generate one based on the required method that joins the children in a single list
7 | * @property {array} children - list of nodes associated with the vtag
8 | * @property {object} props - vtag properties
9 | * @property {key} key - index of the vtag
10 | * @property {boolean} static - define if the node is static for updateElement
11 | * @property {boolean} useKey - If you have a key definition, this property is defined as true
12 | * @property {number} keysLength - number of children associated with the vtag
13 | */
14 | export class Vtag {
15 | /**
16 | * @param {Function|String} tag - Node component or label
17 | * @param {Object} props - Properties of the label
18 | * @param {Array} children - Children assigned to the node
19 | */
20 | constructor(tag, props = {}, children = []) {
21 | props = props === null ? {} : props;
22 | this.tag = tag;
23 | this.keys = {};
24 | this.children = [];
25 | this.props = {
26 | ...props,
27 | children: this.children
28 | };
29 | this.key = props.key;
30 | this.ref = props.ref;
31 | this.static = props.static;
32 | this.useKey = props.key !== undefined;
33 | this.keysLength = 0;
34 | this.mapChildren(isArray(props.children) ? props.children : children);
35 | }
36 | /**
37 | * Clone the current node by keeping props and children by default
38 | * @param {Function|String} tag - Node component or label
39 | * @param {*} props - Properties of the label
40 | * @param {*} children - Children assigned to the node
41 | * @return {Vtag}
42 | */
43 | clone(tag = this.tag, props = this.props, children = this.props.children) {
44 | return new Vtag(tag, props, children);
45 | }
46 | /**
47 | * list the children to attach them to children
48 | * @param {Array} children
49 | */
50 | mapChildren(children) {
51 | let length = children.length;
52 | for (let i = 0; i < length; i++) {
53 | let value = children[i];
54 | if (isArray(value)) {
55 | this.mapChildren(value);
56 | } else {
57 | let key =
58 | value instanceof Vtag && value.useKey
59 | ? value.key
60 | : this.keysLength;
61 | if (this.keys[key]) {
62 | throw new Error("Each key must be unique among children");
63 | } else {
64 | this.keys[key] = true;
65 | this.children.push(value);
66 | this.keysLength++;
67 | }
68 | }
69 | }
70 | }
71 | }
72 | /**
73 | * Prepare the virtual node
74 | * @param {Function|String} tag
75 | * @param {Object} props
76 | * @param {...any} children
77 | * @return {Vtag}
78 | */
79 | export function h(tag, props, ...children) {
80 | return new Vtag(tag || "", props, children);
81 | }
82 |
--------------------------------------------------------------------------------
/test/attrs.test.js:
--------------------------------------------------------------------------------
1 | import { h, render } from "../dist/orby";
2 | import { container } from "./util";
3 |
4 | describe("diff", () => {
5 | test("create tree of nodes", () => {
6 | let scope = container();
7 | let label = "text";
8 | render(
9 |
162 | );
163 | }
164 |
165 | render(, scope);
166 | });
167 | });
168 |
--------------------------------------------------------------------------------
/test/jsdom.test.js:
--------------------------------------------------------------------------------
1 | import { h, render, options } from "../dist/orby";
2 | import { container } from "./util";
3 |
4 | /**
5 | * by default the reading of the document can be associated with the option options.document,
6 | * this allows modifying the behavior of the create function in src/dom.js.
7 | *
8 | * the benefit of this is the possibility of server rendering or using a proxy like
9 | * api to manipulate other elements outside the sun, but keeping the same api DOM
10 | */
11 | describe("diff with jsdom in options", () => {
12 | test("create tree of nodes", () => {
13 | options.document = document;
14 |
15 | let scope = container();
16 | let label = "text";
17 |
18 | render(
19 |