├── .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 ` 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 ` 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 |
313 | Count: {state.count} 314 | 318 | 319 | 320 |
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 <h1> 421 | <Context.Consumer> 422 | {(data)=>data.title} 423 | </Context.Consumer> 424 | </h1> 425 | } 426 | 427 | render( 428 | <Context.Provider> 429 | <Title/> 430 | </Context.Provider>, 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 <h1> 448 | {data.title} 449 | </h1> 450 | } 451 | 452 | render( 453 | <Context.Provider> 454 | <Title/> 455 | </Context.Provider>, 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 | <img src="../../assets/logo.png" width="280px"/> 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 | [<img src="../../assets/counter.png" width="100%"/>](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 <div scoped> 40 | <style>{` 41 | :host{ 42 | padding : .5rem 1rem; 43 | border:none; 44 | background:black; 45 | color:white 46 | } 47 | `}</style> 48 | {props.children} 49 | </div> 50 | } 51 | ``` 52 | 53 | El componente `<Button/>` 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 <button onclick={props.click}>{props.children}</button> 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 <button>{context.message}</button> 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 | <App context={{message:"hi! Orby"}}/>, 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 <button onCreate={handlerWithCreate}>Hi! Orby</button> 110 | } 111 | ``` 112 | 113 | El proceso de DIFF invocara la propiedades `onCreate` solo cuando el nodo `<button/>` 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 <button onCreate={(target:HTMLElement)=>{ 122 | /**algorithm**/ 123 | }}>Hi! Orby</button> 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 <button onCreated={(target:HTMLElement)=>{ 134 | /**algorithm**/ 135 | }}>Hi! Orby</button> 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 <button onRemove={(target:HTMLElement)=>{ 146 | /**algorithm**/ 147 | }}>Hi! Orby</button> 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 <button onRemoved={(target:HTMLElement)=>{ 158 | /**algorithm**/ 159 | }}>Hi! Orby</button> 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 <button onUpdate={(target:HTMLElement, prevProps:Object, nextProps:Object)=>{ 170 | /**algorithm**/ 171 | }}>Hi! Orby</button> 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 <button onUpdated={(target:HTMLElement)=>{ 182 | /**algorithm**/ 183 | }}>Hi! Orby</button> 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 <button onClick={()=>setCount(count+1)}>increment</button>; 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 <button onClick={()=>setCount(count+1)}>increment</button>; 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 <button onClick={()=>setCount(count+1)}>increment</button>; 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 | <div> 310 | Count: {state.count} 311 | <button 312 | onClick={() => dispatch({type: 'reset', payload: initialCount})}> 313 | Reset 314 | </button> 315 | <button onClick={() => dispatch({type: 'increment'})}>+</button> 316 | <button onClick={() => dispatch({type: 'decrement'})}>-</button> 317 | </div> 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 <div scoped> 352 | <style>{`:host{background:crimson}`}</style> 353 | {props.children} 354 | </div> 355 | } 356 | ``` 357 | 358 | ### context 359 | 360 | la propiedad `context`, permite añadir nuevas propiedades al contexto. 361 | 362 | ```jsx 363 | <ParentComponent context={{title:"Hi! Orby"}}> 364 | <ChildComponent></ChildComponent> 365 | </ParentComponent> 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 | <Context context={{ [space]: props.value || valueDefault }}> 30 | {props.children} 31 | </Context> 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 | <div id="parent"> 10 | <div id="child"> 11 | <button>{label}</button> 12 | </div> 13 | </div>, 14 | scope 15 | ); 16 | 17 | expect(scope.querySelector("#parent #child button").textContent).toBe( 18 | label 19 | ); 20 | }); 21 | 22 | test("create tree of nodes", () => { 23 | let scope = container(); 24 | let label = "text"; 25 | render( 26 | <div class="parent"> 27 | <div class="child"> 28 | <button>{label}</button> 29 | </div> 30 | </div>, 31 | scope 32 | ); 33 | 34 | expect(scope.querySelector(".parent .child button").textContent).toBe( 35 | label 36 | ); 37 | }); 38 | 39 | test("create component", () => { 40 | let scope = container(); 41 | let label = "text"; 42 | 43 | function Button(props) { 44 | return <button id="button">{props.label}</button>; 45 | } 46 | 47 | render(<Button label={label} />, scope); 48 | 49 | expect(scope.querySelector("#button").textContent).toBe(label); 50 | }); 51 | 52 | test("context static", () => { 53 | let scope = container(); 54 | let id = 100; 55 | 56 | function Child(props, context) { 57 | expect(context.id).toBe(id); 58 | } 59 | 60 | function Parent(props, context) { 61 | return ( 62 | <div> 63 | <Child /> 64 | </div> 65 | ); 66 | } 67 | 68 | render( 69 | <div context={{ id }}> 70 | <Parent /> 71 | </div>, 72 | scope 73 | ); 74 | }); 75 | test("context dinamic", () => { 76 | let scope = container(); 77 | let id = 100, 78 | cd = 200; 79 | 80 | function Child(props, context) { 81 | expect(context).toEqual({ id, cd }); 82 | } 83 | 84 | function Parent(props, context) { 85 | return ( 86 | <div context={{ cd }}> 87 | <Child /> 88 | </div> 89 | ); 90 | } 91 | 92 | render( 93 | <div context={{ id }}> 94 | <Parent /> 95 | </div>, 96 | scope 97 | ); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/children.test.js: -------------------------------------------------------------------------------- 1 | import { h, render, useState } from "../dist/orby"; 2 | import { container } from "./util"; 3 | describe("children", () => { 4 | test("hidden children", done => { 5 | let scope = container(); 6 | function Test() { 7 | let [state, setState] = useState(); 8 | return ( 9 | <div 10 | onCreated={target => { 11 | expect(target.children.length).toBe(0); 12 | setState(true); 13 | }} 14 | onUpdated={target => { 15 | expect(target.children.length).toBe(4); 16 | done(); 17 | }} 18 | > 19 | {state ? <button /> : undefined} 20 | {state ? <button /> : undefined} 21 | {state ? <button /> : undefined} 22 | {state ? <button /> : undefined} 23 | </div> 24 | ); 25 | } 26 | 27 | render(<Test />, scope); 28 | }); 29 | test("remove children", done => { 30 | let scope = container(); 31 | function Test() { 32 | let [state, setState] = useState(() => { 33 | let list = []; 34 | for (let key = 0; key < 100; key++) { 35 | list.push({ key }); 36 | } 37 | return list; 38 | }); 39 | return ( 40 | <div 41 | onCreated={target => { 42 | expect(target.children.length).toBe(100); 43 | setState([]); 44 | }} 45 | onUpdated={target => { 46 | expect(target.children.length).toBe(0); 47 | done(); 48 | }} 49 | > 50 | {state.map(() => ( 51 | <button /> 52 | ))} 53 | </div> 54 | ); 55 | } 56 | 57 | render(<Test />, scope); 58 | }); 59 | 60 | test("props children", () => { 61 | let scope = container(); 62 | function Test(props) { 63 | return ( 64 | <div 65 | onCreated={target => { 66 | expect(target.children.length).toBe(4); 67 | }} 68 | > 69 | {props.children} 70 | </div> 71 | ); 72 | } 73 | 74 | render( 75 | <Test 76 | children={[ 77 | <span>1</span>, 78 | <span>2</span>, 79 | <span>3</span>, 80 | <span>4</span> 81 | ]} 82 | />, 83 | scope 84 | ); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/component.test.js: -------------------------------------------------------------------------------- 1 | import { h, render, useState, useEffect, options } from "../dist/orby"; 2 | import { container } from "./util"; 3 | 4 | describe("test components", () => { 5 | test("single component", () => { 6 | let scope = container(); 7 | function Test() { 8 | return <h1>component</h1>; 9 | } 10 | 11 | render(<Test />, scope); 12 | expect(scope.innerHTML).toBe("<h1>component</h1>"); 13 | }); 14 | test("higth order", () => { 15 | let scope = container(); 16 | function Tree() { 17 | return <h1>component</h1>; 18 | } 19 | function Two() { 20 | return <Tree />; 21 | } 22 | function One() { 23 | return <Two />; 24 | } 25 | 26 | render(<One />, scope); 27 | expect(scope.innerHTML).toBe("<h1>component</h1>"); 28 | }); 29 | 30 | test("higth order with remove tree", done => { 31 | let scope = container(), 32 | isRemove = false; 33 | function Tree() { 34 | return ( 35 | <h1 36 | onRemove={() => { 37 | isRemove = true; 38 | }} 39 | > 40 | component 41 | </h1> 42 | ); 43 | } 44 | function Two() { 45 | let [hidden, setHidden] = useState(); 46 | useEffect(() => { 47 | setHidden(true); 48 | }); 49 | return hidden ? "" : <Tree />; 50 | } 51 | function One() { 52 | return <Two />; 53 | } 54 | 55 | let lastRender = render(<One />, scope); 56 | 57 | render(<One />, scope, lastRender); 58 | setTimeout(() => { 59 | expect(scope.innerHTML).toBe(""); 60 | expect(isRemove).toBeTruthy(); 61 | done(); 62 | }, options.delay); 63 | }); 64 | 65 | test("higth order async remove", done => { 66 | let scope = container(); 67 | 68 | function Async() { 69 | let [component, setComponent] = useState(""); 70 | useEffect(() => { 71 | setTimeout(() => { 72 | setComponent(<h1>every!</h1>); 73 | }, 10); 74 | }, []); 75 | return component; 76 | } 77 | 78 | function Layer2({ children }) { 79 | let [component, setComponent] = useState(children[0]); 80 | useEffect(() => { 81 | setTimeout(() => { 82 | setComponent(""); 83 | }, 20); 84 | }, []); 85 | return component; 86 | } 87 | function Layer1({ children }) { 88 | return children[0]; 89 | } 90 | 91 | function App() { 92 | return ( 93 | <Layer1> 94 | <Layer2> 95 | <Async /> 96 | </Layer2> 97 | </Layer1> 98 | ); 99 | } 100 | 101 | let last = render(h(App, { tag: "h1" }), scope); 102 | setTimeout(() => { 103 | expect(scope.children.length).toBe(0); 104 | done(); 105 | }, 30); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/createContext.test.js: -------------------------------------------------------------------------------- 1 | import { h, createContext, render } from "../dist/orby"; 2 | import { container } from "./util"; 3 | 4 | describe("context", () => { 5 | test("define context", done => { 6 | let scope = container(); 7 | let state = { name: "default" }; 8 | let Context = createContext(state); 9 | 10 | render( 11 | <Context.Provider> 12 | <div> 13 | <div> 14 | <Context.Consumer> 15 | {data => ( 16 | <h1 17 | onCreated={target => { 18 | expect(target.textContent).toBe( 19 | state.name 20 | ); 21 | done(); 22 | }} 23 | > 24 | {data.name} 25 | </h1> 26 | )} 27 | </Context.Consumer> 28 | </div> 29 | </div> 30 | </Context.Provider>, 31 | scope 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/hook.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | render, 4 | useState, 5 | useEffect, 6 | useReducer, 7 | useMemo 8 | } from "../dist/orby"; 9 | import { container } from "./util"; 10 | 11 | describe("test hooks", () => { 12 | test("useState && updated", () => { 13 | let scope = container(); 14 | function Tag() { 15 | let [count, setCount] = useState(0); 16 | return ( 17 | <div 18 | onCreate={() => setCount(count + 1)} 19 | onUpdated={() => { 20 | expect(count).toBe(1); 21 | }} 22 | /> 23 | ); 24 | } 25 | render(<Tag />, scope); 26 | }); 27 | test("useState && updated", () => { 28 | let scope = container(); 29 | function Tag() { 30 | let [count1, setCount1] = useState(1); 31 | let [count2, setCount2] = useState(10); 32 | let [count3, setCount3] = useState(20); 33 | return ( 34 | <div 35 | onCreate={() => { 36 | setCount1(count1 + 1); 37 | setCount2(count2 + 1); 38 | setCount3(count3 + 1); 39 | }} 40 | onUpdated={() => { 41 | expect({ count1, count2, count3 }).toEqual({ 42 | count1: 2, 43 | count2: 11, 44 | count3: 21 45 | }); 46 | }} 47 | /> 48 | ); 49 | } 50 | render(<Tag />, scope); 51 | }); 52 | 53 | test("useEffect", () => { 54 | let scope = container(); 55 | function Tag() { 56 | useEffect(() => { 57 | expect(scope.querySelector("#tag").outerHTML).toBe( 58 | `<div id="tag"></div>` 59 | ); 60 | }); 61 | return <div id="tag" />; 62 | } 63 | render(<Tag />, scope); 64 | }); 65 | 66 | test("useEffect with remove", done => { 67 | let scope = container(); 68 | function Tag() { 69 | useEffect(() => { 70 | return () => { 71 | setTimeout(() => { 72 | expect(scope.querySelector("#tag")).toBe(null); 73 | done(); 74 | }); 75 | }; 76 | }); 77 | return <div id="tag" />; 78 | } 79 | let fistRender = render(<Tag />, scope); 80 | 81 | render(<div />, scope, fistRender); 82 | }); 83 | }); 84 | 85 | describe("customHooks", () => { 86 | test("useReducer", done => { 87 | let scope = container(); 88 | let prefixCase = "prefix-", 89 | initialAction = { 90 | type: "DEFAULT", 91 | payload: "fist-message" 92 | }, 93 | secondAction = { 94 | type: "UPDATE", 95 | payload: "second-message" 96 | }; 97 | 98 | function reducer(state, action) { 99 | switch (action.type) { 100 | case "UPDATE": 101 | return { message: prefixCase + action.payload }; 102 | default: 103 | return { message: action.payload }; 104 | } 105 | } 106 | 107 | function Test() { 108 | let [state, dispatch] = useReducer(reducer, initialAction); 109 | return ( 110 | <div 111 | onCreated={target => { 112 | expect(target.textContent).toBe(initialAction.payload); 113 | dispatch(secondAction); 114 | }} 115 | onUpdated={target => { 116 | expect(target.textContent).toBe( 117 | prefixCase + secondAction.payload 118 | ); 119 | done(); 120 | }} 121 | > 122 | {state.message} 123 | </div> 124 | ); 125 | } 126 | 127 | render(<Test />, scope); 128 | }); 129 | 130 | test("useMemo basic", done => { 131 | let scope = container(); 132 | 133 | function Test() { 134 | let [state, setState] = useState(); 135 | let countCall = 0; 136 | let list = useMemo( 137 | () => { 138 | let list = []; 139 | countCall++; 140 | for (let i = 0; i < 100; i++) { 141 | list.push(i); 142 | } 143 | return list; 144 | }, 145 | [100] 146 | ); 147 | 148 | return ( 149 | <div 150 | onCreated={() => { 151 | setState(); 152 | }} 153 | onUpdated={target => { 154 | expect(list.length).toBe(100); 155 | done(); 156 | }} 157 | > 158 | {list.map(() => ( 159 | <button /> 160 | ))} 161 | </div> 162 | ); 163 | } 164 | 165 | render(<Test />, 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 | <div id="parent"> 20 | <div id="child"> 21 | <button>{label}</button> 22 | </div> 23 | </div>, 24 | scope 25 | ); 26 | 27 | expect(scope.querySelector("#parent #child button").textContent).toBe( 28 | label 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/keys.test.js: -------------------------------------------------------------------------------- 1 | import { h, render } from "../dist/orby"; 2 | import { container, createList, randomList, randomInsert } from "./util"; 3 | 4 | describe("test keys", () => { 5 | test("keys reverse", () => { 6 | let scope = container(); 7 | let state = createList(); 8 | 9 | let nextState = createList().reverse(); 10 | 11 | let fistRender = render( 12 | <div> 13 | {state.map(({ key }) => ( 14 | <div id={key} key={key} /> 15 | ))} 16 | </div>, 17 | scope 18 | ); 19 | 20 | let fistIds = Array.from(fistRender.querySelectorAll("[id]")); 21 | 22 | let secondRender = render( 23 | <div> 24 | {nextState.map(({ key }) => ( 25 | <div id={key} key={key} /> 26 | ))} 27 | </div>, 28 | scope, 29 | fistRender 30 | ); 31 | 32 | let secondIds = Array.from(secondRender.querySelectorAll("[id]")); 33 | 34 | expect( 35 | secondIds.every((node, index) => node.id === nextState[index].key) 36 | ).toBeTruthy(); 37 | 38 | secondIds.reverse(); 39 | 40 | expect(secondIds.every((node, index) => node === fistIds[index])).toBe( 41 | true 42 | ); 43 | }); 44 | return; 45 | 46 | test("keys random", () => { 47 | let scope = container(); 48 | let state = createList(); 49 | 50 | let nextState = randomList(createList()); 51 | 52 | let fistRender = render( 53 | <div> 54 | {state.map(({ key }) => ( 55 | <div id={key} key={key} /> 56 | ))} 57 | </div>, 58 | scope 59 | ); 60 | 61 | let fistIds = Array.from(fistRender.querySelectorAll("[id]")); 62 | 63 | let secondRender = render( 64 | <div> 65 | {nextState.map(({ key }) => ( 66 | <div id={key} key={key} /> 67 | ))} 68 | </div>, 69 | scope, 70 | fistRender 71 | ); 72 | 73 | let secondIds = Array.from(secondRender.querySelectorAll("[id]")); 74 | 75 | expect( 76 | secondIds.every((node, index) => node.id === nextState[index].key) 77 | ).toBeTruthy(); 78 | 79 | expect( 80 | secondIds 81 | .sort((a, b) => (a.id > b.id ? 1 : -1)) 82 | .every((node, index) => node === fistIds[index]) 83 | ).toBeTruthy(); 84 | }); 85 | 86 | test("keys with insert", () => { 87 | let scope = container(); 88 | let state = createList(); 89 | 90 | let nextState = randomInsert(randomList(createList())); 91 | 92 | let fistRender = render( 93 | <div> 94 | {state.map(({ key }) => ( 95 | <div id={key} key={key} /> 96 | ))} 97 | </div>, 98 | scope 99 | ); 100 | 101 | let fistIds = Array.from(fistRender.querySelectorAll("[id]")); 102 | 103 | let secondRender = render( 104 | <div> 105 | {nextState.map(({ key }) => ( 106 | <div id={key} key={key} /> 107 | ))} 108 | </div>, 109 | scope, 110 | fistRender 111 | ); 112 | 113 | let secondIds = Array.from(secondRender.querySelectorAll("[id]")); 114 | 115 | expect( 116 | secondIds.every((node, index) => node.id === nextState[index].key) 117 | ).toBeTruthy(); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/lifecycle.test.js: -------------------------------------------------------------------------------- 1 | import { h, render } from "../dist/orby"; 2 | import { container } from "./util"; 3 | describe("Lifecycle", () => { 4 | test("create", () => { 5 | let scope = container(); 6 | render( 7 | <div 8 | onCreate={target => { 9 | expect(target.outerHTML).toBe("<div></div>"); 10 | }} 11 | > 12 | my-div 13 | </div>, 14 | scope 15 | ); 16 | }); 17 | test("created", () => { 18 | let scope = container(); 19 | render( 20 | <div 21 | onCreated={target => { 22 | expect(target.outerHTML).toBe("<div>my-div</div>"); 23 | }} 24 | > 25 | my-div 26 | </div>, 27 | scope 28 | ); 29 | }); 30 | test("update", () => { 31 | let scope = container(); 32 | let fistRender = render(<div />, scope); 33 | 34 | render( 35 | <div 36 | onUpdate={target => { 37 | expect(target.outerHTML).toBe("<div></div>"); 38 | }} 39 | > 40 | my-div 41 | </div>, 42 | scope, 43 | fistRender 44 | ); 45 | }); 46 | test("updated", () => { 47 | let scope = container(); 48 | let fistRender = render(<div />, scope); 49 | 50 | render( 51 | <div 52 | onUpdated={target => { 53 | expect(target.outerHTML).toBe("<div>my-div</div>"); 54 | }} 55 | > 56 | my-div 57 | </div>, 58 | scope, 59 | fistRender 60 | ); 61 | }); 62 | test("remove && removed", () => { 63 | let scope = container(); 64 | let withUpdate, 65 | fistRender = render( 66 | <div 67 | onRemove={() => { 68 | withUpdate = true; 69 | }} 70 | onRemoved={() => { 71 | expect(withUpdate).toBe(true); 72 | }} 73 | />, 74 | scope 75 | ); 76 | 77 | render(<a />, scope, fistRender); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/svg.test.js: -------------------------------------------------------------------------------- 1 | import { h, render } from "../dist/orby"; 2 | import { container } from "./util"; 3 | describe("svg", () => { 4 | test("svg basic", () => { 5 | let scope = container(); 6 | render( 7 | <svg 8 | height="100" 9 | width="100" 10 | onCreated={target => { 11 | expect(target.outerHTML).toBe( 12 | `<svg height="100" width="100"><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"></circle></svg>` 13 | ); 14 | }} 15 | > 16 | <circle 17 | cx="50" 18 | cy="50" 19 | r="40" 20 | stroke="black" 21 | stroke-width="3" 22 | fill="red" 23 | /> 24 | </svg>, 25 | scope 26 | ); 27 | }); 28 | test("svg path", () => { 29 | let scope = container(); 30 | render( 31 | <svg 32 | height="496pt" 33 | viewBox="0 0 496 496" 34 | width="496pt" 35 | xmlns="http://www.w3.org/2000/svg" 36 | onCreated={target => { 37 | expect(target.outerHTML).toBe( 38 | `<svg height="496pt" viewBox="0 0 496 496" width="496pt"><path d="m286.761719 99.238281c-4.929688-25.382812-25.367188-45.535156-50.839844-50.167969-4.222656-.757812-8.390625-.964843-12.527344-.925781-3.847656-27.160156-27.191406-48.144531-55.394531-48.144531-14.503906 0-27.695312 5.59375-37.652344 14.671875-7.714844-4.128906-16.25-6.464844-25.121094-6.664063-11.121093-.214843-22.359374 3.832032-31.625 11.441407-21.085937 17.320312-25.769531 32.648437-25.609374 45.109375-12.207032 1.738281-23.429688 7.425781-32.183594 16.449218-10.628906 10.960938-16.238282 25.425782-15.7812502 40.710938.9023442 29.929688 26.9101562 54.28125 57.9921872 54.28125h93.980469v152c0 13.230469 10.769531 24 24 24 13.234375 0 24-10.769531 24-24v-9.472656c2.515625.898437 5.1875 1.472656 8 1.472656 13.234375 0 24-10.769531 24-24v-120.496094c16.128906-2.007812 30.90625-10.078125 41.410156-22.816406 12.207032-14.816406 17.078125-34.296875 13.351563-53.449219zm0 0" fill="#f0bc5e"></path><path d="m272 256c0 17.648438 14.355469 32 32 32 5.472656 0 10.761719-1.433594 15.722656-4.246094l-7.914062-13.90625c-2.519532 1.425782-5.144532 2.152344-7.808594 2.152344-8.820312 0-16-7.175781-16-16 0-2.222656.554688-4.34375 1.417969-6.320312 5.679687 6.761718 13.550781 11.695312 22.679687 13.527343l3.152344-15.6875c-11.152344-2.238281-19.25-12.128906-19.25-23.519531 0-13.230469 10.769531-24 24-24 13.234375 0 24 10.769531 24 24 0 4.910156-1.511719 9.664062-4.375 13.734375l13.089844 9.203125c4.765625-6.777344 7.285156-14.714844 7.285156-22.9375 0-22.054688-17.941406-40-40-40-22.054688 0-40 17.945312-40 40 0 3.289062.539062 6.457031 1.304688 9.535156-5.878907 5.945313-9.304688 13.960938-9.304688 22.464844zm0 0"></path><path d="m435.90625 256.136719c17.839844-23.519531 32.550781-46.90625 42.992188-68.847657 20.832031-43.800781 22.617187-77.609374 5.023437-95.199218-17.601563-17.609375-51.40625-15.816406-95.207031 5.015625-22.273438 10.597656-46.023438 25.605469-69.914063 43.816406-13.75-10.523437-25.398437-19-31.175781-23.160156-.527344 5.839843-1.765625 11.550781-3.832031 16.992187 5.578125 4.070313 13.265625 9.757813 21.960937 16.367188-16.0625 13.03125-32.082031 27.429687-47.640625 42.992187-9.101562 9.09375-17.734375 18.472657-26.113281 27.917969v24.433594c11.59375-13.9375 24.128906-27.746094 37.425781-41.039063 16.136719-16.144531 32.753907-30.984375 49.359375-44.273437 18.160156 14.175781 38.105469 30.582031 51.792969 44.273437 15.886719 15.886719 31.167969 33.191407 45.023437 50.820313-13.480468 16.929687-28.566406 33.882812-45.023437 50.339844-16.550781 16.550781-33.625 31.71875-50.59375 45.207031-17.007813-13.527344-34.03125-28.671875-50.558594-45.207031-13.34375-13.355469-25.871093-27.152344-37.425781-41.042969v24.433593c8.363281 9.429688 16.984375 18.792969 26.113281 27.917969 15.929688 15.929688 32.335938 30.679688 48.785157 43.953125-40 29.785156-78.441407 49.28125-106.898438 53.328125v-87.175781c0 10.414062-6.710938 19.214844-16 22.527344v73.472656c0 30.871094-25.117188 56-56 56-30.878906 0-56-25.128906-56-56v-8h32v-16h-32v-16h48v-16h-48v-16h32v-16h-32v-16h48v-16h-48v-16h32v-16h-32v-16h48v-16h-48v-16h80v-16h-104c-4.40625 0-8-3.585938-8-8v-16c0-4.414062 3.59375-8 8-8h104v-16h-104c-13.230469 0-24 10.769531-24 24v16c0 13.230469 10.769531 24 24 24h8v200c0 39.703125 32.304688 72 72 72 37.21875 0 67.929688-28.382812 71.625-64.640625 32.570312-3.824219 75.777344-25.457031 120.417969-59.3125 23.460937 17.777344 46.773437 32.441406 68.664062 42.855469 23.839844 11.335937 44.71875 17.035156 61.640625 17.035156 14.167969 0 25.550782-3.992188 33.574219-12.015625 29.320313-29.328125 1.953125-97.457031-48.015625-163.785156zm-54.015625-62.023438c-13.519531-13.519531-32.425781-29.226562-50.058594-43.082031 21.839844-16.367188 43.457031-29.824219 63.746094-39.472656 36.671875-17.453125 64.75-20.414063 77.023437-8.160156 12.265626 12.265624 9.289063 40.335937-8.160156 77.027343-9.496094 19.957031-22.648437 41.207031-38.664062 62.6875-13.488282-16.847656-28.296875-33.417969-43.886719-49zm90.71875 214.488281c-12.261719 12.277344-40.335937 9.292969-77.023437-8.160156-19.921876-9.472656-41.121094-22.59375-62.566407-38.5625 16.375-13.207031 32.8125-27.925781 48.871094-43.992187 15.839844-15.839844 30.503906-32.152344 43.71875-48.503907 43.152344 58.425782 68.617187 117.601563 47 139.21875zm0 0"></path><path d="m56 144c13.234375 0 24-10.769531 24-24s-10.765625-24-24-24c-13.230469 0-24 10.769531-24 24s10.769531 24 24 24zm0-32c4.410156 0 8 3.585938 8 8s-3.589844 8-8 8c-4.40625 0-8-3.585938-8-8s3.59375-8 8-8zm0 0"></path><path d="m224 136c13.234375 0 24-10.769531 24-24s-10.765625-24-24-24c-13.230469 0-24 10.769531-24 24s10.769531 24 24 24zm0-32c4.410156 0 8 3.585938 8 8s-3.589844 8-8 8c-4.40625 0-8-3.585938-8-8s3.59375-8 8-8zm0 0"></path><path d="m128 120c13.234375 0 24-10.769531 24-24s-10.765625-24-24-24c-13.230469 0-24 10.769531-24 24s10.769531 24 24 24zm0-32c4.410156 0 8 3.585938 8 8s-3.589844 8-8 8c-4.40625 0-8-3.585938-8-8s3.59375-8 8-8zm0 0"></path><path d="m336 296c-17.644531 0-32-14.351562-32-32s14.355469-32 32-32c17.648438 0 32 14.351562 32 32s-14.351562 32-32 32zm0 0" fill="#f0bc5e"></path></svg>` 39 | ); 40 | }} 41 | > 42 | <path 43 | d="m286.761719 99.238281c-4.929688-25.382812-25.367188-45.535156-50.839844-50.167969-4.222656-.757812-8.390625-.964843-12.527344-.925781-3.847656-27.160156-27.191406-48.144531-55.394531-48.144531-14.503906 0-27.695312 5.59375-37.652344 14.671875-7.714844-4.128906-16.25-6.464844-25.121094-6.664063-11.121093-.214843-22.359374 3.832032-31.625 11.441407-21.085937 17.320312-25.769531 32.648437-25.609374 45.109375-12.207032 1.738281-23.429688 7.425781-32.183594 16.449218-10.628906 10.960938-16.238282 25.425782-15.7812502 40.710938.9023442 29.929688 26.9101562 54.28125 57.9921872 54.28125h93.980469v152c0 13.230469 10.769531 24 24 24 13.234375 0 24-10.769531 24-24v-9.472656c2.515625.898437 5.1875 1.472656 8 1.472656 13.234375 0 24-10.769531 24-24v-120.496094c16.128906-2.007812 30.90625-10.078125 41.410156-22.816406 12.207032-14.816406 17.078125-34.296875 13.351563-53.449219zm0 0" 44 | fill="#f0bc5e" 45 | /> 46 | <path d="m272 256c0 17.648438 14.355469 32 32 32 5.472656 0 10.761719-1.433594 15.722656-4.246094l-7.914062-13.90625c-2.519532 1.425782-5.144532 2.152344-7.808594 2.152344-8.820312 0-16-7.175781-16-16 0-2.222656.554688-4.34375 1.417969-6.320312 5.679687 6.761718 13.550781 11.695312 22.679687 13.527343l3.152344-15.6875c-11.152344-2.238281-19.25-12.128906-19.25-23.519531 0-13.230469 10.769531-24 24-24 13.234375 0 24 10.769531 24 24 0 4.910156-1.511719 9.664062-4.375 13.734375l13.089844 9.203125c4.765625-6.777344 7.285156-14.714844 7.285156-22.9375 0-22.054688-17.941406-40-40-40-22.054688 0-40 17.945312-40 40 0 3.289062.539062 6.457031 1.304688 9.535156-5.878907 5.945313-9.304688 13.960938-9.304688 22.464844zm0 0" /> 47 | <path d="m435.90625 256.136719c17.839844-23.519531 32.550781-46.90625 42.992188-68.847657 20.832031-43.800781 22.617187-77.609374 5.023437-95.199218-17.601563-17.609375-51.40625-15.816406-95.207031 5.015625-22.273438 10.597656-46.023438 25.605469-69.914063 43.816406-13.75-10.523437-25.398437-19-31.175781-23.160156-.527344 5.839843-1.765625 11.550781-3.832031 16.992187 5.578125 4.070313 13.265625 9.757813 21.960937 16.367188-16.0625 13.03125-32.082031 27.429687-47.640625 42.992187-9.101562 9.09375-17.734375 18.472657-26.113281 27.917969v24.433594c11.59375-13.9375 24.128906-27.746094 37.425781-41.039063 16.136719-16.144531 32.753907-30.984375 49.359375-44.273437 18.160156 14.175781 38.105469 30.582031 51.792969 44.273437 15.886719 15.886719 31.167969 33.191407 45.023437 50.820313-13.480468 16.929687-28.566406 33.882812-45.023437 50.339844-16.550781 16.550781-33.625 31.71875-50.59375 45.207031-17.007813-13.527344-34.03125-28.671875-50.558594-45.207031-13.34375-13.355469-25.871093-27.152344-37.425781-41.042969v24.433593c8.363281 9.429688 16.984375 18.792969 26.113281 27.917969 15.929688 15.929688 32.335938 30.679688 48.785157 43.953125-40 29.785156-78.441407 49.28125-106.898438 53.328125v-87.175781c0 10.414062-6.710938 19.214844-16 22.527344v73.472656c0 30.871094-25.117188 56-56 56-30.878906 0-56-25.128906-56-56v-8h32v-16h-32v-16h48v-16h-48v-16h32v-16h-32v-16h48v-16h-48v-16h32v-16h-32v-16h48v-16h-48v-16h80v-16h-104c-4.40625 0-8-3.585938-8-8v-16c0-4.414062 3.59375-8 8-8h104v-16h-104c-13.230469 0-24 10.769531-24 24v16c0 13.230469 10.769531 24 24 24h8v200c0 39.703125 32.304688 72 72 72 37.21875 0 67.929688-28.382812 71.625-64.640625 32.570312-3.824219 75.777344-25.457031 120.417969-59.3125 23.460937 17.777344 46.773437 32.441406 68.664062 42.855469 23.839844 11.335937 44.71875 17.035156 61.640625 17.035156 14.167969 0 25.550782-3.992188 33.574219-12.015625 29.320313-29.328125 1.953125-97.457031-48.015625-163.785156zm-54.015625-62.023438c-13.519531-13.519531-32.425781-29.226562-50.058594-43.082031 21.839844-16.367188 43.457031-29.824219 63.746094-39.472656 36.671875-17.453125 64.75-20.414063 77.023437-8.160156 12.265626 12.265624 9.289063 40.335937-8.160156 77.027343-9.496094 19.957031-22.648437 41.207031-38.664062 62.6875-13.488282-16.847656-28.296875-33.417969-43.886719-49zm90.71875 214.488281c-12.261719 12.277344-40.335937 9.292969-77.023437-8.160156-19.921876-9.472656-41.121094-22.59375-62.566407-38.5625 16.375-13.207031 32.8125-27.925781 48.871094-43.992187 15.839844-15.839844 30.503906-32.152344 43.71875-48.503907 43.152344 58.425782 68.617187 117.601563 47 139.21875zm0 0" /> 48 | <path d="m56 144c13.234375 0 24-10.769531 24-24s-10.765625-24-24-24c-13.230469 0-24 10.769531-24 24s10.769531 24 24 24zm0-32c4.410156 0 8 3.585938 8 8s-3.589844 8-8 8c-4.40625 0-8-3.585938-8-8s3.59375-8 8-8zm0 0" /> 49 | <path d="m224 136c13.234375 0 24-10.769531 24-24s-10.765625-24-24-24c-13.230469 0-24 10.769531-24 24s10.769531 24 24 24zm0-32c4.410156 0 8 3.585938 8 8s-3.589844 8-8 8c-4.40625 0-8-3.585938-8-8s3.59375-8 8-8zm0 0" /> 50 | <path d="m128 120c13.234375 0 24-10.769531 24-24s-10.765625-24-24-24c-13.230469 0-24 10.769531-24 24s10.769531 24 24 24zm0-32c4.410156 0 8 3.585938 8 8s-3.589844 8-8 8c-4.40625 0-8-3.585938-8-8s3.59375-8 8-8zm0 0" /> 51 | <path 52 | d="m336 296c-17.644531 0-32-14.351562-32-32s14.355469-32 32-32c17.648438 0 32 14.351562 32 32s-14.351562 32-32 32zm0 0" 53 | fill="#f0bc5e" 54 | /> 55 | </svg>, 56 | scope 57 | ); 58 | }); 59 | test("svg image", () => { 60 | let scope = container(); 61 | render( 62 | <svg 63 | width="200" 64 | height="200" 65 | onCreated={target => { 66 | expect(target.outerHTML).toBe( 67 | `<svg width="200" height="200"><image xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" height="200" width="200"></image></svg>` 68 | ); 69 | }} 70 | > 71 | <image 72 | xlink="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" 73 | height="200" 74 | width="200" 75 | /> 76 | </svg>, 77 | scope 78 | ); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | export function container() { 2 | return document.createElement("div"); 3 | } 4 | 5 | export function createList(length = 10) { 6 | let list = []; 7 | for (let key = 0; key < length; key++) { 8 | list.push({ key: String(key) }); 9 | } 10 | return list; 11 | } 12 | 13 | export function randomList(list) { 14 | var currentIndex = list.length, 15 | temporaryValue, 16 | randomIndex; 17 | 18 | // While there remain elements to shuffle... 19 | while (0 !== currentIndex) { 20 | // Pick a remaining element... 21 | randomIndex = Math.floor(Math.random() * currentIndex); 22 | currentIndex -= 1; 23 | 24 | // And swap it with the current element. 25 | temporaryValue = list[currentIndex]; 26 | list[currentIndex] = list[randomIndex]; 27 | list[randomIndex] = temporaryValue; 28 | } 29 | 30 | return list; 31 | } 32 | 33 | export function randomInsert(list, length = 100) { 34 | for (let i = 0; i < length; i++) { 35 | let insertIn = Math.floor(Math.random() * list.length); 36 | 37 | let before = list.slice(0, insertIn), 38 | after = list.slice(insertIn), 39 | key = insertIn + "." + i; 40 | 41 | list = before.concat({ key }, after); 42 | } 43 | return list; 44 | } 45 | --------------------------------------------------------------------------------