├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTORS.md ├── DESIGN.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── REFERENCE.md ├── assets └── FractalLogo.png ├── docs ├── API.md ├── ARCHITECTURE.md ├── Hot-Swaping.md ├── Internals.md ├── building UIs.md ├── composing.md ├── dynamic-components.md ├── eventFlow.md ├── introduction.md ├── patterns.md └── tutorial │ ├── contents.md │ ├── counter.md │ └── readme.md ├── fuse-example.js ├── fuse-playground-aot.js ├── package.json ├── src ├── AppViewer │ ├── AppViewer.ts │ └── index.ts ├── core │ ├── common.ts │ ├── component.ts │ ├── core.ts │ ├── eventBus.spec.ts │ ├── handler.ts │ ├── index.ts │ ├── input.ts │ ├── interface.ts │ ├── module.spec.ts │ ├── module.ts │ ├── style.ts │ ├── testUtils.ts │ ├── utils.spec.ts │ └── utils.ts ├── featureExample │ ├── index.html │ └── index.ts ├── groups │ └── style.ts ├── interfaces │ ├── route.ts │ └── view │ │ ├── eventListeners.ts │ │ ├── globalListeners.ts │ │ ├── h.ts │ │ ├── index.ts │ │ ├── is.ts │ │ ├── resizeSensor.ts │ │ ├── sizeBinding.ts │ │ ├── utils.spec.ts │ │ ├── utils.ts │ │ ├── view-effects.ts │ │ ├── view-worker.ts │ │ ├── view.spec.ts │ │ └── vnode.ts ├── playground │ ├── README.md │ ├── Root │ │ ├── List │ │ │ ├── Item.ts │ │ │ └── index.ts │ │ ├── Note.ts │ │ └── index.ts │ ├── aot.html │ ├── aot.ts │ ├── db.ts │ ├── hmr.ts │ ├── index.html │ ├── index.ts │ ├── module.ts │ └── styles.css ├── simpleExample │ ├── index.html │ └── index.ts ├── tasks │ ├── size.ts │ └── view.ts ├── toHTML │ ├── elements.ts │ ├── index.ts │ ├── init.ts │ └── modules │ │ ├── attributes.ts │ │ ├── class.ts │ │ ├── index.ts │ │ ├── props.ts │ │ └── style.ts ├── utils │ ├── aot.ts │ ├── fun.spec.ts │ ├── fun.ts │ ├── hot-swap.ts │ ├── log.ts │ ├── ssr.ts │ ├── worker.spec.ts │ └── worker.ts └── workerExample │ ├── Root.ts │ ├── index.html │ ├── index.ts │ ├── module.ts │ └── worker.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.ts] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.js] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific stuff 2 | /core/ 3 | /groups/ 4 | /tasks/ 5 | /interfaces/ 6 | /AppViewer/ 7 | /examples/ 8 | /simpleExample/ 9 | /examplesAot/ 10 | /benchmarks/ 11 | /utils/ 12 | /prerender/ 13 | /toHTML/ 14 | /playground/ 15 | /featureExample/ 16 | /workerExample/ 17 | /animationExample/ 18 | coverage/ 19 | reports/ 20 | dist/ 21 | src/playground/dist/ 22 | .fusebox 23 | yarn-error.log 24 | 25 | # npm stuff 26 | node_modules/ 27 | npm-debug.log 28 | 29 | # Editor Stuff 30 | .vscode/ 31 | *.sublime-project 32 | *.sublime-workspace 33 | *.bak 34 | *~ 35 | 36 | # System stuff 37 | .DS_Store 38 | .Spotlight-V100 39 | .Trashes 40 | Thumbs.db 41 | ehthumbs.db 42 | Desktop.ini 43 | $RECYCLE.BIN/ 44 | .directory 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - node #for latest node_js version 6 | # after_script: 7 | before_install: 8 | - export CHROME_BIN=/usr/bin/google-chrome 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | - sudo apt-get update 12 | - sudo apt-get install -y libappindicator1 fonts-liberation 13 | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb 14 | - sudo dpkg -i google-chrome*.deb 15 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## Whats next? 4 | 5 | - Use of objects instead of style helper, improve style group handler 6 | - Implement app proxies as a way for easy compose Fractal applications 7 | - Add `nextFrame` helper that returns a Promise (with requestAnimationFrame) 8 | - Add `fork` helper that returns a Promise (with setImmediate) 9 | - Publish fractal-react repository for React support 10 | - Implement timetravel UI 11 | - Implement tree view 12 | - Implement worker support for ModuleAPI event bus functions 13 | - Implement component dispose hook for future implementation of automatic event channel unsubscribe 14 | - Automatic management of event channel subscriptions in Components, when disposed 15 | - Implement a way for merging module definitions (FANCY) 16 | - Evaluate how to handle errors when a task fails 17 | - Filtered console logs in AppViewer UI and console 18 | 19 | ## Done 20 | 21 | # v6.0.6 22 | 23 | - Fix log helpers bug 24 | 25 | # v6.0.5 26 | 27 | - Fix StateOf type signature 28 | - Fix Components type signature 29 | - Fix stateOf type signature 30 | - Remove proxy from `state` in `InputHelpers`, now do not modify state outside actions is a strong convention 31 | 32 | # v6.0.4 33 | 34 | - Reorganize functionality 35 | 36 | # v6.0.3 37 | 38 | - Change compile target to es6 39 | 40 | # v6.0.2 41 | 42 | - Remove component, log and worker from main barrel 43 | 44 | # v6.0.0 45 | 46 | - Rename `dispose` APIs with `destroy` 47 | - Add `onBeforeDestroy` module lifecycle hook 48 | - Add event bus functions to ModuleAPI 49 | - Worker helper to automatically proxy all stuff inside a Module Definition 50 | - Implement worker performance example 51 | - `invokeHandler` now performs event object data extraction 52 | - Implement `dispatch` function and add it to `ModuleAPI` 53 | - Rename `beforeInit` module hook to `onBeforeInit` 54 | - AppViewer POC 55 | - Add guid function 56 | - Add seq helper 57 | - Add `s` property to `InputHelpers` interface, that is a reference to local state 58 | - Deprecate recommended `F.stateOf()` sentence with no arguments in favor of `s` in input parameters 59 | - BREAKING: Add many state type signatures 60 | - Add `s` parameter to input factory 61 | - Add `State` interface 62 | - Remove currification in interfaces 63 | - Add `F.inFn` and `F.actFn` to support React and other tools with function based event handlers 64 | - Add `isServer` and `hydrateState` helpers for prerendering and SSR 65 | - Add support for `state` updates with the result of an action 66 | - Actions cannot replace the state object 67 | - Add proxy to `state` in `InputHelpers` to disallow state mutation from inputs 68 | - Change `init` and `destroy` with `onInit` and `onDestroy` 69 | - Change router events name: `onRouteActive` and `onRouteInactive` 70 | - Add onInit and onDestroy to InputIndex type interface 71 | - Add onRouteActive and onRouteInactive to InputIndex type interface 72 | 73 | # v5.1.2 74 | 75 | - Fix style merge order (Revert) 76 | 77 | # v5.1.1 78 | 79 | - Fix style merge order 80 | 81 | # v5.1.0 82 | 83 | - Extract event bus implementation to a library called `pullable-event-bus` 84 | - Insightful error when nesting components and there is no component 85 | - Make state optional 86 | - Breaking change evaluation, rename toIt with toIn 87 | 88 | # v5.0.12 89 | 90 | - Improve type signature of mapToObj utility function 91 | 92 | # v5.0.11 93 | 94 | - Add task method to ModuleAPI 95 | - Implement optimization and simplification regarding _execute internal function, remove Executable abstraction in favor of more decoupled concepts 96 | 97 | # v5.0.10 98 | 99 | - Improvements in event bus and make `data` F.emit parameter optional 100 | 101 | # v5.0.9 102 | 103 | - Fix bug in `comp` helper function and type signature 104 | 105 | # v5.0.8 106 | 107 | - Fix `comp` helper type signature, properties are now optional 108 | 109 | # v5.0.7 110 | 111 | - Include eventBus as `ev` default task 112 | - Implement Event Bus helpers `F.emit`, `F.on` and `F.off` 113 | - Implement multiple listeners support in eventBus 114 | - Show stack trace in error function of logFns 115 | 116 | # v5.0.6 117 | 118 | - Add `comp` method that makes more clear the component instantiation 119 | - Fixed bug in getPath function 120 | - Remove nest functions, is better to nest dynamic components with actions 121 | 122 | # v5.0.5 123 | 124 | - Pullable event bus implementation (task) 125 | 126 | # v5.0.4 127 | 128 | - Add error message to edge case in toComp function 129 | - Remove optional propagation 130 | - Implement `F.set` helper 131 | 132 | # v5.0.3 133 | 134 | - Fix task type signature, data is optional 135 | 136 | # v5.0.2 137 | 138 | - Fix bug with propagation, add docs and optimize it 139 | 140 | # v5.0.1 141 | 142 | - Handle error in dispatchEv 143 | - Fix route interface 144 | - Update deps 145 | - Fix fuse config 146 | 147 | # v5.0.0 148 | 149 | - Better internal implementation of tasks 150 | - Handlers now receive the component id 151 | - Task helper 152 | - RunIt helper removed 153 | - Tasks should contain sender component id 154 | - clearCache input helper changed to _clearCache for preventing use in production, this is only for development and testing pruposes 155 | 156 | # v4.4.5 157 | 158 | - Fix async functions support and types 159 | 160 | # v4.4.4 161 | 162 | - Fix sum function and remove testing stuff 163 | 164 | # v4.4.3 165 | 166 | - Complete support for async functions 167 | - BREAKING: types when use handlers 168 | 169 | # v4.4.2 170 | 171 | - Support async functions in all core methods 172 | 173 | # v4.4.1 174 | 175 | - Implement `toChildAct` InputHelper 176 | 177 | # v4.4.0 178 | 179 | - BREAKING: Change `F.ev` with `F.in` 180 | - BREAKING: Change module option root by Root 181 | - Add `path` option to `getStates` function of `ComponentHelpers` 182 | - Add `getPath` function to utils 183 | - Add `getPaths` function to utils 184 | 185 | # v4.3.10 186 | 187 | - Fix type in style view helper 188 | 189 | # V4.3.9 190 | 191 | - Implement style helper 192 | 193 | # v4.3.8 194 | 195 | - Fix bug in component helper exceptions and add an optimization 196 | 197 | # v4.3.7 198 | 199 | - Solve bug related to performUpdate in view rendering, state of child component doesn't change 200 | 201 | # v4.3.6 202 | 203 | - Make `toChild` return the result of the input 204 | 205 | # v4.3.5 206 | 207 | - Make `dispatchEv` and `toComp` return the result of the input 208 | 209 | # v4.3.4 210 | 211 | - Add `getStates` to `ComponentHelpers` 212 | - Add multiple assignments to Set default action 213 | - Add `sum` function to utils 214 | - Add CSS type to Style module in Vnode 215 | - Remove actionQueue remanent from Context 216 | 217 | # v4.3.3 218 | 219 | - Make `toComp` data parameter optional (type) 220 | 221 | # v4.3.2 222 | 223 | - Make `toComp` data parameter optional 224 | - Change toChild parameter 'name' for 'childCompName' 225 | - Add waitMS function 226 | 227 | # v4.3.1 228 | 229 | - Add `imageRenderingContrast` CSS object helper 230 | - Add `range` function 231 | 232 | # v4.3.0 233 | 234 | - Fix propagation data 235 | - Router interface MVP 236 | - Optimize propagation (BREAKING) 237 | - Remove navigo router interface 238 | - Fix bundlePaths in ssr helpers, do it not optional 239 | - Change log functions from async to sync 240 | - Fix destroy component hook bug 241 | - Change input hooks from async to sync 242 | - Fix component Root id 243 | 244 | ## v4.2.3 245 | 246 | - Enable input hooks (HOTFIX) 247 | 248 | ## v4.2.2 249 | 250 | - Fix `act` interface helper 251 | 252 | ## v4.2.1 253 | 254 | - Implement `getParentCtx`, `mapAsync`, `filterAsync`, `reduceAsync` and `all` functions 255 | 256 | ## v4.2.0 257 | 258 | - Improve event system, optimization and clean code 259 | - Replace `dispatch` in built-in hanlders 260 | - Remove `dispatch` for moduleAPI (BREAKING CHANGE) 261 | - Implement `toComp` and `dispatchEv` for ModuleAPI 262 | - Delete unused value interface 263 | 264 | ## v4.1.17 265 | 266 | - Move fs-jetpack to dev dependencies 267 | 268 | ## v4.1.16 269 | 270 | - Fix interfaces type to be async 271 | - Add async type to renderHTML function 272 | 273 | ## v4.1.15 274 | 275 | - Implement htmlFn for replacing transformHTML function, allows customization 276 | - Implement transformHTML function in renderHTML 277 | - Implement base url for AOT / SSR 278 | 279 | ## v4.1.14 280 | 281 | - Implement bundlePaths for SSR and AOT and remove bundlePath 282 | 283 | ## v4.1.12-13 (Fix broken build) 284 | 285 | - Add fs-jetpack and always reads utf8 from files in AOT compilation 286 | 287 | ## v4.1.11 288 | 289 | - Await for beforeInit hook 290 | - Fix encoding optional parameter 291 | - Logs and module hooks are now async 292 | 293 | ## v4.1.10 294 | 295 | - Fix initial global values for rendering 296 | - Fix use of render in module definitions 297 | - Fix AOT and SSR 298 | - Change location of prerender template 299 | - Add options to `runModule`, this allow module definitions to be extendable 300 | 301 | ## v4.1.9 302 | 303 | - `clearCache` now clear descendants 304 | - Implement `getDescendantIds` function 305 | 306 | ## v4.1.8 307 | 308 | - Add `clearCache` function to input helpers 309 | 310 | ## v4.1.7 311 | 312 | - Update TypeStyle dependency 313 | - Update Snabbdom dependency 314 | - Optimization in interface recalculation 315 | 316 | ## v4.1.6 317 | 318 | - Fix placholderColor for Firefox 319 | - Remove duplicated parameter in `propagate` function 320 | 321 | ## v4.1.5 322 | 323 | - Implement `optionalBroadcast`, `seqBroadcast` and `seqOptionalBroadcast` to `comps` helper in inputs 324 | 325 | ## v4.1.4 326 | 327 | - Rename vws function to group 328 | - Add vws function for rendering an array of component names 329 | 330 | ## v4.1.3 331 | 332 | - Fix ordering in action records 333 | 334 | ## v4.1.2 335 | 336 | - Fix interface excecution 337 | 338 | ## v4.1.1 339 | 340 | - Add type signature for async interfaces 341 | 342 | ## v4.1.0 343 | 344 | - Interfaces are now async 345 | 346 | ## v4.0.6 347 | 348 | - Add global active flag to modules 349 | - Disable render when init components and add moduleRender option 350 | - Inputs processes can continue execution when hot-swap ocurrs 351 | 352 | ## v4.0.5 353 | 354 | - Updates can be async functions and sync too 355 | 356 | ## v4.0.4 357 | 358 | - Fix component update flag 359 | 360 | ## v4.0.3 361 | 362 | - Add getCompleteNames method 363 | 364 | ## v4.0.2 365 | 366 | - Add getNames method 367 | - Add getCompleteName type signature to ComponentHelpers 368 | 369 | ## v4.0.1 370 | 371 | - All component methods now return values 372 | 373 | ## v4.0.0 374 | 375 | - Fix: init and destroy are not called during hot-swaping 376 | - Fix generic propagation name argument 377 | - Fix lifecycle ordering 378 | - Add init and destroy lifecycle hooks 379 | - Remove unused input helpers 380 | - init and destroy input are handled in the lifecycle 381 | - Merge Contexts and Spaces into only Contexts 382 | - Components are into _nest variable of parent 383 | - Remove input returns 384 | - Add AddComp helper for dynamic composing 385 | - Add _remove default action helper for dynamic composing 386 | - Remove name from components 387 | - Add Set generic action by default 388 | - Add _action and _execute inputs 389 | - Remove return and action 390 | - Add 'record' option to record all actions 391 | - Actions ensures in-order execution 392 | - State always are an onject 393 | - Update TypeStyle dependency - performance boost 394 | 395 | ## v3.3.3 396 | 397 | - Fix async CtxInterface 398 | 399 | ## v3.3.2 400 | 401 | - CtxInterface can be an async function 402 | 403 | ## v3.3.1 404 | 405 | - Fix mistake in getState component helper, bad use of nameFn 406 | 407 | ## v3.3.0 408 | 409 | - Add runIt input helper and default return input to components 410 | 411 | ## v3.2.1 412 | 413 | - Fix executeAll from comps helper 414 | 415 | ## v3.2.0 416 | 417 | - Add 'compGroup', 'comps' and 'vws' helpers 418 | 419 | ## v3.1.2 420 | 421 | - Add sizeTask 422 | - Add act helper to input helpers 423 | 424 | ## v3.1.1 425 | 426 | - Fix coupled group name in style group handler 427 | 428 | ## v3.1.0 429 | 430 | - Add AOT helpers 431 | - Add server side rendering helpers 432 | - Fix error message 433 | 434 | ## v3.0.7 435 | 436 | - Input errors are delegated to caller functions 437 | - Fix: action input helper can be overwritten by the component 438 | 439 | # v3.0.6 440 | 441 | - Inputs can be norma functions not only async 442 | 443 | # v3.0.5 444 | 445 | - Add render global flag to module options 446 | - Remove log unused stuff 447 | 448 | ## v3.0.4 449 | 450 | - Add render global flag for SSR performance 451 | 452 | ## v3.0.0 453 | 454 | - Worker support fixed! 455 | - Handlers are now async 456 | - Add full async support (WIP) 457 | - toIt, toAct and toChild are async by default. Async param is removed (Breaking change) 458 | - Add async inputs support 459 | 460 | ## v2.10.3 461 | 462 | - Hotfix, do not call init on hot-swap 463 | 464 | ## v2.10.2 465 | 466 | - Fix size binding interface 467 | 468 | ## v2.10.1 469 | 470 | - Hotfix to include type definitions in compiled code 471 | 472 | ## v2.10.0 473 | 474 | - Add CSS class to style helpers 475 | - Include action input helper by default 476 | 477 | ## v2.9.6 478 | 479 | - Fix, do not call init when hot-swap 480 | 481 | ## v2.9.4 482 | 483 | - Fix AOT 484 | 485 | ## v2.9.3 486 | 487 | - Make view interface handler universal (SSR & Prerendering) 488 | - Add cb to view interface handler 489 | - Fix style group handler implementation 490 | 491 | ## v2.9.2 492 | 493 | - Hotfix for v2.9.1 494 | 495 | ## v2.9.1 496 | 497 | - Adapt style group handler for SSR and prerender 498 | 499 | ## v2.9.0 500 | 501 | - Size binding snabbdom module for bind the element size to the state 502 | - Integrate ResizeSensor for listening element size changes 503 | - Fix type of event and global listeners module for accepting arrays of InputData 504 | - Inject input helpers to component hooks 505 | 506 | ## v2.8.0 507 | 508 | - Implement path updates (fixes bugs in interface cache implementation) 509 | - Cached interfaces (CRAZY optimization, now interfaces are blazingly more faster) 510 | - Root context delegation 511 | - Fix deepmerge issue 512 | 513 | ## v2.7.0 514 | 515 | - Test case for hotfix in `action` function in component helpers 516 | - Add deepmerge as a dependency 517 | - Hotfix in `action` function in component helpers 518 | - Add deepmerge and deepmergeAll functions to functional helpers 519 | - Add `styles` function for making a new component by merging the component style 520 | 521 | ## v2.6.0 522 | 523 | - Test of interfaceOrder 524 | - Test of async notifyInterfaceHandlers 525 | - Add ignore to global event listener type signature 526 | - Add interfaceOrder to modules 527 | - notifyInterfaceHandlers works async 528 | 529 | ## v2.5.0 530 | 531 | - Add router interface handler 532 | 533 | ## v2.4.1 534 | 535 | - Replace css property -moz-placeholder by placeholder-shown for Firefox 51+ 536 | 537 | ## v2.4.0 538 | 539 | - Add selfPropagated property to global event listeners 540 | - Refactor event propagation 541 | - Add isDescendant view helper 542 | - Make input and interface helpers internal methods differnt with _, for example act with _act 543 | 544 | ## v2.3.2 545 | 546 | - Fix default prevented behaviour 547 | 548 | ## v2.3.1 549 | 550 | - Global event listeners do not handle prevented events by default 551 | - Add listenPrevented options to event listeners 552 | 553 | ## v2.3.0 554 | 555 | - Global event listeners handle all the events, and normal do not handle events that are prevented 556 | - stopPropagation is not allowed by design 557 | 558 | ## v2.2.1 559 | 560 | - Fix implementation of dynamic propagation 561 | 562 | ## v2.2.0 563 | 564 | - Reimplemented propagation in simple, dynamic and general, as a sequence 565 | - Fix sendMsg and toAct functions 566 | - Add error message when toChild is executed with an invalid child name 567 | - Add async option to sendMsg function 568 | - Change the order of isAsync and isPropagate arguments 569 | - Fix global event listeners async 570 | - Add async option to toIt, toChild and toAct 571 | 572 | ## v2.1.1 573 | 574 | - Remove mori helpers from core 575 | 576 | ## v2.1.0 577 | 578 | - Ignore log.ts and style.ts coverage for now 579 | - Remove cs() unused function from style.ts 580 | - Due to Webpack 2 has tree shaking and is the desired build tool, we should have one import for all the core functions, migration all helpers to core index 581 | 582 | ## v2.0.4 583 | 584 | - Fix input import in core.ts 585 | - Fix input import in log.ts 586 | 587 | ## v2.0.3 588 | 589 | - Fix dependencies from input refactor 590 | - Add input to core (fix) 591 | - Rename inputs for input 592 | 593 | ## v2.0.2 594 | 595 | - Fix CtxNest type signature and worker.ts 596 | 597 | ## v2.0.1 598 | 599 | - Add stateOf to interface helpers 600 | 601 | ## v2.0.0 602 | 603 | - Add stateOf, toIt, toChild, nest, unnest, nestAll and unnestAll to input helpers and curry them 604 | - Group all input helpers to inputs.ts 605 | - Interface ctx argument replaced by helpers object, increase redability and speed 606 | - Currying all the interface helpers and group on interface.ts 607 | - Add an interface index to ComponentSpace increasing speed 608 | - Inputs ctx argument replaced by input helpers object 609 | - Currying interfaces for optimize speed 610 | - Fix logging stuff in globalListeners 611 | 612 | ## v1.6.0 613 | 614 | - toParent has removed because enforce coupling of components 615 | - act has new signature 616 | - Add toAct helper 617 | 618 | ## v1.5.6 619 | 620 | - Global listeners are attached to main container 621 | 622 | ## v1.5.5 623 | 624 | - Fix bug related to global-local listeners 625 | 626 | ## v1.5.4 627 | 628 | - Event options are optional in core interface 629 | 630 | ## v1.5.3 631 | 632 | - Fix bad npm upload 633 | 634 | ## v1.5.2 635 | 636 | - Add options parameter to event listeners at Fractal core, with `default` and `propagate` options 637 | 638 | ## v1.5.1 639 | 640 | - View event listeners can control preventDefault and stopPropagation via context data _default and _propagate properties 641 | 642 | ## v1.5.0 643 | 644 | - Add global events handler to view interface 645 | 646 | ## v1.4.13 647 | 648 | - Component init hooks are executed after first `notifyInterfaceHandlers` 649 | - Add Components type to components parameter of nest function 650 | 651 | ## v1.4.12 652 | 653 | - Add VNode to View interface exports 654 | 655 | ## v1.4.11 656 | 657 | - Rename `merge` core function to `nest` 658 | 659 | ## v1.4.10 660 | 661 | - Fix type signature of `assoc` functional helper 662 | 663 | ## v1.4.9 664 | 665 | - Fix build 666 | 667 | ## v1.4.8 668 | 669 | - Refactor view interface file structure 670 | - Include h as a view method 671 | - Include vnode into core view methods 672 | 673 | ## v1.4.7 674 | 675 | - Add ignore to On interface for event handlers 676 | 677 | ## v1.4.6 678 | 679 | - Update snabbdom version 680 | - Implement VNodeData and other stuff for h types 681 | - Add `interfaces/h` for using without import snabbdom 682 | 683 | ## v1.4.5 684 | 685 | - Fix view event listeners snabbdom module event pausing 686 | 687 | ## v1.4.4 688 | 689 | - Add merge to functional utils fun.ts 690 | 691 | ## v1.4.3 692 | 693 | - Fix execute function 694 | - Move toIt to core 695 | 696 | ## v1.4.2 697 | 698 | - Fix log when state is not an object 699 | 700 | ## v1.4.1 701 | 702 | - Fix log functions 703 | 704 | ## v1.4.0 705 | 706 | - Add isPropagated optional parameter to dispatch function 707 | - Refactor and simplify API 708 | - Remove onDispatch event from Module and log helpers 709 | - Add beforeInput and afterInput events to Context and Module 710 | - Better Logging functions 711 | 712 | ## v1.3.1 713 | 714 | - Speed up core replacing for-Object.keys loop by for-in 715 | - Message Interchange fuctions propagation is optional, true by default 716 | 717 | ## v1.3.0 718 | 719 | - Remove useless parameter from `execute` function 720 | - Remove useless parameter from `propagate` function 721 | 722 | ## v1.2.1 723 | 724 | - Fix nested propagation 725 | 726 | ## v1.2.0 727 | 728 | - Fix component message interchange by adding propagation 729 | - Add scope to global component listeners (parent -child communication) 730 | 731 | ## v1.1.0 732 | 733 | - Add functional utils to `fun.ts` 734 | - Add assoc, evolve and evolveKey function to fun utils 735 | - Add type Interface and refactor View type in View interface 736 | - Relocate utils to root folder (src) 737 | 738 | ## v1.0.8 739 | 740 | - Parent can observe any child input 741 | 742 | ## v1.0.7 743 | 744 | - Make msg parameter of sendMsg and toIt optional in utils/component 745 | 746 | ## v1.0.6 747 | 748 | - Add ignore and pass options to view event handlers 749 | 750 | ## v1.0.5 751 | 752 | - Add event stoping in view event handlers 753 | 754 | ## v1.0.4 755 | 756 | - Add ignored view event listeners 757 | 758 | ## v1.0.3 759 | 760 | - Fix bug in dispatch 761 | 762 | ## v1.0.2 763 | 764 | - Fix some types to be useful 765 | 766 | ## v1.0.1 767 | 768 | - Add functional utils (fun) with pipe and mapToObj and remove them from component utils 769 | 770 | ## v1.0.0 :rose: 771 | 772 | - Allow use `Actions` for typesafe actions (BREAKING CHANGE) 773 | - Create Id type that can be Number | String, for avoid using in dynamic components 774 | - Update examples 775 | 776 | ## v0.7.7 777 | 778 | - Add toIt helper for sending messages to the same component 779 | - toChild log an error when child does not have the input 780 | 781 | ## v0.7.6 782 | 783 | - toParent helper log an error when parent does not handle child messages 784 | 785 | ## v0.7.5 786 | 787 | - Improve implementation of child -> parent communication via toParent for better performance and clarity 788 | 789 | ## v0.7.4 790 | 791 | - Add stateOf name parameter for fixing API 792 | - Add clickable style helper 793 | - Improve update examples 794 | 795 | ## v0.7.3 796 | 797 | - Fix broken stuff after removing core/stateOf 798 | 799 | ## v0.7.2 800 | 801 | - Remove core/stateOf duplicated method 802 | 803 | ## v0.7.1 804 | 805 | - Fix bug with notifyHandlers 806 | 807 | ## v0.7.0 808 | 809 | - Add sendMsg and toChild function to component helpers for better messaging 810 | - Fix a test and coverage in worker helpers 811 | 812 | ## v0.6.8 813 | 814 | - Add fetch value option for generic inputs 815 | 816 | ## v0.6.7 817 | 818 | - Fix edge case with child components with dynamic parents 819 | 820 | ## v0.6.6 821 | 822 | - Fix error with reattach when parent component don't have defs 823 | 824 | ## v0.6.5 825 | 826 | - Add error when component defs of a parent does not have definition of a dynamic component 827 | 828 | ## v0.6.4 829 | 830 | - Fix bug in hot-swaping 831 | - Add isStatic parameter to merge and mergeAll 832 | - Add defs to component type for dynamic components 833 | 834 | ## v0.6.3 835 | 836 | - Fix bug with context 837 | 838 | ## v0.6.2 839 | 840 | - Add isStatic META property 841 | - Fix bug related to hot-swaping when dynamic modules are involved 842 | 843 | ## v0.6.1 844 | 845 | - Add self component helper 846 | 847 | ## v0.6.0 848 | 849 | - Add global notifier for parent components, use case dynamic lists of components 850 | - Fix log utils for displaying when a component is removed 851 | - Fix bug related to unmerge when name is zero 852 | 853 | ## v0.5.1 854 | 855 | - Fix bug in reattach funtionality 856 | 857 | ## v0.5.0 858 | 859 | - Multiple event data fetching with an array of arrays 860 | - Covered act helper in utils/components 861 | - A gap is defined with undefined (optional) 862 | 863 | ## v0.4.0 864 | 865 | - Add act generic action dispatcher to component utils 866 | 867 | ## v0.3.2 868 | 869 | - Fix types of ofuscator, absoluteCenter and placeholderColor helpers 870 | 871 | ## v0.3.1 872 | 873 | - Add obfuscator helper to style utils 874 | 875 | ## v0.3.0 876 | 877 | - Add support for multiple key-value fetching in computeEvent 878 | - Add support for multiple key-value fetching in computeEvent at the end of a path 879 | - Remove Handler type from handler definitions, it should be internal, REASON: improve DX 880 | 881 | ## v0.2.5 882 | 883 | - Change ViewInterface for View in view interface 884 | - Fix ViewInterface in all examples 885 | - Add keywords to package.json 886 | 887 | ## v0.2.4 888 | 889 | - Add Actions interface to core 890 | - Add Components interface to core 891 | - Make component inputs optional 892 | - Make data parameter optional in Input and Action interfaces 893 | - Add missing types to testForm example 894 | - Update typescript version 895 | 896 | ## v0.2.3 897 | 898 | - Make style group handler containerName parameter optional 899 | 900 | ## v0.2.2 901 | 902 | - Fix type of style group handler 903 | - Fix unused containerName option in style group handler 904 | 905 | ## v0.2.1 906 | 907 | - Fix debugNames and add a debug option to style group handler 908 | 909 | ## v0.2.0 910 | 911 | - Fix bug when call dispatch from a child component 912 | - Add onDispatch event to module definition 913 | - Add onDispatch function to log helpers 914 | - Use onDispatch in testForm example 915 | 916 | ## v0.1.0 917 | 918 | - Add mapToObj helper 919 | - Following SEMVER from this version 920 | 921 | ## v0.0.10 922 | 923 | - Fixed bug in hot-swaping related to edge case in mergeStates 924 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Staff 2 | 3 | The staff is composed by: 4 | 5 | - [Carlos Galarza](https://github.com/carloslfu) 6 | 7 | # Contributors 8 | 9 | Special thanks to: 10 | 11 | - Put your name here :) 12 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Fractal is now in Typescript 2 | 3 | ## Philosophy 4 | 5 | We will focus on next topics: 6 | 7 | - Simple API + simple architecture (do other diagram like architecture diagram, but focus on what is useful for users) 8 | - Focus on small and emmbedable components 9 | - Tasks are atomic 10 | - Small code (removing some dependencies), ALAP 11 | - Modules never crash at runtime, but performs a good error hadling and reporting 12 | - Mori for persistent data structures 13 | - Typed views via new Snabbdom with Typescript 14 | - Better composing API 15 | - Better and simple docs, live docs 16 | - Behaviors optimization pattern, via tasks 17 | - Tween.js task for animations 18 | - Router pattern (urls) 19 | 20 | ## Dependencies 21 | 22 | - Fractal core should not have any dependencies, this is a design choice 23 | - Maintain package.json as simple as possible, less dependencies, less scripts ... 24 | 25 | ## Concepts 26 | 27 | - component: is a small or big part of your app, and is designed for composition. 28 | - group: is a container for each component that is initialized with it and handled by group handlers. Used for styles. 29 | - interface: is the part of a component that is responsible of communications (external world, AKA side effects) 30 | - state: is the part of a component related to their data 31 | - action: is a part of a component and is the unique way to update its state 32 | - task: is an information related to a specific side effect, tasks are dispatched by components via inputs (see later) 33 | - handler: is a part of an module that handle interfaces of root component, groups or tasks. Performs certain type of side effects. Can be a task handler or an interface handler 34 | - input: is a part of a component that is a dispatcher for actions and tasks 35 | - lifecycle: it is a set of hooks for the execution of something. Modules and components has init and destroy hooks 36 | - module: a module runs one component (AKA root component), connecting it to external world. A component can be composed of more components in a tree 37 | - Fractal arquitecture: is the whole way in that data and functionality are handled 38 | 39 | Development: 40 | 41 | - hot swapping: hability of swapping state between updates during development 42 | 43 | Internals: 44 | 45 | - component context: is the context of a component merged and running in a module 46 | 47 | ## Rules 48 | 49 | - IMPORTANT: styles showld be separated and are a function of palette and other globals (maximum customization and weight) 50 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | We use BDD for building Fractal. This documents describe all about how Factal works. 4 | 5 | ## Stucture 6 | 7 | See the reference (TODO: put a link to structure reference). 8 | 9 | ## Versioning 10 | 11 | We follow semver. 12 | 13 | ## Design Principles 14 | 15 | - All-in-JS: No templates, no html, no JSX, no CSS, no whatever CSS preprocessor. We simply use JS libraries and plain JS / TS: 16 | - TypeStyle for styles 17 | - Snabbdom for DOM 18 | - TypeScript as preferred language but everything works in JS pretty well. We use TS for coding experience and catching a few bugs. 19 | - We prefer imperative APIs and controled mutation for a more intuitive use. 20 | 21 | Soon ... We follow the Fractal Standards for Software Quality (TODO: put the link) that describes conventions for coding 22 | 23 | ## Core Dependencies 24 | 25 | There are no dependencies for Fractal core, this has a design choice. 26 | 27 | ## Third party dependencies 28 | 29 | We make use of: 30 | 31 | - Snabbdom for vdom: used in examples and view interface handlers 32 | - Typestyle for safe styles: used in examples 33 | 34 | ## TODOs - Short Term 35 | 36 | Shor term tasks are documented in [CHANGES Document](/CHANGES.md) 37 | 38 | ## Roadmap (What is next?) 39 | 40 | - Complete the test suite 41 | - Implement and document test utils 42 | - Document component lifecycle 43 | - OPTIMIZATION: Implement 'prepare' function for dynamic components. This allow to not reprocess styles (groups) for every component added 44 | - Document prerendering (AOT) and SSR 45 | - Document sizeTask 46 | - Document Cached interfaces, this is basically that interfaces are cached by default 47 | - Document interfaceOrder. This method is used to set the order of the initial evaluation of interfaces 48 | - Document global events listener selfPropagated option 49 | - Document that global and normal event listeners do not handle events that are prevented ( default: false ) and there are a listenPrevented option to reverse this behaviour 50 | - Document that we don't support stop propagation of DOM events as a design choice because of [this article](https://css-tricks.com/dangers-stopping-event-propagation/) 51 | - Document simple, dynamic and general propagation and it's secuential evaluation 52 | - Document and build an example of pausing View events (global and local) 53 | - Document and build an example of global listeners 54 | - Document ignored and passed view event handlers 55 | - Flux-Challenge example and do a PR to Staltz repo (WIP) 56 | - Todo-mvc example in a separated repo 57 | - Implement an example of charts (D3) (SERVICE 3PARTY-DOM-INTEGRATION) 58 | - Evaluate if multiple handlers in an element are an ati-pattern, if so deprecate it 59 | - Change examples for the way we import components as router example does, note that hot-swaping changes too 60 | - Add error index (DX) 61 | - Cancelable callbacks from subcribables in tasks, via an instance index 62 | - Router Docs (ASAP) 63 | - Simplify toHTML function and remove lodash stuff (CRITICAL - Introduced as a hotfix for toHTML function) 64 | - Example of RTC in Fractal 65 | - Official support for [Popmotion](https://github.com/Popmotion/popmotion) as an optional animation library 66 | - Split patch and diff in Snabbdom 67 | - Porting all the drivers and task handlers from fractal.js to handlers in Fractal 68 | 69 | ## Ideas 70 | 71 | What maybe great for this repo: 72 | 73 | - Implement the forms example and document the composing tools 74 | - Implement an i18n middleware example 75 | - Implement ramda-mori helpers for Persistent Data Structures 76 | - Upgrate old Fractal examples and publish them 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-present Carlos Galarza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/FractalBlocks/Fractal.svg?branch=master)](https://travis-ci.org/FractalBlocks/Fractal) 2 | [![Join the chat at https://gitter.im/Fractal-core/Lobby](https://badges.gitter.im/Fractal-core/Lobby.svg)](https://gitter.im/Fractal-core/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | # 4 | We believe in more than frameworks, we believe that minimalist and well crafted software will change the world. Lets build your ideas with elegance and simplicity with Fractal. 5 | 6 | ## TLDR 7 | 8 | We use better tools, we move fast, we love innovation ... Oh and Fractal is fractal, this means you can embed any Fractal app inside another with no effort. 9 | 10 | ## Features 11 | 12 | - Minimal, simple and composable architecture that scales 13 | - Simple inter-component comunication via messages, you can broadcast messages to all components and listen for them 14 | - Async operations made simple. Fractal make a wide use of async functions and you can use it in your app 15 | - We gives you powerful patterns and composing tools that helps you to build small and large apps like Legos 16 | - Excellent developer experience. We love to improve your experience, for now we have hot-swapping support but wait more dev tools in the short term 17 | - Lazy loading support 18 | - Prerendering and Server Side Rendering support 19 | - Blazingly fast because we use caching. This is possible because actions are the unique way of updating the state and the component view changes only depends on state so we only compute components that are actually needed. For DOM updates we use Snabbdom that it's pretty fast but you can use whatever DOM library you want, e.g. React 20 | - Excellent error / warn handling: 21 | - Your app will never crash for a runtime error (Actually under testing, if you have an uncovered case please open an issue) 22 | - Application logs are meaningful. No more WTF?!! errors, if you see one, report it and we will fix it in a timely manner 23 | - You can easly serialize side effects, this means you can run Fractal in a Web Worker, in a server via Websockets or even in a remote browser via WebRTC, crazy right? :'). An example comming soon... 24 | - High code quality, we love that!! We helps you to achive it in your proyect :heart: 25 | 26 | See the [design document](https://github.com/FractalBlocks/Fractal/blob/master/DESIGN.md). In order to be scalable, Fractal is implemented using [Typescript](https://www.typescriptlang.org/) 27 | 28 | We make use of Fractal in our projects and this library is continuosly evolving, be in touch... 29 | 30 | ## How it works? 31 | 32 | Concepts: 33 | 34 | - Module: A engine for you app, connect a component tree to the external world. 35 | - Component: Is a part of your app, a component have: 36 | - State: The component changing data. 37 | - Inputs: Async processes that recieve messages (data) from other components or outside the app, in an Input you can: 38 | - Send messages to other Inputs (same Component). 39 | - Send messages to other Components via Inputs. 40 | - Send messages outside the aplication with Tasks. 41 | - Perform side effects but only while prototyping, after that you should use Tasks. 42 | - Actions: The only way to change the state. 43 | - Interfaces: Here lives continous and hierachical connections with external world, the most common Interface is the `view`. 44 | - Groups: Data that can be used when a component is initialized, we use it for styles. 45 | - Task: A mechanism to send messages to external world in a clean way (AKA perform side effects nicely), e.g. get some data from your local database. 46 | - Handlers: Perform side effects, they receive messages from Tasks and hierachical structures from Interfaces and handle them. 47 | 48 | Techniques: 49 | 50 | - Fractal architecture: This is a kind of unidirectional user interface architecture see an insightful article [here](https://staltz.com/unidirectional-user-interface-architectures.html) by @staltz. 51 | - Reactive views: We use [Snabbdom](https://github.com/snabbdom/snabbdom) for rendering. 52 | - CSS in JS: We create styles in JS using [TypeStyle](https://github.com/typestyle/typestyle), see this [Vjeux slides](https://speakerdeck.com/vjeux/react-css-in-js). 53 | - Typescript: We use [TypeScript](https://www.typescriptlang.org/) that is the same as JS but optionally you can have some types, we use it for better tooling [see this Eric Elliot article](https://medium.com/javascript-scene/the-shocking-secret-about-static-types-514d39bf30a3). Our lemma is to be fast with dynamic types but use types for stable code and some data modeling. We use it for: 54 | - A nice autocomplete for Styles and JavaScript 55 | - Errors in Styles (via TypeStyle) 56 | - Type some data models 57 | - Type functions 58 | - Catch syntax errors and a few bugs 59 | - Hot-swapping: Live develop your application with no state loss. It gives you a nice developer experience. 60 | - JS Bundling / Loading: [FuseBox](https://github.com/fuse-box/fuse-box) :fire::fire::fire: We love it! is fast, clear and powerful ... Oh! I said it's fast? .. Well, it's blazingly fast, let's try it! 61 | 62 | ## Future 63 | 64 | - Nice in-app debugger with timetravel debugging and a component tree view 65 | - A list of all possible errors can happen with their respective solution, this can evolve including links within error logs 66 | - We have high code quality standards and development flows you and your team can follow, from prototyping to production code (We will publish these soon ...) 67 | - Prototype with ease and transform to a production level code with easy (Guide comming soon...) 68 | - We support hot-swapping code in production :rose: (soon) 69 | 70 | ## Getting started 71 | 72 | The recomended way is using FuseBox, please download the [Fractal-quickstart](https://github.com/FractalBlocks/Fractal-quickstart) or [Fractal-featured](https://github.com/FractalBlocks/Fractal-featured) repo depending on your needs, this gives you all things ready to start hacking. 73 | 74 | Or in nodejs, browserify, Webpack like environments: 75 | 76 | ```bash 77 | npm i --save fractal-core 78 | ``` 79 | 80 | Or with yarn: 81 | 82 | ```bash 83 | yarn add fractal-core 84 | ``` 85 | 86 | Note for VSCoders, you can use the [Fractal VSCode Extension](https://github.com/FractalBlocks/Fractal-vscode-extension) 87 | 88 | ## Examples 89 | 90 | Working examples you can start hacking immediately: 91 | 92 | - [Counter](https://stackblitz.com/edit/fractal-counter?file=Root%2Findex.ts) 93 | - [Simple Todo List](https://stackblitz.com/edit/fractal-simple-todo-list?file=Root/index.ts) 94 | - [Filesystem View](https://stackblitz.com/edit/fractal-filesystem-view?file=Root%2Findex.ts) 95 | - [API Users Posts](https://stackblitz.com/edit/fractal-api-users-posts?file=Root/index.ts) 96 | - [Conway's Game of Life - Flat Version](https://stackblitz.com/edit/fractal-game-of-life-flat?file=Root%2Findex.ts) 97 | 98 | ## Tutorial 99 | 100 | See our [tutorial](https://github.com/FractalBlocks/Fractal/blob/master/docs/tutorial/readme.md) and the examples (We are refactoring examples and tutorial, be in touch ...). 101 | 102 | Note: The tutorial is not updated, we will update it soon. 103 | 104 | ## Related Projects 105 | 106 | - [Fractal Quickstart](https://github.com/FractalBlocks/Fractal-quickstart) 107 | - [Fractal Featured](https://github.com/FractalBlocks/Fractal-featured) 108 | - [Fractal VSCode Extension](https://github.com/FractalBlocks/Fractal-vscode-extension) 109 | - Fractal CLI, Comming soon... 110 | 111 | ## Development Notes 112 | 113 | See our [Development Documentation](https://github.com/FractalBlocks/Fractal/blob/master/DEVELOPMENT.md) 114 | -------------------------------------------------------------------------------- /REFERENCE.md: -------------------------------------------------------------------------------- 1 | # Reference (OUTDATED) 2 | 3 | ## TODOs 4 | 5 | - Update this doc 6 | 7 | ## Code structure 8 | 9 | Overview of files, the src/ folder 10 | 11 | - core/: All Core Features of Fractal 12 | - -- index.ts: Enty point to all core functions and interfaces 13 | - -- core.ts: Set of core interfaces, e.g. components, updates ... 14 | - -- handler.ts: Set of interfaces for handlers 15 | - -- module.ts: Interfaces and functions for making Fractal Modules 16 | - interfaces/: Set of built-in Fractal Interfaces, do something when state changes 17 | - -- event.ts: Executes a callback 18 | - -- view.ts: Render a Snabbdom view 19 | - groups/ 20 | - -- style.ts: Style group for attaching styles to components using TypeStyle 21 | - tasks/ 22 | - utils/ 23 | - -- component.ts: Helpers for composition and building components 24 | - -- style.ts: Helpers for styling using TypeStyle 25 | - -- worker.ts: Helper for running Fractal inside workers 26 | - -- mori.ts: Helpers for mori PDS 27 | - -- log.ts(DEV): Helpers for logging during development 28 | - -- reattach.ts(DEV): Helpers for hot-swaping during development 29 | 30 | TODO: complete docs 31 | -------------------------------------------------------------------------------- /assets/FractalLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FractalBlocks/Fractal/91b77ada3f5cc2f9fd7f0c9b31e3f3f885f6c1d5/assets/FractalLogo.png -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## core 4 | 5 | ### dispatch 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Architecture (WIP) 2 | 3 | Fractal is a fully featured framework to make frontend apps using a simple and powerful architecture. It is based on the functional programming paradigm and other aproaches that simplify UI development. It may also be used in other contexts, note that Fractal.js is the implementation of the Fractal architecture for the web platform, but its designed to be language agnostic. 4 | 5 | Fractal is an unidirectional user interface architeture that is fractal (autosimilar): 6 | 7 | > A unidirectional architecture is said to be fractal if subcomponents are structured in the same way as the whole is. 8 | > -[ Andre Staltz](http://staltz.com/unidirectional-user-interface-architectures.html) 9 | 10 | Fractal modules are based on the [Model View Update architecture](http://staltz.com/unidirectional-user-interface-architectures.html#elm). This means that each module is mostly structured in this way. 11 | 12 | Fractal offers a complete architecture with useful patterns and conventions that allows you center in usability, design and business logic instead of architecture. 13 | 14 | All the application logic is contained into a main module and is hierachicaly structured and composed following the MVU pattern. 15 | 16 | If you want to learn more about Fractal's main foundations check out: 17 | 18 | - An awesome article called [Unidirectional user interface architectures](http://staltz.com/unidirectional-user-interface-architectures.html) by [Andre Staltz](http://staltz.com/) 19 | - A nice repo and discuss in [functional-frontend-architecture](https://github.com/paldepind/functional-frontend-architecture) by [Simon Friss Vindum](https://github.com/paldepind) 20 | - [Controlling Time and Space: understanding the many formulations of FRP](https://www.youtube.com/watch?v=Agu6jipKfYw) talk by Evan Czaplicki 21 | - An article on why Fractal is [implemented in Typescript](http://staltz.com/all-js-libraries-should-be-authored-in-typescript.html) (See [Fractal Framework](https://github.com/fractalPlatform/Fractal)) (TODO-EVALUATE) 22 | - [CSS in JS](https://vimeo.com/116209150) talk by Christopher Chedeau. [Slides here](https://speakerdeck.com/vjeux/react-css-in-js) 23 | - [Virtual DOM approach](https://medium.com/@yelouafi/react-less-virtual-dom-with-snabbdom-functions-everywhere-53b672cb2fe3#.nfir9w2fb) 24 | -------------------------------------------------------------------------------- /docs/Hot-Swaping.md: -------------------------------------------------------------------------------- 1 | # Hot-Swaping 2 | 3 | It is done in a pretty simple way. We save 'actions' all the time during development, so each time hot-swapping is activated, the whole component tree is recalculated and 'actions' are replayed. After that state should be consistent. 4 | 5 | One of the reasons we encourage that dynamic components should be wrapped in actions its because this way actions are replayed over the lattest components code. 6 | -------------------------------------------------------------------------------- /docs/Internals.md: -------------------------------------------------------------------------------- 1 | # How Fractal works in detail 2 | 3 | The purpose of this document is describing the internal working of Fractal, to aim to be simple and easy to understand by new users. 4 | 5 | ## Start time 6 | 7 | - All starts with the call of the `run` function, it creates a process we call Module, next steps are performed by the Module 8 | - Build the initial component tree by reading _nest property of components state, starts with Root component and initialize the whole component tree. Component tree s internally flattened so all components live in an object where the key is the component identifier and the value is the execution her context. For each component: 9 | - Create the execution context (We call it a Context) 10 | - Process her groups passing them to group handlers, a good oppotunity to process styles. 11 | - Call `init` input. 12 | - Root component is connected to interface handlers and perform the initial execution. The view is an interface, so at this time the initial render is done. 13 | 14 | ## Runtime 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/building UIs.md: -------------------------------------------------------------------------------- 1 | # Building UIs 2 | 3 | ## Capturing user interactions (Binding UI) 4 | 5 | - Figure out what kind of interaction are, see the list of DOM Events 6 | - Capture user event (DOM Event) 7 | - Change the state 8 | -------------------------------------------------------------------------------- /docs/composing.md: -------------------------------------------------------------------------------- 1 | # Composing 2 | 3 | # Types 4 | 5 | - Normal: Common components 6 | - Stateless: Components with no state 7 | - Meta: High order components, component factories 8 | -------------------------------------------------------------------------------- /docs/dynamic-components.md: -------------------------------------------------------------------------------- 1 | # Dynamic Components 2 | 3 | Dynamically added components either loaded statically or dynamically (e.g. with code splitting) should be wrapped in an action, this way we decouple components code from the action it-self, so an action should never receive a component as parameter. 4 | -------------------------------------------------------------------------------- /docs/eventFlow.md: -------------------------------------------------------------------------------- 1 | TODO: fix this 2 | 3 | function string makes easy to serialize InputData, if '*' the data fetched are the whole event object, if 'other' extract 'other' property from event object 4 | all this stuff allow to serialize communication between root component and handlers, this means you can execute a root component in a worker (even remotely in a host computer) 5 | and handlers still dispatch inputs, a solution for serializing event callbacks. 6 | this function is executed by interface / task handlers and his result is passed as a value to the dispatched component input passing as argument EventData 7 | Event Flow: 8 | - InputData (from component interface / task) comes with some data depending on the context 9 | - If needed, some handy / fancy CHANNEL serialize and transmit it 10 | - External things occurs, if InputData[3] is true the function string (InputData[2]) are excecuted, if not the value is taken, giving EventData as event data 11 | - If EventData has transferred via CHANNEL, the EventData is returned via this CHANNEL 12 | - the interface / task handler pass EventData to dispatch function 13 | - dispatch function fires the input in the respective component passing the event data 14 | 15 | The objective of this flow is allow handlers to be excecuted in workers or even remotely o.O 16 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Basic Concepts 2 | 3 | Let's start! 4 | 5 | ## Component 6 | 7 | A component is an small part of your app, think in it as a living cell or a block of lego. Components can be nested into other components for building more complex ones. 8 | 9 | ### State 10 | 11 | Are a set of variables (changing data) that are related to your component's functionality 12 | 13 | ### Action 14 | 15 | The unique way for updating the state is to execute an Action, an Action takes the state the unique thing it does is to update it 16 | 17 | ### Task 18 | 19 | A Task is a way of sending things to external world aka perform side effects. Is similar to what in some frameworks are named services. You cn use it for performing DB operations, feching data and intercomunicate your app. 20 | 21 | ### Input 22 | 23 | Inputs are processes that have the main logic of the component and in are the center of processing. Inputs are related with all the logic that are related with communications. Inputs can do: 24 | 25 | - Execute Actions, this means change the state. 26 | - Execute Tasks 27 | - Execute Inputs of child components or the component itself. 28 | 29 | ### Interface 30 | 31 | An interface is a way for building hierachical side effects that are recalculated each time state changes. The most common one is the View. 32 | 33 | ### Group 34 | 35 | 36 | 37 | ## Module 38 | 39 | A module is who makes your app work, it has a Root component that is the entry point for your app component tree. A module have Handlers for managing all kind of side effects components does, there are Task, Interface and Group Handlers. 40 | 41 | ## Comunications 42 | 43 | 44 | 45 | ## Child input listeners (AKA input propagation) 46 | 47 | ## Component Message Interchange (AKA messages) 48 | -------------------------------------------------------------------------------- /docs/patterns.md: -------------------------------------------------------------------------------- 1 | # List of common patterns in Fractal 2 | 3 | ## Event reaction 4 | 5 | When the user makes something in the View 6 | 7 | ## State binding 8 | 9 | When the user makes something in the View and you need to have those changes in the state for subsequent use 10 | 11 | ## Parent - child communication, message passing 12 | 13 | When the child needs something that are in the parent's state 14 | 15 | ## Child - Parent communication, propagation 16 | 17 | When the child needs to send something to the parent 18 | -------------------------------------------------------------------------------- /docs/tutorial/contents.md: -------------------------------------------------------------------------------- 1 | # Contents 2 | 3 | ## Done 4 | 5 | - [Tutorial Overview](readme.md) 6 | - [Let's start simple, a counter](counter.md) 7 | 8 | ## Soon 9 | 10 | - Styling our Counter 11 | - Composing components 12 | - Sending messages between components 13 | - Fetching content 14 | - Routing 15 | - Handling complex inputs with FRP 16 | - Real world example 17 | - Server Side Rendering (SSR) 18 | -------------------------------------------------------------------------------- /docs/tutorial/counter.md: -------------------------------------------------------------------------------- 1 | # Let's start simple, a counter 2 | 3 | Nice!. We will start with our first component, the Counter. This is how Counter looks like: 4 | 5 | ``` 6 | 0 [+] [-] 7 | ``` 8 | 9 | This component has 3 visual elements: 10 | 11 | - Value (0 by default) 12 | - Increment button (+) 13 | - Decrement button (-) 14 | 15 | Well, lets decompose the Counter behaviour :O 16 | 17 | The Counter should have the next requirements: 18 | 19 | 1. Save the `count` that is numeric and should be 0 by default 20 | 2. A text for showing the actual `count` 21 | 3. A button for increment the `count` 22 | 4. A button for decrement the `count` 23 | 24 | Well now what are the possible interactions? 25 | 26 | 5. Click on Increment Button should add 1 to the `count` 27 | 6. Click on Decrement Button should substract 1 to the `count` 28 | 29 | Oh! pretty simple, right?. We will implement it using the above requirements. 30 | 31 | ## Save the count that is numeric and should be 0 by default 32 | 33 | Well, we have a site where we save things, this is called the state and is an object containing all the thing we want to be saved in our component, so you have to describe the initial value of it. In this case: 34 | 35 | ```typescript 36 | export let state = { 37 | count: 0, // count starts in zero 38 | } 39 | ``` 40 | 41 | ## A text for showing the actual count 42 | 43 | ```typescript 44 | const view: View = 45 | F => s => h('div', [ 46 | h('div', s.count + ''), 47 | // Above line show the count, the "+ ''" part is for casting count number to string 48 | ]) 49 | ``` 50 | 51 | The `h` function is a helper to create view elements. The `s` is the state variable, that containt our `count`. 52 | 53 | 54 | Lets see the structure of the `h` function. There are three forms of using it: 55 | 56 | ```typescript 57 | // h(tagName, string | arrayOfElements) 58 | h('div', 'Hello!') 59 | h('div', []) 60 | // h(tagName, options, string | arrayOfElements) 61 | h('div', {}, 'Hello!') 62 | h('div', {}, []) 63 | ``` 64 | 65 | We will use the options object later, this is used for settting attributes, properties, styles, data and events to the element. 66 | 67 | You can nest elements too: 68 | 69 | ```typescript 70 | h('div', [ 71 | h('div', 'element 1'), 72 | h('div', 'element 2'), 73 | ]) 74 | ``` 75 | 76 | ## A button for increment 77 | 78 | This one is part of the view, so see the code: 79 | 80 | ```typescript 81 | const view: View = 82 | F => s => h('div', [ 83 | h('div', s.count + ''), 84 | h('button', '+'), // <--- increment button 85 | ]) 86 | ``` 87 | 88 | ## A button for decrement 89 | 90 | ```typescript 91 | const view: View = 92 | F => s => h('div', [ 93 | h('div', s.count + ''), 94 | h('button', '+'), 95 | h('button', '-'), // <--- decrement button 96 | ]) 97 | ``` 98 | 99 | Right now we have our component view almost complete. We need to work on the interactions that are only two 100 | 101 | ## Click on Increment Button should add 1 to the count 102 | 103 | For achive it, we need to listen clicks in the Increment Button: 104 | 105 | ```typescript 106 | const view: View = 107 | F => s => h('div', [ 108 | h('div', s.count + ''), 109 | h('button', { 110 | on: { // 'on' is used to group event listeners such as: click, mouseover, keydown ... 111 | click: F.act('Inc'), // this line associate the click event with the 'Inc' action 112 | }, 113 | }, '+'), 114 | h('button', '-'), 115 | ]) 116 | ``` 117 | 118 | So when you click the increment button the `Inc` action is executed and the count increments by 1 119 | 120 | What is an action? In short is the unique way for changing the state. All the changes you want to do in the state, you should execute an action 121 | 122 | Let's go ahead and declare our `Inc` action: 123 | 124 | ```typescript 125 | export const actions: Actions = { 126 | // actionName: param -> actualState -> newState, 127 | Inc: () => s => { 128 | s.count++ 129 | return s 130 | }, 131 | } 132 | ``` 133 | 134 | Now a more detailed explamation. An Action is a function that returns an update. An update is a function that takes the `state` and returns the next `state`. So, we are going to execute 'Inc' action when the user clicked the Increment Button 135 | 136 | Just one thing more!! 137 | 138 | ## Click on Decrement Button should substract 1 to the count 139 | 140 | Let's replicate the same as with the Increment Button 141 | 142 | ```typescript 143 | export const actions: Actions = { 144 | Inc: () => s => { 145 | s.count++ 146 | return s 147 | }, 148 | Dec: () => s => { 149 | s.count-- 150 | return s 151 | }, 152 | } 153 | 154 | const view: View = 155 | F => s => h('div', [ 156 | h('div', s.count + ''), 157 | h('button', { 158 | on: { 159 | click: F.act('Inc'), 160 | }, 161 | }, '+'), 162 | h('button', { 163 | on: { 164 | click: F.act('Dec'), 165 | }, 166 | }, '-'), 167 | ]) 168 | ``` 169 | 170 | This is it!! we have our first component, the Counter. Lets run it!, use the [Fractal-quickstart](https://github.com/FractalBlocks/Fractal-quickstart), so visit and follow [this steps](https://github.com/FractalBlocks/Fractal-quickstart#fractal-quickstart) to setup the quickstart. 171 | 172 | In the `app/` [folder](https://github.com/FractalBlocks/Fractal-quickstart/tree/master/app) we have all the files for running our application. 173 | 174 | In an editor, lets copy the code below and paste it to `Root/index.ts` file in `app/` folder. 175 | 176 | ```typescript 177 | import { 178 | Actions, 179 | Interfaces, 180 | } from 'fractal-core' 181 | import { View, h } from 'fractal-core/interfaces/view' 182 | 183 | export let state = { 184 | count: 0, 185 | } 186 | 187 | export type S = typeof state 188 | // Above line is for saving the structure of the state in the type `S`, this is related with the type system (in TS) and we use it for better dev tooling like autocompletion and to detect certain kind of bugs. 189 | 190 | export const actions: Actions = { 191 | Inc: () => s => { 192 | s.count++ 193 | return s 194 | }, 195 | Dec: () => s => { 196 | s.count-- 197 | return s 198 | }, 199 | } 200 | 201 | const view: View = 202 | F => s => h('div', [ 203 | h('div', s.count + ''), 204 | h('button', { 205 | on: { click: F.act('Inc') }, 206 | }, '+'), 207 | h('button', { 208 | on: { click: F.act('Dec') }, 209 | }, '-'), 210 | ]) 211 | 212 | export const interfaces: Interfaces = { view } 213 | ``` 214 | 215 | Let's start the development server with `npm start` and open [http://localhost:3000](http://localhost:3000) in a new tab. Now you will see our awesome Counter, nice right?. Oh!! try the hot swapping feature, it's nice and so useful during development, change the count, modify the div that show the count as follows: 216 | 217 | ```typescript 218 | const view: View = 219 | ({ ev }) => s => h('div', [ 220 | h('button', { 221 | on: { click: ev('inc') }, 222 | }), 223 | h('div', 'Count: ' + s + ''), 224 | h('button', { 225 | on: { click: ev('dec') }, 226 | }, '-'), 227 | ]) 228 | ``` 229 | 230 | Save and see the browser, our code have been charged immediately without reload the page and without reseting our count. This is it! 231 | 232 | 233 | Next section we will see how [Styling our Counter]() :heart: (Soon ...) (TODO: link) 234 | 235 | See the full [contents here](contents.md) 236 | -------------------------------------------------------------------------------- /docs/tutorial/readme.md: -------------------------------------------------------------------------------- 1 | # Tutorial Overview 2 | 3 | We love simplicity and minimalism, and if you are dealing with the inherent complexity of software, you should too. 4 | 5 | Well, lets start with the end in mind!!. Suppose you want to make a form like that: 6 | 7 | ``` 8 | Diary of 9 | Name ___________ 10 | Products 11 | TEAS 0 [+] [-] 12 | Apples 0 [+] [-] 13 | [Save] 14 | ``` 15 | 16 | We can divide it in components: 17 | 18 | - Form 19 | - Teas per day - Counter 20 | - Apples per day - Counter 21 | - Name - Input 22 | - Animated submit button - Button 23 | 24 | So we have built a component hierarchy. Go to the [Next step](counter.md) to see our first component! 25 | 26 | See the full [contents here](contents.md) 27 | -------------------------------------------------------------------------------- /fuse-example.js: -------------------------------------------------------------------------------- 1 | const { 2 | FuseBox, 3 | Sparky, 4 | CSSPlugin, 5 | JSONPlugin, 6 | EnvPlugin, 7 | WebIndexPlugin, 8 | } = require('fuse-box') 9 | 10 | let ENV = 'development' 11 | 12 | let fuse, name 13 | 14 | Sparky.task('init', () => { 15 | fuse = FuseBox.init({ 16 | home: '.', 17 | output: 'dist/$name.js', 18 | tsConfig: './tsconfig.json', 19 | experimentalFeatures: true, 20 | useTypescriptCompiler: true, 21 | // sourceMaps: true, 22 | plugins: [ 23 | CSSPlugin(), 24 | JSONPlugin(), 25 | EnvPlugin({ ENV }), 26 | WebIndexPlugin({ 27 | template: `./src/${name}/index.html`, 28 | }), 29 | ], 30 | }) 31 | 32 | }) 33 | 34 | Sparky.task('production', () => { 35 | DEV = 'production' 36 | }) 37 | 38 | Sparky.task('default', ['init'], () => { 39 | let example = fuse 40 | .bundle('Root') 41 | .instructions(`> src/${name}/index.ts`) 42 | 43 | if (name.includes('worker')) { 44 | console.log('Worker enabled') 45 | worker = fuse 46 | .bundle('worker') 47 | .instructions(`> src/${name}/worker.ts`) 48 | .watch('src/**/**.ts') 49 | } 50 | 51 | example.watch('src/**/**.ts').hmr({ 52 | socketURI: 'ws://localhost:3000', 53 | }) 54 | fuse.dev({ port: 3000 }) 55 | fuse.run() 56 | }) 57 | 58 | Sparky.task('simpleExample', () => { 59 | name = 'simpleExample' 60 | Sparky.start('default') 61 | }) 62 | 63 | Sparky.task('playground', () => { 64 | name = 'playground' 65 | Sparky.start('default') 66 | }) 67 | 68 | Sparky.task('featureExample', () => { 69 | name = 'featureExample' 70 | Sparky.start('default') 71 | }) 72 | 73 | Sparky.task('workerExample', () => { 74 | name = 'workerExample' 75 | Sparky.start('default') 76 | }) 77 | -------------------------------------------------------------------------------- /fuse-playground-aot.js: -------------------------------------------------------------------------------- 1 | const { 2 | FuseBox, 3 | EnvPlugin, 4 | Sparky, 5 | } = require('fuse-box') 6 | 7 | Sparky.task('default', () => { 8 | let fuse = FuseBox.init({ 9 | homeDir: '.', 10 | output: './src/playground/dist/$name.js', 11 | tsConfig : './tsconfig.json', 12 | experimentalFeatures: true, 13 | useTypescriptCompiler: true, 14 | sourceMaps: false, 15 | cache: false, 16 | plugins: [ 17 | EnvPlugin({ ENV: 'production' }), 18 | ], 19 | }) 20 | 21 | fuse.bundle('aot').instructions('> src/playground/aot.ts') 22 | 23 | fuse.bundle('app').instructions('> src/playground/index.ts') 24 | 25 | fuse.run() 26 | }) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fractal-core", 3 | "version": "6.0.6", 4 | "description": "A minimalist and well crafted app, content or component is our conviction", 5 | "main": "core/index.js", 6 | "typings": "core/index.d.ts", 7 | "keywords": [ 8 | "frontend", 9 | "architecture", 10 | "fractal", 11 | "functional-programming", 12 | "typescript", 13 | "craft" 14 | ], 15 | "files": [ 16 | "core/", 17 | "groups/", 18 | "interfaces/", 19 | "tasks/", 20 | "toHTML/", 21 | "typings/", 22 | "utils/", 23 | "AppViewer/", 24 | "LICENSE" 25 | ], 26 | "scripts": { 27 | "compile": "tsc", 28 | "test": "tsc && ava **/*.spec.js", 29 | "benchmarks": "ts-node benchmarks" 30 | }, 31 | "author": "Carlos Galarza ", 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/FractalBlocks/Fractal.git" 35 | }, 36 | "license": "MIT", 37 | "dependencies": { 38 | "@types/core-js": "^0.9.43", 39 | "@types/node": "^8.0.26", 40 | "deep-equal": "^1.0.1", 41 | "deepmerge": "^2.1.0", 42 | "fs-jetpack": "^1.2.0", 43 | "fuse-box": "^3.2.2", 44 | "lodash.escape": "^4.0.1", 45 | "lodash.forown": "^4.4.0", 46 | "lodash.kebabcase": "^4.1.1", 47 | "lodash.remove": "^4.7.0", 48 | "lodash.uniq": "^4.5.0", 49 | "object-assign": "^4.1.1", 50 | "parse-sel": "^1.0.0", 51 | "pullable-event-bus": "^0.0.3", 52 | "setimmediate": "^1.0.5", 53 | "snabbdom": "^0.7.0", 54 | "typestyle": "^1.5.1" 55 | }, 56 | "devDependencies": { 57 | "ava": "^0.25.0", 58 | "typescript": "^2.7.2", 59 | "uglify-es": "^3.3.9" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/AppViewer/AppViewer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Actions, 3 | Inputs, 4 | Interfaces, 5 | StyleGroup, 6 | getStyle, 7 | State, 8 | } from '../core' 9 | import { View, h } from '../interfaces/view' 10 | 11 | export const state: State = {} 12 | 13 | export type S = typeof state 14 | 15 | export const inputs: Inputs = (s, F) => ({ 16 | }) 17 | 18 | export const actions: Actions = { 19 | } 20 | 21 | const view: View = async (s, F) => { 22 | let style = getStyle(F) 23 | 24 | return h('div', { 25 | key: F.ctx.name, 26 | class: style('base'), 27 | }, [ 28 | 'App Viewer', 29 | ]) 30 | } 31 | 32 | export const interfaces: Interfaces = { view } 33 | 34 | const style: StyleGroup = { 35 | base: { 36 | width: '200px', 37 | height: '100%', 38 | overflow: 'auto', 39 | border: '2px solid grey', 40 | }, 41 | } 42 | 43 | export const groups = { style } 44 | -------------------------------------------------------------------------------- /src/AppViewer/index.ts: -------------------------------------------------------------------------------- 1 | import { run, Module } from '../core' 2 | import { guid } from '../utils/fun' 3 | import { styleHandler } from '../groups/style' 4 | import { viewHandler } from '../interfaces/view' 5 | 6 | import * as AppViewer from './AppViewer' 7 | 8 | export const attachAppViewer = async (app: Module): Promise => { 9 | if (typeof window === undefined) return 10 | const id = guid().replace(/-/g, '') 11 | const appViewerElm = document.createElement('div') 12 | appViewerElm.id = '_' + id 13 | document.body.appendChild(appViewerElm) 14 | return run({ 15 | Root: AppViewer, 16 | groups: { 17 | style: styleHandler(), 18 | }, 19 | interfaces: { 20 | view: viewHandler('#_' + id), 21 | }, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/core/common.ts: -------------------------------------------------------------------------------- 1 | import { Context, Actions, Action, performUpdate } from '.' 2 | 3 | // generic action input 4 | export const action = (ctx: Context, actions: Actions) => async ([arg1, arg2]: any): Promise => { 5 | let name 6 | let value 7 | if (arg1 instanceof Array) { 8 | name = arg1[0] 9 | value = arg1[1] 10 | if (arg2 !== undefined) { 11 | // add fetch value 12 | // TODO: test it!! 13 | value = (value !== undefined) ? [value, arg2]: arg2 14 | } 15 | } else { 16 | name = arg1 17 | value = arg2 18 | } 19 | if (ctx.global.record) { 20 | ctx.global.records.push({ id: ctx.id, actionName: name, value }) 21 | } 22 | let result = await performUpdate(ctx, await actions[name](value)) 23 | return result 24 | } 25 | 26 | // generic execute input 27 | export const SetAction: Action = (args): any => s => { 28 | if (args[0] instanceof Array) { 29 | // Multiple assignments 30 | for (let i = 0, arg; arg = args[i]; i++) { 31 | s[arg[0]] = arg[1] 32 | } 33 | } else { 34 | // Single assignment 35 | s[args[0]] = args[1] 36 | } 37 | return s 38 | } 39 | 40 | export const AddComp = (compFn): Action => (compArgs?: any): any => s => { 41 | let [name, component] = compFn(s._compCounter, compArgs) 42 | s._nest[name] = component 43 | s._compCounter++ 44 | s._compUpdated = true 45 | return s 46 | } 47 | 48 | export const _removeAction: Action = (name): any => s => { 49 | delete s._nest[name] 50 | s._compUpdated = true 51 | return s 52 | } 53 | -------------------------------------------------------------------------------- /src/core/component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Context, 4 | Group, 5 | Module, 6 | toIn, 7 | StyleGroup, 8 | mergeStyles, 9 | clone, 10 | State, 11 | } from '../core' 12 | 13 | // set of helpers for building components 14 | 15 | // send a message to an input of a component from outside a Module 16 | /* istanbul ignore next */ 17 | export async function sendMsg (mod: Module, id: string, inputName: string, msg?) { 18 | let ctx = mod.rootCtx 19 | await toIn(ctx.components[id])(inputName, msg) 20 | } 21 | 22 | export function setGroup (name: string, group: Group) { 23 | return function (comp: Component): Component { 24 | comp.groups[name] = group 25 | return comp 26 | } 27 | } 28 | 29 | export function spaceOf (ctx: Context): any { 30 | return ctx.components[ctx.id] 31 | } 32 | 33 | // make a new component from another merging her state 34 | export function props (state) { 35 | return function (comp: Component): Component { 36 | let newComp = Object.assign({}, comp) // shallow clone 37 | newComp.state = clone(Object.assign(comp.state, state)) 38 | return newComp 39 | } 40 | } 41 | 42 | export function styles (style: StyleGroup) { 43 | return function (comp: Component): Component{ 44 | let newComp = Object.assign({}, comp) // shallow clone 45 | newComp.groups.style = clone(mergeStyles(comp.groups.style, style)) 46 | return newComp 47 | } 48 | } 49 | 50 | export const compGroup = (groupName: string, arr: any[][], fn: any) => arr.reduce( 51 | (comps, c) => { 52 | comps[groupName + '_' + c[0]] = fn(c[1]) 53 | return comps 54 | } 55 | , {}) 56 | 57 | /** 58 | * Get the component descendants ids 59 | * @param ctx Any Context 60 | * @param id The component id 61 | */ 62 | export const getDescendantIds = (ctx: Context, id: string): string[] => { 63 | let searchStr = id + '$' 64 | return Object.keys(ctx.components).filter(compId => compId.includes(searchStr)) 65 | } 66 | 67 | /** 68 | * Get the context of the parent of a component 69 | * @param ctx The component context 70 | */ 71 | export const getParentCtx = (ctx: Context): Context => ctx.components[(ctx.id + '').split('$').slice(0, -1).join('$')] 72 | 73 | export interface CompOptions { 74 | state?: any 75 | style?: StyleGroup, 76 | } 77 | 78 | /** 79 | * Function for concateniting properties to a component, makes a copy fisrt 80 | * @param component The target component 81 | * @param options Options to concatenate 82 | */ 83 | export const comp = (component: Component, options: CompOptions): Component => { 84 | const newComp = clone(component) 85 | if (options.state) { 86 | newComp.state = Object.assign(newComp.state, options.state) 87 | } 88 | if (options.style) { 89 | newComp.groups.style = mergeStyles(newComp.groups.style, options.style) 90 | } 91 | return newComp 92 | } 93 | -------------------------------------------------------------------------------- /src/core/core.ts: -------------------------------------------------------------------------------- 1 | import { EventBus } from 'pullable-event-bus' 2 | import { HandlerMsg, HandlerObject } from './handler' 3 | import { InterfaceHelpers } from './interface' 4 | import { InputHelpers } from './input' 5 | import { Module } from './module' 6 | 7 | export interface Component { 8 | // the changing stuff (AKA variables) 9 | state?: S 10 | // Inputs are dispatchers of actions and tasks 11 | inputs?: Inputs 12 | // unique way to change the state 13 | actions?: Actions 14 | // a way to suscribe to external events and perform continous side effects (recalculated on every state change) 15 | interfaces: Interfaces 16 | // general purpose groups, commonly used for styles 17 | groups?: { 18 | [name: string]: Group 19 | } 20 | } 21 | 22 | export interface State { 23 | [prop: string]: any 24 | _nest?: Components 25 | _compNames?: string[] 26 | _compUpdated?: boolean 27 | } 28 | 29 | export interface Components { 30 | [name: string]: Component 31 | } 32 | 33 | export interface Interfaces { 34 | [name: string]: Interface 35 | } 36 | 37 | export type Group = any 38 | 39 | export interface Inputs { 40 | (s?: S, F?: InputHelpers): InputIndex 41 | } 42 | 43 | export interface InputIndex { 44 | onInit?: Input 45 | onDestroy?: Input 46 | onRouteActive?: Input 47 | onRouteInactive?: Input 48 | [name: string]: Input 49 | } 50 | 51 | export interface Input { 52 | (data?: any): void 53 | } 54 | 55 | export interface Action { 56 | (data?: any): Update | Promise> 57 | } 58 | 59 | export interface Actions { 60 | [name: string]: Action 61 | } 62 | 63 | export interface EventOptions { 64 | default?: boolean 65 | listenPrevented?: boolean 66 | selfPropagated?: boolean // only for global events 67 | } 68 | 69 | // is the data of an event, refers to some event of a component - Comunications stuff 70 | /* NOTE: function strings can be: 71 | - '*': which means, serialize all the event object 72 | - 'other': which means, serialize the 'other' attribute of the event object 73 | */ 74 | export interface InputData extends Array { 75 | 0: string // component identifier 76 | 1: string // input name 77 | 2?: any // context parameter 78 | 3?: any // a param function string / value is optional 79 | 4?: EventOptions 80 | } 81 | 82 | // event data comes from an interface / task handler as a result of processing InputData - Comunications stuff 83 | export interface EventData extends Array { 84 | 0: string // component index identifier 85 | 1: string // input name 86 | 2?: any // data 87 | } 88 | 89 | export interface Update { 90 | (state: S): Promise | void | S 91 | } 92 | 93 | export interface Interface { 94 | (state: S, F: InterfaceHelpers): Promise 95 | } 96 | 97 | export interface InterfaceIndex { 98 | [name: string]: Interface 99 | } 100 | 101 | // a task executes some kind of side effect (output) - Comunications stuff 102 | export interface Task extends Array { 103 | 0: string // task name 104 | 1?: HandlerMsg // task data 105 | } 106 | 107 | // describes an excecution context 108 | export interface Context { 109 | // name for that component in the index 110 | id: string 111 | // sintax sugar: the name is the last part of the id (e.g. the id is Main$child the name is child) 112 | name: string 113 | state: S 114 | inputs: InputIndex 115 | actions: Actions 116 | interfaces: InterfaceIndex 117 | interfaceHelpers: InterfaceHelpers 118 | interfaceValues: { // caches interfaces 119 | [name: string]: any 120 | } 121 | // groups of the component (related to a component space) 122 | groups: { 123 | [name: string]: Group, 124 | }, 125 | // global component index 126 | components: ContextIndex 127 | groupHandlers: { 128 | [name: string]: HandlerObject 129 | } 130 | taskHandlers: { 131 | [name: string]: HandlerObject 132 | } 133 | interfaceHandlers: { 134 | [name: string]: HandlerObject 135 | } 136 | // global flags delegation 137 | global: { 138 | // record all actions 139 | record: boolean 140 | records: ActionRecord[] 141 | log: boolean 142 | // flag for disabling rendering workflow, useful in SSR for performance 143 | render: boolean 144 | moduleRender: boolean 145 | hotSwap: boolean 146 | // root context delegation 147 | rootCtx: Context 148 | // active flag, this flag can stop input excecution (used in hot-swap) 149 | active: boolean 150 | }, 151 | eventBus: EventBus, 152 | // input hooks delegation 153 | beforeInput? (ctxIn: Context, inputName: string, data: any): void 154 | afterInput? (ctxIn: Context, inputName: string, data: any): void 155 | // error and warning delegation 156 | warn: { 157 | (source: string, description: string): void 158 | } 159 | error: { 160 | (source: string, description: string): void 161 | }, 162 | } 163 | 164 | export interface ActionRecord { 165 | id: string 166 | actionName: string 167 | value: any 168 | } 169 | 170 | export interface ContextIndex { 171 | [id: string]: Context 172 | } 173 | 174 | export interface RunModule { 175 | (root: Component, DEV: boolean, options?, viewCb?): Promise 176 | } 177 | -------------------------------------------------------------------------------- /src/core/eventBus.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { clone, _ } from '../core' 3 | import { createApp } from '../core/testUtils' 4 | 5 | // Event Bus tests 6 | 7 | export const ChildComp = { 8 | state: { result: '', count: 0 }, 9 | inputs: (s, F) => ({ 10 | inc: async () => { 11 | await F.toAct('Inc') 12 | let res = await F.emit('myEvent', s.count) 13 | await F.set('result', res) 14 | }, 15 | changed: async value => {}, 16 | }), 17 | actions: { 18 | Inc: () => s => { 19 | s.count++ 20 | }, 21 | }, 22 | interfaces: {}, 23 | } 24 | 25 | export const ReceptorComp = { 26 | state: {}, 27 | inputs: (s, F) => ({ 28 | onInit: async () => { 29 | F.on('myEvent', F.in('myEvent', _, '*'), true) 30 | }, 31 | myEvent: async count => { 32 | return count * 3 + 1 33 | }, 34 | }), 35 | actions: {}, 36 | interfaces: {}, 37 | } 38 | 39 | test('Event bus with pullable and normal subscribers', async t => { 40 | const app = await createApp({ 41 | state: { 42 | result: '', 43 | _nest: { 44 | Child: clone(ChildComp), 45 | R1: clone(ReceptorComp), 46 | R2: clone(ReceptorComp), 47 | R3: clone(ReceptorComp), 48 | }, 49 | }, 50 | inputs: (s, F) => ({ 51 | onInit: async () => { 52 | F.on('myEvent', F.in('myEvent', _, '*')) 53 | }, 54 | myEvent: async count => { 55 | await F.set('result', count) 56 | }, 57 | }), 58 | }) 59 | 60 | await app.moduleAPI.toComp('Root$Child', 'inc') 61 | t.is(app.rootCtx.components.Root.state.result, 1, 'Should propagate events to not pullable susbscribers') 62 | t.deepEqual(app.rootCtx.components.Root$Child.state.result, [4, 4, 4], 'Should pull results from subscribers before sending the event') 63 | }) 64 | 65 | test('Event bus from Module API', async t => { 66 | const app = await createApp({ 67 | state: { result: 0 }, 68 | inputs: (s, F) => ({ 69 | onInit: async () => { 70 | F.on('myEvent', F.in('myEvent', _, '*')) 71 | }, 72 | myEvent: async num => { 73 | await F.set('result', num + 1) 74 | }, 75 | }), 76 | }) 77 | 78 | await app.moduleAPI.emit('myEvent', 2) 79 | t.is(app.rootCtx.components.Root.state.result, 3, 'Should send the message') 80 | }) 81 | -------------------------------------------------------------------------------- /src/core/handler.ts: -------------------------------------------------------------------------------- 1 | import { ModuleAPI } from './module' 2 | 3 | // Not used at all only for code documentation 4 | export interface Handler { 5 | (...params): HandlerInterface | Promise 6 | } 7 | 8 | // interface function passed via ModuleDef 9 | export interface HandlerInterface { 10 | (mod: ModuleAPI): HandlerObject | Promise 11 | } 12 | 13 | export interface HandlerObject { 14 | state: any 15 | handle: HandlerFunction 16 | destroy: { 17 | (): void | Promise 18 | } 19 | } 20 | 21 | export interface HandlerFunction { 22 | (id: string, value: HandlerMsg): Promise 23 | } 24 | 25 | export type HandlerMsg = any 26 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | // core functions 2 | 3 | export * from './core' 4 | export * from './module' 5 | export * from './input' 6 | export * from './interface' 7 | export * from './handler' 8 | export * from './common' 9 | export * from './utils' 10 | export * from './style' 11 | export * from './component' 12 | -------------------------------------------------------------------------------- /src/core/input.ts: -------------------------------------------------------------------------------- 1 | import { Context, InterfaceHelpers, CtxPerformTask, EventData, dispatchEv, State, getDescendantIds } from '.' 2 | import { Emit, Off, Descriptor } from 'pullable-event-bus' 3 | import { _in, _act, _actFn, _inFn } from './interface' 4 | import { getPath } from '../utils/fun' 5 | import { 6 | toIn, 7 | CtxToIn, 8 | performTask, 9 | } from './module' 10 | 11 | export interface FractalOn { 12 | (evName: string, evData: EventData, pullable?: boolean): Descriptor 13 | } 14 | 15 | export interface InputHelpers extends InterfaceHelpers { 16 | state: S 17 | toIn: CtxToIn 18 | toChild: CtxToChild 19 | toChildAct: CtxToChildAct 20 | toAct: CtxToAct 21 | set: CtxSet 22 | task: CtxPerformTask 23 | emit: Emit 24 | on: FractalOn 25 | off: Off 26 | comps: CtxComponentHelpers 27 | _clearCache: CtxClearCache 28 | } 29 | 30 | export const makeInputHelpers = (ctx: Context): InputHelpers => ({ 31 | state: ctx.state, 32 | ctx, 33 | in: _in(ctx), 34 | act: _act(ctx), 35 | inFn: _inFn(ctx), 36 | actFn: _actFn(ctx), 37 | stateOf: _stateOf(ctx), 38 | toIn: toIn(ctx), 39 | toChild: toChild(ctx), 40 | toChildAct: toChildAct(ctx), 41 | toAct: toAct(ctx), 42 | set: set(ctx), 43 | task: performTask(ctx), 44 | emit: ctx.eventBus.emit, 45 | on: (evName, evData, pullable) => { 46 | const _dispatchEv = dispatchEv(ctx) 47 | return ctx.eventBus.on(evName, data => _dispatchEv(data, evData), pullable) 48 | }, 49 | off: ctx.eventBus.off, 50 | comps: _componentHelpers(ctx), 51 | _clearCache: _clearCache(ctx), 52 | }) 53 | 54 | export interface CtxStateOf { 55 | (name?: string): S 56 | } 57 | 58 | // extract state from a child component 59 | export const _stateOf = (ctx: Context): CtxStateOf => (name: string) => { 60 | let id = ctx.id + '$' + name 61 | let childCtx = ctx.components[id] 62 | if (childCtx) { 63 | return childCtx.state 64 | } else { 65 | ctx.error('stateOf', `there are no child '${name}' in context '${ctx.id}'`) 66 | } 67 | } 68 | 69 | // --- Message interchange between components 70 | 71 | export interface CtxToChild { 72 | (childCompName: string, inputName: string, msg?): void 73 | } 74 | 75 | // send a message to an input of a component from its parent 76 | export const toChild = (ctx: Context) => async ( 77 | childCompName, 78 | inputName, 79 | msg = undefined 80 | ) => { 81 | let childId = ctx.id + '$' + childCompName 82 | let compCtx = ctx.components[childId] 83 | if (compCtx) { 84 | return await toIn(compCtx)(inputName, msg) 85 | } else { 86 | ctx.error('toChild', `there are no child '${childCompName}' in space '${ctx.id}'`) 87 | } 88 | } 89 | 90 | export interface CtxToChildAct { 91 | (childCompName: string, actionName: string, msg?): void 92 | } 93 | 94 | // execute an action of a component from its parent 95 | export const toChildAct = (ctx: Context) => async ( 96 | childCompName, 97 | actionName, 98 | msg = undefined 99 | ) => { 100 | let childId = ctx.id + '$' + childCompName 101 | let compCtx = ctx.components[childId] 102 | if (compCtx) { 103 | return await toIn(compCtx)('_action', [actionName, msg]) 104 | } else { 105 | ctx.error('toChild', `there are no child '${childCompName}' in space '${ctx.id}'`) 106 | } 107 | } 108 | 109 | // --- 110 | 111 | export interface CtxToAct { 112 | (actionName: string, data?: any): Promise 113 | } 114 | 115 | // generic action caller 116 | export const toAct = (ctx: Context): CtxToAct => { 117 | let _toIn = toIn(ctx) 118 | return async (actionName, data) => 119 | await _toIn('_action', [actionName, data]) 120 | } 121 | 122 | export interface CtxSet { 123 | (arg0: any, arg1?: any): Promise 124 | } 125 | 126 | // Set Action caller (syntax sugar) 127 | export const set = (ctx: Context): CtxSet => { 128 | let _toIn = toIn(ctx) 129 | return async (arg0, arg1) => 130 | await _toIn('_action', ['Set', arg0 instanceof Array ? arg0 : [arg0, arg1]]) 131 | } 132 | 133 | export type SubscriptionInfo = Descriptor 134 | 135 | export interface CtxClearCache { 136 | (interfaceName: string, childNames?: string[]): void 137 | } 138 | 139 | /** 140 | * Clears the interface cache of a component and its descendants 141 | * @param ctx The component Context 142 | */ 143 | export const _clearCache = (ctx: Context): CtxClearCache => { 144 | return (interfaceName: string, childNames?: string[]) => { 145 | let descendantIds, childId 146 | if (childNames) { 147 | for (let i = 0, childName; childName = childNames[i]; i++) { 148 | childId = ctx.id + '$' + childName 149 | ctx.components[childId].interfaceValues[interfaceName] = undefined 150 | descendantIds = getDescendantIds(ctx, childId) 151 | for (let j = 0, descId; descId = descendantIds[j]; j++) { 152 | ctx.components[descId].interfaceValues[interfaceName] = undefined 153 | } 154 | } 155 | } else { 156 | ctx.components[ctx.id].interfaceValues[interfaceName] = undefined 157 | descendantIds = getDescendantIds(ctx, childId) 158 | for (let j = 0, descId; descId = descendantIds[j]; j++) { 159 | ctx.components[descId].interfaceValues[interfaceName] = undefined 160 | } 161 | } 162 | } 163 | } 164 | 165 | // --- Child component helpers: functions for traversing and broadcasting messages to child components 166 | 167 | export interface Instruction extends Array { 168 | 0: string // component name 169 | 1: string // input 170 | 2: any // data 171 | } 172 | 173 | export interface ComponentHelpers { 174 | getState (key: string, options?: { 175 | exceptions?: string[] 176 | nameFn? (name: string): string 177 | }): any 178 | getStates (options?: { 179 | path?: string[] 180 | exceptions?: string[] 181 | nameFn? (name: string): string 182 | }): any 183 | executeAll (insts: Instruction[]): void 184 | broadcast (inputName: string, data?: any): void 185 | optionalBroadcast (inputName: string, data?: any): void 186 | seqBroadcast (inputName: string, data?: any): Promise 187 | seqOptionalBroadcast (inputName: string, data?: any): Promise 188 | getNames (): string[] 189 | getCompleteNames (): string[] 190 | } 191 | 192 | export interface CtxComponentHelpers { 193 | (groupName: string): ComponentHelpers 194 | } 195 | 196 | export const getName = (name: string) => name.split('_')[1] 197 | 198 | export const getCompleteNames = (state: any, groupName: string) => 199 | Object.keys(state._nest) 200 | .filter(name => name.split('_')[0] === groupName) 201 | 202 | export const getNames = (state: any, groupName: string) => 203 | getCompleteNames(state, groupName) 204 | .map(n => n.split('_')[1]) 205 | 206 | export const _componentHelpers = (ctx: Context): CtxComponentHelpers => { 207 | let _toChild = toChild(ctx) 208 | let stateOf = _stateOf(ctx) 209 | return groupName => { 210 | let completeNames = Object.keys(ctx.components[ctx.id].state._nest) 211 | .filter(name => name.split('_')[0] === groupName) 212 | let componentNames = completeNames.map(n => n.split('_')[1]) 213 | return { 214 | getState (key: string, options): any { 215 | let obj = {} 216 | let name 217 | let exceptions = options && options.exceptions 218 | let nameFn = options && options.nameFn 219 | for (let i = 0, len = completeNames.length; i < len; i++) { 220 | if (exceptions && exceptions.indexOf(componentNames[i]) === -1 || !exceptions) { 221 | name = componentNames[i] 222 | name = nameFn ? nameFn(name) : name 223 | obj[name] = stateOf(completeNames[i])[key] 224 | } 225 | } 226 | return obj 227 | }, 228 | getStates (options): any { 229 | let obj = {} 230 | let name, state 231 | let exceptions = options && options.exceptions 232 | let path = options && options.path 233 | let nameFn = options && options.nameFn 234 | for (let i = 0, len = completeNames.length; i < len; i++) { 235 | if (exceptions && exceptions.indexOf(completeNames[i]) === -1 || !exceptions) { 236 | name = getName(completeNames[i]) 237 | name = nameFn ? nameFn(name) : name 238 | state = stateOf(completeNames[i]) 239 | obj[name] = path ? getPath(path, state) : state 240 | } 241 | } 242 | return obj 243 | }, 244 | executeAll (insts) { 245 | for (let i = 0, inst; inst = insts[i]; i++) { 246 | _toChild(groupName + '_' + inst[0], inst[1], inst[2]) 247 | } 248 | }, 249 | broadcast (inputName, data) { 250 | for (let i = 0, name; name = completeNames[i]; i++) { 251 | _toChild(name, inputName, data) 252 | } 253 | }, 254 | optionalBroadcast (inputName, data) { 255 | for (let i = 0, name; name = completeNames[i]; i++) { 256 | if (ctx.components[ctx.id + '$' + name].inputs[inputName]) { 257 | _toChild(name, inputName, data) 258 | } 259 | } 260 | }, 261 | async seqBroadcast (inputName, data) { 262 | for (let i = 0, name; name = completeNames[i]; i++) { 263 | await _toChild(name, inputName, data) 264 | } 265 | }, 266 | async seqOptionalBroadcast (inputName, data) { 267 | for (let i = 0, name; name = completeNames[i]; i++) { 268 | if (ctx.components[ctx.id + '$' + name].inputs[inputName]) { 269 | await _toChild(name, inputName, data) 270 | } 271 | } 272 | }, 273 | getNames() { 274 | return componentNames 275 | }, 276 | getCompleteNames() { 277 | return completeNames 278 | } 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/core/interface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | InputData, 4 | EventOptions, 5 | EventData, 6 | State, 7 | } from './core' 8 | import { 9 | HandlerMsg, 10 | } from './handler' 11 | import { toIn } from './module' 12 | import { _stateOf, _componentHelpers, CtxStateOf } from './input' 13 | 14 | export interface InterfaceHelpers { 15 | ctx: Context 16 | interfaceOf?: CtxInterfaceOf 17 | stateOf: CtxStateOf 18 | in: CtxIn 19 | act: CtxAct 20 | inFn: CtxInFn 21 | actFn: CtxActFn 22 | vw?: CtxVw 23 | vws?: CtxVws 24 | group?: CtxGroup 25 | } 26 | 27 | export const makeInterfaceHelpers = (ctx: Context): InterfaceHelpers => ({ 28 | ctx, 29 | interfaceOf: _interfaceOf(ctx), 30 | stateOf: _stateOf(ctx), 31 | in: _in(ctx), 32 | act: _act(ctx), 33 | inFn: _inFn(ctx), 34 | actFn: _actFn(ctx), 35 | vw: _vw(ctx), 36 | vws: _vws(ctx), 37 | group: _group(ctx), 38 | }) 39 | 40 | export interface CtxInterfaceOf { 41 | (name: string, interfaceName: string): Promise 42 | } 43 | 44 | // gets an interface message from a certain component 45 | export const _interfaceOf = (ctx: Context) => async (name: string, interfaceName) => { 46 | let id = `${ctx.id}$${name}` 47 | let compCtx = ctx.components[id] 48 | if (!compCtx) { 49 | ctx.error('interfaceOf', `there are no component space '${id}'`) 50 | return {} 51 | } 52 | if (!compCtx.interfaces[interfaceName]) { 53 | ctx.error( 54 | 'interfaceOf', 55 | `there are no interface '${interfaceName}' in component '${compCtx.name}' from space '${id}'` 56 | ) 57 | return {} 58 | } 59 | // search in interface cache 60 | let cache = compCtx.interfaceValues[interfaceName] 61 | if (cache) { 62 | return cache 63 | } else { 64 | // caches interface 65 | compCtx.interfaceValues[interfaceName] 66 | = await compCtx.interfaces[interfaceName](compCtx.state, compCtx.interfaceHelpers) 67 | return compCtx.interfaceValues[interfaceName] 68 | } 69 | } 70 | 71 | export interface CtxIn { 72 | (inputName: string, context?: any, param?: any, options?: EventOptions): InputData 73 | } 74 | 75 | // create an InputData array 76 | export const _in = (ctx: Context): CtxIn => (inputName, context, param, options) => { 77 | return [ctx.id, inputName, context, param, options] 78 | } 79 | 80 | export interface CtxAct { 81 | (actionName: string, context?: any, param?: any, options?: EventOptions): InputData 82 | } 83 | 84 | // generic action dispatcher 85 | export const _act = (ctx: Context): CtxAct => { 86 | let _inCtx = _in(ctx) 87 | return (actionName, context, param, options): InputData => 88 | _inCtx('_action', [actionName, context], param, options) 89 | } 90 | 91 | 92 | export interface CtxInFn { 93 | (inputName: string, context?: any, param?: any, options?: EventOptions): void 94 | } 95 | 96 | // create an InputData array 97 | export const _inFn = (ctx: Context): CtxInFn => { 98 | const dispatchCtx = dispatch(ctx) 99 | return (inputName, context, param, options) => { 100 | return (event: Event) => invokeHandler( 101 | ctx.error, dispatchCtx, [ctx.id, inputName, context, param, options], event 102 | ) 103 | } 104 | } 105 | 106 | export interface CtxActFn { 107 | (actionName: string, context?: any, param?: any, options?: EventOptions): void 108 | } 109 | 110 | // generic action dispatcher 111 | export const _actFn = (ctx: Context): CtxActFn => { 112 | let _inFnCtx = _inFn(ctx) 113 | return (actionName, context, param, options): void => 114 | _inFnCtx('_action', [actionName, context], param, options) 115 | } 116 | 117 | export interface CtxVw { 118 | (componentName: string): Promise 119 | } 120 | 121 | // extract component view interface, sintax sugar 122 | export const _vw = (ctx: Context): CtxVw => { 123 | let _interfaceOfCtx = _interfaceOf(ctx) 124 | return async componentName => await _interfaceOfCtx(componentName, 'view') 125 | } 126 | 127 | export interface CtxVws { 128 | (names: string[]): Promise 129 | } 130 | 131 | // extract view interfaces based on component names 132 | export const _vws = (ctx: Context): CtxVws => { 133 | let _interfaceOfCtx = _interfaceOf(ctx) 134 | return async names => { 135 | let views = [] 136 | for (let i = 0, len = names.length; i < len; i++) { 137 | views.push(await _interfaceOfCtx(names[i], 'view')) 138 | } 139 | return views 140 | } 141 | } 142 | 143 | export interface CtxGroup { 144 | (groupName: string): Promise 145 | } 146 | 147 | // extract view interfaces from a component group 148 | export const _group = (ctx: Context): CtxGroup => { 149 | let _interfaceOfCtx = _interfaceOf(ctx) 150 | let comps = _componentHelpers(ctx) 151 | return async groupName => { 152 | let views = [] 153 | let componentNames = comps(groupName).getCompleteNames() 154 | for (let i = 0, len = componentNames.length; i < len; i++) { 155 | views.push(await _interfaceOfCtx(componentNames[i], 'view')) 156 | } 157 | return views 158 | } 159 | } 160 | 161 | /** 162 | * Extract a path or some paths from an Event Object 163 | * @param path An array that contains an object path 164 | * @param event An Event Object 165 | */ 166 | function computePath (path: any[], event) { 167 | let data 168 | let actual = event 169 | for (let i = 0, len = path.length; i < len; i++) { 170 | if (path[i] instanceof Array) { 171 | data = {} 172 | let keys = path[i] 173 | for (let i = 0, len = keys.length; i < len; i++) { 174 | data[keys[i]] = actual[keys[i]] 175 | } 176 | } else { 177 | actual = actual[path[i]] 178 | } 179 | } 180 | if (!data) { 181 | data = actual 182 | } 183 | return data 184 | } 185 | 186 | export function computeEvent(eventData: any, iData: InputData): EventData { 187 | let data 188 | let haveContext = iData[2] !== undefined 189 | let haveParam = iData[3] !== undefined 190 | 191 | if (iData[3] === '*') { 192 | // serialize the whole object (note that DOM events are not serializable, use paths instead) 193 | data = JSON.parse(JSON.stringify(eventData)) 194 | } else if (iData[3] !== undefined) { 195 | // have fetch parameter 196 | if (iData[3] instanceof Array) { 197 | // fetch parameter is a path, e.g. ['target', 'value'] 198 | let param = iData[3] 199 | if (param[1] && param[1] instanceof Array) { 200 | data = [] 201 | for (let i = 0, len = param.length; i < len; i++) { 202 | data[i] = computePath(param[i], eventData) 203 | } 204 | } else { 205 | // only one path 206 | data = computePath(param, eventData) 207 | } 208 | } else { 209 | // fetch parameter is only a getter, e.g. 'target' 210 | data = eventData[iData[3]] 211 | } 212 | } 213 | if (!haveContext && !haveParam) { 214 | return [iData[0], iData[1]] // dispatch an input with no arguments 215 | } 216 | return [ 217 | iData[0], // component id 218 | iData[1], // component input name 219 | haveContext && haveParam 220 | ? [iData[2], data] 221 | : haveParam 222 | ? data 223 | : iData[2] 224 | ] 225 | } 226 | 227 | export const dispatchEv = (ctx: Context) => async (event: any, iData: InputData) => { 228 | let compCtx = ctx.components[iData[0] + ''] 229 | if (!compCtx) { 230 | ctx.error('Dispatch Event (dispatchEv)', `There is no component with id: ${iData[0]}`) 231 | return 232 | } 233 | let cInputData = computeEvent(event, iData) 234 | return await toIn(compCtx)(cInputData[1], cInputData[2]) 235 | } 236 | 237 | export interface DispatchCtx { 238 | (eventData: EventData): Promise 239 | } 240 | 241 | export const dispatch = (ctx: Context): DispatchCtx => async (eventData: EventData) => { 242 | let compCtx = ctx.components[eventData[0] + ''] 243 | if (!compCtx) { 244 | ctx.error('Dispatch EventData (dispatch)', `There is no component with id: ${eventData[0]}`) 245 | return 246 | } 247 | return await toIn(compCtx)(eventData[1], eventData[2]) 248 | } 249 | 250 | /** 251 | * Executes an input of aa certain component, passing to some data to him 252 | */ 253 | export interface CtxToComp { 254 | (id: string, inputName: string, data?: any): any 255 | } 256 | 257 | /** 258 | * toComp function factory 259 | * @param ctx 260 | * @returns CtxToComp 261 | */ 262 | export const toComp = (ctx: Context): CtxToComp => async (id: string, inputName: string, data?: any) => { 263 | let compCtx = ctx.components[id] 264 | if (!compCtx) { 265 | ctx.error('Execute component input (toComp)', `There is no component with id: ${id}`) 266 | return 267 | } 268 | return await toIn(compCtx)(inputName, data) 269 | } 270 | 271 | export const invokeHandler = (error, dispatchCtx: DispatchCtx, handler: InputData | InputData[] | 'ignore', event: Event) => { 272 | if (handler instanceof Array && typeof handler[0] === 'string') { 273 | let options = handler[4] 274 | if ((options && options.listenPrevented !== true || !options) && event.defaultPrevented) { 275 | return 276 | } 277 | if (options && options.default === false) { 278 | event.preventDefault() 279 | } 280 | setImmediate(() => { 281 | dispatchCtx(computeEvent(event, handler)) 282 | }) 283 | } else if (handler instanceof Array) { 284 | // call multiple handlers 285 | for (var i = 0; i < handler.length; i++) { 286 | invokeHandler(error, dispatchCtx, handler[i], event) 287 | } 288 | } else if (handler === 'ignore') { 289 | // this handler is ignored 290 | event.preventDefault() 291 | } else if (handler === '' && handler === undefined) { 292 | // this handler is passed 293 | return 294 | } else { 295 | error('Interface helpers - invokeHandler', 'event handler of type ' + typeof handler + 'are not allowed, data: ' + JSON.stringify(handler)) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/core/module.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { clone } from '.' 3 | import { createApp, ChildComp } from './testUtils' 4 | 5 | // Propagation tests 6 | 7 | test('Propagation: Individual', async t => { 8 | const app = await createApp({ 9 | state: { 10 | _nest: { 11 | Child: clone(ChildComp), 12 | }, 13 | }, 14 | inputs: (s, F) => ({ 15 | $Child_changed: async r => await F.set('result', r), // Individual propagation 16 | }) 17 | }) 18 | await app.moduleAPI.toComp('Root$Child', 'inc') 19 | t.deepEqual(app.rootCtx.components.Root.state.result, 1, 'Expect the input value') 20 | }) 21 | 22 | test('Propagation: Groupal', async t => { 23 | const app = await createApp({ 24 | state: { 25 | _nest: { 26 | A_1: clone(ChildComp), 27 | A_2: clone(ChildComp), 28 | A_3: clone(ChildComp), 29 | }, 30 | }, 31 | inputs: (s, F) => ({ 32 | $A_changed: async r => await F.set('result', r), 33 | }) 34 | }) 35 | await app.moduleAPI.toComp('Root$A_2', 'inc') 36 | t.deepEqual(app.rootCtx.components.Root.state.result, ['2', 1], 'Expect the component scoped name and the value') 37 | }) 38 | 39 | test('Propagation: Global', async t => { 40 | const app = await createApp({ 41 | state: { 42 | _nest: { 43 | A_1: clone(ChildComp), 44 | B: clone(ChildComp), 45 | A_2: clone(ChildComp), 46 | C: clone(ChildComp), 47 | }, 48 | }, 49 | inputs: (s, F) => ({ 50 | $_changed: async r => await F.set('result', r), 51 | }) 52 | }) 53 | await app.moduleAPI.toComp('Root$A_1', 'inc') 54 | t.deepEqual(app.rootCtx.components.Root.state.result, ['A_1', 1], 'Expect the component name and the value') 55 | }) 56 | 57 | test('Lifecycle Hooks', async t => { 58 | 59 | const compSeq = [] 60 | const moduleSeq = [] 61 | 62 | const mod = await createApp({ 63 | state: { sequence: [] }, 64 | inputs: (s, F) => ({ 65 | onInit: async () => { 66 | compSeq.push('onInit') 67 | }, 68 | onDestroy: async () => { 69 | compSeq.push('onDestroy') 70 | }, 71 | }), 72 | }, { 73 | onBeforeInit: () => { 74 | moduleSeq.push('onBeforeInit') 75 | }, 76 | onInit: () => { 77 | moduleSeq.push('onInit') 78 | }, 79 | onBeforeDestroy: () => { 80 | moduleSeq.push('onBeforeDestroy') 81 | }, 82 | onDestroy: () => { 83 | moduleSeq.push('onDestroy') 84 | }, 85 | }) 86 | 87 | await mod.moduleAPI.destroy() 88 | 89 | t.deepEqual( 90 | compSeq, 91 | ['onInit', 'onDestroy'], 92 | 'Component Lifecycle' 93 | ) 94 | 95 | t.deepEqual( 96 | moduleSeq, 97 | ['onBeforeInit', 'onInit', 'onBeforeDestroy', 'onDestroy'], 98 | 'Module Lifecycle' 99 | ) 100 | 101 | }) 102 | -------------------------------------------------------------------------------- /src/core/module.ts: -------------------------------------------------------------------------------- 1 | require('setimmediate') // Polyfill setImmediate 2 | import { 3 | Component, 4 | Update, 5 | Context, 6 | Components, 7 | InputData, 8 | action, 9 | SetAction, 10 | _removeAction, 11 | clone, 12 | HandlerInterface, 13 | HandlerObject, 14 | makeInterfaceHelpers, 15 | dispatchEv, 16 | toComp, 17 | makeInputHelpers, 18 | State, 19 | dispatch, 20 | EventData, 21 | } from '.' 22 | import { 23 | makeEventBus, 24 | Off, 25 | Emit, 26 | On, 27 | } from 'pullable-event-bus' 28 | 29 | export interface ModuleDef { 30 | Root: Component 31 | record?: boolean 32 | log?: boolean 33 | render?: boolean // initial render flag 34 | active?: boolean 35 | // Handlers 36 | groups?: HandlerInterfaceIndex 37 | tasks?: HandlerInterfaceIndex 38 | interfaces: HandlerInterfaceIndex 39 | interfaceOrder?: Array 40 | // Lifecycle hooks for modules 41 | onBeforeInit? (mod: ModuleAPI): Promise 42 | onInit? (mod: ModuleAPI): Promise 43 | onBeforeDestroy? (mod: ModuleAPI): Promise 44 | onDestroy? (mod: ModuleAPI): Promise 45 | // Hooks for inputs 46 | beforeInput? (ctxIn: Context, inputName: string, data: any): void 47 | afterInput? (ctxIn: Context, inputName: string, data: any): void 48 | // Callbacks for log messages 49 | warn? (source: string, description: string): Promise 50 | error? (source: string, description: string): Promise 51 | } 52 | 53 | // a gap is defined with undefined (optional) 54 | export const _ = undefined 55 | 56 | export interface HandlerInterfaceIndex { 57 | [name: string]: HandlerInterface | Promise 58 | } 59 | 60 | export interface HandlerObjectIndex { 61 | [name: string]: HandlerObject 62 | } 63 | 64 | export interface Module { 65 | moduleDef: ModuleDef 66 | isDisposed: boolean 67 | // API to module 68 | moduleAPI: ModuleAPI 69 | // Root component context 70 | rootCtx: Context 71 | } 72 | 73 | // API from module to handlers 74 | export interface ModuleAPI { 75 | on: On, 76 | off: Off, 77 | emit: Emit, 78 | dispatchEv (event: any, iData: InputData): Promise 79 | dispatch (eventData: EventData): Promise 80 | toComp (id: string, inputName: string, data?: any): Promise 81 | destroy (): void 82 | attach (comp: Component, app?: Module, middleFn?: MiddleFn): Promise 83 | setGroup (id: string, name: string, space: any): void 84 | task: CtxPerformTask 85 | warn (source, description): void 86 | error (source, description): void 87 | } 88 | 89 | // MiddleFn is used for merge the states on hot-swaping (reattach) 90 | export interface MiddleFn { 91 | ( 92 | ctx: Context, 93 | app: Module 94 | ): void 95 | } 96 | 97 | export const handlerTypes = ['interface', 'task', 'group'] 98 | 99 | export interface CtxNest { 100 | (name: string, component: Component, isStatic?: boolean): void 101 | } 102 | 103 | async function _nest (ctx: Context, name: string, component: Component): Promise> { 104 | if (!component) { 105 | ctx.error('_nest', `Error when trying to create a component named ${name} in component ${ctx.id}`) 106 | } 107 | // namespaced name if is a child 108 | let id = ctx.id === 'Root' && name === 'Root' ? 'Root' : ctx.id + '$' + name 109 | // state default 110 | component.state = component.state || {} 111 | component.state._nest = component.state._nest || {} 112 | component.state._compCounter = 0 113 | // Component state 114 | const state = clone(component.state) 115 | // Component context 116 | let childCtx: Context = { 117 | id, // the component id 118 | name, 119 | groups: {}, 120 | // delegation 121 | eventBus: ctx.eventBus, 122 | global: ctx.global, 123 | components: ctx.components, 124 | groupHandlers: ctx.groupHandlers, 125 | interfaceHandlers: ctx.interfaceHandlers, 126 | taskHandlers: ctx.taskHandlers, 127 | beforeInput: ctx.beforeInput, 128 | afterInput: ctx.afterInput, 129 | warn: ctx.warn, 130 | error: ctx.error, 131 | // Component related context 132 | state, 133 | inputs: {}, // input helpers needs to be initialized after ComponentSpace, because references 134 | actions: component.actions, 135 | interfaces: component.interfaces, 136 | interfaceHelpers: {}, 137 | interfaceValues: {}, 138 | } 139 | // Create interface helpers 140 | childCtx.interfaceHelpers = makeInterfaceHelpers(childCtx) 141 | 142 | ctx.components[id] = childCtx 143 | 144 | if (component.inputs) { 145 | childCtx.inputs = component.inputs( 146 | childCtx.state, 147 | makeInputHelpers(childCtx), 148 | ) 149 | } else { 150 | childCtx.inputs = {} 151 | } 152 | if (component.actions) { // reserved inputs: _action and _return 153 | if (!childCtx.inputs._action) { 154 | // action helper enabled by default 155 | childCtx.inputs._action = action(childCtx, component.actions) 156 | } 157 | if (!childCtx.actions.Set) { 158 | childCtx.actions.Set = SetAction 159 | } 160 | if (!childCtx.actions._remove) { 161 | childCtx.actions._remove = _removeAction 162 | } 163 | } 164 | 165 | // composition 166 | if (Object.keys(childCtx.state._nest).length > 0) { 167 | let components = childCtx.state._nest 168 | for (name in components) { 169 | await _nest(childCtx, name, components[name]) 170 | } 171 | } 172 | childCtx.state._compNames = Object.keys(childCtx.state._nest) 173 | 174 | if (component.groups) { 175 | // Groups are handled automatically only when comoponent are initialized 176 | await handleGroups(childCtx, component) 177 | } 178 | 179 | if (childCtx.inputs.onInit && !childCtx.global.hotSwap) { 180 | // component lifecycle hook: onInit 181 | await childCtx.inputs.onInit() 182 | } 183 | 184 | return childCtx 185 | } 186 | 187 | async function handleGroups (ctx: Context, component: Component) { 188 | let space: HandlerObject 189 | let name 190 | for (name in component.groups) { 191 | space = ctx.groupHandlers[name] 192 | if (space) { 193 | await space.handle(ctx.id, component.groups[name]) 194 | } else { 195 | ctx.error( 196 | 'nest', 197 | `module has no group handler for '${name}' of component '${ctx.name}' from space '${ctx.id}'` 198 | ) 199 | } 200 | } 201 | } 202 | 203 | export interface CtxNestAll { 204 | (components: Components, isStatic?: boolean): Promise 205 | } 206 | 207 | // add many components to the component index 208 | export const nestAll = (ctx: Context): CtxNestAll => async (components, isStatic = false) => { 209 | let name 210 | for (name in components) { 211 | await _nest(ctx, name, components[name]) 212 | } 213 | } 214 | 215 | export interface CtxUnnest { 216 | (name?: string): Promise 217 | } 218 | 219 | // remove a component to the component index, if name is not defined destroy the root 220 | export const unnest = (ctx: Context): CtxUnnest => async name => { 221 | let id = name !== undefined ? ctx.id + '$' + name : ctx.id 222 | let componentSpace = ctx.components[id] 223 | if (!componentSpace) { 224 | return ctx.error('unnest', `there is no component with name '${name}' at component '${ctx.id}'`) 225 | } 226 | // decomposition 227 | let components = componentSpace.components 228 | if (components) { 229 | await unnestAll(componentSpace)(Object.keys(componentSpace.state._nest)) 230 | } 231 | // component lifecycle hook: onDestroy 232 | if (ctx.inputs.onDestroy && !ctx.global.hotSwap) { 233 | await ctx.inputs.onDestroy() 234 | } 235 | delete ctx.components[id] 236 | } 237 | 238 | export interface CtxUnnestAll { 239 | (components: string[]): Promise 240 | } 241 | 242 | // add many components to the component index 243 | export const unnestAll = (ctx: Context): CtxUnnestAll => async components => { 244 | let _unnest = unnest(ctx) 245 | for (let i = 0, len = components.length; i < len; i++) { 246 | await _unnest(components[i]) 247 | } 248 | } 249 | 250 | export async function propagate (ctx: Context, inputName: string, data: any) { 251 | // notifies parent if name starts with $ 252 | let id = ctx.id 253 | let idParts = (id + '').split('$') 254 | let componentSpace = ctx.components[id] 255 | if (idParts.length > 1) { 256 | // is not root? 257 | let parentId = idParts.slice(0, -1).join('$') 258 | let parentSpace = ctx.components[parentId] 259 | let parentInputName 260 | let nameParts = componentSpace.name.split('_') 261 | /** Component Input Listeners 262 | * - Individual: $CompName_inputName or $GroupName_compName_inputName -> data 263 | * - Groupal: $GroupName_inputName -> [name, data] 264 | * - Global: $_inputName -> [name, data] 265 | */ 266 | // Individual 267 | parentInputName = `$${componentSpace.name}_${inputName}` 268 | if (parentSpace.inputs[parentInputName]) { 269 | await toIn(parentSpace)(parentInputName, data) 270 | } 271 | // Groupal 272 | if (nameParts.length === 2) { 273 | parentInputName = `$${nameParts[0]}_${inputName}` 274 | if (parentSpace.inputs[parentInputName]) { 275 | await toIn(parentSpace)(parentInputName, [nameParts[1], data]) 276 | } 277 | } 278 | // Global 279 | parentInputName = `$_${inputName}` 280 | if (parentSpace.inputs[parentInputName]) { 281 | await toIn(parentSpace)(parentInputName, [componentSpace.name, data]) 282 | } 283 | } 284 | } 285 | 286 | export interface CtxToIn { 287 | (inputName: string, data?): Promise 288 | } 289 | 290 | // send a message to an input of a component from itself 291 | export const toIn = (ctx: Context): CtxToIn => { 292 | let id = ctx.id 293 | let componentSpace = ctx.components[id] 294 | return async (inputName, data) => { 295 | if (!ctx.global.active) { 296 | return 297 | } 298 | let input = componentSpace.inputs[inputName] 299 | if (input === undefined) { 300 | ctx.error( 301 | 'execute', 302 | `there are no input named '${inputName}' in component '${componentSpace.name}' from space '${id}'` 303 | ) 304 | return 305 | } 306 | if (ctx.beforeInput) ctx.beforeInput(ctx, inputName, data) 307 | let result = await input(data) 308 | if (ctx.afterInput) ctx.afterInput(ctx, inputName, data) 309 | await propagate(ctx, inputName, data) 310 | return result 311 | } 312 | } 313 | 314 | export async function performUpdate (compCtx: Context, update: Update): Promise { 315 | const state = compCtx.state 316 | const stateUpdates = await update(state) 317 | if (stateUpdates) { 318 | let key 319 | for (key in stateUpdates) { 320 | state[key] = stateUpdates[key] 321 | } 322 | } 323 | if (state._compUpdated) { 324 | compCtx.global.render = false 325 | let compNames = state._compNames 326 | let newCompNames = Object.keys(state._nest) 327 | let newNames = newCompNames.filter(n => compNames.indexOf(n) < 0) 328 | let removeNames = compNames.filter(n => newCompNames.indexOf(n) < 0) 329 | for (let i = 0, len = newNames.length; i < len; i++) { 330 | await _nest(compCtx, newNames[i], state._nest[newNames[i]]) 331 | } 332 | for (let i = 0, len = removeNames.length; i < len; i++) { 333 | await unnest(compCtx)(removeNames[i]) 334 | } 335 | state._compUpdated = false 336 | state._compNames = newCompNames 337 | compCtx.global.render = true 338 | } 339 | if (compCtx.global.moduleRender && compCtx.global.render) { 340 | calcAndNotifyInterfaces(compCtx) // root context 341 | } else { 342 | compCtx.interfaceValues = {} 343 | } 344 | } 345 | 346 | export interface CtxPerformTask { 347 | (name: string, data?: any): Promise 348 | } 349 | 350 | export function performTask (ctx: Context): CtxPerformTask { 351 | return (name, data) => { 352 | if (!ctx.taskHandlers[name]) { 353 | ctx.error( 354 | 'execute', 355 | `there are no task handler for '${name}' in component '${ctx.name}' from space '${ctx.id}'` 356 | ) 357 | return 358 | } 359 | return ctx.taskHandlers[name].handle(ctx.id, data) 360 | } 361 | } 362 | 363 | export function calcAndNotifyInterfaces (ctx: Context) { 364 | // calc and caches interfaces 365 | let space = ctx.components[ctx.id] 366 | let idParts = (ctx.id + '').split('$') 367 | for (let name in space.interfaces) { 368 | setImmediate(async () => { 369 | // remove cache of parent component spaces 370 | let parts = idParts.slice(0) 371 | for (let i = parts.length - 1; i >=0 ; i--) { 372 | ctx.components[parts.join('$')].interfaceValues[name] = undefined 373 | parts.pop() 374 | } 375 | // permorms interface recalculation 376 | let rootSpace = ctx.components.Root 377 | for (let name in rootSpace.interfaces) { 378 | if (ctx.interfaceHandlers[name]) { 379 | ctx.interfaceHandlers[name].handle('Root', await rootSpace.interfaces[name](rootSpace.state, rootSpace.interfaceHelpers)) 380 | } else { 381 | // This only can happen when this method is called for a context that is not the root 382 | ctx.error('notifyInterfaceHandlers', `module does not have interface handler named '${name}' for component '${space.name}' from space '${ctx.id}'`) 383 | } 384 | } 385 | }) 386 | } 387 | } 388 | 389 | // function for running a root component 390 | export async function run (moduleDef: ModuleDef): Promise { 391 | // root component 392 | let component: Component 393 | let moduleAPI: ModuleAPI 394 | // root context 395 | let ctx: Context 396 | 397 | // Prevents cross references 398 | moduleDef = clone(moduleDef) 399 | 400 | // Add event bus as default `ev` task handler 401 | moduleDef.tasks = moduleDef.tasks ? moduleDef.tasks : {} 402 | 403 | // attach root component 404 | async function attach (comp: Component, app?: Module, middleFn?: MiddleFn): Promise { 405 | // root component, take account of hot swapping 406 | component = comp ? comp : moduleDef.Root 407 | // if is hot swapping, do not recalculate context 408 | // bootstrap context (level 0 in hierarchy) 409 | if (!middleFn) { 410 | ctx = { // because of rootCtx delegation 411 | id: 'Root', 412 | name: 'Root', 413 | groups: {}, 414 | global: { 415 | record: moduleDef.hasOwnProperty('record') ? moduleDef.record : false, 416 | records: [], 417 | log: moduleDef.hasOwnProperty('log') ? moduleDef.log : false, 418 | moduleRender: moduleDef.hasOwnProperty('render') ? moduleDef.render : true, 419 | render: true, 420 | active: moduleDef.hasOwnProperty('active') ? moduleDef.active : true, 421 | }, 422 | eventBus: makeEventBus(), 423 | // component index 424 | components: {}, 425 | groupHandlers: {}, 426 | taskHandlers: {}, 427 | interfaces: {}, 428 | interfaceHandlers: {}, 429 | inputs: {}, 430 | // error and warning handling 431 | beforeInput: moduleDef.beforeInput ? moduleDef.beforeInput : _, 432 | afterInput: moduleDef.afterInput || _, 433 | warn: moduleDef.warn || _, 434 | error: moduleDef.error || _, 435 | } 436 | // API for modules 437 | moduleAPI = { 438 | on: ctx.eventBus.on, 439 | off: ctx.eventBus.off, 440 | emit: ctx.eventBus.emit, 441 | // dispatch function type used for handlers 442 | dispatchEv: dispatchEv(ctx), 443 | dispatch: dispatch(ctx), 444 | toComp: toComp(ctx), 445 | destroy, 446 | // set a space of a certain component 447 | setGroup: (id, name, space) => { 448 | ctx.components[id].groups[name] = space 449 | }, 450 | attach, 451 | task: performTask(ctx), 452 | // delegated methods 453 | warn: ctx.warn, 454 | error: ctx.error, 455 | } 456 | 457 | // module lifecycle hook: onBeforeInit 458 | if (moduleDef.onBeforeInit && !middleFn) { 459 | await moduleDef.onBeforeInit(moduleAPI) 460 | } 461 | 462 | } 463 | 464 | // if is not hot swapping 465 | if (!middleFn) { 466 | // pass ModuleAPI to every Interface, Task and Space HandlerFunction 467 | let handlers: HandlerInterfaceIndex 468 | for (let c = 0, len = handlerTypes.length; c < len; c++) { 469 | handlers = moduleDef[handlerTypes[c] + 's'] 470 | if (handlers) { 471 | let name 472 | for (name in handlers) { 473 | ctx[handlerTypes[c] + 'Handlers'][name] = await (await handlers[name])(moduleAPI) 474 | } 475 | } 476 | } 477 | } 478 | 479 | if (middleFn) { 480 | ctx.global.hotSwap = true 481 | } 482 | 483 | let lastModuleRender = ctx.global.moduleRender 484 | ctx.global.moduleRender = false 485 | // Root component 486 | let root = await _nest(ctx, 'Root', component) 487 | ctx.global.moduleRender = lastModuleRender 488 | // Root context (level 1) 489 | ctx.global.rootCtx = root 490 | // middle function for hot-swapping 491 | if (middleFn) { 492 | await middleFn(ctx.global.rootCtx, app) 493 | } 494 | 495 | // pass initial value to each Interface Handler 496 | // -- interfaceOrder 497 | let interfaceOrder = moduleDef.interfaceOrder 498 | let name 499 | let errorNotHandler = name => ctx.error( 500 | 'InterfaceHandlers', 501 | `'$.Root' component has no interface called '${name}', missing interface handler` 502 | ) 503 | let rootCtx = ctx.global.rootCtx 504 | if (interfaceOrder) { 505 | for (let i = 0; name = interfaceOrder[i]; i++) { 506 | if (ctx.interfaceHandlers[name]) { 507 | ctx.interfaceHandlers[name].handle( 508 | 'Root', 509 | await rootCtx.interfaces[name](ctx.components.Root.state, ctx.components.Root.interfaceHelpers) 510 | ) 511 | } else { 512 | return errorNotHandler(name) 513 | } 514 | } 515 | } 516 | for (name in rootCtx.interfaces) { 517 | if (interfaceOrder && interfaceOrder.indexOf(name) !== -1) { 518 | continue // interface evaluated yet 519 | } 520 | if (ctx.interfaceHandlers[name]) { 521 | ctx.interfaceHandlers[name].handle( 522 | 'Root', 523 | await rootCtx.interfaces[name](ctx.components.Root.state, ctx.components.Root.interfaceHelpers) 524 | ) 525 | } else { 526 | return errorNotHandler(name) 527 | } 528 | } 529 | 530 | // module lifecycle hook: onInit 531 | if (moduleDef.onInit && !middleFn) { 532 | await moduleDef.onInit(moduleAPI) 533 | } 534 | 535 | return { 536 | moduleDef, 537 | // reattach root component, used for hot swapping 538 | isDisposed: false, 539 | // root context 540 | moduleAPI, 541 | rootCtx: ctx.global.rootCtx, 542 | } 543 | 544 | } 545 | 546 | async function destroy () { 547 | if (moduleDef.onBeforeDestroy) { 548 | await moduleDef.onBeforeDestroy(moduleAPI) 549 | } 550 | // destroy all handlers 551 | let handlers: HandlerObjectIndex 552 | for (let c = 0, len = handlerTypes.length; c < len; c++) { 553 | handlers = ctx[`${handlerTypes[c]}Handlers`] 554 | let name 555 | for (name in handlers) { 556 | await handlers[name].destroy() 557 | } 558 | } 559 | await unnest(ctx.global.rootCtx)() 560 | ctx = undefined 561 | this.isDisposed = true 562 | if (moduleDef.onDestroy) { 563 | await moduleDef.onDestroy(moduleAPI) 564 | } 565 | } 566 | 567 | return await attach(undefined) 568 | } 569 | -------------------------------------------------------------------------------- /src/core/style.ts: -------------------------------------------------------------------------------- 1 | import { types, getStyles as _getStyles } from 'typestyle' 2 | import { TypeStyle } from 'typestyle/lib/internal/typestyle' 3 | import { deepmerge } from '../utils/fun' 4 | import { h } from '../interfaces/view' 5 | import { VNode } from '../interfaces/view/vnode' 6 | import { InterfaceHelpers } from '../core' 7 | 8 | export type CSS = types.NestedCSSProperties 9 | 10 | export const getStyles = _getStyles 11 | 12 | export interface StyleClasses { 13 | base: string 14 | [className: string]: string 15 | } 16 | 17 | export interface StyleGroup { 18 | base: CSS 19 | [className: string]: CSS 20 | } 21 | 22 | export interface ComponentGroups { 23 | [className: string]: StyleGroup 24 | } 25 | 26 | /* istanbul ignore next */ 27 | export function styleGroup (instance: TypeStyle, stylesObj: StyleGroup, moduleName: string): StyleClasses { 28 | let classes = {} 29 | for (let key in stylesObj) { 30 | if (moduleName !== undefined) { 31 | classes[key] = instance.style(stylesObj[key], { $debugName: `_${moduleName}_${key}__` }) 32 | } else { 33 | classes[key] = instance.style(stylesObj[key]) 34 | } 35 | } 36 | return classes 37 | } 38 | 39 | /* istanbul ignore next */ 40 | export function hasBaseObject (obj: Object): boolean { 41 | for (let key in obj) { 42 | if (obj[key] !== null && typeof obj[key] === 'object' && key == 'base') { 43 | return true 44 | } 45 | } 46 | return false 47 | } 48 | 49 | // function for ngClass with one dynamic property 50 | /* istanbul ignore next */ 51 | export function c(className: string, condition: boolean): any { 52 | return { 53 | [className]: condition, 54 | } 55 | } 56 | 57 | /* istanbul ignore next */ 58 | export function mergeStyles (group1: StyleGroup, group2: StyleGroup): StyleGroup { 59 | let mergedGroup: StyleGroup = { base: {} } 60 | for(let i = 0, keys = Object.keys(group1), len = keys.length; i < len; i++) { 61 | mergedGroup[keys[i]] = group1[keys[i]] 62 | } 63 | for(let i = 0, keys = Object.keys(group2), len = keys.length; i < len; i++) { 64 | if (mergedGroup[keys[i]] && typeof mergedGroup[keys[i]] === 'object') { 65 | mergedGroup[keys[i]] = deepmerge(mergedGroup[keys[i]], group2[keys[i]]) 66 | } else { 67 | mergedGroup[keys[i]] = group2[keys[i]] 68 | } 69 | } 70 | return mergedGroup 71 | } 72 | 73 | export const getStyle = (F: InterfaceHelpers): any => { 74 | const style = F.ctx.groups.style 75 | return (...args) => { 76 | let obj = {} 77 | for (let i = 0, len = args.length, arg; i < len; i += 2) { 78 | arg = args[i + 1] 79 | obj[style[args[i]]] = arg === undefined ? true : arg 80 | } 81 | return obj 82 | } 83 | } 84 | 85 | export const placeholderColor = (color: string): CSS => ({ 86 | $nest: { 87 | '&::-webkit-input-placeholder': { /* Chrome/Opera/Safari */ 88 | $unique: true, 89 | color: color, 90 | }, 91 | '&::-moz-placeholder': { /* Firefox */ 92 | $unique: true, 93 | color: color, 94 | opacity: 1, 95 | }, 96 | '&:-ms-input-placeholder': { /* IE 10+ */ 97 | $unique: true, 98 | color: color, 99 | }, 100 | }, 101 | }) 102 | 103 | export const absoluteCenter: CSS = { 104 | display: 'flex', 105 | alignItems: 'center', 106 | justifyContent: 'center', 107 | } 108 | 109 | export const clickable: CSS = { 110 | cursor: 'pointer', 111 | userSelect: 'none', 112 | '-moz-user-select': 'none', 113 | } 114 | 115 | export const obfuscator: CSS = { 116 | position: 'absolute', 117 | top: '0px', 118 | left: '0px', 119 | width: '100%', 120 | height: '100%', 121 | backgroundColor: 'rgba(0,0,0,0.5)', 122 | display: 'none', 123 | } 124 | 125 | // Evaluate deprecation 126 | export const imageRenderingContrast: CSS = { 127 | 'imageRendering': [ 128 | 'optimizeSpeed', /* */ 129 | '-moz-crisp-edges', /* Firefox */ 130 | '-o-crisp-edges', /* Opera */ 131 | '-webkit-optimize-contrast', /* Chrome (and Safari) */ 132 | 'optimize-contrast', /* CSS3 Proposed */ 133 | ], 134 | '-ms-interpolation-mode': 'nearest-neighbor', /* IE8+ */ 135 | } 136 | 137 | export const iconView = (iconName, options = {}): VNode => h('svg', deepmerge({class: {['svg_' + iconName]: true}}, options), [ 138 | h('use', {attrs: { 'xlink:href': 'assets/icons-bundle.min.svg#' + iconName }}), 139 | ]) 140 | -------------------------------------------------------------------------------- /src/core/testUtils.ts: -------------------------------------------------------------------------------- 1 | import { run, Module } from '../core' 2 | import { logFns } from '../utils/log' 3 | import { deepmerge } from '../utils/fun' 4 | 5 | export const ChildComp = { 6 | state: { count: 0 }, 7 | inputs: (s, F) => ({ 8 | inc: async () => { 9 | await F.toAct('Inc') 10 | await F.toIn('changed', s.count) 11 | }, 12 | changed: async value => {}, 13 | }), 14 | actions: { 15 | Inc: () => s => { 16 | s.count++ 17 | return s 18 | }, 19 | }, 20 | interfaces: {}, 21 | } 22 | 23 | export const createApp = (comp?, mod?): Promise => { 24 | 25 | const Root = { 26 | state: { result: '' }, 27 | inputs: (s, F) => ({}), 28 | actions: {}, 29 | interfaces: {}, 30 | } 31 | 32 | const DEV = true 33 | 34 | return run(deepmerge({ 35 | Root: deepmerge(Root, comp || {}), 36 | record: DEV, 37 | log: DEV, 38 | interfaces: {}, 39 | error: logFns.error, 40 | }, mod || {})) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/core/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { clone } from './utils' 3 | 4 | test('Clone an object', t => { 5 | const obj = { 6 | nested: { 7 | deep: { 8 | a: 1, 9 | b: [1, 2, 3, { z:10 }], 10 | } 11 | }, 12 | arr: ['a', 2, { key: 'value' }], 13 | } 14 | t.deepEqual(clone(obj), obj) 15 | }) 16 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '.' 2 | import { deepmerge } from '../utils/fun' 3 | 4 | /** 5 | * Deep clone object 6 | * @param object Object to clone 7 | * @returns The cloned object 8 | */ 9 | export function clone (object: T): T { 10 | var out, v, key 11 | out = Array.isArray(object) ? [] : {} 12 | for (key in object) { 13 | v = object[key] 14 | out[key] = (typeof v === 'object') ? clone (v) : v 15 | } 16 | return out 17 | } 18 | 19 | export const isServer = typeof window === 'undefined' 20 | 21 | export const isBrowser = !isServer 22 | 23 | export const hydrateState = (ctx: Context) => { 24 | if ((window as any).ssrInitialized) { 25 | let components = (window as any).ssrComponents 26 | let name 27 | for (name in components) { 28 | ctx.components[name].state = deepmerge(ctx.components[name].state, components[name].state) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/featureExample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple Example 5 | 6 | 7 |
8 | 9 |
10 | 11 | $bundles 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/featureExample/index.ts: -------------------------------------------------------------------------------- 1 | // All in one file example 2 | import { getStyle, Actions, StyleGroup, run, Inputs, Interfaces, clone } from '../core' 3 | import { h, View, viewHandler } from '../interfaces/view' 4 | import { styleHandler } from '../groups/style' 5 | 6 | namespace ChildComp { 7 | 8 | export const state = { 9 | count: 0, 10 | } 11 | 12 | export type S = typeof state 13 | 14 | export const inputs: Inputs = (s, F) => ({ 15 | inc: async name => { 16 | await F.toAct('Inc') 17 | await F.toIn('changed', F.stateOf().count) 18 | }, 19 | changed: async value => {}, 20 | }) 21 | 22 | export const actions: Actions = { 23 | Inc: () => s => { 24 | s.count++ 25 | return s 26 | }, 27 | } 28 | 29 | const view: View = async (s, F) => { 30 | const style = getStyle(F) 31 | return h('div', { 32 | key: F.ctx.name, 33 | class: style('base'), 34 | on: { click: F.in('inc', F.ctx.name) }, 35 | }, 'Count ' + s.count) 36 | } 37 | 38 | export const interfaces: Interfaces = { view } 39 | 40 | const style: StyleGroup = { 41 | base: { 42 | color: 'green', 43 | fontSize: '40px', 44 | }, 45 | } 46 | 47 | export const groups = { style } 48 | 49 | } 50 | 51 | namespace Root { 52 | 53 | export const state = { 54 | count: 0, 55 | _nest: { 56 | C1: clone(ChildComp), 57 | C2: clone(ChildComp), 58 | C3: clone(ChildComp), 59 | C4: clone(ChildComp), 60 | A_1: clone(ChildComp), 61 | A_2: clone(ChildComp), 62 | A_3: clone(ChildComp), 63 | }, 64 | } 65 | 66 | export type S = typeof state 67 | 68 | export const inputs: Inputs = (s, F) => ({ 69 | $C3_changed: async n => console.log('Individual propagation ', n), 70 | $A_changed: async n => console.log('Groupal propagation ', n), 71 | $_changed: async n => console.log('Global propagation ', n), 72 | }) 73 | 74 | export const actions: Actions = {} 75 | 76 | const view: View = async (s, F) => { 77 | const style = getStyle(F) 78 | return h('div', { 79 | class: style('base'), 80 | }, 81 | await F.vws(['C1', 'C2', 'C3', 'C4', 'A_1', 'A_2', 'A_3']) 82 | ) 83 | } 84 | 85 | export const interfaces: Interfaces = { view } 86 | 87 | const style: StyleGroup = { 88 | base: {}, 89 | } 90 | 91 | export const groups = { style } 92 | 93 | } 94 | 95 | const DEV = true 96 | 97 | run({ 98 | Root, 99 | record: DEV, 100 | log: DEV, 101 | groups: { 102 | style: styleHandler('', DEV), 103 | }, 104 | interfaces: { 105 | view: viewHandler('#app'), 106 | }, 107 | // ...logFns, 108 | }) 109 | -------------------------------------------------------------------------------- /src/groups/style.ts: -------------------------------------------------------------------------------- 1 | import { Handler, styleGroup } from '../core' 2 | import { createTypeStyle } from 'typestyle' 3 | import { TypeStyle } from 'typestyle/lib/internal/typestyle' 4 | 5 | // insert styles in a DOM container at head 6 | 7 | export const styleHandler: Handler = (containerName?: string, debug = false, groupName = 'style') => mod => { 8 | let container 9 | if (typeof window !== 'undefined') { 10 | container = document.createElement('style') 11 | // named container 12 | if (containerName !== '' && containerName !== undefined) { 13 | container.id = containerName 14 | } 15 | document.head.appendChild(container) 16 | } 17 | let instance: TypeStyle = createTypeStyle(container) 18 | let state: any = { 19 | container, 20 | instance, 21 | } 22 | let name, parts, style 23 | return { 24 | state, 25 | handle: async (id, styleObj) => { 26 | if (debug) { 27 | parts = id.split('$') 28 | name = parts[parts.length - 1] 29 | } 30 | style = styleGroup(instance, styleObj, name) 31 | instance.forceRenderStyles() 32 | mod.setGroup(id, groupName, style) 33 | }, 34 | destroy: () => { 35 | state = {} 36 | if (container) { 37 | container.remove() 38 | } 39 | }, 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/interfaces/route.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '../core' 2 | 3 | interface LocationObj { 4 | href: string 5 | pathname: string 6 | search: string 7 | hash: string 8 | } 9 | 10 | type RouterMode = 'history' | 'hash' 11 | 12 | export const routeHandler: Handler = (root: string, mode: RouterMode = 'history', locationObj?: LocationObj) => mod => { 13 | let state = { 14 | parts: undefined, 15 | pathname: '', 16 | path: [], 17 | query: '', 18 | queryObj: {}, 19 | } 20 | async function checkAppChanges (parts, isInit: boolean) { 21 | // build actual app route 22 | let pathname = '', path = [] 23 | for (let i = 0, part; part = parts[i]; i++) { 24 | pathname += (pathname ? '/' : '') + part[1] 25 | path.push([part[0], part[1]]) 26 | } 27 | state.pathname = root + pathname 28 | state.path = path 29 | if (isInit) { 30 | await checkUrlChanges() 31 | } else { 32 | if (mode === 'history') { 33 | history.pushState(null, null, state.pathname) 34 | } else { 35 | locationObj.href = locationObj.href.replace(/#(.*)$/, '') + '#' + state.pathname 36 | } 37 | } 38 | } 39 | async function checkUrlChanges () { 40 | let pathname 41 | if (mode === 'history') { 42 | pathname = locationObj.pathname 43 | } else { 44 | pathname = locationObj.hash.substr(1) 45 | } 46 | if (pathname !== state.pathname) { 47 | let parts = pathname.substr(1).split('/') 48 | let changed = false 49 | for (let i = 0, pathPart, part; pathPart = state.path[i]; i++) { 50 | part = parts[i] 51 | if (part !== pathPart[1] && !changed) { 52 | changed = true 53 | await mod.dispatchEv(part, [pathPart[0], 'onRouteActive', [part]]) 54 | } else { 55 | if (changed) { 56 | await mod.dispatchEv(part, [pathPart[0], 'onRouteInactive']) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | if (!locationObj) { 63 | locationObj = window.location 64 | } 65 | if (typeof window !== undefined) { 66 | setInterval(() => { 67 | if (!state.parts) return 68 | }, 50) 69 | } 70 | return { 71 | state, 72 | handle: async (id, parts) => { 73 | await checkAppChanges(parts, !state.parts) 74 | state.parts = parts 75 | }, 76 | destroy: async () => {}, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/interfaces/view/eventListeners.ts: -------------------------------------------------------------------------------- 1 | import { VNode, VNodeData } from './vnode' 2 | import { Module } from 'snabbdom/modules/module' 3 | import { InputData, ModuleAPI, invokeHandler } from '../../core' 4 | 5 | export interface On { 6 | [event: string]: InputData | InputData[] | 'ignore' 7 | } 8 | 9 | export const eventListenersModule = (mod: ModuleAPI): Module => { 10 | 11 | function handleEvent (event: Event, vnode: VNode) { 12 | var name = event.type, 13 | on = (vnode.data as VNodeData).on 14 | 15 | // call event handler(s) if exists 16 | if (on && on[name]) { 17 | invokeHandler(mod.error, mod.dispatch, on[name], event) 18 | } 19 | } 20 | 21 | function createListener () { 22 | return function handler(event: Event) { 23 | handleEvent(event, (handler as any).vnode) 24 | } 25 | } 26 | 27 | function updateEventListeners (oldVnode: VNode, vnode?: VNode): void { 28 | var oldOn = (oldVnode.data as VNodeData).on, 29 | oldListener = (oldVnode as any).listener, 30 | oldElm: Element = oldVnode.elm as Element, 31 | on = vnode && (vnode.data as VNodeData).on, 32 | elm: Element = (vnode && vnode.elm) as Element, 33 | name: string 34 | 35 | // optimization for reused immutable handlers 36 | if (oldOn === on) { 37 | return 38 | } 39 | 40 | // remove existing listeners which no longer used 41 | if (oldOn && oldListener) { 42 | // if element changed or deleted we remove all existing listeners unconditionally 43 | if (!on) { 44 | for (name in oldOn) { 45 | // remove listener if element was changed or existing listeners removed 46 | oldElm.removeEventListener(name, oldListener, false) 47 | } 48 | } else { 49 | for (name in oldOn) { 50 | // remove listener if existing listener removed 51 | if (!on[name]) { 52 | oldElm.removeEventListener(name, oldListener, false) 53 | } 54 | } 55 | } 56 | } 57 | 58 | // add new listeners which has not already attached 59 | if (on) { 60 | // reuse existing listener or create new 61 | var listener = (vnode as any).listener = (oldVnode as any).listener || createListener() 62 | // update vnode for listener 63 | listener.vnode = vnode 64 | 65 | // if element changed or added we add all needed listeners unconditionally 66 | if (!oldOn) { 67 | for (name in on) { 68 | // add listener if element was changed or new listeners added 69 | elm.addEventListener(name, listener, false) 70 | } 71 | } else { 72 | for (name in on) { 73 | // add listener if new listener added 74 | if (!oldOn[name]) { 75 | elm.addEventListener(name, listener, false) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | return { 83 | create: updateEventListeners, 84 | update: updateEventListeners, 85 | destroy: updateEventListeners, 86 | } as any 87 | } 88 | 89 | export default eventListenersModule 90 | -------------------------------------------------------------------------------- /src/interfaces/view/globalListeners.ts: -------------------------------------------------------------------------------- 1 | import { VNode, VNodeData } from './vnode' 2 | import { Module } from 'snabbdom/modules/module' 3 | import { InputData, ModuleAPI, computeEvent } from '../../core' 4 | import { isDescendant } from './utils' 5 | 6 | export interface OnGlobal { 7 | [event: string]: InputData | InputData[] | 'ignore' 8 | } 9 | 10 | function getContainer (lastContainer) { 11 | let elm = ( lastContainer).elm ? ( lastContainer).elm : lastContainer 12 | return elm 13 | } 14 | 15 | export const globalListenersModule = (mod: ModuleAPI, state: { lastContainer: VNode | Element }): Module => { 16 | 17 | function invokeHandler(handler: InputData | InputData[] | 'ignore', event: Event, vnode: VNode): void { 18 | if (handler instanceof Array && typeof handler[0] === 'string') { 19 | let options = handler[4] 20 | if ( 21 | (options && options.listenPrevented !== true || !options) && event.defaultPrevented 22 | || (options && options.selfPropagated !== true || !options) 23 | && (isDescendant(vnode.elm, event.srcElement) || vnode.elm === event.srcElement) 24 | ) { 25 | return 26 | } 27 | if (options && options.default === false) { 28 | event.preventDefault() 29 | } 30 | // call function handler 31 | setImmediate(() => { 32 | mod.dispatch(computeEvent(event, handler)) 33 | }) 34 | } else if (handler instanceof Array) { 35 | // call multiple handlers 36 | for (var i = 0; i < handler.length; i++) { 37 | invokeHandler(handler[i], event, vnode) 38 | } 39 | } else if (handler === 'ignore') { 40 | // this handler is ignored 41 | event.preventDefault() 42 | } else if (handler === '' && handler === undefined) { 43 | // this handler is passed 44 | return 45 | } else { 46 | mod.error('ViewInterface-globalListenersModule', 'event handler of type ' + typeof handler + 'are not allowed, data: ' + JSON.stringify(handler)) 47 | } 48 | } 49 | 50 | function handleEvent(event: Event, vnode: VNode) { 51 | var name = event.type, 52 | global = (vnode.data as VNodeData).global 53 | 54 | // call event handler(s) if exists 55 | if (global && global[name]) { 56 | invokeHandler(global[name], event, vnode) 57 | } 58 | } 59 | 60 | function createListener() { 61 | return function handler(event: Event) { 62 | handleEvent(event, (handler as any).vnode) 63 | } 64 | } 65 | 66 | function updateEventListeners(oldVnode: VNode, vnode?: VNode): void { 67 | var oldGlobal = (oldVnode.data as VNodeData).global, 68 | oldListener = (oldVnode as any).globalListener, 69 | global = vnode && (vnode.data as VNodeData).global, 70 | name: string 71 | 72 | // optimization for reused immutable handlers 73 | if (oldGlobal === global) { 74 | return 75 | } 76 | 77 | // remove existing listeners which no longer used 78 | if (oldGlobal && oldListener) { 79 | // if element changed or deleted we remove all existing listeners unconditionally 80 | if (!global) { 81 | for (name in oldGlobal) { 82 | // remove listener if element was changed or existing listeners removed 83 | let elm = getContainer(state.lastContainer) 84 | elm.removeEventListener(name, oldListener, false) 85 | } 86 | } else { 87 | for (name in oldGlobal) { 88 | // remove listener if existing listener removed 89 | if (!global[name]) { 90 | let elm = getContainer(state.lastContainer) 91 | elm.removeEventListener(name, oldListener, false) 92 | } 93 | } 94 | } 95 | } 96 | 97 | // add new listeners which has not already attached 98 | if (global) { 99 | // reuse existing listener or create new 100 | var globalListener = (vnode as any).globalListener = (oldVnode as any).globalListener || createListener() 101 | // update vnode for listener 102 | globalListener.vnode = vnode 103 | 104 | // if element changed or added we add all needed listeners unconditionally 105 | if (!oldGlobal) { 106 | for (name in global) { 107 | // add listener if element was changed or new listeners added 108 | let elm = getContainer(state.lastContainer) 109 | elm.addEventListener(name, globalListener, false) 110 | } 111 | } else { 112 | for (name in global) { 113 | // add listener if new listener added 114 | if (!oldGlobal[name]) { 115 | let elm = getContainer(state.lastContainer) 116 | elm.addEventListener(name, globalListener, false) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | return { 124 | create: updateEventListeners, 125 | update: updateEventListeners, 126 | destroy: updateEventListeners, 127 | } as any 128 | } 129 | 130 | export default globalListenersModule 131 | -------------------------------------------------------------------------------- /src/interfaces/view/h.ts: -------------------------------------------------------------------------------- 1 | // Copied from snabbdom 2 | // Commit: https://github.com/snabbdom/snabbdom/commit/c5d513dfd90fca1188e63cf8abac5cc3eb06bdcf 3 | import {vnode, VNode as _VNode, VNodeData} from './vnode' 4 | import * as is from './is' 5 | 6 | export type VNode = _VNode 7 | 8 | /* istanbul ignore next */ 9 | function addNS(data: any, children: Array | undefined, sel: string | undefined): void { 10 | data.ns = 'http://www.w3.org/2000/svg' 11 | if (sel !== 'foreignObject' && children !== undefined) { 12 | for (let i = 0; i < children.length; ++i) { 13 | let childData = children[i].data 14 | if (childData !== undefined) { 15 | addNS(childData, (children[i] as VNode).children as Array, children[i].sel) 16 | } 17 | } 18 | } 19 | } 20 | 21 | export function h(sel: string): VNode; 22 | export function h(sel: string, data: VNodeData): VNode; 23 | export function h(sel: string, text: string): VNode; 24 | export function h(sel: string, data: VNodeData, text: string): VNode; 25 | export function h(sel: string, data: VNodeData, children: Array): VNode; 26 | export function h(sel: string, children: Array): VNode; 27 | /* istanbul ignore next */ 28 | export function h(sel: any, b?: any, c?: any): VNode { 29 | var data: VNodeData = {}, children: any, text: any, i: number; 30 | if (c !== undefined) { 31 | data = b; 32 | if (is.array(c)) { children = c; } 33 | else if (is.primitive(c)) { text = c; } 34 | else if (c && c.sel) { children = [c]; } 35 | } else if (b !== undefined) { 36 | if (is.array(b)) { children = b; } 37 | else if (is.primitive(b)) { text = b; } 38 | else if (b && b.sel) { children = [b]; } 39 | else { data = b; } 40 | } 41 | if (is.array(children)) { 42 | for (i = 0; i < children.length; ++i) { 43 | if (is.primitive(children[i])) children[i] = (vnode as any)(undefined, undefined, undefined, children[i]); 44 | } 45 | } 46 | if ( 47 | sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && 48 | (sel.length === 3 || sel[3] === '.' || sel[3] === '#') 49 | ) { 50 | addNS(data, children, sel) 51 | } 52 | return vnode(sel, data, children, text, undefined) 53 | } 54 | 55 | export default h 56 | -------------------------------------------------------------------------------- /src/interfaces/view/index.ts: -------------------------------------------------------------------------------- 1 | import { Interface, ModuleAPI, Handler } from '../../core' 2 | import { init } from 'snabbdom' 3 | import classModule from 'snabbdom/modules/class' 4 | import attributesModule from 'snabbdom/modules/attributes' 5 | import propsModule from 'snabbdom/modules/props' 6 | import styleModule from 'snabbdom/modules/style' 7 | import eventListenersModule from './eventListeners' 8 | import globalListenersModule from './globalListeners' 9 | import sizeBindingModule from './sizeBinding' 10 | import { default as _h } from './h' 11 | import { VNode as _VNode } from './vnode' 12 | 13 | export const h = _h 14 | export type VNode = _VNode 15 | 16 | export type View = Interface 17 | 18 | /* istanbul ignore next */ 19 | export const viewHandler: Handler = (selectorElm, cb?: { (value: VNode): void }) => (mod: ModuleAPI) => { 20 | let selector = (typeof selectorElm === 'string') ? selectorElm : '' 21 | let state: { lastContainer: VNode | Element } = { 22 | lastContainer: undefined, 23 | } 24 | 25 | // Common snabbdom patch function (convention over configuration) 26 | let patchFn = init([ 27 | classModule, 28 | attributesModule, 29 | propsModule, 30 | styleModule, 31 | eventListenersModule(mod), 32 | globalListenersModule(mod, state), 33 | sizeBindingModule(mod), 34 | ]) 35 | 36 | function handler (vnode: VNode) { 37 | let vnode_mapped = h('div' + selector, { key: selector }, [vnode]) 38 | state.lastContainer = patchFn( state.lastContainer, vnode_mapped) 39 | } 40 | 41 | return { 42 | state, 43 | handle: async (__, value: VNode) => { 44 | if (typeof window === 'undefined') { 45 | if (cb) { 46 | cb(value) 47 | } 48 | return 49 | } 50 | if (!state.lastContainer) { 51 | let container = selector !== '' ? document.querySelector(selector) : selectorElm 52 | if (!container) { 53 | return mod.error('view', `There are no element matching selector '${selector}'`) 54 | } 55 | state.lastContainer = container 56 | handler( state.lastContainer) 57 | handler(value) 58 | } else { 59 | handler(value) 60 | } 61 | if (cb) { 62 | cb(value) 63 | } 64 | }, 65 | destroy: () => {}, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/interfaces/view/is.ts: -------------------------------------------------------------------------------- 1 | // Copied from snabbdom 2 | // Commit: https://github.com/snabbdom/snabbdom/commit/f552b0e8eda30a84e59f212e98651463ec71a53f 3 | export const array = Array.isArray 4 | 5 | /* istanbul ignore next */ 6 | export function primitive(s: any): s is (string | number) { 7 | return typeof s === 'string' || typeof s === 'number'; 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/view/resizeSensor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Taken and adapted from: 3 | * https://github.com/marcj/css-element-queries/blob/master/src/ResizeSensor.js 4 | * Copyright Marc J. Schmidt. See the LICENSE (MIT) 5 | * directory of this distribution and at 6 | * https://github.com/marcj/css-element-queries/blob/master/LICENSE. 7 | */ 8 | 9 | // TODO: remove the OOP stuff in favor to a data-function functional approach 10 | 11 | export interface ExtHTMLElement extends HTMLElement { 12 | resizedAttached: EventQueue 13 | resizeSensor: HTMLElement 14 | } 15 | 16 | /* istanbul ignore next */ 17 | export class EventQueue { 18 | 19 | q: Array 20 | 21 | constructor() { 22 | this.q = [] 23 | } 24 | 25 | add(ev) { 26 | this.q.push(ev) 27 | } 28 | 29 | call() { 30 | var i, j 31 | for (i = 0, j = this.q.length; i < j; i++) { 32 | this.q[i].call(this) 33 | } 34 | } 35 | 36 | remove(ev) { 37 | var newQueue = [], i, j 38 | for (i = 0, j = this.q.length; i < j; i++) { 39 | if (this.q[i] !== ev) newQueue.push(this.q[i]) 40 | } 41 | this.q = newQueue 42 | } 43 | 44 | length() { 45 | return this.q.length 46 | } 47 | 48 | } 49 | 50 | /** 51 | * Class for dimension change detection. 52 | */ 53 | /* istanbul ignore next */ 54 | export class ResizeSensor { 55 | 56 | element: ExtHTMLElement 57 | callback: { (res: any): void } 58 | 59 | constructor (element: HTMLElement, callback: { (res: any): void }) { 60 | this.element = element 61 | this.callback = callback 62 | this.attachResizeEvent( element, callback) 63 | } 64 | 65 | attachResizeEvent (element: ExtHTMLElement, resized: { (res: any): void }) { 66 | if (element.resizedAttached) { 67 | element.resizedAttached.add(resized) 68 | return 69 | } 70 | 71 | element.resizedAttached = new EventQueue() 72 | element.resizedAttached.add(resized) 73 | 74 | element.resizeSensor = document.createElement('div') 75 | element.resizeSensor.className = 'resize-sensor' 76 | var style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;'; 77 | var styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;' 78 | 79 | element.resizeSensor.style.cssText = style 80 | element.resizeSensor.innerHTML = 81 | '
' + 82 | '
' + 83 | '
' + 84 | '
' + 85 | '
' + 86 | '
' 87 | element.appendChild(element.resizeSensor) 88 | 89 | if (element.resizeSensor.offsetParent !== element) { 90 | element.style.position = 'relative' 91 | } 92 | 93 | var expand: HTMLElement = element.resizeSensor.childNodes[0] 94 | var expandChild: HTMLElement = expand.childNodes[0] 95 | var shrink: HTMLElement = element.resizeSensor.childNodes[1] 96 | var dirty, rafId, newWidth, newHeight 97 | var lastWidth = element.offsetWidth 98 | var lastHeight = element.offsetHeight 99 | 100 | var reset = function () { 101 | expandChild.style.width = '100000px' 102 | expandChild.style.height = '100000px' 103 | 104 | expand.scrollLeft = 100000 105 | expand.scrollTop = 100000 106 | 107 | shrink.scrollLeft = 100000 108 | shrink.scrollTop = 100000 109 | } 110 | 111 | // setTimeout waits until rendering is done 112 | setTimeout(() => reset(), 0) 113 | 114 | var onResized = function () { 115 | rafId = 0 116 | 117 | if (!dirty) return 118 | 119 | lastWidth = newWidth 120 | lastHeight = newHeight 121 | 122 | if (element.resizedAttached) { 123 | element.resizedAttached.call() 124 | } 125 | } 126 | 127 | var onScroll = function () { 128 | newWidth = element.offsetWidth 129 | newHeight = element.offsetHeight 130 | dirty = newWidth != lastWidth || newHeight != lastHeight 131 | 132 | if (dirty && !rafId) { 133 | rafId = requestAnimationFrame(onResized) 134 | } 135 | 136 | reset() 137 | } 138 | 139 | var addEvent = function (el, name, cb) { 140 | if (el.attachEvent) { 141 | el.attachEvent('on' + name, cb) 142 | } else { 143 | el.addEventListener(name, cb) 144 | } 145 | } 146 | 147 | addEvent(expand, 'scroll', onScroll) 148 | addEvent(shrink, 'scroll', onScroll) 149 | } 150 | 151 | detach (ev) { 152 | let elem = this.element 153 | if(elem.resizedAttached && typeof ev == 'function') { 154 | elem.resizedAttached.remove(ev) 155 | if(elem.resizedAttached.length()) return 156 | } 157 | if (elem.resizeSensor) { 158 | if (elem.contains(elem.resizeSensor)) { 159 | elem.removeChild(elem.resizeSensor) 160 | } 161 | delete elem.resizeSensor 162 | delete elem.resizedAttached 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/interfaces/view/sizeBinding.ts: -------------------------------------------------------------------------------- 1 | import { VNode, VNodeData } from './vnode' 2 | import { Module } from 'snabbdom/modules/module' 3 | import { InputData, ModuleAPI } from '../../core' 4 | import { ResizeSensor } from './resizeSensor' 5 | 6 | export type SizeBinding = InputData | InputData[] | 'ignore' 7 | 8 | // TODO: CRITICAL, improve performance or deprecate this way in favor of task size evaluator 9 | 10 | /* istanbul ignore next */ 11 | export const sizeBindingModule = (mod: ModuleAPI): Module => { 12 | 13 | function invokeHandler (evHandler: SizeBinding, vnode: VNode, eventData) { 14 | if (evHandler instanceof Array && typeof evHandler[0] === 'string') { 15 | setTimeout(() => { 16 | mod.dispatchEv(eventData, evHandler) 17 | }, 0) 18 | } else if (evHandler instanceof Array) { 19 | // call multiple handlers 20 | for (var i = 0; i < evHandler.length; i++) { 21 | invokeHandler(evHandler[i], vnode, eventData) 22 | } 23 | } else if (evHandler === 'ignore') { 24 | // this handler is ignored 25 | return 26 | } else if (evHandler === '' && evHandler === undefined) { 27 | // this handler is passed 28 | return 29 | } else { 30 | mod.error('ViewInterface-sizeBindingModule', 'event handler of type ' + typeof evHandler + 'are not allowed, data: ' + JSON.stringify(evHandler)) 31 | } 32 | } 33 | 34 | function createListener () { 35 | return function handler() { 36 | var vnode = (handler as any).vnode 37 | var evHandler = vnode.data.size 38 | var eventData = vnode.elm.getBoundingClientRect() 39 | invokeHandler(evHandler, vnode, eventData) 40 | } 41 | } 42 | 43 | function updateSizeListener (oldVnode: VNode, vnode?: VNode): void { 44 | var oldSize = (oldVnode.data as VNodeData).size, 45 | oldResizeListener = (oldVnode as any).resizeListener, 46 | oldResizeSensor = (oldVnode as any).resizeSensor, 47 | size = vnode && (vnode.data as VNodeData).size, 48 | elm: HTMLElement = (vnode && vnode.elm) as HTMLElement 49 | 50 | // optimization for reused immutable handlers 51 | if (oldSize === size) { 52 | return 53 | } 54 | 55 | // remove existing listeners which no longer used 56 | if (oldSize && oldResizeListener) { 57 | // if element changed or deleted we remove all existing listeners unconditionally 58 | if (!size) { 59 | // remove listener if element was changed or existing listeners removed 60 | oldResizeSensor.detach(oldResizeListener) 61 | } 62 | } 63 | 64 | // add new listeners which has not already attached 65 | if (size) { 66 | // reuse existing listener or create new 67 | var resizeListener = (vnode as any).resizeListener = (oldVnode as any).listener || createListener() 68 | ;(vnode as any).resizeSensor = (oldVnode as any).listener || new ResizeSensor(elm, resizeListener) 69 | // update vnode for listener 70 | resizeListener.vnode = vnode 71 | } 72 | } 73 | 74 | return { 75 | create: updateSizeListener, 76 | update: updateSizeListener, 77 | destroy: updateSizeListener, 78 | } as any 79 | } 80 | 81 | export default sizeBindingModule 82 | -------------------------------------------------------------------------------- /src/interfaces/view/utils.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | // import { isDescendant } from './utils' 3 | 4 | // describe('View helpers', () => { 5 | 6 | // describe('isDescendant evaluate if an element is descendant of another element', () => { 7 | 8 | // let ancestor = document.createElement('div') 9 | // let intermediate = document.createElement('div') 10 | // ancestor.appendChild(intermediate) 11 | // let descendant = document.createElement('div') 12 | // intermediate.appendChild(descendant) 13 | // let notDescendant = document.createElement('div') 14 | 15 | // it('should return true if the element is decendant of the other', () => { 16 | // expect(isDescendant(ancestor, descendant)).toEqual(true) 17 | // }) 18 | 19 | // it('should return false if the element is not decendant of the other', () => { 20 | // expect(isDescendant(ancestor, notDescendant)).toEqual(false) 21 | // }) 22 | 23 | // }) 24 | 25 | // }) 26 | -------------------------------------------------------------------------------- /src/interfaces/view/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* istanbul ignore next */ 3 | export const isDescendant = (parent, child) => { 4 | var node = child.parentNode 5 | while (node != null) { 6 | if (node == parent) { 7 | return true 8 | } 9 | node = node.parentNode 10 | } 11 | return false 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/view/view-effects.ts: -------------------------------------------------------------------------------- 1 | // import { Handler, Context } from '../core' 2 | // import { init } from 'snabbdom' 3 | // import classModule from 'snabbdom/modules/class' 4 | // import attributesModule from 'snabbdom/modules/attributes' 5 | // import propsModule from 'snabbdom/modules/props' 6 | // import eventlistenersModule from './viewEventlisteners' 7 | // import styleModule from 'snabbdom/modules/style' 8 | // import h from 'snabbdom/h' 9 | // import { VNode } from 'snabbdom/vnode' 10 | 11 | // export interface ViewInterface { 12 | // (ctx: Context, s): VNode 13 | // } 14 | 15 | // export const viewHandler: Handler = selectorElm => mod => { 16 | // let selector = (typeof selectorElm === 'string') ? selectorElm : '' 17 | // let lastContainer 18 | // let state 19 | 20 | // // Common snabbdom patch function (convention over configuration) 21 | // let patchFn = init([ 22 | // classModule, 23 | // attributesModule, 24 | // propsModule, 25 | // eventlistenersModule(mod), 26 | // styleModule, 27 | // ]) 28 | 29 | // function wraperPatch (o, n) { 30 | // let newContainer = patchFn(o, n) 31 | // lastContainer = newContainer 32 | // return newContainer 33 | // } 34 | 35 | // function handler (vnode: VNode) { 36 | // let vnode_mapped = h('div' + selector, { key: selector }, [vnode]) 37 | // state = wraperPatch(state, vnode_mapped) 38 | // } 39 | 40 | // return { 41 | // state, 42 | // handle: (value: VNode) => { 43 | // if (!state) { 44 | // let container = selector !== '' ? document.querySelector(selector) : selectorElm 45 | // if (!container) { 46 | // return mod.error('view', `There are no element matching selector '${selector}'`) 47 | // } 48 | // state = container 49 | // handler(state) 50 | // handler(value) 51 | // } else { 52 | // handler(value) 53 | // } 54 | // }, 55 | // dispose: () => {}, 56 | // } 57 | // } 58 | -------------------------------------------------------------------------------- /src/interfaces/view/view-worker.ts: -------------------------------------------------------------------------------- 1 | // snabbdom fully working over a worker 2 | -------------------------------------------------------------------------------- /src/interfaces/view/view.spec.ts: -------------------------------------------------------------------------------- 1 | // DRAFT (TODO) 2 | // import { Module } from '../index' 3 | // import { viewHandler } from './view' 4 | // import testBed from './_testBed.spec' 5 | // import h from 'snabbdom/h' 6 | 7 | 8 | // describe('View interface behaviours', function() { 9 | 10 | // // Element placeholder for inserting the app 11 | // let appElement, app: Module 12 | 13 | // jasmine.DEFAULT_TIMEOUT_INTERVAL = 50000 14 | 15 | // beforeAll(done => { 16 | // appElement = document.createElement('div') 17 | // appElement.id = 'app' 18 | // document.body.appendChild(appElement) 19 | // app = testBed(i => (ctx, s) => 20 | // h('div', { 21 | // hook: { 22 | // insert: done, 23 | // }, 24 | // on: { 25 | // click: i.inc(ctx), 26 | // }, 27 | // }, s.count + '') 28 | // , viewHandler('#app')) 29 | // }) 30 | 31 | // // TODO: clea this!! 32 | // // afterEach(() => { 33 | // // document.getElementById('app').remove() 34 | // // if (!app.isDisposed) { 35 | // // // app.dispose() 36 | // // } 37 | // // }) 38 | 39 | // it('should have initial state', () => { 40 | // let appElm = document.getElementById('app') 41 | // expect(appElm.children[0].textContent).toBe('0') 42 | // }) 43 | 44 | // it('should react to input', done => { 45 | // let expectSubscriber = vnode => { 46 | // expect(vnode.elm.children[0].textContent).toBe('1') 47 | // app.interfaces['interfaceObj'].state$.unsubscribe(expectSubscriber) 48 | // done() 49 | // } 50 | // app.interfaces['interfaceObj'].state$.subscribe(expectSubscriber) 51 | // // create and dispatch a click event 52 | // let event = new Event('click') 53 | // let appElm = document.getElementById('app') 54 | // appElm.children[0].dispatchEvent(event) 55 | // }) 56 | 57 | // }) 58 | -------------------------------------------------------------------------------- /src/interfaces/view/vnode.ts: -------------------------------------------------------------------------------- 1 | import { CSS } from '../../core' 2 | import { Hooks } from 'snabbdom/hooks' 3 | import { AttachData } from 'snabbdom/helpers/attachto' 4 | import { On } from './eventListeners' 5 | import { OnGlobal } from './globalListeners' 6 | import { SizeBinding } from './sizeBinding' 7 | import { Attrs } from 'snabbdom/modules/attributes' 8 | import { Classes } from 'snabbdom/modules/class' 9 | import { Props } from 'snabbdom/modules/props' 10 | import { Dataset } from 'snabbdom/modules/dataset' 11 | import { Hero } from 'snabbdom/modules/hero' 12 | 13 | export declare type Key = string | number 14 | 15 | export interface VNode { 16 | sel: string | undefined 17 | data: VNodeData | undefined 18 | children: Array | undefined 19 | elm: Node | undefined 20 | text: string | undefined 21 | key: Key 22 | } 23 | 24 | export interface VNodeData { 25 | props?: Props 26 | attrs?: Attrs 27 | class?: Classes 28 | style?: CSS 29 | dataset?: Dataset 30 | on?: On 31 | global?: OnGlobal 32 | size?: SizeBinding 33 | hero?: Hero 34 | attachData?: AttachData 35 | hook?: Hooks 36 | key?: Key 37 | ns?: string 38 | fn?: () => VNode 39 | args?: Array 40 | } 41 | 42 | /* istanbul ignore next */ 43 | export function vnode(sel: string | undefined, 44 | data: any | undefined, 45 | children: Array | undefined, 46 | text: string | undefined, 47 | elm: Element | Text | undefined): VNode { 48 | let key = data === undefined ? undefined : data.key; 49 | return {sel: sel, data: data, children: children, 50 | text: text, elm: elm, key: key}; 51 | } 52 | 53 | export default vnode 54 | -------------------------------------------------------------------------------- /src/playground/README.md: -------------------------------------------------------------------------------- 1 | # Playground App 2 | 3 | This is a notes app 4 | 5 | Structure: 6 | 7 | - Root 8 | - List 9 | - Note 10 | 11 | ## Runing 12 | 13 | ```bash 14 | node fuse-playground 15 | ``` 16 | 17 | ## Compiling 18 | 19 | For compiling run (note this supports AOT): 20 | ```bash 21 | node fuse-playground-aot 22 | node src/playground/dist/aot 23 | ``` 24 | -------------------------------------------------------------------------------- /src/playground/Root/List/Item.ts: -------------------------------------------------------------------------------- 1 | import { Actions, Inputs, Interfaces, StyleGroup, _, absoluteCenter, getStyle } from '../../../core' 2 | import { View, h } from '../../../interfaces/view' 3 | 4 | export const state = { 5 | checked: false, 6 | title: '', 7 | } 8 | 9 | export type S = typeof state 10 | 11 | export const inputs: Inputs = (s, F) => ({ 12 | remove: async () => {}, 13 | select: async id => {}, 14 | }) 15 | 16 | export const actions: Actions = { 17 | SetChecked: checked => s => { 18 | s.checked = checked 19 | return s 20 | }, 21 | SetItem: item => s => ({ 22 | ...s, 23 | ...item, 24 | }) 25 | } 26 | 27 | const view: View = async (s, F) => { 28 | let style = getStyle(F) 29 | 30 | return h('li', { 31 | key: F.ctx.name, 32 | class: style('base'), 33 | }, [ 34 | h('input', { 35 | class: style('checkbox'), 36 | props: { 37 | type: 'checkbox', 38 | checked: s.checked, 39 | }, 40 | on: { 41 | change: F.act('SetChecked', _, ['target', 'checked']), 42 | }, 43 | }), 44 | h('div', { 45 | class: style('title'), 46 | on: { click: F.in('select') }, 47 | }, s.title), 48 | h('div', { 49 | class: style('remove'), 50 | on: { 51 | click: F.in('remove'), 52 | }, 53 | }, [ 54 | h('div', {class: style('removeLine')}), 55 | ]), 56 | ]) 57 | } 58 | 59 | export const interfaces: Interfaces = { view } 60 | 61 | const style: StyleGroup = { 62 | base: { 63 | display: 'flex', 64 | justifyContent: 'space-between', 65 | alignItems: 'center', 66 | borderBottom: '1px solid #C1B8B8', 67 | }, 68 | checkbox: {}, 69 | title: { 70 | padding: '10px 5px', 71 | cursor: 'pointer', 72 | }, 73 | remove: { 74 | width: '24px', 75 | height: '24px', 76 | borderRadius: '50%', 77 | backgroundColor: '#DB4343', 78 | cursor: 'pointer', 79 | userSelect: 'none', 80 | boxShadow: '1px 1px 0px 0px #3f3f3f', 81 | ...absoluteCenter, 82 | $nest: { 83 | '&:hover': { 84 | backgroundColor: '#DE3030', 85 | }, 86 | }, 87 | }, 88 | removeLine: { 89 | width: 'calc(100% - 8px)', 90 | height: '3px', 91 | borderRadius: '1px', 92 | backgroundColor: 'white', 93 | }, 94 | } 95 | 96 | export const groups = { style } 97 | -------------------------------------------------------------------------------- /src/playground/Root/List/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Actions, 3 | Inputs, 4 | Interfaces, 5 | StyleGroup, 6 | clickable, 7 | _, 8 | getStyle, 9 | props, 10 | } from '../../../core' 11 | import { View, h } from '../../../interfaces/view' 12 | import { assoc } from '../../../utils/fun' 13 | 14 | import * as Item from './Item' 15 | 16 | export const state = { 17 | text: '', 18 | _nest: {}, 19 | _compUpdated: false, 20 | } 21 | 22 | export type S = typeof state 23 | 24 | export const inputs: Inputs = (s, F) => ({ 25 | onInit: async () => { 26 | let items = await F.task('db', ['subscribe', '*', F.in('updateItem', _, '*')]) 27 | await F.toAct('SetItems', items) 28 | }, 29 | inputKeyup: async ([keyCode, text]) => { 30 | if (keyCode === 13 && text !== '') { 31 | await F.toAct('SetText', '') 32 | await F.toIn('add', text) 33 | } else { 34 | await F.toAct('SetText', text) 35 | } 36 | }, 37 | add: async text => { 38 | await F.task('db', ['addItem', { title: text, body: '', _timestamp: Date.now() }]) 39 | }, 40 | updateItem: async ([name, id, item]) => { 41 | if (name === 'add') { 42 | await F.toAct('AddItem', [id, item]) 43 | } else if (name === 'set') { 44 | await F.toChild('Item_' + id, '_action', ['SetItem', item]) 45 | } else if (name === 'remove') { 46 | await F.toAct('_remove', 'Item_' + id) 47 | } 48 | }, 49 | setCheckAll: async (checked: boolean) => { 50 | await F.comps('Item').broadcast('_action', ['SetChecked', checked]) 51 | }, 52 | removeChecked: async () => { 53 | let names = F.comps('Item').getNames() 54 | for (let i = 0, len = names.length; i < len; i++) { 55 | if (F.stateOf('Item_' + names[i]).checked) { 56 | setImmediate(() => F.toIn('$Item_remove', [names[i]])) 57 | } 58 | } 59 | }, 60 | $Item_remove: async ([name]) => { 61 | await F.task('db', ['remove', name]) 62 | }, 63 | $Item_select: async ([id]) => { 64 | await F.toIn('select', id) 65 | }, 66 | select: async id => {}, 67 | }) 68 | 69 | export const actions: Actions = { 70 | SetText: assoc('text'), 71 | AddItem: ([id, data]) => s => { 72 | s._nest['Item_' + id] = props(data)(Item) 73 | s._compUpdated = true 74 | return s 75 | }, 76 | SetItems: items => s => { 77 | Object.keys(items).map( 78 | id => ( actions.AddItem([id, items[id]]))(s) 79 | ) 80 | return s 81 | }, 82 | } 83 | 84 | const view: View = async (s, F) => { 85 | let style = getStyle(F) 86 | 87 | return h('div', { 88 | key: F.ctx.name, 89 | class: style('base'), 90 | }, [ 91 | h('input', { 92 | class: style('input'), 93 | attrs: { placeholder: 'Type and hit enter' }, 94 | props: { value: s.text }, 95 | on: { 96 | keyup: F.in('inputKeyup', _, [ 97 | ['keyCode'], 98 | ['target', 'value'], 99 | ]), 100 | }, 101 | }), 102 | h('div', { class: style('menuBar') }, [ 103 | h('div', { 104 | class: style('menuItem'), 105 | on: { click: F.in('setCheckAll', true) }, 106 | }, 'check all'), 107 | h('div', { 108 | class: style('menuItem'), 109 | on: { click: F.in('setCheckAll', false) }, 110 | }, 'uncheck all'), 111 | h('div#el', { 112 | class: style('menuItem'), 113 | on: { click: F.in('removeChecked') }, 114 | }, 'remove checked'), 115 | ]), 116 | h('ul', { class: style('list') }, 117 | await F.group('Item'), 118 | ), 119 | ]) 120 | } 121 | 122 | export const interfaces: Interfaces = { view } 123 | 124 | const generalFont = { 125 | fontFamily: 'sans-serif', 126 | fontSize: '22px', 127 | color: '#292828', 128 | } 129 | 130 | const style: StyleGroup = { 131 | base: { 132 | width: '400px', 133 | overflow: 'auto', 134 | padding: '20px', 135 | ...generalFont, 136 | }, 137 | input: { 138 | width: '100%', 139 | padding: '5px', 140 | ...generalFont, 141 | $nest: { 142 | '&:focus': { 143 | outline: '2px solid #13A513', 144 | }, 145 | }, 146 | }, 147 | menuBar: { 148 | padding: '3px', 149 | display: 'flex', 150 | justifyContent: 'center', 151 | }, 152 | menuItem: { 153 | margin: '5px', 154 | padding: '3px 5px', 155 | fontSize: '16px', 156 | borderRadius: '4px', 157 | color: '#565656', 158 | border: '1px solid #e2dfdf', 159 | ...clickable, 160 | $nest: { 161 | '&:hover': { 162 | backgroundColor: '#eaeaea', 163 | }, 164 | }, 165 | }, 166 | list: { 167 | width: '100%', 168 | margin: '0', 169 | padding: '0', 170 | }, 171 | } 172 | 173 | export const groups = { style } 174 | -------------------------------------------------------------------------------- /src/playground/Root/Note.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Actions, 3 | Inputs, 4 | Interfaces, 5 | StyleGroup, 6 | _, 7 | getStyle, 8 | } from '../../core' 9 | import { View, h } from '../../interfaces/view' 10 | import { waitMS } from '../../utils/fun' 11 | 12 | export const state = { 13 | activeChild: '', 14 | id: '', 15 | title: '', 16 | body: '', 17 | count: 0, 18 | _timestamp: 0, 19 | } 20 | 21 | export type S = typeof state 22 | 23 | export const inputs: Inputs = (s, F) => ({ 24 | onInit: async () => { 25 | if (typeof window !== 'undefined') { 26 | F.toIn('self') 27 | } 28 | }, 29 | self: async () => { 30 | await waitMS(1000) 31 | await F.toAct('Inc') 32 | F.toIn('self') 33 | }, 34 | set: async ([name, value]) => { 35 | await F.task('db', ['setItemProps', s.id, { [name]: value }]) 36 | }, 37 | setNoteFromId: async id => { 38 | if (s.id !== '') { 39 | await F.task('db', ['unsubscribe', s.id]) 40 | } 41 | let note = await F.task('db', ['subscribe', id, F.in('setNote', _, '*')]) 42 | await F.toAct('SetNote', ['set', id, note]) 43 | await F.set('id', id) 44 | }, 45 | setNote: async ([evName, id, item]) => { 46 | await F.toAct('SetNote', [evName, id, item]) 47 | if (evName === 'remove') { 48 | await F.toIn('removed') 49 | } 50 | }, 51 | removed: async () => {}, 52 | }) 53 | 54 | export const actions: Actions = { 55 | Inc: async () => async s => { 56 | s.count++ 57 | }, 58 | SetNote: ([evName, id, item]) => s => { 59 | s.id = evName === 'remove' ? '' : id 60 | if (evName === 'add' || evName === 'set') { 61 | s.title = item.title 62 | s.body = item.body 63 | } 64 | }, 65 | } 66 | 67 | const view: View = async (s, F) => { 68 | let style = getStyle(F) 69 | 70 | return h('div', { 71 | key: F.ctx.name, 72 | class: style('base'), 73 | }, s.id == '' ? [ 74 | h('div', { 75 | class: style('title'), 76 | }, 'No one selected ... time: ' + s.count), 77 | ] : [ 78 | h('input', { 79 | class: style('title'), 80 | props: { value: s.title }, 81 | on: { change: F.in('set', 'title', ['target', 'value']) }, 82 | }), 83 | h('textarea', { 84 | class: style('body'), 85 | props: { value: s.body }, 86 | on: { change: F.in('set', 'body', ['target', 'value']) }, 87 | }), 88 | ]) 89 | } 90 | 91 | export const interfaces: Interfaces = { view } 92 | 93 | const style: StyleGroup = { 94 | base: { 95 | width: '100%', 96 | height: '100%', 97 | padding: '20px', 98 | display: 'flex', 99 | flexDirection: 'column', 100 | overflow: 'auto', 101 | }, 102 | title: { 103 | width: '100%', 104 | fontSize: '34px', 105 | paddingBottom: '20px', 106 | border: 'none', 107 | outline: 'none', 108 | }, 109 | body: { 110 | width: '100%', 111 | height: 'calc(100% - 63px)', 112 | fontSize: '21px', 113 | color: '#484747', 114 | border: 'none', 115 | outline: 'none', 116 | }, 117 | } 118 | 119 | export const groups = { style } 120 | -------------------------------------------------------------------------------- /src/playground/Root/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Actions, 3 | Inputs, 4 | Interfaces, 5 | StyleGroup, 6 | clone, 7 | Interface, 8 | getStyle, 9 | State, 10 | hydrateState, 11 | isBrowser, 12 | styles 13 | } from '../../core' 14 | import { View, h } from '../../interfaces/view' 15 | 16 | import * as List from './List' 17 | import * as Note from './Note' 18 | 19 | export const state: State = { 20 | activeChild: '', 21 | _nest: { 22 | List: clone(List), 23 | Note: styles({ base: { width: 'calc(100% - 400px)' }})(clone(Note)), 24 | }, 25 | } 26 | 27 | export type S = typeof state 28 | 29 | export const inputs: Inputs = (s, F) => ({ 30 | onInit: async () => { 31 | if (isBrowser) { 32 | hydrateState(F.ctx) 33 | } 34 | }, 35 | onRouteActive: async ([id]) => { 36 | if (id === '') return 37 | const item = await F.task('db', ['getItem', id]) 38 | if (item) { 39 | await F.toIn('$List_select', id) 40 | } else { 41 | await F.toAct('Set', ['activeChild', '']) 42 | } 43 | }, 44 | $List_select: async id => { 45 | await F.toAct('Set', ['activeChild', id]) 46 | await F.toChild('Note', 'setNoteFromId', id) 47 | }, 48 | $Note_removed: async id => { 49 | await F.toAct('Set', ['activeChild', '']) 50 | }, 51 | }) 52 | 53 | export const actions: Actions = { 54 | } 55 | 56 | const route: Interface = async (s, F) => [ 57 | [F.ctx.id, s.activeChild], 58 | ] 59 | 60 | const view: View = async (s, F) => { 61 | let style = getStyle(F) 62 | 63 | return h('div', { 64 | key: F.ctx.name, 65 | class: style('base'), 66 | }, [ 67 | await F.vw('List'), 68 | await F.vw('Note'), 69 | ]) 70 | } 71 | 72 | export const interfaces: Interfaces = { route, view } 73 | 74 | const style: StyleGroup = { 75 | base: { 76 | width: '100%', 77 | height: '100%', 78 | display: 'flex', 79 | overflow: 'auto', 80 | fontFamily: 'Sans serif', 81 | color: '#292828', 82 | }, 83 | } 84 | 85 | export const groups = { style } 86 | -------------------------------------------------------------------------------- /src/playground/aot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <!--##TITLE##--> 12 | 13 | 14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /src/playground/aot.ts: -------------------------------------------------------------------------------- 1 | import { prerender } from '../utils/aot' 2 | 3 | import { runModule } from './module' 4 | import * as Root from './Root/index' 5 | 6 | prerender({ 7 | htmlFile: 'src/playground/aot.html', 8 | cssFile: 'src/playground/styles.css', 9 | outputFile: 'src/playground/dist/index.html', 10 | }, { 11 | root: Root, 12 | runModule, 13 | bundlePaths: ['app.js'], 14 | cb: async () => {}, 15 | }) 16 | -------------------------------------------------------------------------------- /src/playground/db.ts: -------------------------------------------------------------------------------- 1 | import { Handler, _ } from '../core/index' 2 | 3 | export interface Item { 4 | title: string 5 | body: string 6 | _timestamp: number 7 | } 8 | 9 | let value: any = {} 10 | 11 | if (typeof window !== 'undefined') { 12 | let isInitialized = localStorage.getItem('memoryDB') 13 | value = JSON.parse(isInitialized || '{}') 14 | if (!isInitialized) { 15 | localStorage.setItem('memoryDB', '{}') 16 | } 17 | } 18 | 19 | let memoryDB: { [id: string]: Item } = value 20 | 21 | export const getItem = (id: string) => memoryDB[id] 22 | 23 | export const setItem = (id: string, item: Item) => { 24 | memoryDB[id] = item 25 | changed(['set', id, item]) 26 | return item 27 | } 28 | 29 | export const setItemProps = (id: string, itemProps: any) => { 30 | memoryDB[id] = { 31 | ...memoryDB[id], 32 | ...itemProps, 33 | } 34 | changed(['set', id, memoryDB[id]]) 35 | return memoryDB[id] 36 | } 37 | 38 | export const addItem = (item: Item) => { 39 | let id = guid() 40 | memoryDB[id] = item 41 | changed(['add', id, item]) 42 | return id 43 | } 44 | 45 | export const removeItem = (id: string) => { 46 | delete memoryDB[id] 47 | changed(['remove', id]) 48 | } 49 | 50 | let changeListener 51 | function changed (evData) { 52 | if (changeListener) { 53 | changeListener(evData) 54 | } 55 | save() 56 | } 57 | 58 | const save = () => localStorage.setItem('memoryDB', JSON.stringify(memoryDB)) 59 | 60 | export const getDB = () => memoryDB 61 | 62 | const getData = pattern => pattern === '*' ? memoryDB : memoryDB[pattern] 63 | 64 | export const dbTask: Handler = () => mod => { 65 | let subs = [] 66 | changeListener = evData => { 67 | for (let i = 0, sub; sub = subs[i]; i++) { 68 | mod.dispatchEv(evData, sub[2]) 69 | } 70 | } 71 | 72 | return { 73 | state: _, 74 | handle: async (id, [name, ...data]) => { 75 | if (name === 'getItem') { 76 | return getItem(data[0]) 77 | } else if (name === 'setItem') { 78 | return setItem(data[0], data[1]) 79 | } else if (name === 'setItemProps') { 80 | return setItemProps(data[0], data[1]) 81 | } else if (name === 'addItem') { 82 | return addItem(data[0]) 83 | } else if (name === 'getDB') { 84 | return getDB() 85 | } else if (name === 'remove') { 86 | removeItem(data[0]) 87 | return 'removed' 88 | } else if (name === 'subscribe') { 89 | let sub = [id].concat(data) 90 | subs.push(sub) 91 | // initial fetch 92 | return getData(sub[1]) 93 | } else if (name === 'unsubscribe') { 94 | let idx = -1 95 | for (let i = 0, sub; sub = subs[i]; i++) { 96 | if (data[0] === sub[0] && data[1] === sub[1]) { 97 | idx = i 98 | } 99 | } 100 | if (idx !== -1) { 101 | subs.splice(idx, 1) 102 | return 103 | } 104 | } else { 105 | mod.error('db handler', `Unhandled command type '${name}'`) 106 | return 107 | } 108 | }, 109 | destroy: () => {}, 110 | } 111 | } 112 | 113 | function guid () { 114 | function s4() { 115 | return Math.floor((1 + Math.random()) * 0x10000) 116 | .toString(16) 117 | .substring(1); 118 | } 119 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 120 | s4() + '-' + s4() + s4() + s4(); 121 | } 122 | -------------------------------------------------------------------------------- /src/playground/hmr.ts: -------------------------------------------------------------------------------- 1 | import { 2 | hotSwap, 3 | } from '../utils/hot-swap' 4 | 5 | declare const FuseBox 6 | 7 | if (process.env.ENV === 'development') { 8 | 9 | const customizedHMRPlugin = { 10 | hmrUpdate: async data => { 11 | if (data.type === 'js') { 12 | FuseBox.flush() 13 | FuseBox.dynamic(data.path, data.content) 14 | if (FuseBox.mainFile && data.path.includes('Root')) { 15 | let Root = await import('./Root') 16 | ;(window as any).app = await (window as any).app.moduleAPI.attach(Root, (window as any).app, hotSwap) 17 | } else if (FuseBox.mainFile) { 18 | ;(window as any).app.moduleAPI.dispose() 19 | FuseBox.import(FuseBox.mainFile) 20 | } 21 | return true 22 | } 23 | } 24 | } 25 | 26 | if (!process.env.hmrRegistered) { 27 | process.env.hmrRegistered = false 28 | FuseBox.addPlugin(customizedHMRPlugin) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Examples 5 | 6 | 7 |
8 | 9 |
10 | 11 | $bundles 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/playground/index.ts: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | import { runModule } from './module' 3 | import './hmr' 4 | import { attachAppViewer } from '../AppViewer' 5 | 6 | import * as Root from './Root' 7 | 8 | let DEV = process.env.ENV === 'development' 9 | 10 | ;(async () => { 11 | 12 | const app = await runModule(Root, DEV) 13 | ;(window as any).app = app 14 | 15 | attachAppViewer(app) 16 | 17 | // For testing purposes 18 | // ;(window as any).test = () => { 19 | // let count = 1 20 | // let interval = setInterval(async () => { 21 | // await sendMsg(app, 'Root$List', 'inputKeyup', [13, 'iteration - ' + count]) 22 | // count++ 23 | // if (count === 50) { 24 | // clearInterval(interval) 25 | // } 26 | // }) 27 | // } 28 | 29 | })() 30 | -------------------------------------------------------------------------------- /src/playground/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | run, 3 | // DEV 4 | RunModule, 5 | } from '../core' 6 | import { viewHandler } from '../interfaces/view' 7 | import { routeHandler } from '../interfaces/route' 8 | import { styleHandler } from '../groups/style' 9 | import * as DB from './db' 10 | 11 | export const runModule: RunModule = async (Root, DEV = false) => run({ 12 | Root, 13 | record: DEV, 14 | log: DEV, 15 | groups: { 16 | style: styleHandler('', DEV), 17 | }, 18 | tasks: { 19 | db: DB.dbTask(), 20 | }, 21 | interfaceOrder: ['route', 'view'], 22 | interfaces: { 23 | route: routeHandler('/', 'hash'), 24 | view: viewHandler('#app'), 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/playground/styles.css: -------------------------------------------------------------------------------- 1 | /* Global styles */ 2 | 3 | html, body { 4 | box-sizing: border-box; 5 | margin: 0px; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | #app { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | *, *:before, *:after { 15 | box-sizing: inherit; 16 | } 17 | 18 | /* App styles (fonts and other globals) */ 19 | -------------------------------------------------------------------------------- /src/simpleExample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple Example 5 | 6 | 7 |
8 | 9 |
10 | 11 | $bundles 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/simpleExample/index.ts: -------------------------------------------------------------------------------- 1 | // All in one file example 2 | import { Component, getStyle, Actions, StyleGroup, run, Inputs } from '../core' 3 | import { h, View, viewHandler } from '../interfaces/view' 4 | import { styleHandler } from '../groups/style' 5 | 6 | // Component 7 | 8 | const state = { 9 | count: 0, 10 | } 11 | 12 | type S = typeof state 13 | 14 | const inputs: Inputs = (s, F) => ({ 15 | inc: async () => { 16 | await F.toAct('Inc') 17 | setImmediate(() => { 18 | F.toIn('inc') 19 | }) 20 | }, 21 | }) 22 | 23 | const actions: Actions = { 24 | Inc: () => s => { 25 | s.count++ 26 | }, 27 | } 28 | 29 | const view: View = async (s, F) => { 30 | const style = getStyle(F) 31 | return h('div', { 32 | class: style('base'), 33 | on: { click: F.in('inc') }, 34 | }, 'Count ' + s.count) 35 | } 36 | 37 | const style: StyleGroup = { 38 | base: { 39 | color: 'green', 40 | fontSize: '40px', 41 | }, 42 | } 43 | 44 | const Root: Component = { 45 | state, 46 | inputs, 47 | actions, 48 | interfaces: { view }, 49 | groups: { style }, 50 | } 51 | 52 | const DEV = true 53 | 54 | run({ 55 | Root, 56 | record: DEV, 57 | log: DEV, 58 | groups: { 59 | style: styleHandler('', DEV), 60 | }, 61 | interfaces: { 62 | view: viewHandler('#app'), 63 | }, 64 | }) 65 | -------------------------------------------------------------------------------- /src/tasks/size.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from '../core/handler' 2 | 3 | export const sizeHandler: Handler = () => mod => ({ 4 | state: undefined, 5 | handle: async (__, [selector, prop, cb]) => { 6 | let elements = document.querySelectorAll(selector) 7 | let propValues = [] 8 | for (let i = 0, len = elements.length; i < len; i++) { 9 | let element = elements[i] 10 | let bbox = element.getBoundingClientRect() 11 | propValues.push(bbox[prop]) 12 | } 13 | mod.dispatchEv(propValues, cb) 14 | }, 15 | destroy: () => {}, 16 | }) 17 | -------------------------------------------------------------------------------- /src/tasks/view.ts: -------------------------------------------------------------------------------- 1 | // Space for view tasks 2 | -------------------------------------------------------------------------------- /src/toHTML/elements.ts: -------------------------------------------------------------------------------- 1 | 2 | // All SVG children elements, not in this list, should self-close 3 | 4 | export const CONTAINER = { 5 | // http://www.w3.org/TR/SVG/intro.html#TermContainerElement 6 | 'a': true, 7 | 'defs': true, 8 | 'glyph': true, 9 | 'g': true, 10 | 'marker': true, 11 | 'mask': true, 12 | 'missing-glyph': true, 13 | 'pattern': true, 14 | 'svg': true, 15 | 'switch': true, 16 | 'symbol': true, 17 | 'text': true, 18 | 19 | // http://www.w3.org/TR/SVG/intro.html#TermDescriptiveElement 20 | 'desc': true, 21 | 'metadata': true, 22 | 'title': true 23 | } 24 | 25 | // http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements 26 | 27 | export const VOID = { 28 | area: true, 29 | base: true, 30 | br: true, 31 | col: true, 32 | embed: true, 33 | hr: true, 34 | img: true, 35 | input: true, 36 | keygen: true, 37 | link: true, 38 | meta: true, 39 | param: true, 40 | source: true, 41 | track: true, 42 | wbr: true 43 | } 44 | -------------------------------------------------------------------------------- /src/toHTML/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { init } from './init' 3 | import modules from './modules' 4 | 5 | var toHTML = init([ 6 | modules.attributes, 7 | modules.props, 8 | modules.class, 9 | modules.style 10 | ]) 11 | 12 | export default toHTML 13 | -------------------------------------------------------------------------------- /src/toHTML/init.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as parseSelector from 'parse-sel' 3 | import { VOID as VOID_ELEMENTS } from './elements' 4 | import { CONTAINER as CONTAINER_ELEMENTS } from './elements' 5 | 6 | export function init (modules) { 7 | function parse (vnode, node) { 8 | var result = [] 9 | var attributes = new Map([ 10 | // These can be overwritten because that’s what happens in snabbdom 11 | ['id', node.id], 12 | ['class', node.className] 13 | ]) 14 | 15 | modules.forEach(function (fn, index) { 16 | fn(vnode, attributes) 17 | }) 18 | attributes.forEach(function (value, key) { 19 | if (value && value !== '') { 20 | result.push(key + '="' + value + '"') 21 | } 22 | }) 23 | 24 | return result.join(' ') 25 | } 26 | 27 | return function renderToString (vnode) { 28 | if (!vnode.sel && vnode.text) { 29 | return vnode.text 30 | } 31 | 32 | vnode.data = vnode.data || {} 33 | 34 | // Support thunks 35 | if (vnode.data.hook && 36 | typeof vnode.data.hook.init === 'function' && 37 | typeof vnode.data.fn === 'function') { 38 | vnode.data.hook.init(vnode) 39 | } 40 | 41 | var node = parseSelector(vnode.sel) 42 | var tagName = node.tagName 43 | var attributes = parse(vnode, node) 44 | var svg = vnode.data.ns === 'http://www.w3.org/2000/svg' 45 | var tag = [] 46 | 47 | if (tagName === '!') { 48 | return '' 49 | } 50 | 51 | // Open tag 52 | tag.push('<' + tagName) 53 | if (attributes.length) { 54 | tag.push(' ' + attributes) 55 | } 56 | if (svg && CONTAINER_ELEMENTS[tagName] !== true) { 57 | tag.push(' /') 58 | } 59 | tag.push('>') 60 | 61 | // Close tag, if needed 62 | if ((VOID_ELEMENTS[tagName] !== true && !svg) || 63 | (svg && CONTAINER_ELEMENTS[tagName] === true)) { 64 | if (vnode.data.props && vnode.data.props.innerHTML) { 65 | tag.push(vnode.data.props.innerHTML) 66 | } else if (vnode.text) { 67 | tag.push(vnode.text) 68 | } else if (vnode.children) { 69 | vnode.children.forEach(function (child) { 70 | tag.push(renderToString(child)) 71 | }) 72 | } 73 | tag.push('') 74 | } 75 | 76 | return tag.join('') 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/toHTML/modules/attributes.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as forOwn from 'lodash.forown' 3 | import * as escape from 'lodash.escape' 4 | 5 | // data.attrs 6 | 7 | module.exports = function attrsModule (vnode, attributes) { 8 | var attrs = vnode.data.attrs || {} 9 | 10 | forOwn(attrs, function (value, key) { 11 | attributes.set(key, escape(value)) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/toHTML/modules/class.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as forOwn from 'lodash.forown' 3 | import * as remove from 'lodash.remove' 4 | import * as uniq from 'lodash.uniq' 5 | 6 | // data.class 7 | 8 | module.exports = function classModule (vnode, attributes) { 9 | var values 10 | var _add = [] 11 | var _remove = [] 12 | var classes = vnode.data.class || {} 13 | var existing = attributes.get('class') 14 | existing = existing.length > 0 ? existing.split(' ') : [] 15 | 16 | forOwn(classes, function (value, key) { 17 | if (value === true) { 18 | _add.push(key) 19 | } else { 20 | _remove.push(key) 21 | } 22 | }) 23 | 24 | values = remove(uniq(existing.concat(_add)), function (value) { 25 | return _remove.indexOf(value) < 0 26 | }) 27 | 28 | if (values.length) { 29 | attributes.set('class', values.join(' ')) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/toHTML/modules/index.ts: -------------------------------------------------------------------------------- 1 | import * as classModule from './class' 2 | import * as props from './props' 3 | import * as attributes from './attributes' 4 | import * as style from './style' 5 | 6 | export default { 7 | class: classModule, 8 | props, 9 | attributes, 10 | style 11 | } 12 | -------------------------------------------------------------------------------- /src/toHTML/modules/props.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as forOwn from 'lodash.forown' 3 | import * as escape from 'lodash.escape' 4 | 5 | // https://developer.mozilla.org/en-US/docs/Web/API/element 6 | var omit = [ 7 | 'attributes', 8 | 'childElementCount', 9 | 'children', 10 | 'classList', 11 | 'clientHeight', 12 | 'clientLeft', 13 | 'clientTop', 14 | 'clientWidth', 15 | 'currentStyle', 16 | 'firstElementChild', 17 | 'innerHTML', 18 | 'lastElementChild', 19 | 'nextElementSibling', 20 | 'ongotpointercapture', 21 | 'onlostpointercapture', 22 | 'onwheel', 23 | 'outerHTML', 24 | 'previousElementSibling', 25 | 'runtimeStyle', 26 | 'scrollHeight', 27 | 'scrollLeft', 28 | 'scrollLeftMax', 29 | 'scrollTop', 30 | 'scrollTopMax', 31 | 'scrollWidth', 32 | 'tabStop', 33 | 'tagName' 34 | ] 35 | 36 | // data.props 37 | 38 | module.exports = function propsModule (vnode, attributes) { 39 | var props = vnode.data.props || {} 40 | 41 | forOwn(props, function (value, key) { 42 | if (omit.indexOf(key) > -1) { 43 | return 44 | } 45 | if (key === 'htmlFor') { 46 | key = 'for' 47 | } 48 | if (key === 'className') { 49 | key = 'class' 50 | } 51 | 52 | attributes.set(key.toLowerCase(), escape(value)) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/toHTML/modules/style.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assign from 'object-assign' 3 | import * as forOwn from 'lodash.forown' 4 | import * as escape from 'lodash.escape' 5 | import * as kebabCase from 'lodash.kebabcase' 6 | 7 | // data.style 8 | 9 | module.exports = function styleModule (vnode, attributes) { 10 | var values = [] 11 | var style = vnode.data.style || {} 12 | 13 | // merge in `delayed` properties 14 | if (style.delayed) { 15 | assign(style, style.delayed) 16 | } 17 | 18 | forOwn(style, function (value, key) { 19 | // omit hook objects 20 | if (typeof value === 'string' || typeof value === 'number') { 21 | var kebabKey = kebabCase(key) 22 | values.push((key.match(/^--.*/) ? '--' + kebabKey : kebabKey) + ': ' + escape(value)) 23 | } 24 | }) 25 | 26 | if (values.length) { 27 | attributes.set('style', values.join('; ')) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/aot.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-jetpack' 2 | import { renderHTML, StaticRenderOptions } from './ssr' 3 | 4 | export interface PrerenderOptions { 5 | outputFile: string 6 | cssFile?: string 7 | htmlFile: string 8 | } 9 | 10 | export async function prerender (preOp: PrerenderOptions, op: StaticRenderOptions) { 11 | try { 12 | op.encoding = op.encoding || 'utf-8' 13 | let html = fs.read(preOp.htmlFile, 'utf8') 14 | let css = preOp.cssFile ? fs.read(preOp.cssFile, 'utf8') : '' 15 | let htmlResult = await renderHTML( { 16 | ...op, 17 | html, 18 | css, 19 | }) 20 | fs.write(preOp.outputFile, htmlResult, { atomic: true }) 21 | console.log('Guardado en archivo ' + preOp.outputFile) 22 | } catch (err) { 23 | throw err 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/fun.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { 3 | assoc, 4 | evolve, 5 | evolveKey, 6 | pipe, 7 | mapToObj, 8 | merge, 9 | mapAsync, 10 | filterAsync, 11 | reduceAsync, 12 | all, 13 | range, 14 | sum, 15 | getPath, 16 | getPaths 17 | } from './fun' 18 | 19 | // Functional utils tests 20 | 21 | test('assoc', t => { 22 | 23 | let obj = {} 24 | assoc('key')('value')(obj) 25 | t.is(obj['key'], 'value', 'should assoc a value to a key in an object') 26 | 27 | }) 28 | 29 | test('evolve: should apply many functions to the values of an object given the keys', t => { 30 | 31 | let obj = { 32 | count: 0, 33 | } 34 | evolve({ 35 | count: x => x + 1, 36 | name: () => 'Fun', 37 | })(obj) 38 | t.is(obj['count'], 1) 39 | t.is(obj['name'], 'Fun') 40 | 41 | }) 42 | 43 | test('evolveKey', t => { 44 | 45 | let obj = { count: 0 } 46 | evolveKey('count')(x => x + 1)(obj) 47 | t.is( 48 | obj['count'], 49 | 1, 50 | 'should apply a function to a value of an object given the key' 51 | ) 52 | 53 | }) 54 | 55 | test('pipe function for piping functions', t => { 56 | let fun = pipe( 57 | x => x + 1, 58 | x => x + 1, 59 | x => x - 1, 60 | x => x * 2, 61 | ) 62 | 63 | t.is(fun(0), 2) 64 | t.is(fun(1), 4) 65 | t.is(fun(2), 6) 66 | t.is(fun(3), 8) 67 | 68 | }) 69 | 70 | test('mapToObj helper', t => { 71 | 72 | t.deepEqual( 73 | mapToObj([1, 2, 3], (idx, value) => ['a' + idx, `a${value}elm`]), 74 | { 75 | a0: 'a1elm', 76 | a1: 'a2elm', 77 | a2: 'a3elm', 78 | } 79 | ) 80 | 81 | }) 82 | 83 | test('merge helper', t => { 84 | 85 | t.deepEqual( 86 | merge({ a: 1, b: 2 })({ c: 3, d: 4 }), 87 | { 88 | a: 1, 89 | b: 2, 90 | c: 3, 91 | d: 4, 92 | } 93 | ) 94 | 95 | }) 96 | 97 | test('mapAsync helper', async t => { 98 | 99 | t.deepEqual( 100 | await mapAsync([1, 2, 3, 4], async (el, i, array) => [el, i]), 101 | [[1, 0], [2, 1], [3, 2], [4, 3]], 102 | ) 103 | 104 | }) 105 | 106 | test('filterAsync helper', async t => { 107 | 108 | t.deepEqual( 109 | await filterAsync([1, 2, 3, 4], async el => el < 3), 110 | [1, 2], 111 | ) 112 | 113 | }) 114 | 115 | test('reduceAsync helper', async t => { 116 | 117 | t.deepEqual( 118 | await reduceAsync([1, 2, 3, 4], async (ac, el, i) => ac.concat([el, i]), []), 119 | [1, 0, 2, 1, 3, 2, 4, 3], 120 | ) 121 | 122 | }) 123 | 124 | test('all helper', async t => { 125 | 126 | t.deepEqual( 127 | await all([1, 2, 3, 4].map(el => Promise.resolve(el))), 128 | [1, 2, 3, 4], 129 | ) 130 | 131 | }) 132 | 133 | test('range helper', t => { 134 | 135 | t.deepEqual( 136 | range(1, 4), 137 | [1, 2, 3, 4], 138 | 'Ascendant range', 139 | ) 140 | 141 | t.deepEqual( 142 | range(4, -4), 143 | [4, 3, 2, 1, 0, -1, -2, -3, -4], 144 | 'Descendant range', 145 | ) 146 | 147 | }) 148 | 149 | test('sum helper', t => { 150 | 151 | t.is( 152 | sum([1, 2, 3, 4]), 153 | 10, 154 | ) 155 | 156 | }) 157 | 158 | test('getPath helper', t => { 159 | 160 | t.is( 161 | getPath(['a', 'b', 'c', 'd'], {a:{b:{c:{ d: 10 }}}}), 162 | 10, 163 | ) 164 | 165 | }) 166 | 167 | test('getPaths helper', t => { 168 | 169 | t.deepEqual( 170 | getPaths( 171 | [ 172 | ['a', 'b', 'c', 'd'], 173 | ['z', 'x', 'y', 'w'], 174 | ['z', 'x', 'y', 't'], 175 | ], 176 | {a:{b:{c:{ d: 10 }}},z:{x:{y:{ w: 11, t: 12 }}}}, 177 | ), 178 | [10, 11, 12], 179 | ) 180 | 181 | }) 182 | 183 | -------------------------------------------------------------------------------- /src/utils/fun.ts: -------------------------------------------------------------------------------- 1 | // -- Useful Functions 2 | // Use them for building actions in a declarative and concise way 3 | 4 | export const assoc = (key: string) => (value: any) => obj => { 5 | obj[key] = value 6 | return obj 7 | } 8 | 9 | export interface FnIndex { 10 | [key: string]: { (any): any } 11 | } 12 | 13 | export const evolve = (index: FnIndex) => obj => { 14 | for (let key in index) { 15 | obj[key] = index[key](obj[key]) 16 | } 17 | return obj 18 | } 19 | 20 | export const evolveKey = (key: string) => (fn: { (any): any }) => obj => { 21 | obj[key] = fn(obj[key]) 22 | return obj 23 | } 24 | 25 | // pipe allows to pipe functions (left composing) 26 | export function pipe (...args) { 27 | return function (value) { 28 | let result = value 29 | for (let i = 0, len = args.length; i < len; i++) { 30 | result = args[i](result) 31 | } 32 | return result 33 | } 34 | } 35 | 36 | export interface KeyValuePair extends Array { 37 | 0: string 38 | 1: any 39 | } 40 | 41 | export function mapToObj (arr: U[], fn: { (idx, value?: U): KeyValuePair } ): { [key: string]: any } { 42 | let result = {}, aux 43 | for (let i = 0, len = arr.length; i < len; i++) { 44 | aux = fn(i, arr[i]) 45 | result[aux[0]] = aux[1] 46 | } 47 | return result 48 | } 49 | 50 | export function merge (objSrc) { 51 | return function (obj) { 52 | let key 53 | for (key in objSrc) { 54 | obj[key] = objSrc[key] 55 | } 56 | return obj 57 | } 58 | } 59 | 60 | import * as _deepmerge from 'deepmerge/dist/umd' 61 | 62 | export const deepmerge = _deepmerge 63 | export const deepmergeAll = _deepmerge.all 64 | 65 | export interface AsyncMapFn { 66 | (element: U, index: number, array: U[]): Promise 67 | } 68 | 69 | export const mapAsync = async (arr: U[], fn: AsyncMapFn): Promise => { 70 | let res = [] 71 | for (let i = 0, len = arr.length; i < len; i++) { 72 | res[i] = await fn(arr[i], i, arr) 73 | } 74 | return res 75 | } 76 | 77 | export interface AsyncFilterFn { 78 | (element: U, index: number, array: U[]): Promise 79 | } 80 | 81 | export const filterAsync = async (arr: U[], fn: AsyncFilterFn): Promise => { 82 | let res = [] 83 | for (let i = 0, len = arr.length; i < len; i++) { 84 | if (await fn(arr[i], i, arr)) { 85 | res.push(arr[i]) 86 | } 87 | } 88 | return res 89 | } 90 | 91 | export interface AsyncReduceFn { 92 | (acumulator: V, element: U, index: number): Promise 93 | } 94 | 95 | export const reduceAsync = async (arr: U[], fn: AsyncReduceFn, v0: V): Promise => { 96 | for (let i = 0, len = arr.length; i < len; i++) { 97 | v0 = await fn(v0, arr[i], i) 98 | } 99 | return v0 100 | } 101 | 102 | export const all = async (arr: Promise[]) => await Promise.all(arr) 103 | 104 | export const seq = async (...arr: Promise[]) => { 105 | let element 106 | for (element of arr) { 107 | await element 108 | } 109 | } 110 | 111 | export const range = (a: number, b: number) => { 112 | let res = [] 113 | if (a < b) { 114 | b++ 115 | for (; a < b; a++) { 116 | res.push(a) 117 | } 118 | } else { 119 | b-- 120 | for (; a > b; a--) { 121 | res.push(a) 122 | } 123 | } 124 | return res 125 | } 126 | 127 | export const waitMS = (ms: number) => new Promise(res => setTimeout(res, ms)) 128 | 129 | // Math 130 | 131 | export const sum = (numbers: number[]) => numbers.reduce((acc, n) => acc + n, 0) 132 | 133 | // Path helpers 134 | 135 | export const getPath = (path: string[], obj: any) => { 136 | let actual = obj 137 | for (let i = 0, len = path.length; i < len; i++) { 138 | actual = actual[path[i]] 139 | } 140 | return actual 141 | } 142 | 143 | export const getPaths = (paths: string[][], obj: any) => { 144 | let res = [] 145 | for (let i = 0, path; path = paths[i]; i++) { 146 | res[i] = getPath(path, obj) 147 | } 148 | return res 149 | } 150 | 151 | export function guid () { 152 | function s4() { 153 | return Math.floor((1 + Math.random()) * 0x10000) 154 | .toString(16) 155 | .substring(1) 156 | } 157 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4() 158 | } 159 | -------------------------------------------------------------------------------- /src/utils/hot-swap.ts: -------------------------------------------------------------------------------- 1 | import { ActionRecord } from '../core/core' 2 | import { MiddleFn, Module } from '../core/module' 3 | import { toAct } from '../core/input' 4 | 5 | export const hotSwap: MiddleFn = async (ctx, app: Module) => { 6 | let records = app.rootCtx.global.records 7 | ctx.global.records = [] 8 | ctx.global.render = false 9 | ctx.global.log = false 10 | let record: ActionRecord 11 | let comp 12 | for (let i = 0, len = records.length; i < len; i++) { 13 | record = records[i] 14 | comp = ctx.components[record.id] 15 | if (comp) { 16 | await toAct(comp)(record.actionName, record.value) 17 | } 18 | } 19 | ctx.global.render = true 20 | ctx.global.log = true 21 | app.rootCtx = ctx 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { clone, Context } from '../core' 2 | 3 | export const warn = async (source: string, description: string) => 4 | console.warn(`source: ${source}, description: ${description}`) 5 | 6 | export const error = (source: string, description: string) => { 7 | throw new Error(`source: ${source}, description: ${description}`) 8 | } 9 | 10 | export const beforeInput = (ctx: Context, inputName, data) => { 11 | if (!ctx.global.log) return 12 | let state = ctx.components[ctx.id].state 13 | if (typeof state === 'object') { 14 | state = clone(state) 15 | } 16 | console.groupCollapsed( 17 | `%c input %c${inputName} %cfrom %c${ctx.id}`, 18 | 'color: #626060; font-size: 12px;', 19 | 'color: #3b3a3a; font-size: 14px;', 20 | 'color: #626060; font-size: 12px;', 21 | 'color: #3b3a3a; font-size: 14px;' 22 | ) 23 | console.info('%c input data ', 'color: rgb(9, 157, 225); font-weight: bold;', data) 24 | console.info('%c prev state ', 'color: #AFAFAF; font-weight: bold;', state) 25 | } 26 | 27 | // color for actions (not yet implemented) #58C6F8 28 | 29 | export const afterInput = (ctx: Context, inputName, data) => { 30 | if (!ctx.global.log) return 31 | let state = ctx.components[ctx.id].state 32 | if (typeof state === 'object') { 33 | state = clone(state) 34 | } 35 | console.info('%c next state ', 'color: #3CA43F; font-weight: bold;', state) 36 | console.groupEnd() 37 | } 38 | 39 | export const logFns = { 40 | warn, 41 | error, 42 | beforeInput, 43 | afterInput, 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/ssr.ts: -------------------------------------------------------------------------------- 1 | import { Component, RunModule, Module } from '../core' 2 | import _toHTML from '../toHTML' 3 | 4 | export const toHTML = _toHTML 5 | 6 | export interface StaticRenderOptions { 7 | root: Component 8 | runModule: RunModule 9 | bundlePaths: string[], 10 | encoding?: string 11 | html?: string 12 | css?: string 13 | url?: string, // Canonical url 14 | componentNames?: any, // will be merged client-side 15 | title?: string 16 | description?: string 17 | keywords?: string 18 | author?: string 19 | extras?: string 20 | lang?: string 21 | isStatic?: boolean // is isS this means there are no need of JS at all 22 | version?: string 23 | base?: string // Base URL 24 | htmlFn? (op: StaticRenderOptions, renderData: RenderData, app: Module) // Replace default template transform function 25 | cb? (app: Module): Promise 26 | } 27 | 28 | export interface RenderData { 29 | view: any 30 | style: string 31 | } 32 | 33 | export const renderHTML = (op: StaticRenderOptions): Promise => { 34 | return new Promise((resolve, reject) => { 35 | return (async () => { 36 | try { 37 | var app = await op.runModule(op.root, false, { render: false }) 38 | if (op.cb) await op.cb(app) 39 | const Root = app.rootCtx.components.Root 40 | let view = await Root.interfaces['view'](Root.state, Root.interfaceHelpers) 41 | let styleStr = (op.css || '') + app.rootCtx.groupHandlers['style'].state.instance.getStyles() 42 | let renderData = { 43 | view, 44 | style: styleStr, 45 | } 46 | let html 47 | if (op.htmlFn) { 48 | html = await op.htmlFn(op, renderData, app) 49 | } else { 50 | html = await transformHTML(op, renderData, app) 51 | } 52 | resolve(html) 53 | } catch (err) { 54 | reject(err) 55 | } 56 | })() 57 | }) 58 | } 59 | 60 | export function transformHTML (op: StaticRenderOptions, renderData: RenderData, app: Module) { 61 | let html = op.html.replace('', _toHTML(renderData.view)) 62 | html = html.replace('', '') 63 | html = html.replace('', op.encoding || 'utf-8') 64 | html = html.replace('', op.description || '') 65 | html = html.replace('', op.keywords || '') 66 | html = html.replace('', op.author || '') 67 | html = html.replace('', op.title || '') 68 | html = html.replace('', op.url || '/') 69 | html = html.replace('', op.base || '/') 70 | let bundles = op.bundlePaths.map( 71 | p => `` 72 | ).join('') 73 | html = html.replace('', bundles) 74 | html = html.replace('', op.extras || '') 75 | html = html.replace('', op.lang || 'en') 76 | html = html.replace('', op.version || '') 77 | let components = {} 78 | let key 79 | let subkey 80 | for (key in app.rootCtx.components) { 81 | if (op.componentNames && op.componentNames.indexOf(key) === -1) { 82 | continue 83 | } 84 | components[key] = {} 85 | for (subkey in app.rootCtx.components[key]) { 86 | if (['state'].indexOf(subkey) !== -1) { 87 | // avoid cyclic structure 88 | components[key][subkey] = app.rootCtx.components[key][subkey] 89 | } 90 | } 91 | } 92 | html = html.replace('', JSON.stringify(components)) 93 | html = html.replace('', 'true') 94 | return html 95 | } 96 | -------------------------------------------------------------------------------- /src/utils/worker.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { ModuleDef } from '../core' 3 | import { runInWorker, WorkerAPI, runWorker } from './worker' 4 | 5 | test('Should run a Module in a worker', async t => { 6 | 7 | const results = { 8 | interfaceValue: 0, 9 | groupValue: 0, 10 | taskValue: 0, 11 | } 12 | 13 | const moduleDef: ModuleDef = { 14 | Root: { 15 | state: { value: 10 }, 16 | inputs: (s, F) => ({ 17 | onInit: async () => { 18 | F.task('myTask', { value: 3 }) 19 | }, 20 | }), 21 | interfaces: { 22 | myInterface: async (s, F) => ({ 23 | value: s.value, 24 | group: F.ctx.groups.myGroup.value 25 | }), 26 | }, 27 | groups: { myGroup: { value: 7 } }, 28 | }, 29 | interfaces: { 30 | myInterface: mod => ({ 31 | state: {}, 32 | handle: async (id, value) => { 33 | results.interfaceValue = value.value 34 | results.groupValue = value.group 35 | }, 36 | destroy: () => {}, 37 | }), 38 | }, 39 | groups: { 40 | myGroup: mod => ({ 41 | state: {}, 42 | handle: async (id, value) => { 43 | mod.setGroup(id, 'myGroup', { value: value.value + 1 }) 44 | }, 45 | destroy: () => {}, 46 | }), 47 | }, 48 | tasks: { 49 | myTask: mod => ({ 50 | state: {}, 51 | handle: async (id, value) => { 52 | results.taskValue = value.value 53 | }, 54 | destroy: () => {}, 55 | }), 56 | }, 57 | } 58 | 59 | // worker side 60 | 61 | const workerContext: WorkerAPI = { 62 | postMessage: msg => setTimeout(() => worker.onmessage({ data: msg })), 63 | } 64 | 65 | runInWorker(moduleDef, undefined, workerContext) 66 | 67 | // Main thread 68 | 69 | const worker: WorkerAPI = { 70 | postMessage: msg => setTimeout(() => workerContext.onmessage({ data: msg })), 71 | } 72 | 73 | await runWorker({ 74 | worker, 75 | ...moduleDef 76 | }) 77 | 78 | t.is(results.interfaceValue, 10, 'should execute interface') 79 | t.is(results.groupValue, 8, 'should execute group') 80 | t.is(results.taskValue, 3, 'should execute task') 81 | 82 | }) 83 | -------------------------------------------------------------------------------- /src/utils/worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HandlerObject, 3 | ModuleAPI, 4 | InputData, 5 | ModuleDef, 6 | EventData, 7 | Module, 8 | run, 9 | clone, 10 | handlerTypes, 11 | } from '../core' 12 | 13 | declare var self: WorkerAPI 14 | 15 | export interface WorkerAPI { 16 | postMessage (value: any): void 17 | onmessage? (ev: WorkerEvent): void 18 | } 19 | 20 | export interface WorkerEvent { 21 | data: any 22 | } 23 | 24 | export interface SyncQueue { 25 | queue: Waiter[] 26 | addWaiter(Waiter): void 27 | next(data): void 28 | } 29 | 30 | export interface Waiter { 31 | (data): boolean 32 | } 33 | 34 | export function makeSyncQueue (): SyncQueue { 35 | let queue: Waiter[] = [] 36 | return { 37 | queue, 38 | addWaiter (waiter) { 39 | queue.push(waiter) 40 | }, 41 | next (data) { 42 | if (queue[0] && queue[0](data)) { 43 | queue.shift() 44 | } 45 | }, 46 | } 47 | } 48 | 49 | export const workerHandler = (type: 'interface' | 'task' | 'group', name: string, syncQueue: SyncQueue, workerAPI?: WorkerAPI) => (mod: ModuleAPI) => { 50 | let _self = workerAPI ? workerAPI : self 51 | let waiter 52 | return { 53 | state: undefined, 54 | handle: async (id, value) => { 55 | if (type === 'group') { 56 | waiter = new Promise((resolve) => { 57 | syncQueue.addWaiter(data => { 58 | if (data[0] === 'setGroup') { 59 | resolve() 60 | return true 61 | } 62 | }) 63 | }) 64 | } 65 | _self.postMessage([type, name, 'handle', id, value]) 66 | return waiter 67 | }, 68 | destroy: () => { 69 | _self.postMessage([type, name, 'destroy']) 70 | }, 71 | } 72 | } 73 | 74 | export const workerLog = (type: 'warn' | 'error', workerAPI?: WorkerAPI) => { 75 | let _self = workerAPI ? workerAPI : self 76 | return (source: string, description: string) => { 77 | _self.postMessage(['log', type, source, description]) 78 | } 79 | } 80 | 81 | // receives messages from runWorker in the WorkerSide 82 | export const createWorkerListener = (syncQueue: SyncQueue, workerAPI?: WorkerAPI): any => (mod: ModuleAPI) => { 83 | let _self = workerAPI ? workerAPI : self 84 | // allows to dispatch inputs from the main thread 85 | _self.onmessage = ev => { 86 | let data = ev.data 87 | switch (data[0]) { 88 | case 'dispatchEv': 89 | mod.dispatchEv(data[1], data[2]) 90 | break 91 | case 'dispatch': 92 | mod.dispatch(data[1]) 93 | break 94 | case 'toComp': 95 | mod.toComp(data[1], data[2], data[3]) 96 | break 97 | case 'setGroup': 98 | mod.setGroup(data[1], data[2], data[3]) 99 | break 100 | case 'task': 101 | mod.task(data[1], data[2]) 102 | break 103 | case 'destroy': 104 | mod.destroy() 105 | _self.postMessage(['destroy']) 106 | break 107 | case 'nest': 108 | // not implemented yet, should deserialize a component with a safe eval 109 | mod.error('workerListener', `unimplemented method`) 110 | break 111 | case 'nestAll': 112 | // not implemented yet, should deserialize a list of components with a safe eval 113 | mod.error('workerListener', `unimplemented method`) 114 | break 115 | case 'unnest': 116 | // not implemented yet, should deserialize a component with a safe eval 117 | mod.error('workerListener', `unimplemented method`) 118 | break 119 | case 'unnestAll': 120 | // not implemented yet, should deserialize a list of components with a safe eval 121 | mod.error('workerListener', `unimplemented method`) 122 | break 123 | default: 124 | mod.error('workerListener', `unknown message type recived from worker: ${data.join(', ')}`) 125 | } 126 | syncQueue.next(data) 127 | } 128 | } 129 | 130 | export interface WorkerModuleDef extends ModuleDef { 131 | worker: any 132 | Root: any 133 | } 134 | 135 | export interface WorkerModule { 136 | worker: WorkerAPI 137 | moduleAPI: ModuleAPI 138 | groupObjects: { [name: string]: HandlerObject } 139 | taskObjects: { [name: string]: HandlerObject } 140 | interfaceObjects: { [name: string]: HandlerObject } 141 | } 142 | 143 | export async function runWorker (def: WorkerModuleDef): Promise { 144 | let worker: WorkerAPI = def.worker 145 | 146 | let groupObjects: { [name: string]: HandlerObject } = {} 147 | let taskObjects: { [name: string]: HandlerObject } = {} 148 | let interfaceObjects: { [name: string]: HandlerObject } = {} 149 | 150 | let attach: any = async comp => { 151 | def.error('reattach', 'unimplemented method') 152 | } 153 | 154 | // const eventHandlerRegister = {} 155 | 156 | // API for modules (Main Thread) 157 | let moduleAPI: ModuleAPI = { 158 | on: (eventName, eventData, pullable) => ['ev', 12],// worker.postMessage(['on', eventName, eventData, pullable]), 159 | off: descriptor => worker.postMessage(['off', descriptor]), 160 | emit: (eventName, data) => new Promise(() => {}), // worker.postMessage(['emit', eventName, data]), 161 | // dispatch function type used for handlers 162 | dispatchEv: async (event: any, iData: InputData) => worker.postMessage(['dispatchEv', event, iData]), 163 | dispatch: async (eventData: EventData) => worker.postMessage(['dispatch', eventData]), 164 | toComp: async (id: string, inputName: string, data: any) => 165 | worker.postMessage(['toComp', id, inputName, data]), 166 | destroy, 167 | attach, 168 | // delegated methods 169 | setGroup: (id, name, group) => worker.postMessage(['setGroup', id, name, group]), 170 | task: async (name: string, data?: any) => worker.postMessage(['task', name, data]), 171 | warn: def.warn, 172 | error: def.error, 173 | } 174 | if (def.groups) { 175 | for (let i = 0, names = Object.keys(def.groups), len = names.length ; i < len; i++) { 176 | groupObjects[names[i]] = await (await def.groups[names[i]])(moduleAPI) 177 | } 178 | } 179 | if (def.tasks) { 180 | for (let i = 0, names = Object.keys(def.tasks), len = names.length ; i < len; i++) { 181 | taskObjects[names[i]] = await (await def.tasks[names[i]])(moduleAPI) 182 | } 183 | } 184 | if (def.interfaces) { 185 | for (let i = 0, names = Object.keys(def.interfaces), len = names.length ; i < len; i++) { 186 | interfaceObjects[names[i]] = await (await def.interfaces[names[i]])(moduleAPI) 187 | } 188 | } 189 | 190 | let initTrap 191 | 192 | worker.onmessage = async ev => { 193 | let data = ev.data 194 | switch (data[0]) { 195 | case 'initialized': 196 | initTrap() 197 | break 198 | case 'interface': 199 | if (data[2] === 'handle') { 200 | await interfaceObjects[data[1]].handle('Root', data[4]) 201 | break 202 | } else if (data[2] === 'destroy') { 203 | interfaceObjects[data[1]].destroy() 204 | break 205 | } 206 | case 'task': 207 | if (data[2] === 'handle') { 208 | await taskObjects[data[1]].handle(data[3], data[4]) 209 | break 210 | } else if (data[2] === 'destroy') { 211 | await taskObjects[data[1]].destroy() 212 | break 213 | } 214 | case 'group': 215 | if (data[2] === 'handle') { 216 | await groupObjects[data[1]].handle(data[3], data[4]) 217 | break 218 | } else if (data[2] === 'destroy') { 219 | groupObjects[data[1]].destroy() 220 | break 221 | } 222 | case 'log': 223 | if (moduleAPI[data[1]]) { 224 | moduleAPI[data[1]](data[2], data[3]) 225 | break 226 | } 227 | case 'destroy': 228 | if (def.onDestroy) { 229 | def.onDestroy(moduleAPI) 230 | } 231 | break 232 | default: 233 | moduleAPI.error('runWorker', `unknown message type recived from worker: ${data.join(', ')}`) 234 | } 235 | } 236 | 237 | await new Promise(resolve => { 238 | initTrap = resolve 239 | }) 240 | 241 | function destroy () { 242 | worker.postMessage(['destroy']) 243 | } 244 | 245 | return { 246 | worker, 247 | moduleAPI, 248 | groupObjects, 249 | taskObjects, 250 | interfaceObjects, 251 | } 252 | } 253 | 254 | export interface ExceptionsObject { 255 | interfaces: string[] 256 | tasks: string[] 257 | groups: string[] 258 | } 259 | 260 | export const runInWorker = async (moduleDef: ModuleDef, exceptions?: ExceptionsObject, workerAPI?: WorkerAPI): Promise => { 261 | let _self = workerAPI ? workerAPI : self 262 | const syncQueue = makeSyncQueue() 263 | const workerModule: ModuleDef = clone(moduleDef) 264 | const workerListener = createWorkerListener(syncQueue, workerAPI) 265 | // Inject into onBeforeInit hook 266 | workerModule.onBeforeInit 267 | = moduleDef.onBeforeInit 268 | ? mod => { 269 | workerListener(mod) 270 | workerModule.onBeforeInit(mod) 271 | } 272 | : workerListener 273 | 274 | // Make a proxy for handler inside the worker for comunicating to the main thread 275 | let handlerType, handlerName, handlerTypePlural 276 | for (handlerType of handlerTypes) { 277 | handlerTypePlural = handlerType + 's' 278 | for (handlerName in moduleDef[handlerTypePlural]) { 279 | if (exceptions && exceptions[handlerTypePlural].indexOf(handlerName) === -1 || !exceptions) { 280 | workerModule[handlerTypePlural][handlerName] = workerHandler(handlerType, handlerName, syncQueue, workerAPI) 281 | } 282 | } 283 | } 284 | 285 | const mod = await run(workerModule) 286 | _self.postMessage(['initialized']) 287 | 288 | return mod 289 | } 290 | -------------------------------------------------------------------------------- /src/workerExample/Root.ts: -------------------------------------------------------------------------------- 1 | import { Inputs, Interfaces, StyleGroup, Actions, getStyle } from '../core' 2 | import { View, h } from '../interfaces/view' 3 | 4 | export const state = { 5 | count: 0, 6 | } 7 | 8 | export type S = typeof state 9 | 10 | export const inputs: Inputs = (s, F) => ({ 11 | inc: async () => { 12 | if (s.count === 0) { 13 | console.time('time') 14 | } 15 | await F.toAct('Inc') 16 | if (s.count < 200) { 17 | setImmediate(() => { 18 | F.toIn('inc') 19 | }) 20 | } else { 21 | console.timeEnd('time') 22 | } 23 | }, 24 | }) 25 | 26 | export const actions: Actions = { 27 | Inc: () => s => { 28 | let a = 100 29 | for (let i = 0; i < 5000; i++) { 30 | for (let j = 0; j < 1000; j++) { 31 | a++ 32 | a = a * 10 -1 33 | a = Math.round((a - 1) / 10) 34 | } 35 | } 36 | a = s.count 37 | s.count = a 38 | s.count++ 39 | }, 40 | } 41 | 42 | const view: View = async (s, F) => { 43 | const style = getStyle(F) 44 | return h('div', { class: style('base') }, [ 45 | h('div', { 46 | class: style('count'), 47 | on: { click: F.in('inc') }, 48 | }, 'Count ' + s.count), 49 | // Just for testing expirience 50 | h('button', 'hello'), 51 | h('select', [ 52 | h('option', '1. One'), 53 | h('option', '2. Two'), 54 | h('option', '3. Three'), 55 | h('option', '4. Four'), 56 | ]), 57 | h('div', { class: style('hoverable') }, 'hover me 1'), 58 | h('div', { class: style('hoverable') }, 'hover me 2'), 59 | ]) 60 | } 61 | 62 | export const interfaces: Interfaces = { view } 63 | 64 | const style: StyleGroup = { 65 | base: { 66 | width: '100%', 67 | height: '100%', 68 | }, 69 | count: { 70 | color: 'green', 71 | fontSize: '40px', 72 | }, 73 | hoverable: { 74 | margin: '100px', 75 | width: '100%', 76 | height: '100vh', 77 | backgroundColor: '#17619b', 78 | transition: 'background-color 0.4s', 79 | $nest: { 80 | '&:hover': { 81 | backgroundColor: '#3882bd', 82 | }, 83 | }, 84 | }, 85 | } 86 | 87 | export const groups = { style } 88 | -------------------------------------------------------------------------------- /src/workerExample/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Worker Example 5 | 6 | 7 |
8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/workerExample/index.ts: -------------------------------------------------------------------------------- 1 | import { run } from '../core' 2 | import { moduleDef } from './module' 3 | import { runWorker } from '../utils/worker' 4 | 5 | // TODO: make this variable dynamic, implement a toggle button for that 6 | const runInWorker = true 7 | 8 | if (runInWorker) { 9 | // Running Fractal in a worker thread 10 | runWorker({ 11 | worker: new Worker('worker.js'), 12 | ...moduleDef, 13 | }) 14 | } else { 15 | // Running Fractal in the main thread 16 | run(moduleDef) 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/workerExample/module.ts: -------------------------------------------------------------------------------- 1 | import { ModuleDef } from '../core' 2 | import { styleHandler } from '../groups/style' 3 | import { viewHandler } from '../interfaces/view' 4 | import * as Root from './Root' 5 | 6 | const DEV = true 7 | 8 | export const moduleDef: ModuleDef = { 9 | Root, 10 | record: DEV, 11 | log: DEV, 12 | groups: { 13 | style: styleHandler('', DEV), 14 | }, 15 | interfaces: { 16 | view: viewHandler('#app'), 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/workerExample/worker.ts: -------------------------------------------------------------------------------- 1 | import { moduleDef } from './module' 2 | import { runInWorker } from '../utils/worker' 3 | 4 | runInWorker(moduleDef) 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "outDir": "./", 5 | "declaration": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "removeComments": false, 10 | "noUnusedLocals": true, 11 | "typeRoots": [ 12 | "./node_modules/@types" 13 | ], 14 | "lib": ["dom", "es5", "es2015", "scripthost"] 15 | }, 16 | "formatCodeOptions": { 17 | "indentSize": 2, 18 | "tabSize": 2 19 | }, 20 | "files": [ 21 | "src/core/index.ts" 22 | ], 23 | "include": [ 24 | "src/**/*.ts", 25 | "src/**/*.spec.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------