├── .gitignore ├── .prettierrc ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── docs ├── assets.json ├── index.html └── static │ └── js │ ├── 15.854c9d6f.js │ ├── 15.e2ac49079e2d157a0b49.js.map │ ├── app.c7d38faa.js │ ├── app.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-00-about.33cb42e7.js │ ├── docs-source-0-introduction-00-about.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-05-installation.a5841abe.js │ ├── docs-source-0-introduction-05-installation.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-10-getting-started.2b91f695.js │ ├── docs-source-0-introduction-10-getting-started.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-20-objects-and-factories.0928d7eb.js │ ├── docs-source-0-introduction-20-objects-and-factories.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-30-with-react.7cb56417.js │ ├── docs-source-0-introduction-30-with-react.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-40-philosophy.e2ac49079e2d157a0b49.js.map │ ├── docs-source-0-introduction-40-philosophy.fea48fa9.js │ ├── docs-source-1-advanced-examples.85d2c45c.js │ ├── docs-source-1-advanced-examples.e2ac49079e2d157a0b49.js.map │ ├── docs-source-1-advanced-models.d824a22c.js │ ├── docs-source-1-advanced-models.e2ac49079e2d157a0b49.js.map │ ├── docs-source-1-advanced-preprocessors.2cd0bb51.js │ ├── docs-source-1-advanced-preprocessors.e2ac49079e2d157a0b49.js.map │ ├── pkgs-core-readme.4285a63b.js │ ├── pkgs-core-readme.e2ac49079e2d157a0b49.js.map │ ├── pkgs-react-readme.e1c9a28e.js │ ├── pkgs-react-readme.e2ac49079e2d157a0b49.js.map │ ├── pkgs-updaters-readme.429519e2.js │ ├── pkgs-updaters-readme.e2ac49079e2d157a0b49.js.map │ ├── runtime~app.e2ac49079e2d157a0b49.js │ ├── runtime~app.e2ac49079e2d157a0b49.js.map │ ├── vendors.15c669d7.js │ └── vendors.e2ac49079e2d157a0b49.js.map ├── docs_source ├── 0_introduction │ ├── 00_about.mdx │ ├── 05_installation.mdx │ ├── 10_getting-started.mdx │ ├── 20_objects-and-factories.mdx │ ├── 30_with-react.mdx │ └── 60_philosophy.mdx ├── 1_convenience │ ├── 40_preprocessors.mdx │ ├── 50_updaters.mdx │ └── models.mdx └── 2_advanced │ ├── examples.mdx │ ├── mobx.mdx │ ├── organizing_state.mdx │ └── performance.mdx ├── doczrc.js ├── examples └── boxes │ ├── .babelrc │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── components │ │ ├── arrow-view.js │ │ ├── box-view.js │ │ ├── canvas.js │ │ ├── fun-stuff.js │ │ └── sidebar.js │ ├── index.css │ ├── index.js │ └── stores │ │ ├── domain-state.js │ │ └── time.js │ └── yarn.lock ├── jest.config.json ├── jest.config.test.json ├── lerna.json ├── package.json ├── pkgs ├── core │ ├── README.mdx │ ├── core.ts │ ├── package.json │ └── tests │ │ ├── base.spec.ts │ │ ├── config.spec.ts │ │ ├── object.spec.ts │ │ ├── perf │ │ ├── index.js │ │ ├── perf.js │ │ └── perf.txt │ │ └── scheduling.spec.ts ├── models │ ├── README.mdx │ ├── models.ts │ ├── package.json │ └── tests │ │ └── model.spec.ts ├── react │ ├── README.mdx │ ├── package.json │ ├── react.ts │ └── tests │ │ ├── rview.spec.tsx │ │ └── useval.spec.tsx ├── types │ ├── README.mdx │ ├── package.json │ ├── tests │ │ ├── __snapshots__ │ │ │ ├── sarcastic.spec.ts.snap │ │ │ └── types.spec.ts.snap │ │ ├── sarcastic.spec.ts │ │ └── types.spec.ts │ └── types.ts ├── updaters │ ├── README.mdx │ ├── package.json │ ├── tests │ │ └── updaters.spec.ts │ └── updaters.ts └── utils │ ├── README.mdx │ ├── package.json │ ├── tests │ └── utils.spec.ts │ └── utils.ts ├── scripts ├── build.js ├── link.js └── package-template.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | dist 4 | .rpt2_cache 5 | .rts2_cache* 6 | /coverage 7 | .docz 8 | mangle.json 9 | lerna-debug.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | env: 5 | - NODE_ENV=TEST 6 | cache: 7 | yarn: true 8 | directories: 9 | - 'node_modules' 10 | script: 11 | - yarn coverage 12 | - yarn build 13 | - yarn test 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | // Note; this config requires node 8.4 or higher 9 | "type": "node", 10 | "protocol": "auto", 11 | "request": "launch", 12 | "name": "debug unit test", 13 | "stopOnEntry": false, 14 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 15 | "args": ["--verbose", "--runInBand", "--config", "jest.config.json", "-i", "${fileBasename}"], 16 | "windows": { 17 | "program": "${workspaceRoot}\\node_modules\\jest-cli\\bin\\jest.js" 18 | 19 | // "args": ["--verbose", "--runInBand", "--config", "jest.config.json"] 20 | }, 21 | "runtimeArgs": ["--nolazy"] 22 | }, 23 | { 24 | // Note; this config requires node 8.4 or higher 25 | "type": "node", 26 | "protocol": "inspector", 27 | "request": "launch", 28 | "name": "debug unit test - minified", 29 | "stopOnEntry": false, 30 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 31 | "windows": { 32 | "program": "${workspaceRoot}\\node_modules\\jest-cli\\bin\\jest.js" 33 | }, 34 | "outFiles": ["pkgs/core/"], 35 | "args": ["--verbose", "--runInBand", "--config", "jest.config.test.json", "-i", "${fileBasename}"], 36 | "runtimeArgs": ["--nolazy"], 37 | "sourceMaps": true 38 | }, 39 | { 40 | "type": "node", 41 | "request": "launch", 42 | "name": "Launch Program", 43 | "program": "${file}", 44 | "sourceMaps": true 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michel Weststrate 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RVal 2 | 3 | 4 | [![npm](https://img.shields.io/npm/v/rval.svg)](https://www.npmjs.com/package/rval) [![size](http://img.badgesize.io/https://unpkg.com/rval/dist/core.mjs?compression=gzip)](http://img.badgesize.io/https://unpkg.com/rval/dist/core.mjs?compression=gzip) [![Build Status](https://travis-ci.org/mweststrate/rval.svg?branch=master)](https://travis-ci.org/mweststrate/rval) [![Coverage Status](https://coveralls.io/repos/github/mweststrate/rval/badge.svg?branch=master)](https://coveralls.io/github/mweststrate/rval?branch=master) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/michelweststrate) [![Donate](https://img.shields.io/badge/donate-buy%20me%20a%20coffee-orange.svg)](https://www.buymeacoffee.com/mweststrate) 5 | 6 | 7 | Docs in progress: 8 | 9 | https://mweststrate.github.io/rval 10 | 11 | STATE DESIGN 12 | 13 | mutability granularity 14 | refs 15 | 16 | preprocessor validation 17 | 18 | preprocessor objects 19 | 20 | preprocessors 21 | - validation 22 | - conversion 23 | - equality checks 24 | - models 25 | - combing them 26 | 27 | 28 | models 29 | 30 | 31 | async 32 | 33 | 34 | # Api 35 | 36 | ## `val` 37 | 38 | ## `sub` 39 | 40 | ## `drv` 41 | 42 | ## `batch` 43 | 44 | ## batched: 45 | 46 | ## effect 47 | 48 | ## Immutability and freezing 49 | 50 | ## Working with objects 51 | 52 | ## Working with arrays 53 | 54 | ## Object models 55 | 56 | ## Scheduling details 57 | 58 | ## Private context 59 | 60 | ## Strictness options 61 | 62 | # API 63 | 64 | 65 | Tips: 66 | - subscribe before read, or use `fireImmediately` 67 | - typing self-object referring derivations 68 | - share methouds by pulling out / `this` / prototype or Object.create (add tests!) 69 | - dependency injection through type generation in closure 70 | - maps versus array entries 71 | - comparison preprocessors 72 | 73 | Differences with MobX: 74 | 75 | - No 2 phase tracking, slower, but enables custom scheduling of computations 76 | - Clear mutability / immutablility story 77 | - No object modification, decorators, cloning 78 | - small, with isolated tracking, fit for in-library usage 79 | 80 | Patterns 81 | 82 | - objects 83 | - objects with models 84 | - arrays 85 | - maps 86 | - serialization, deserialization 87 | - capturing parent ref (see test "todostore - with parent") 88 | - with react 89 | - with immer (`v(p(v(), draft => { })))`) 90 | - working with references 91 | 92 | Comparison with mobx 93 | - factory + getter / setters -> observable. More convenient, but, pit of success 94 | - sub(drv(x), noop) === autorun(x) 95 | - more scheduling control; effect 96 | 97 | Comparison with Rx 98 | - focus on values, not events 99 | - push / pull vs. push 100 | - transparent tracking 101 | 102 | Todo: 103 | 104 | * [x] build all the packages 105 | * [x] generate types `yarn tsc index.ts -t es2015 -d --outDir dist && mv dist/index.d.ts dist/rval.d.ts && rm dist/index.js &&` 106 | * [x] test against generated packages 107 | * [x] setup CI 108 | * [x] ~`sub({ scheduler, onInvalidate(f (track)))})`~ -> `effect` 109 | * [x] setup coveralls 110 | * [x] rval-models 111 | * [x] rval-react 112 | * [x] rval-immer 113 | * [x] custom schedulers 114 | * [x] custom preprocessors 115 | * [x] eliminate Reaction class 116 | * [x] setup minification with minified class members 117 | * [x] swap export statement in `tests/rval.ts` in CI to test minified build 118 | * [x] mobx like evaluation order of drv 119 | * [x] `drv` with setter 120 | * [x] combine preprocessor array 121 | * [x] support currying for sub: `sub(listener)(val)` 122 | * [x] rename RvalContext to RvalInstance 123 | * [x] support `this.rvalProps(this.rvalProps() + 1)` -> `this.rvalProps(x => x + 1)`? 124 | * [x] re-enable minification ootb 125 | * [x] fix sourcemaps for minified builds 126 | * [x] use prop mangling for smaller builds 127 | * [x] fast class / object test 128 | * [x] updaters `inc1`, `inc`, `push`, `set`, `delete`, `assign`, `toggle` 129 | * [x] utils `assignVals`, `toJS 130 | * [x] setter for `drv`? 131 | * [x] host docs 132 | * [x] check https://reactpixi.org/#/stage / https://docs.setprotocol.com/#/#support-and-community- for setup of edit button, menu nesting, hosting 133 | * [x] `sub`, pass in previous value as second argument 134 | * [x] implement `SubscribeOptions` 135 | * [x] keepAlive drv option, using effect 136 | * [x] publish all script 137 | * [x] tests and types for utils 138 | * [x] kill with-immmer? 139 | * [x] improve updaters typings 140 | * [x] verify callign actions in reactions work correctly 141 | * [x] move `invariant` to preprocessors? 142 | * [x] add `toJS` on all model types 143 | * [x] rval-validation 144 | * [x] kill `run` 145 | * [x] fix debugging with minification 146 | * [x] use yalc? https://www.google.com/url?q=https%3A%2F%2Fgithub.com%2Fwhitecolor%2Fyalc%2F&sa=D&sntz=1&usg=AFQjCNGCTXoCduIMdVHx5xm-uAs_REX3MA 147 | * [ ] add missing mobx optimizations 148 | * [ ] contributing and debugging 149 | * [ ] docs 150 | * [ ] add `reference` to models? 151 | * [ ] contributing & debugging guide. `reserved` section in package.json! 152 | * [ ] add (mobx like) performance tests 153 | * [ ] rval.js.org CDN 154 | * [ ] smart lack of act detection. Only have `act`, no `run`? 155 | * [ ] rename MDX files to md 156 | * [ ] rview as wrapper 157 | * [ ] deep merge model tree? 158 | * [ ] RVAL return this in setter for chaining? 159 | * [ ] cheat sheet 160 | * [ ] efficient map structure 161 | * [ ] find neat solution to globally shared instance 162 | 163 | Later 164 | * [ ] rval-remote 165 | * [ ] config: warn on unbatched writes 166 | * [ ] config: warn on untracked, stale reads 167 | * [ ] strict mode: only reads from actions or reactions. Only updates from actions. 168 | * [ ] eliminate classes from code base 169 | * [ ] `drv(( tick ) => ())` to communicate staleness from inside drv (probably also needs onHot / onCold callback in such case) 170 | * [ ] dynamically switch between hook and non-hook implementations (and explain differences) 171 | * [ ] support name option 172 | * [ ] abstraction for creating drv / vals and subscribing in hook based component automatically? 173 | * [ ] MobX global state compatibility? -------------------------------------------------------------------------------- /docs/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.js": "/rval/static/js/app.c7d38faa.js", 3 | "app.js.map": "/rval/static/js/app.e2ac49079e2d157a0b49.js.map", 4 | "docs-source-0-introduction-00-about.js": "/rval/static/js/docs-source-0-introduction-00-about.33cb42e7.js", 5 | "docs-source-0-introduction-00-about.js.map": "/rval/static/js/docs-source-0-introduction-00-about.e2ac49079e2d157a0b49.js.map", 6 | "docs-source-0-introduction-05-installation.js": "/rval/static/js/docs-source-0-introduction-05-installation.a5841abe.js", 7 | "docs-source-0-introduction-05-installation.js.map": "/rval/static/js/docs-source-0-introduction-05-installation.e2ac49079e2d157a0b49.js.map", 8 | "docs-source-0-introduction-10-getting-started.js": "/rval/static/js/docs-source-0-introduction-10-getting-started.2b91f695.js", 9 | "docs-source-0-introduction-10-getting-started.js.map": "/rval/static/js/docs-source-0-introduction-10-getting-started.e2ac49079e2d157a0b49.js.map", 10 | "docs-source-0-introduction-20-objects-and-factories.js": "/rval/static/js/docs-source-0-introduction-20-objects-and-factories.0928d7eb.js", 11 | "docs-source-0-introduction-20-objects-and-factories.js.map": "/rval/static/js/docs-source-0-introduction-20-objects-and-factories.e2ac49079e2d157a0b49.js.map", 12 | "docs-source-0-introduction-30-with-react.js": "/rval/static/js/docs-source-0-introduction-30-with-react.7cb56417.js", 13 | "docs-source-0-introduction-30-with-react.js.map": "/rval/static/js/docs-source-0-introduction-30-with-react.e2ac49079e2d157a0b49.js.map", 14 | "docs-source-0-introduction-40-philosophy.js": "/rval/static/js/docs-source-0-introduction-40-philosophy.fea48fa9.js", 15 | "docs-source-0-introduction-40-philosophy.js.map": "/rval/static/js/docs-source-0-introduction-40-philosophy.e2ac49079e2d157a0b49.js.map", 16 | "docs-source-1-advanced-examples.js": "/rval/static/js/docs-source-1-advanced-examples.85d2c45c.js", 17 | "docs-source-1-advanced-examples.js.map": "/rval/static/js/docs-source-1-advanced-examples.e2ac49079e2d157a0b49.js.map", 18 | "docs-source-1-advanced-models.js": "/rval/static/js/docs-source-1-advanced-models.d824a22c.js", 19 | "docs-source-1-advanced-models.js.map": "/rval/static/js/docs-source-1-advanced-models.e2ac49079e2d157a0b49.js.map", 20 | "docs-source-1-advanced-preprocessors.js": "/rval/static/js/docs-source-1-advanced-preprocessors.2cd0bb51.js", 21 | "docs-source-1-advanced-preprocessors.js.map": "/rval/static/js/docs-source-1-advanced-preprocessors.e2ac49079e2d157a0b49.js.map", 22 | "pkgs-core-readme.js": "/rval/static/js/pkgs-core-readme.4285a63b.js", 23 | "pkgs-core-readme.js.map": "/rval/static/js/pkgs-core-readme.e2ac49079e2d157a0b49.js.map", 24 | "pkgs-react-readme.js": "/rval/static/js/pkgs-react-readme.e1c9a28e.js", 25 | "pkgs-react-readme.js.map": "/rval/static/js/pkgs-react-readme.e2ac49079e2d157a0b49.js.map", 26 | "pkgs-updaters-readme.js": "/rval/static/js/pkgs-updaters-readme.429519e2.js", 27 | "pkgs-updaters-readme.js.map": "/rval/static/js/pkgs-updaters-readme.e2ac49079e2d157a0b49.js.map", 28 | "runtime~app.js": "/rval/static/js/runtime~app.e2ac49079e2d157a0b49.js", 29 | "runtime~app.js.map": "/rval/static/js/runtime~app.e2ac49079e2d157a0b49.js.map", 30 | "vendors.js": "/rval/static/js/vendors.15c669d7.js", 31 | "vendors.js.map": "/rval/static/js/vendors.e2ac49079e2d157a0b49.js.map", 32 | "static/js/15.854c9d6f.js": "/rval/static/js/15.854c9d6f.js", 33 | "static/js/15.e2ac49079e2d157a0b49.js.map": "/rval/static/js/15.e2ac49079e2d157a0b49.js.map", 34 | "index.html": "/rval/index.html" 35 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Rval
-------------------------------------------------------------------------------- /docs/static/js/15.854c9d6f.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[15],{"./.docz/app/imports.js":function(n,o,t){"use strict";t.r(o),t.d(o,"imports",function(){return d});var d={"docs_source/0_introduction/00_about.mdx":function(){return t.e(1).then(t.bind(null,"./docs_source/0_introduction/00_about.mdx"))},"docs_source/0_introduction/05_installation.mdx":function(){return t.e(2).then(t.bind(null,"./docs_source/0_introduction/05_installation.mdx"))},"docs_source/0_introduction/10_getting-started.mdx":function(){return t.e(3).then(t.bind(null,"./docs_source/0_introduction/10_getting-started.mdx"))},"docs_source/0_introduction/20_objects-and-factories.mdx":function(){return t.e(4).then(t.bind(null,"./docs_source/0_introduction/20_objects-and-factories.mdx"))},"docs_source/0_introduction/30_with-react.mdx":function(){return t.e(5).then(t.bind(null,"./docs_source/0_introduction/30_with-react.mdx"))},"docs_source/0_introduction/40_philosophy.mdx":function(){return t.e(6).then(t.bind(null,"./docs_source/0_introduction/40_philosophy.mdx"))},"docs_source/1_advanced/examples.mdx":function(){return t.e(7).then(t.bind(null,"./docs_source/1_advanced/examples.mdx"))},"docs_source/1_advanced/models.mdx":function(){return t.e(8).then(t.bind(null,"./docs_source/1_advanced/models.mdx"))},"docs_source/1_advanced/preprocessors.mdx":function(){return t.e(9).then(t.bind(null,"./docs_source/1_advanced/preprocessors.mdx"))},"pkgs/core/README.mdx":function(){return t.e(10).then(t.bind(null,"./pkgs/core/README.mdx"))},"pkgs/react/README.mdx":function(){return t.e(11).then(t.bind(null,"./pkgs/react/README.mdx"))},"pkgs/updaters/README.mdx":function(){return t.e(12).then(t.bind(null,"./pkgs/updaters/README.mdx"))}}}},0,[1,2,3,4,5,6,7,8,9,10,11,12]]); 2 | //# sourceMappingURL=15.e2ac49079e2d157a0b49.js.map -------------------------------------------------------------------------------- /docs/static/js/15.e2ac49079e2d157a0b49.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./.docz/app/imports.js"],"names":["__webpack_require__","r","__webpack_exports__","d","imports","docs_source/0_introduction/00_about.mdx","e","then","bind","docs_source/0_introduction/05_installation.mdx","docs_source/0_introduction/10_getting-started.mdx","docs_source/0_introduction/20_objects-and-factories.mdx","docs_source/0_introduction/30_with-react.mdx","docs_source/0_introduction/40_philosophy.mdx","docs_source/1_advanced/examples.mdx","docs_source/1_advanced/models.mdx","docs_source/1_advanced/preprocessors.mdx","pkgs/core/README.mdx","pkgs/react/README.mdx","pkgs/updaters/README.mdx"],"mappings":"gHAAAA,EAAAC,EAAAC,GAAAF,EAAAG,EAAAD,EAAA,4BAAAE,IAAO,IAAMA,EAAU,CACrBC,0CAA2C,kBACzCL,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,oDACFC,iDAAkD,kBAChDT,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,2DACFE,oDAAqD,kBACnDV,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,8DACFG,0DAA2D,kBACzDX,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,oEACFI,+CAAgD,kBAC9CZ,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,yDACFK,+CAAgD,kBAC9Cb,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,yDACFM,sCAAuC,kBACrCd,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,gDACFO,oCAAqC,kBACnCf,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,8CACFQ,2CAA4C,kBAC1ChB,EAAAM,EAAA,GAAAC,KAAAP,EAAAQ,KAAA,qDACFS,uBAAwB,kBACtBjB,EAAAM,EAAA,IAAAC,KAAAP,EAAAQ,KAAA,iCACFU,wBAAyB,kBACvBlB,EAAAM,EAAA,IAAAC,KAAAP,EAAAQ,KAAA,kCACFW,2BAA4B,kBAC1BnB,EAAAM,EAAA,IAAAC,KAAAP,EAAAQ,KAAA","file":"static/js/15.854c9d6f.js","sourcesContent":["export const imports = {\n 'docs_source/0_introduction/00_about.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-0-introduction-00-about\" */ 'docs_source/0_introduction/00_about.mdx'),\n 'docs_source/0_introduction/05_installation.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-0-introduction-05-installation\" */ 'docs_source/0_introduction/05_installation.mdx'),\n 'docs_source/0_introduction/10_getting-started.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-0-introduction-10-getting-started\" */ 'docs_source/0_introduction/10_getting-started.mdx'),\n 'docs_source/0_introduction/20_objects-and-factories.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-0-introduction-20-objects-and-factories\" */ 'docs_source/0_introduction/20_objects-and-factories.mdx'),\n 'docs_source/0_introduction/30_with-react.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-0-introduction-30-with-react\" */ 'docs_source/0_introduction/30_with-react.mdx'),\n 'docs_source/0_introduction/40_philosophy.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-0-introduction-40-philosophy\" */ 'docs_source/0_introduction/40_philosophy.mdx'),\n 'docs_source/1_advanced/examples.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-1-advanced-examples\" */ 'docs_source/1_advanced/examples.mdx'),\n 'docs_source/1_advanced/models.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-1-advanced-models\" */ 'docs_source/1_advanced/models.mdx'),\n 'docs_source/1_advanced/preprocessors.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"docs-source-1-advanced-preprocessors\" */ 'docs_source/1_advanced/preprocessors.mdx'),\n 'pkgs/core/README.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"pkgs-core-readme\" */ 'pkgs/core/README.mdx'),\n 'pkgs/react/README.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"pkgs-react-readme\" */ 'pkgs/react/README.mdx'),\n 'pkgs/updaters/README.mdx': () =>\n import(/* webpackPrefetch: true, webpackChunkName: \"pkgs-updaters-readme\" */ 'pkgs/updaters/README.mdx'),\n}\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/app.c7d38faa.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{"./.docz/app/db.json":function(e){e.exports={config:{title:"Rval",description:"My awesome app using docz",menu:[],ordering:"descending",version:"0.0.1",repository:"https://github.com/mweststrate/rval",native:!1,codeSandbox:!0,themeConfig:{},base:"/rval/",dest:"docs",propsParser:!1,hashRouter:!0},entries:{"docs_source/0_introduction/00_about.mdx":{name:"About RVal",order:0,menu:"Introduction",route:"/",id:"40e7c723e6cd44e2fb55eed5fa5b8126",filepath:"docs_source/0_introduction/00_about.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/0_introduction/00_about.mdx",slug:"docs-source-0-introduction-00-about",headings:[{depth:1,slug:"about-rval",value:"About RVal"},{depth:2,slug:"quick-example",value:"Quick example"},{depth:2,slug:"philosophy",value:"Philosophy"}]},"docs_source/0_introduction/05_installation.mdx":{name:"Installation",order:1,menu:"Introduction",route:"/introduction/installation",id:"8c812cec5cfa47c09046c0ad4028d9e9",filepath:"docs_source/0_introduction/05_installation.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/0_introduction/05_installation.mdx",slug:"docs-source-0-introduction-05-installation",headings:[{depth:2,slug:"packages",value:"Packages"}]},"docs_source/0_introduction/10_getting-started.mdx":{name:"The Basics",order:2,menu:"Introduction",route:"/introduction/the-basics",id:"881f0239ef16525c0578edcfeec12cfe",filepath:"docs_source/0_introduction/10_getting-started.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/0_introduction/10_getting-started.mdx",slug:"docs-source-0-introduction-10-getting-started",headings:[{depth:1,slug:"getting-started-with-rval",value:"Getting started with RVal"},{depth:2,slug:"val-reactive-values",value:"val : reactive values"},{depth:2,slug:"drv-derived-values",value:"drv : derived values"},{depth:2,slug:"sub-listing-to-changes",value:"sub : listing to changes"},{depth:2,slug:"act-batching-updates",value:"act : batching updates"}]},"docs_source/0_introduction/20_objects-and-factories.mdx":{name:"Working with objects",order:3,menu:"Introduction",route:"/introduction/factories",id:"f11a1014b5943e7acc23512d5c0f57f0",filepath:"docs_source/0_introduction/20_objects-and-factories.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/0_introduction/20_objects-and-factories.mdx",slug:"docs-source-0-introduction-20-objects-and-factories",headings:[{depth:1,slug:"working-with-objects-and-arrays",value:"Working with objects and arrays"},{depth:2,slug:"a-first-object-factory",value:"A first object factory"},{depth:2,slug:"creating-collections",value:"Creating collections"},{depth:2,slug:"simplifying-updates-with-updaters",value:"Simplifying updates with updaters"},{depth:2,slug:"next-steps",value:"Next steps"},{depth:2,slug:"background-what-about-classes",value:"Background: what about classes?"}]},"docs_source/0_introduction/30_with-react.mdx":{name:"Using react",order:4,menu:"Introduction",route:"/introduction/react",id:"ec224d754815113334362af6422d0f07",filepath:"docs_source/0_introduction/30_with-react.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/0_introduction/30_with-react.mdx",slug:"docs-source-0-introduction-30-with-react",headings:[]},"docs_source/0_introduction/40_philosophy.mdx":{name:"Philosophy",order:5,menu:"Introduction",route:"/introduction/philosophy",id:"7fb81491976fe813a74bb7d04fe1ee76",filepath:"docs_source/0_introduction/40_philosophy.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/0_introduction/40_philosophy.mdx",slug:"docs-source-0-introduction-40-philosophy",headings:[{depth:1,slug:"the-philosophy-of-rval",value:"The Philosophy of RVal"},{depth:2,slug:"functions-solidify-state",value:"Functions solidify state"}]},"docs_source/1_advanced/examples.mdx":{name:"Examples",order:3,menu:"Advanced",route:"/advanced/examples",id:"852af60ceb032d002177699d60ea205a",filepath:"docs_source/1_advanced/examples.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/1_advanced/examples.mdx",slug:"docs-source-1-advanced-examples",headings:[]},"docs_source/1_advanced/models.mdx":{name:"Working with models",order:1,menu:"Advanced",route:"/advanced/models",id:"8e346cafc38eaef81418b120d0433853",filepath:"docs_source/1_advanced/models.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/1_advanced/models.mdx",slug:"docs-source-1-advanced-models",headings:[]},"docs_source/1_advanced/preprocessors.mdx":{name:"Using preprocessors",order:0,menu:"Advanced",route:"/advanced/preprocessors",id:"2c3f02cbe7776436e609ca868dd69c16",filepath:"docs_source/1_advanced/preprocessors.mdx",link:"https://github.com/mweststrate/rval/edit/master/docs_source/1_advanced/preprocessors.mdx",slug:"docs-source-1-advanced-preprocessors",headings:[]},"pkgs/core/README.mdx":{name:"Core api",order:1,menu:"API",route:"/api/core",id:"b36d139fb3bf841133c7e5bb2f02087a",filepath:"pkgs/core/README.mdx",link:"https://github.com/mweststrate/rval/edit/master/pkgs/core/README.mdx",slug:"pkgs-core-readme",headings:[{depth:1,slug:"r-valcore-api",value:"@r-val/core api"},{depth:2,slug:"val",value:"val"}]},"pkgs/react/README.mdx":{name:"React bindings",order:1,menu:"API",route:"/api/react",id:"36aa066c925eb504a630abe2dc46d199",filepath:"pkgs/react/README.mdx",link:"https://github.com/mweststrate/rval/edit/master/pkgs/react/README.mdx",slug:"pkgs-react-readme",headings:[]},"pkgs/updaters/README.mdx":{name:"Updaters",order:1,menu:"API",route:"/api/updaters",id:"0645ac7f29fb88970c6bd085191b5c70",filepath:"pkgs/updaters/README.mdx",link:"https://github.com/mweststrate/rval/edit/master/pkgs/updaters/README.mdx",slug:"pkgs-updaters-readme",headings:[{depth:1,slug:"r-valupdaters",value:"@r-val/updaters"},{depth:2,slug:"inc1",value:"inc1"}]}}}},"./.docz/app/index.jsx":function(e,t,o){"use strict";o.r(t);var s=o("./node_modules/react/index.js"),d=o.n(s),a=o("./node_modules/react-dom/index.js"),r=o.n(a),c=o("./.docz/app/root.jsx"),i=[],n=[],u=function(){return n.forEach(function(e){return e&&e()})},l=document.querySelector("#root");!function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:c.a;i.forEach(function(e){return e&&e()}),r.a.render(d.a.createElement(e,null),l,u)}(c.a)},"./.docz/app/root.jsx":function(e,t,o){"use strict";(function(e){var s=o("./node_modules/react/index.js"),d=o.n(s),a=o("./node_modules/react-hot-loader/index.js"),r=o("./node_modules/docz-theme-default/dist/index.m.js");t.a=Object(a.hot)(e)(function(){return d.a.createElement(r.a,null)})}).call(this,o("./node_modules/webpack/buildin/harmony-module.js")(e))},0:function(e,t,o){o("./node_modules/react-dev-utils/webpackHotDevClient.js"),o("./node_modules/@babel/polyfill/lib/index.js"),e.exports=o("./.docz/app/index.jsx")}},[[0,13,14]]]); 2 | //# sourceMappingURL=app.e2ac49079e2d157a0b49.js.map -------------------------------------------------------------------------------- /docs/static/js/app.e2ac49079e2d157a0b49.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./.docz/app/index.jsx","webpack:///./.docz/app/root.jsx"],"names":["__webpack_require__","r","__webpack_exports__","react__WEBPACK_IMPORTED_MODULE_0__","react__WEBPACK_IMPORTED_MODULE_0___default","n","react_dom__WEBPACK_IMPORTED_MODULE_1__","react_dom__WEBPACK_IMPORTED_MODULE_1___default","_root__WEBPACK_IMPORTED_MODULE_2__","_onPreRenders","_onPostRenders","onPostRender","forEach","f","root","document","querySelector","Component","arguments","length","undefined","Root","ReactDOM","render","a","createElement","module","react_hot_loader__WEBPACK_IMPORTED_MODULE_1__","docz_theme_default__WEBPACK_IMPORTED_MODULE_2__","hot"],"mappings":"uxLAAAA,EAAAC,EAAAC,GAAA,IAAAC,EAAAH,EAAA,iCAAAI,EAAAJ,EAAAK,EAAAF,GAAAG,EAAAN,EAAA,qCAAAO,EAAAP,EAAAK,EAAAC,GAAAE,EAAAR,EAAA,wBAIMS,EAAgB,GAChBC,EAAiB,GAGjBC,EAAe,kBAAMD,EAAeE,QAAQ,SAAAC,GAAC,OAAIA,GAAKA,OAEtDC,EAAOC,SAASC,cAAc,UACrB,WAAsB,IAArBC,EAAqBC,UAAAC,OAAA,QAAAC,IAAAF,UAAA,GAAAA,UAAA,GAATG,IAJFZ,EAAcG,QAAQ,SAAAC,GAAC,OAAIA,GAAKA,MAMxDS,IAASC,OAAOnB,EAAAoB,EAAAC,cAACR,EAAD,MAAeH,EAAMH,GAGvCY,CAAOF,2DChBP,SAAAK,GAAA,IAAAvB,EAAAH,EAAA,iCAAAI,EAAAJ,EAAAK,EAAAF,GAAAwB,EAAA3B,EAAA,4CAAA4B,EAAA5B,EAAA,qDAMe6B,kBAAIH,EAAJG,CAFF,kBAAMzB,EAAAoB,EAAAC,cAACG,EAAA,EAAD","file":"static/js/app.c7d38faa.js","sourcesContent":["import React from 'react'\nimport ReactDOM from 'react-dom'\nimport Root from './root'\n\nconst _onPreRenders = []\nconst _onPostRenders = []\n\nconst onPreRender = () => _onPreRenders.forEach(f => f && f())\nconst onPostRender = () => _onPostRenders.forEach(f => f && f())\n\nconst root = document.querySelector('#root')\nconst render = (Component = Root) => {\n onPreRender()\n ReactDOM.render(, root, onPostRender)\n}\n\nrender(Root)\n","import React from 'react'\nimport { hot } from 'react-hot-loader'\nimport Theme from 'docz-theme-default'\n\nconst Root = () => \n\nexport default hot(module)(Root)\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/docs-source-0-introduction-00-about.33cb42e7.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[1],{"./docs_source/0_introduction/00_about.mdx":function(e,n,t){"use strict";t.r(n),t.d(n,"default",function(){return u});var a=t("./node_modules/react/index.js"),o=t.n(a),r=t("./node_modules/@mdx-js/tag/dist/index.js");function i(e){return(i="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function m(e,n){if(null==e)return{};var t,a,o=function(e,n){if(null==e)return{};var t,a,o={},r=Object.keys(e);for(a=0;a=0||(o[t]=e[t]);return o}(e,n);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(o[t]=e[t])}return o}function l(e,n){for(var t=0;t{`About RVal`}\n{`RVal is a minimalistic, transparent reactive programming library, heavily inspired by `}{`MobX`}{`,\nwith the same core principle: `}{`Everything that can be derived from state, should be derived. Automatically`}{`.`}\n{`However, the goals are slightly different and the library is in terms of API surface and bundle size `}{`much`}{` smaller.\nRVal core principles include:`}\n\n{`🍭 `}{`Minimalistic`}{` A very simple, minimalistic core API`}\n{`🛅 `}{`Functions + immutable objects`}{`: Functions and (stateful) immutable objects as the primary means to organize state`}\n{`🎯 `}{`Convention driven`}{`: An idiomatic way of working, that guides devs into the `}{`Pit of Success`}\n{`📦 `}{`Embeddable`}{`: A low level building block, that is small (~2KB minified gzipped) so that it can easily be embedded in existing libraries and frameworks as state management libraries.`}\n{`🐆 `}{`Fast`}{`: Rock solid performance, leveraging the battle tested algorithms of MobX.`}\n{`☔ `}{`Versatile`}{`: no dependencies, no modern syntax or language requirements, non-intrusive, applicable in any ES5 JavaScript stack`}\n{`🎓 `}{`Gradual learning curve`}{`: Opt-in utilities that help with applying best practices`}\n{`💪 `}{`Strongly typed`}{`: Shouldn't need further explanation in 2019`}\n\n{`Ok, that is a pretty vague, generic list of things that sound positive. Hardly saying anything.\nIt boils down to this: RVal, is small, conceptually simple and powerful.\nBut mostly: `}\n{`The proof of the pudding is in the eating.`}{`.`}\n{`Here is a term that you will encounter when reading the introduction:\nIt's all about `}{`reactive values`}{` and `}{`immutable stateful objects`}{`.\nRead the introduction to find out what that abomination of seemingly conflicting concepts means.\nBut at least: you won't be needing `}{`this`}{`, `}{`let`}{`, `}{`var`}{` or `}{`class`}{`.`}\n{`Quick example`}\n{`TODO`}\n{`Sandbox link`}\n{`Philosophy`}\n{`TODO link`}\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/docs-source-0-introduction-05-installation.a5841abe.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[2],{"./docs_source/0_introduction/05_installation.mdx":function(e,n,t){"use strict";t.r(n),t.d(n,"default",function(){return s});var o=t("./node_modules/react/index.js"),r=t.n(o),a=t("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function u(e,n){if(null==e)return{};var t,o,r=function(e,n){if(null==e)return{};var t,o,r={},a=Object.keys(e);for(o=0;o=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}function i(e,n){for(var t=0;t{`Yarn / NPM`}\n{`CDN`}\n{`Packages`}\n{`TODO`}\n{`Table, with name, goal, cdn, umdname, bundlesize`}\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/docs-source-0-introduction-30-with-react.7cb56417.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[5],{"./docs_source/0_introduction/30_with-react.mdx":function(t,e,n){"use strict";n.r(e),n.d(e,"default",function(){return s});var o=n("./node_modules/react/index.js"),r=n.n(o),u=n("./node_modules/@mdx-js/tag/dist/index.js");function c(t){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"===typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function i(t,e){if(null==t)return{};var n,o,r=function(t,e){if(null==t)return{};var n,o,r={},u=Object.keys(t);for(o=0;o=0||(r[n]=t[n]);return r}(t,e);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(t);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(r[n]=t[n])}return r}function f(t,e){for(var n=0;n\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/docs-source-1-advanced-examples.85d2c45c.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[7],{"./docs_source/1_advanced/examples.mdx":function(e,t,n){"use strict";n.r(t),n.d(t,"default",function(){return s});var o=n("./node_modules/react/index.js"),r=n.n(o),u=n("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,t){if(null==e)return{};var n,o,r=function(e,t){if(null==e)return{};var n,o,r={},u=Object.keys(e);for(o=0;o=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function f(e,t){for(var n=0;n\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/docs-source-1-advanced-models.d824a22c.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[8],{"./docs_source/1_advanced/models.mdx":function(e,t,n){"use strict";n.r(t),n.d(t,"default",function(){return s});var o=n("./node_modules/react/index.js"),r=n.n(o),u=n("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,t){if(null==e)return{};var n,o,r=function(e,t){if(null==e)return{};var n,o,r={},u=Object.keys(e);for(o=0;o=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function f(e,t){for(var n=0;n\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/docs-source-1-advanced-preprocessors.2cd0bb51.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[9],{"./docs_source/1_advanced/preprocessors.mdx":function(e,t,n){"use strict";n.r(t),n.d(t,"default",function(){return s});var o=n("./node_modules/react/index.js"),r=n.n(o),u=n("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,t){if(null==e)return{};var n,o,r=function(e,t){if(null==e)return{};var n,o,r={},u=Object.keys(e);for(o=0;o=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}function f(e,t){for(var n=0;n\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/pkgs-core-readme.4285a63b.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[10],{"./pkgs/core/README.mdx":function(e,n,t){"use strict";t.r(n),t.d(n,"default",function(){return s});var o=t("./node_modules/react/index.js"),r=t.n(o),a=t("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,n){if(null==e)return{};var t,o,r=function(e,n){if(null==e)return{};var t,o,r={},a=Object.keys(e);for(o=0;o=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}function u(e,n){for(var t=0;t fn)")," or ",r.a.createElement(a.MDXTag,{name:"inlineCode",components:n,parentName:"p"},"fnHolder(replace(fn))")))}}])&&u(t.prototype,o),c&&u(t,c),n}()}}]); 2 | //# sourceMappingURL=pkgs-core-readme.e2ac49079e2d157a0b49.js.map -------------------------------------------------------------------------------- /docs/static/js/pkgs-core-readme.e2ac49079e2d157a0b49.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///./pkgs/core/README.mdx"],"names":["MDXContent","props","_this","_classCallCheck","this","_possibleConstructorReturn","_getPrototypeOf","call","layout","React","Component","_this$props","components","_objectWithoutProperties","react__WEBPACK_IMPORTED_MODULE_0___default","a","createElement","_mdx_js_tag__WEBPACK_IMPORTED_MODULE_1__","name","id","parentName"],"mappings":"k6CAKqBA,cACnB,SAAAA,EAAYC,GAAO,IAAAC,EAAA,mGAAAC,CAAAC,KAAAJ,IACjBE,EAAAG,EAAAD,KAAAE,EAAAN,GAAAO,KAAAH,KAAMH,KACDO,OAAS,KAFGN,yPADmBO,IAAMC,kDAKnC,IAAAC,EAC0BP,KAAKH,MAA9BW,EADDD,EACCC,WADDC,EAAAF,EAAA,gBAGP,OAAOG,EAAAC,EAAAC,cAACC,EAAA,OAAD,CACEC,KAAK,UAELN,WAAYA,GAAYE,EAAAC,EAAAC,cAACC,EAAA,OAAD,CAAQC,KAAK,KAAKN,WAAYA,EAAYX,MAAO,CAACkB,GAAK,kBAAvD,mBACrCL,EAAAC,EAAAC,cAACC,EAAA,OAAD,CAAQC,KAAK,KAAKN,WAAYA,EAAYX,MAAO,CAACkB,GAAK,QAAvD,OACAL,EAAAC,EAAAC,cAACC,EAAA,OAAD,CAAQC,KAAK,IAAIN,WAAYA,GAA7B,0BAAoEE,EAAAC,EAAAC,cAACC,EAAA,OAAD,CAAQC,KAAK,aAAaN,WAAYA,EAAYQ,WAAW,KAA7D,sBAApE,OAA4KN,EAAAC,EAAAC,cAACC,EAAA,OAAD,CAAQC,KAAK,aAAaN,WAAYA,EAAYQ,WAAW,KAA7D","file":"static/js/pkgs-core-readme.4285a63b.js","sourcesContent":["\n import React from 'react'\n import { MDXTag } from '@mdx-js/tag'\n \n\nexport default class MDXContent extends React.Component {\n constructor(props) {\n super(props)\n this.layout = null\n }\n render() {\n const { components, ...props } = this.props\n\n return {`@r-val/core api`}\n{`val`}\n{`How to set a function? `}{`fnHolder(() => fn)`}{` or `}{`fnHolder(replace(fn))`}\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/pkgs-react-readme.e1c9a28e.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[11],{"./pkgs/react/README.mdx":function(e,n,t){"use strict";t.r(n),t.d(n,"default",function(){return s});var o=t("./node_modules/react/index.js"),r=t.n(o),a=t("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function l(e,n){if(null==e)return{};var t,o,r=function(e,n){if(null==e)return{};var t,o,r={},a=Object.keys(e);for(o=0;o=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}function u(e,n){for(var t=0;t{`useVal`}\n\n{`context`}\n\n{`useLocalVal`}\n\n{`context`}\n\n{`useLocalDrv`}\n\n{`inputs`}\n{`context`}\n\n{`rview`}\n\n{`chidren`}\n{`memo (true, false, array)`}\n{`context`}\n\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/pkgs-updaters-readme.429519e2.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[12],{"./pkgs/updaters/README.mdx":function(e,t,n){"use strict";n.r(t),n.d(t,"default",function(){return s});var r=n("./node_modules/react/index.js"),o=n.n(r),u=n("./node_modules/@mdx-js/tag/dist/index.js");function c(e){return(c="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},u=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function a(e,t){for(var n=0;n{`@r-val/updaters`}\n{`inc1`}\n \n }\n}\n "],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/runtime~app.e2ac49079e2d157a0b49.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var t,o,c=r[0],i=r[1],d=r[2],a=r[3]||[],s=0,u=[];s=0&&r._disposeHandlers.splice(n,1)},check:E,apply:x,status:function(e){if(!e)return f;p.push(e)},addStatusHandler:function(e){p.push(e)},removeStatusHandler:function(e){var r=p.indexOf(e);r>=0&&p.splice(r,1)},data:a[e]};return o=void 0,r}var p=[],f="idle";function h(e){f=e;for(var r=0;r0;){var o=t.pop(),i=o.id,d=o.chain;if((c=H[i])&&!c.hot._selfAccepted){if(c.hot._selfDeclined)return{type:"self-declined",chain:d,moduleId:i};if(c.hot._main)return{type:"unaccepted",chain:d,moduleId:i};for(var a=0;a ")),O.type){case"self-declined":r.onDeclined&&r.onDeclined(O),r.ignoreDeclined||(E=new Error("Aborted because of self decline: "+O.moduleId+x));break;case"declined":r.onDeclined&&r.onDeclined(O),r.ignoreDeclined||(E=new Error("Aborted because of declined dependency: "+O.moduleId+" in "+O.parentId+x));break;case"unaccepted":r.onUnaccepted&&r.onUnaccepted(O),r.ignoreUnaccepted||(E=new Error("Aborted because "+d+" is not accepted"+x));break;case"accepted":r.onAccepted&&r.onAccepted(O),D=!0;break;case"disposed":r.onDisposed&&r.onDisposed(O),P=!0;break;default:throw new Error("Unexception type "+O.type)}if(E)return h("abort"),Promise.reject(E);if(D)for(d in b[d]=y[d],l(v,O.outdatedModules),O.outdatedDependencies)Object.prototype.hasOwnProperty.call(O.outdatedDependencies,d)&&(p[d]||(p[d]=[]),l(p[d],O.outdatedDependencies[d]));P&&(l(v,[O.moduleId]),b[d]=g)}var I,A=[];for(t=0;t0;)if(d=q.pop(),c=H[d]){var U={},N=c.hot._disposeHandlers;for(o=0;o=0&&R.parents.splice(I,1))}}for(d in p)if(Object.prototype.hasOwnProperty.call(p,d)&&(c=H[d]))for(T=p[d],o=0;o=0&&c.children.splice(I,1);for(d in h("apply"),i=m,b)Object.prototype.hasOwnProperty.call(b,d)&&(e[d]=b[d]);var B=null;for(d in p)if(Object.prototype.hasOwnProperty.call(p,d)&&(c=H[d])){T=p[d];var C=[];for(t=0;t rview(() => ( 51 | <> 52 | {counter()} 53 | 54 | 55 | )) 56 | 57 | setInterval(() => { 58 | counter(c => c + 1) 59 | }, 1000) 60 | 61 | render(, document.body) 62 | ``` 63 | 64 | This example can be tried in [code sandbox](https://codesandbox.io/s/m297j6w38). Some quick highlights: 65 | 66 | 1. This application has a single _reactive value_, called `counter`, initialized to `0`. 67 | 2. The component is reading the current value, by calling `counter()`, that simply returns the current state. 68 | 3. The `onClick` handler replaces the internal state of the `counter` with the value `0` again. 69 | 4. The `setInterval` updates the `counter` every second, using a function that takes the current state of the `counter`, and produces a new state. 70 | 5. The [`rview`](#/introduction/react) function creates a _reactive view_, which takes a render callback, and re-render's automatically when any of the reactive values inside it's function body changes. That is why we see the counter actually ticking (Note that the `counter` is neither part of the state or props of the component). 71 | 72 | Hopefully this example will strike you as slightly boring. Good, this library wasn't build to write counters. 73 | The following example is slightly more elaborate. Or you can just jump ahead to all the [core concepts](#/introduction/the-basics). 74 | 75 | ## Slightly more elaborate example 76 | 77 | TODO 78 | 79 | ## More examples 80 | 81 | An overview of more examples can be found in the [examples](#/advanced/examples) section. 82 | 83 | ## Philosophy 84 | 85 | Interesting in the _why_ of RVal? In that case check out the [philosophy](#/introduction/philosophy) section. 86 | 87 | [^1]: Compared to Mobx, the goals are slightly different and the library is in terms of API surface and bundle size _much_ smaller. -------------------------------------------------------------------------------- /docs_source/0_introduction/05_installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Installation 3 | order: 1 4 | menu: Introduction 5 | route: /introduction/installation 6 | --- 7 | 8 | Yarn / NPM 9 | 10 | CDN 11 | 12 | ## Packages 13 | 14 | TODO 15 | 16 | Table, with name, goal, cdn, umdname, bundlesize 17 | -------------------------------------------------------------------------------- /docs_source/0_introduction/10_getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: The Basics 3 | order: 2 4 | menu: Introduction 5 | route: /introduction/the-basics 6 | --- 7 | 8 | # Getting started with RVal 9 | 10 | The core of `RVal` is four functions which have all a very simple contract: `val`, `drv`, `sub` and `act`. 11 | Yes, they all have three-letter names. That's kind of cool I think. Not sure yet why. 12 | 13 | ## `val`: reactive values 14 | 15 | _“Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function.” — John Carmack_ 16 | 17 | In RVal, the universe revolves around _reactive values_. 18 | Creating your first reactive value is easy by leveraging `val(initialValue)`: 19 | 20 | ```javascript 21 | import { val } from "@r-val/core"; 22 | 23 | const myLuckyNumber = val(13) 24 | ``` 25 | 26 | `val` returns a function that returns any **value** you've put into it. 27 | So `myLuckyNumber` is now a function, returning the original number, and we can call it: 28 | 29 | ```javascript 30 | console.log(myLuckyNumber()) 31 | // prints: 13 32 | ``` 33 | 34 | Fancy! But what use is it to create a function that just returns the original value? 35 | We'll find out in a bit. 36 | First, there is another trick the function can do: We can call it with a new lucky number, 37 | (in case `13` didn't work out after all): 38 | 39 | ```javascript 40 | myLuckyNumber(42) 41 | 42 | console.log(myLuckyNumber()) 43 | // prints: 42 44 | ``` 45 | 46 | By passing an argument to the function, we can update it's internal state. 47 | When calling the reactive value function without argumens, it will always return the value we've passed into it the last time. 48 | 49 | You can put any value you like into a reactive value. 50 | But, for all practical purposes, you've should consider this value to be immutable. 51 | This will greatly benefit the understanding of the code base once it grows big enough. 52 | But, more on that later. 53 | 54 | See the [Philosophy](docs-philosophy) for some more background on this idea. 55 | 56 | ## `drv`: derived values 57 | 58 | _“No, officer 👮‍♀️, I didn't drv(thunk)!” — Erik Rasmussen_ 59 | 60 | In my humble opinion, good lucky numbers should at least be odd. So we can quickly run a check for that: 61 | 62 | ```javascript 63 | const myLuckyNumber = val(13) 64 | const isGoodLuckyNumber = myLuckyNumber() % 2 === 1 65 | ``` 66 | 67 | That works. But is a bit limited, if we update `myLuckyNumber` later on, this change won't be reflected 68 | in `isGoodLuckyNumber`. 69 | But, using `drv` we can repeat a similar trick as we did for `val`: 70 | Instead of capturing some state, `drv` captures a computation. 71 | It returns a function, that, when invoked, runs the computation once. 72 | 73 | ```javascript 74 | const myLuckyNumber = val(13) 75 | const isGoodLuckyNumber = drv(() => myLuckyNumber() % 2 === 1) 76 | 77 | console.log(isGoodLuckyNumber()) // true 78 | myLuckyNumber(42) 79 | console.log(isGoodLuckyNumber()) // false 80 | ``` 81 | 82 | `drv` can be used to **derive** arbitrarly simple or complex values based on other reactive values, created by either `drv` or `val`. 83 | 84 | The critical reader might think at this point: “That's nice and dandy, but you couldn't we just have used `const isGoodLuckyNumber = () => myLuckyNumber() % 2 === 1`?”. 85 | And that is true, glad you ask. That would indeed have yielded the same output. 86 | But! Using `drv` brings in a few new possiblities: 87 | 88 | First, `drv` will memoize[^(usually)] it's results. That is: as long as `myLuckyNumber` doesn't change, invoking `isGoodLuckyNumber()` multiple times won't actually re-evaluate the original expression, but just return a memoized result. 89 | 90 | Secondly, and more importantly. So far we having been pulling values through our system by explicitly calling `myLuckyNumber()` or `isGoodLuckyNumber()`. 91 | But in a reactive system, the control flow is inversed[^(a.k.a. inversion of control)]. 92 | To build a reactive system, we have to push our values to consumers and actively _notify_ them. 93 | 94 | ## `sub`: listing to changes 95 | 96 | _“If a tree falls in a forest and no one is around to hear it, does it make a sound?” — The Chautauquan, 1883_ 97 | 98 | And that is where `sub` comes in! 99 | With `sub`, one can create a consumer of a reactive value created using `val` or `drv`. 100 | In other words, it sets up a **subscription**. 101 | This creates a _we'll call you_ basis of operation: 102 | 103 | ```javascript 104 | const myLuckyNumber = val(13) 105 | const isGoodLuckyNumber = drv(() => myLuckyNumber() % 2 === 1) 106 | 107 | const cancelPrint = sub(isGoodLuckyNumber, isGood => { 108 | console.log(isGood) 109 | }) 110 | 111 | myLuckyNumber(42) // prints: 'false' 112 | myLuckyNumber(33) // prints: 'true' 113 | myLuckyNumber(55) // (doesn't print, isGoodLuckyNumber didn't produce a new value) 114 | myLuckyNumber(2) // prints: 'false' 115 | 116 | cancelPrint() // stop listening to future updates 117 | ``` 118 | 119 | Finally, we did something that we couldn't have achieved by using just plain functions and omitting `drv` or `val`. 120 | What `drv` and `val` achieve is that they set up a dependency system, so that when we update state, this state is propagated through the derived values, to the subscriptions. 121 | Transparent reactive programming is used to determine the optimal program flow, and based on the [MobX](https://mobx.js.org) package. 122 | 123 | Note that we didn't pass `isGoodLuckyNumber()`! We want to subscribe to the function's future output, not to it's current value (`13`). 124 | Also note that `sub` returns a function. This function has only one purpose: cancelling future executions of the subscription. 125 | 126 | A remarkable property about `sub` is that you don't need them as often as you would initially think. 127 | They are not needed to propagate values through your system. `drv` can take care of that. 128 | `sub` is generally only need to achieve side effects; visible output from your system. 129 | Such as updating the UI, logging, making network effects etc. 130 | 131 | 132 | ## `act`: batching updates 133 | 134 | _“No act of kindness, no matter how small, is ever wasted...” — Aesop_ 135 | 136 | You might have noticed that in the previous listening or side effects where immediately fired when emitting an update to `myLuckyNumber`. 137 | This is just the default behavior and there are several ways to influence it. 138 | First of all, we can use techniques like debounce to roun our subscriptions less frequently. 139 | But more importantly, we can hold of the reactive system to wait a bit until we've done all our updates, 140 | so that changes will be broadcased as one atomic update. 141 | To express that, there is `act` to group multiple changes into a single **activity**. 142 | `act` takes accepts a function, and returns a function with the same signature, that, when invoked, will batch the updates. 143 | 144 | ```javascript 145 | const cancelPrint = sub(isGoodLuckyNumber, isGood => { 146 | console.log(isGood) 147 | }) 148 | 149 | const assignNumbers = act(() => { 150 | myLuckyNumber(42) 151 | myLuckyNumber(33) 152 | myLuckyNumber(55) 153 | myLuckyNumber(2) 154 | }) 155 | 156 | assignNumbers()// prints only once, at the end of the activity: 'false' 157 | ``` 158 | 159 | That's all! Note that `act` only batches _synchronosly_ run updates. Passing an `async` function to `act` is in that regard mostly useless. 160 | 161 | --- 162 | 163 | Your system will most probably not be expressible in a single `val`, `drv` and `sub`. 164 | But, we've now covered the entire basic mechanism of "rval"! 165 | In the next sections we will focus on organizing many different values, objects etc. 166 | And see how we can hook up the UI. 167 | 168 | [^(usually)]: `drv` will by default memoize as long as there is at least one active subscription. Without subscriptions, memoization will be disabled to avoid accidentally leaking memory. The behavior can be overriden by using the `keepAlive` option. (TODO). 169 | [^(a.k.a. inversion of control)]: Wiki: [Inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) -------------------------------------------------------------------------------- /docs_source/0_introduction/30_with-react.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Using react 3 | order: 4 4 | menu: Introduction 5 | route: /introduction/react 6 | --- 7 | 8 | # Using RVal with React 9 | 10 | RVal is in essence framework agnostic. 11 | But, official bindings for React are available through the `@r-val/react` package. 12 | (This package is a very thin layer around the `effect` function). 13 | 14 | ## `useVal`: Subscribe with hooks 15 | 16 | The simplest way to consume a reactive value in a component is to use the `useVal` hook. 17 | Note that `useVal` uses React hooks, which aren't officially released yet! 18 | We just start with those because they are conceptually the most straight-forward, but RVal will work with any version of React 16. 19 | 20 | The `useVal` hook can be passed any reactive value (either an `val` or `drv`), and makes sure the component subscribes to the changes in this value. 21 | It simply returns the current state of the reactive value passed. 22 | Note that `useVal` doesn't return a setter function! To update the reactive value, just call the reactive value directly with a new value, as shown below in the `onClick` handler: 23 | 24 | ```javascript 25 | import React from "react" 26 | import { render } from "react-dom" 27 | import { val } from "@r-val/core" 28 | import { useVal } from "@r-val/react" 29 | 30 | const counter = val(0) 31 | 32 | const Counter = () => { 33 | const c = useVal(counter) 34 | return (<> 35 | {c} 36 | 39 | ) 40 | } 41 | 42 | setInterval(() => { 43 | counter(c => c + 1) 44 | }, 1000); 45 | 46 | render(, document.body) 47 | ``` 48 | 49 | [Try online](https://codesandbox.io/s/m297j6w38) 50 | 51 | ## `rview`: Reactive views 52 | 53 | With `useVal`, we can pick one by one all the reactive values we wan't to subscribe to. 54 | This is quite explicit, but it easy to subscribe to to few, or too many reactive values. 55 | And more important, `useVal` is quite limited; due to the nature of hooks it is not possible to 56 | conditionally subscribe to reactive values. 57 | While in practice not all values might always be relevant in all states of the component! 58 | 59 | So, that is where `rview` comes in! 60 | `rview` is conceptually very similar to `drv`, except that it is specialized to produce and update React components. 61 | `rview` takes a render callback (without arguments) and returns an `RView` component instance. 62 | The neat thing is that `rview` will automatically keep track of all the reactive values that are used in the render callback, and subscribes to them, 63 | so that you don't have to. 64 | (And it will even unsubscribe from reactive values that are (temporarily) unused). 65 | So with `rview`, our previous counter simply boils down to the following, and note that `counter()` can be called directly in the render callback: 66 | 67 | 68 | ```javascript 69 | import React from "react" 70 | import { render } from "react-dom" 71 | import { val } from "@r-val/core" 72 | import { useVal } from "@r-val/react" 73 | 74 | const counter = val(0) 75 | 76 | const Counter = () => rview(() => ( 77 | <> 78 | {counter()} 79 | 82 | 83 | )) 84 | 85 | setInterval(() => { 86 | counter(c => c + 1) 87 | }, 1000); 88 | 89 | render(, document.body) 90 | ``` 91 | as [code sandbox](https://codesandbox.io/s/m297j6w38) 92 | 93 | _Tip: `rview` doesn't require hooks, so it works in any React 16.+ version._ 94 | 95 | Note that `rview` is quite optimized out of the box. When using RVal with `rview`, you shouldn't need `shouldComponentUpdate` hooks, and it is recommended to write `PureComponent` / `memo` based components. 96 | 97 | ## Further details 98 | 99 | ...can be found in the [API reference for the React bindings](#/api/react). 100 | -------------------------------------------------------------------------------- /docs_source/0_introduction/60_philosophy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Philosophy 3 | order: 6 4 | menu: Introduction 5 | route: /introduction/philosophy 6 | --- 7 | 8 | # The Philosophy of RVal 9 | 10 | ## Reactive values, why? 11 | 12 | Why reactive values? In essence most of our programming work consists of transfering in-memory information from one place to another, transforming the information into new information, that is either human or machine digestable. 13 | Data tranformations always introduces reduces redundant copies of data that need to be kept in sync with the original data. 14 | In very trivial example of this problem might look like: 15 | 16 | ```javascript 17 | const user = { 18 | firstName: "Jane", 19 | lastName: "Stanford", 20 | fullName: "Jane Stanford" 21 | } 22 | 23 | document.body.innerHTML = `

Hello ${user.fullName}

24 | ``` 25 | 26 | This simple snippet introduces a redundant copy of the original user's name in the `fullName` property, and in the DOM. 27 | Now it has become the programmers responsibility to make sure any futher changes to the `user` are propagated properly: 28 | 29 | ```javascript 30 | function updateFirstName(newName) { 31 | user.firstName = newName 32 | user.fullName = user.firstName + " " + user.lastName 33 | document.body.innerHTML = `

Hello ${user.fullName}

34 | } 35 | ``` 36 | 37 | This is the problem that any state management abstraction, regardless the framework or paradigm that is used, is trying to solve. 38 | RVal introduces a handful of primitives that help you to solve this problem in any context, by automating the question: 39 | _when_ should _which_ transformation be applied? 40 | 41 | Here is a quick overview in how RVal helps solving that problem. 42 | First, we should recognize that imperatively computing new information, such as the DOM represantation, introduces stale values. 43 | However, we can avoid ever storing such information by storing _computations_, rather than _values_. 44 | The process for that is as simple as creating a _thunk_ (argumentless function) that capture the computation, rather than imperatively producing new values: 45 | 46 | ```javascript 47 | const user = { 48 | firstName: "Jane", 49 | lastName: "Stanford", 50 | fullName: () => user.firstName + " " + user.lastName 51 | } 52 | 53 | const rendering = () => `

Hello ${user.fullName()}

` 54 | 55 | document.body.innerHTML = rendering() 56 | 57 | function updateFirstName(newName) { 58 | user.firstName = newName 59 | document.body.innerHTML = rendering() 60 | } 61 | ``` 62 | 63 | We've made things slightly better now; we don't have to imperatively update `user.fullName` anymore if the name changes. 64 | Similarly, we could captured the rendered representation of the user in the thunk called `rendering`. 65 | 66 | By storing computations instead of values, we have reduced the amount of redundant information. 67 | However, we still have to make sure that our changes are propagated, for example by updating the DOM whenever we change the `firstName` property. 68 | 69 | But, what if we could _subscribe_ to our thunks? And thereby avoid the need to manually propagate state changes, and increasing decoupling in the process? 70 | In other words, what if we could write something like: 71 | 72 | ```javascript 73 | const user = { /* as-is */ } 74 | const rendering = () => `

Hello ${user.fullName()}

` 75 | 76 | on(rendering, () => { 77 | document.body.innerHTML = rendering() 78 | }) 79 | 80 | function updateFirstName(newName) { 81 | user.firstName = newName 82 | } 83 | ``` 84 | 85 | Well, here is the good news: This is exactly the kind of things RVal allows you to write, by introducing three concepts: 86 | 87 | 1. `val(value)` to create pieces of information that can change over time 88 | 2. `drv(thunk)` to create thunks that can be subscribed to 89 | 3. `sub(something, listener)` to create a listener that fires whenever the provided reactive value or thunk changes 90 | 91 | With those concepts, we can rewrite our above listing as a combination of reactive values and thunks, that propagate the changes when needed! 92 | 93 | ```javascript 94 | import { val, drv, sub } from "@r-val/core" 95 | 96 | const user = { 97 | firstName: val("Jane"), 98 | lastName: val("Stanford"), 99 | fullName: drv(() => user.firstName() + " " + user.lastName()) 100 | } 101 | 102 | const rendering = drv(`

Hello ${user.fullName()}

`) 103 | 104 | // subscribe to the 'rendering' thunk 105 | sub(rendering, () => { 106 | document.body.innerHTML = rendering() 107 | }) 108 | 109 | function updateFirstName(newName) { 110 | // change the `firstName` reactive value to 'newName' 111 | // rval will make sure that any derivation and subscription impacted by this 112 | // change will be re-evaluated (and nothing more). 113 | user.firstName(newName) 114 | } 115 | ``` 116 | 117 | 118 | ## Functions solidify state 119 | 120 | At this point you might be wondering: 121 | "But _why_ is it interesting to trap our state inside these `val` functions?" 122 | 123 | By trapping all our pieces of state inside `val` functions, 124 | we achieved a very interesting property: 125 | We've practically forced ourselfs to have a single source of truth. 126 | Instead of passing the _values of our state_ around, we can now pass a _references to our state_ around. 127 | The benefit of this that it will stop us from accidentally creating redundant copies of our state. 128 | 129 | Take for example the following contrived function. 130 | It creates a random number generator, which is more likely to generate our lucky number than any other number: 131 | 132 | ```javascript 133 | function createNumberGenerator(luckyNumber) { 134 | return function numberGenerator() { 135 | return Math.random() < 0.5 ? luckyNumber : Math.round(Math.random() * 100) 136 | } 137 | } 138 | 139 | let luckyNumber = 13 140 | const generator = createNumberGenerator(luckyNumber) 141 | console.log(generator()) // 13 142 | console.log(generator()) // 50 143 | console.log(generator()) // 49 144 | 145 | luckyNumber = 42 146 | console.log(generator()) // 28 147 | console.log(generator()) // 13 148 | console.log(generator()) // 13 149 | ``` 150 | 151 | Now at this point, updating our `luckyNumber` variable doesn't get reflected in the `numberGenerator` anymore. 152 | We are forced now to create a new number generator to reflect the change in our preference. 153 | The problem is that the `luckyNumber` has been "trapped" as argument to `createNumberGenerator`. 154 | The argument is basically a _copy_ of the original `luckyNumber` variable. 155 | 156 | However, it is easy to see that by passing _functions_ around, we avoid this whole problem. 157 | Because `luckyNumber` itself now becomes a `const` reference to the function that traps our lucky number. 158 | (Yes, `let` and `var` really become anti-patterns when using `rval`). 159 | 160 | 161 | ```javascript 162 | function createNumberGenerator(luckyNumber) { 163 | return function numberGenerator() { 164 | // luckyNumber get's evaluated lazily when generating numbers 165 | return Math.random() < 0.5 ? luckyNumber() : Math.round(Math.random() * 100) 166 | } 167 | } 168 | 169 | const luckyNumber = val(13) // luckyNumber is a const now! 170 | const generator = createNumberGenerator(luckyNumber) 171 | console.log(generator()) // 13 172 | console.log(generator()) // 13 173 | console.log(generator()) // 22 174 | 175 | luckyNumber(42) // change our minds 176 | console.log(generator()) // 42 177 | console.log(generator()) // 8 178 | console.log(generator()) // 42 179 | ``` 180 | 181 | By capturing values in functions, it becomes much more explicit when we want to pass a _reference_, and when a _value_. 182 | If we want our number generator to take a one-time snapshot of the state as `luckyNumber` we can be explicit about it and _explicitly_ pass a copy of the current state `createNumberGenerator(luckyNumber())`. 183 | On the other hand, we can also explicitly pass a reference to the state by just passing the `luckyNumber` function itself as we did above. 184 | 185 | As it turns out, in many cases it is very intersting to pass around a reference instead of a value. 186 | Especially when we are building systems that are supposed to be reactive, such as a user interface. 187 | But that for later sections. 188 | 189 | --- 190 | 191 | Note that the essence of `val` is simply this: 192 | 193 | ```javascript 194 | function val(initial) { 195 | let state = initial 196 | return function() { 197 | if (!arguments.length) 198 | return state 199 | state = arguments[0] 200 | } 201 | } 202 | ``` 203 | 204 | RVal's implementation is a little more involved, but that is because it is possible to subscribe to the `state` of a `val`. 205 | But the above is how you can conceptually think about reactive values. 206 | 207 | But, let's [get started](#/introduction/the-basics) with the core "rval" api first if you didn't check it out yet! 208 | -------------------------------------------------------------------------------- /docs_source/1_convenience/40_preprocessors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Using preprocessors 3 | order: 6 4 | menu: Introduction 5 | route: /introduction/preprocessors 6 | --- 7 | 8 | 9 | # Pre-processors 10 | 11 | By default, any new value state stored in a `val` is automatically deeply frozen, even the value is a plain array or object. 12 | 13 | The `preProcessor` argument to apply some preprocessing on the values that are being stored, or perform validations. 14 | The signature of a pre-processor is: `(newValue, currentValue, rvalInstance) -> newValue`. Where: 15 | * `newValue` the value that is about to be stored 16 | * `currentValue` the value that is currently stored 17 | * `rvalInstance` the current RVal instance (see the `rval()` function) 18 | * The return value is the value that will be passed to the next pre-processor, or if there is none, that will be stored. 19 | 20 | It is possible to chain multiple pre processors, by simply passing an array of preprocessors to `val`. 21 | 22 | `val` uses pointer equality to detect and propagate updates. So if your previous detects that the current update can or needs to be aborted, just `return currentValue`. 23 | 24 | For example, the following reactive value `profit` has two pre-processors. 25 | The first one automatically converts new values to strings, 26 | the second one performs some validation, refusing to store numbers that 27 | are smaller then the previous value. 28 | 29 | ```javascript 30 | function convertToNumber(newValue) { 31 | if (typeof newValue === "number") 32 | return newValue 33 | else 34 | return parseFloat(newValue) 35 | } 36 | 37 | const profit = val(0, [ 38 | // convert input to number 39 | convertToNumber, 40 | // check if profits increase 41 | (newValue, currentValue) => { 42 | if (newValue < currentValue) 43 | throw new Error("Invariant failed! Profits should increase") 44 | return newValue 45 | } 46 | ]) 47 | 48 | profit(-5) // Throws exception: Profits should increase 49 | 50 | profit("7") // Parses the number, and sets profit to number 7 51 | ``` 52 | 53 | 54 | ## Type checking 55 | 56 | sarcastic 57 | @r-val/types 58 | 59 | ## Type conversio 60 | 61 | ## Factories 62 | 63 | ## Debugging / logging 64 | 65 | ## Side effects 66 | -------------------------------------------------------------------------------- /docs_source/1_convenience/50_updaters.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Using updaters 3 | order: 5 4 | menu: Introduction 5 | route: /introduction/updaters 6 | --- -------------------------------------------------------------------------------- /docs_source/1_convenience/models.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Working with models 3 | order: 1 4 | menu: Advanced 5 | route: /advanced/models 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs_source/2_advanced/examples.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Examples 3 | order: 3 4 | menu: Advanced 5 | route: /advanced/examples 6 | --- 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs_source/2_advanced/mobx.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Relation to MobX 3 | order: 3 4 | menu: Advanced 5 | route: /advanced/mobx 6 | --- 7 | 8 | - much smaller api 9 | - easier to grok: make it clearer what we are reacting to, and what thigns are reactive. While still supporting transparent reactivy 10 | - no messing, patching or wrapping of objects and arrays 11 | - no decorators 12 | - easier build requirements 13 | - generally better interoperability with other libraries, especially functional ones 14 | - modular 15 | - fewer, but clearer customization hooks 16 | 17 | 18 | - lightweight version of mobx-state-tree 19 | - thin react bindings, inspired by mobx-react-lite 20 | -------------------------------------------------------------------------------- /docs_source/2_advanced/organizing_state.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Designing the state shape 3 | order: 3 4 | menu: Advanced 5 | route: /advanced/state-design 6 | --- 7 | 8 | # Designing the state shape 9 | 10 | ## Trees, everywhere 11 | 12 | ## Choosing immutability granularity level 13 | 14 | ## Composition, normalization and association 15 | 16 | ## Working with references 17 | -------------------------------------------------------------------------------- /docs_source/2_advanced/performance.mdx: -------------------------------------------------------------------------------- 1 | 2 | 3 | group mutations of collections -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // menu: [ 3 | // { 4 | // name: "Introduction" 5 | // }, 6 | // { 7 | // name: "Advanced" 8 | // }, 9 | // { 10 | // name: "API" 11 | // } 12 | // ], 13 | base: '/rval/', 14 | dest: 'docs', 15 | propsParser: false, 16 | hashRouter: true 17 | } -------------------------------------------------------------------------------- /examples/boxes/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/boxes/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/boxes/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | /.cache 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /examples/boxes/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | Make sure to run `yarn build && yarn link-examples` in the root folder of this repository to make sure the latest version of the r-val sources are linked into the example. -------------------------------------------------------------------------------- /examples/boxes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boxes", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "@babel/core": "^7.2.2", 7 | "parcel-bundler": "^1.11.0" 8 | }, 9 | "dependencies": { 10 | "@r-val/core": "^0.0.4", 11 | "@r-val/react": "^0.0.4", 12 | "@r-val/utils": "^0.0.4", 13 | "node-uuid": "^1.4.8", 14 | "react": "16.8.0-alpha.1", 15 | "react-dom": "16.8.0-alpha.1", 16 | "react-draggable": "^3.0.5" 17 | }, 18 | "scripts": { 19 | "start": "parcel public/index.html" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /examples/boxes/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RVal boxes demo 8 | 9 | 10 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/boxes/src/components/arrow-view.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { rview, useLocalDrv } from '@r-val/react' 3 | 4 | // TODO: when adding a new box, all arrow views will respond due to the lookup! 5 | const ArrowView = ({ arrow, store }) => { 6 | const from = useLocalDrv(() => store.boxes()[arrow.from]) 7 | const to = useLocalDrv(() => store.boxes()[arrow.to]) 8 | return rview(() => { 9 | const [x1, y1, x2, y2] = [from.x() + from.width() / 2, from.y() + 30, to.x() + to.width() / 2, to.y() + 30] 10 | console.log('rendering arrow ' + arrow.id) 11 | return 12 | }, true) 13 | } 14 | 15 | export default ArrowView 16 | -------------------------------------------------------------------------------- /examples/boxes/src/components/box-view.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { DraggableCore } from 'react-draggable' 3 | import { act } from '@r-val/core' 4 | import { rview } from '@r-val/react' 5 | 6 | class BoxView extends PureComponent { 7 | render() { 8 | const { box } = this.props 9 | return rview(() => { 10 | console.log('rendering box ' + box.id) 11 | return ( 12 | 13 |
22 | {box.name()} 23 |
24 |
25 | ) 26 | }) 27 | } 28 | 29 | handleClick = act(e => { 30 | this.props.store.selection(this.props.box) 31 | e.stopPropagation() 32 | }) 33 | 34 | handleDrag = act((_e, dragInfo) => { 35 | const { box } = this.props 36 | box.x(box.x() + dragInfo.deltaX) 37 | box.y(box.y() + dragInfo.deltaY) 38 | }) 39 | } 40 | 41 | export default BoxView 42 | -------------------------------------------------------------------------------- /examples/boxes/src/components/canvas.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { rview } from '@r-val/react' 3 | import { act } from '@r-val/core' 4 | 5 | import BoxView from './box-view' 6 | import ArrowView from './arrow-view' 7 | import Sidebar from './sidebar' 8 | import FunStuff from './fun-stuff' 9 | 10 | class Canvas extends Component { 11 | render() { 12 | const { store } = this.props 13 | return ( 14 |
15 | {rview(() => ( 16 |
17 | 18 | {Object.entries(store.arrows()).map(([id, arrow]) => ( 19 | 20 | ))} 21 | 22 | {Object.entries(store.boxes()).map(([id, box]) => ( 23 | 24 | ))} 25 |
26 | ))} 27 | 28 | 29 |
30 | ) 31 | } 32 | 33 | onCanvasClick = act(e => { 34 | const { store } = this.props 35 | if (e.ctrlKey === true && store.selection()) { 36 | const id = store.addBox('Hi.', e.clientX - 50, e.clientY - 20) 37 | store.addArrow(store.selection().id, id) 38 | store.selectionId(id) 39 | } 40 | }) 41 | } 42 | 43 | export default Canvas 44 | -------------------------------------------------------------------------------- /examples/boxes/src/components/fun-stuff.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {generateStuff} from '../stores/domain-state'; 4 | import * as history from '../stores/time'; 5 | 6 | export default (({store}) => (
7 | 8 | 9 | 10 |
)); 11 | -------------------------------------------------------------------------------- /examples/boxes/src/components/sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react' 2 | import { rview } from '@r-val/react' 3 | import { act } from '@r-val/core' 4 | 5 | class Sidebar extends PureComponent { 6 | render() { 7 | const { selection } = this.props 8 | return rview(() => 9 | selection() ? ( 10 |
11 | 12 |
13 | ) : ( 14 |
15 | ) 16 | ) 17 | } 18 | 19 | onChange = e => { 20 | this.props.selection().name(e.target.value) 21 | } 22 | } 23 | 24 | export default Sidebar 25 | -------------------------------------------------------------------------------- /examples/boxes/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | padding: 0; 19 | background-color: #2d3e4e 20 | } 21 | 22 | .box { 23 | padding: 20px 0; 24 | border: 2px solid white; 25 | position: absolute; 26 | border-radius: 8px; 27 | background-color: #2d3e4e; 28 | cursor: pointer; 29 | color: white; 30 | white-space: nowrap; 31 | font-family: arial; 32 | font-weight: 800; 33 | text-align: center; 34 | -webkit-touch-callout: none; 35 | -webkit-user-select: none; 36 | -khtml-user-select: none; 37 | -moz-user-select: none; 38 | -ms-user-select: none; 39 | user-select: none; 40 | } 41 | 42 | .box-selected { 43 | background-color: #f99b1d; 44 | } 45 | 46 | .canvas svg { 47 | background-color: #2d3e4e; 48 | position: absolute; 49 | left: 0px; 50 | top: 0px; 51 | } 52 | 53 | .canvas svg, .canvas { 54 | width: 100%; 55 | height: 100%; 56 | } 57 | 58 | .canvas path { 59 | stroke-width: 2; 60 | stroke: white; 61 | marker-end: url(#markerArrow); 62 | } 63 | 64 | #markerArrow { 65 | stroke-width: 1; 66 | stroke: white; 67 | fill: #2d3e4e; 68 | } 69 | 70 | .canvas path { 71 | fill: '#000000'; 72 | } 73 | 74 | .sidebar { 75 | position: fixed; 76 | bottom: 0px; 77 | top: 0px; 78 | right: -340px; 79 | width: 300px; 80 | padding: 100px 40px; 81 | background-color: #f0f0ee; 82 | box-shadow: 0px 0px 9px #000; 83 | transition: 1.2s cubic-bezier(0.32, 1, 0.23, 1); 84 | transform: translate3d(-0px, 0, 0); 85 | } 86 | 87 | .sidebar-open { 88 | transform: translate3d(-340px, 0, 0) 89 | } 90 | 91 | input, button { 92 | padding: 4px; 93 | font-size: 1.2em; 94 | } 95 | 96 | input { 97 | width: 100%; 98 | } 99 | 100 | .funstuff { 101 | position: fixed; 102 | bottom: 0; 103 | } 104 | -------------------------------------------------------------------------------- /examples/boxes/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import './index.css'; 5 | 6 | import Canvas from './components/canvas'; 7 | import { createBoxesStore } from './stores/domain-state'; 8 | import { trackChanges } from "./stores/time"; 9 | 10 | const store = createBoxesStore() 11 | 12 | trackChanges(store) 13 | 14 | ReactDOM.render(, document.getElementById('root')); 15 | -------------------------------------------------------------------------------- /examples/boxes/src/stores/domain-state.js: -------------------------------------------------------------------------------- 1 | import { v4 } from 'node-uuid' 2 | import { val, drv, act } from '@r-val/core' 3 | 4 | function createBox(data, selectionId) { 5 | const name = val(data.name) 6 | const x = val(data.x) 7 | const y = val(data.y) 8 | const width = drv(() => name().length * 15) 9 | const selected = drv(() => selectionId() === data.id) 10 | return { 11 | id: data.id, 12 | name, 13 | x, 14 | y, 15 | width, 16 | selected, 17 | } 18 | } 19 | 20 | export function createBoxesStore() { 21 | // TODO: support initial state, use models? 22 | const boxes = val({}) 23 | const arrows = val({}) 24 | const selectionId = val(null) 25 | const selection = drv( 26 | () => selectionId() ? boxes()[selectionId()] : null, 27 | (val) => selectionId(val ? val.id : null) 28 | ) 29 | 30 | function addBox(name, x, y) { 31 | const id = v4() 32 | boxes({ ...boxes(), [id]: createBox({id, name, x, y}, selectionId) }) 33 | return id 34 | } 35 | 36 | function addArrow(fromId, toId) { 37 | const id = v4() 38 | arrows({ 39 | ...arrows(), 40 | [id]: { 41 | id, 42 | from: fromId, 43 | to: toId, 44 | }, 45 | }) 46 | } 47 | 48 | function load(snapshot) { 49 | // without models, this is a bit cumersome 50 | const values = {} 51 | Object.keys(snapshot.boxes).forEach(key => { 52 | values[key] = createBox(snapshot.boxes[key], selectionId) 53 | }) 54 | boxes(values) 55 | arrows(snapshot.arrows) 56 | selectionId(snapshot.selectionId) 57 | } 58 | 59 | const id1 = addBox('Roosendaal', 100, 100) 60 | const id2 = addBox('Prague', 650, 300) 61 | const id3 = addBox('Tel Aviv', 150, 300) 62 | addArrow(id1, id2) 63 | addArrow(id2, id3) 64 | 65 | return { 66 | boxes, 67 | arrows, 68 | selectionId, 69 | selection, 70 | addBox, 71 | addArrow, 72 | load: act(load) 73 | } 74 | } 75 | 76 | // /** 77 | // Generate 'amount' new random arrows and boxes 78 | // */ 79 | export const generateStuff = act(function(store, amount) { 80 | const ids = Object.keys(store.boxes()) 81 | for (var i = 0; i < amount; i++) { 82 | const id = store.addBox('#' + i, Math.random() * window.innerWidth * 0.5, Math.random() * window.innerHeight) 83 | // TODO: addArrow / addBox is slow and should use one single update instead 84 | store.addArrow(ids[Math.floor(Math.random() * ids.length)], id) 85 | ids.push(id) 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /examples/boxes/src/stores/time.js: -------------------------------------------------------------------------------- 1 | import { sub, drv } from "@r-val/core" 2 | import { toJS } from "@r-val/utils" 3 | 4 | const states = []; 5 | let currentFrame = -1; 6 | let undoing = false 7 | 8 | export function trackChanges(store) { 9 | const snapshot = drv(() => toJS(store)) 10 | sub(snapshot, state => { 11 | // console.dir(state) 12 | if (!undoing) { 13 | states.splice(++currentFrame) 14 | states.push(toJS(state)) 15 | } 16 | }) 17 | } 18 | 19 | export function previousState(store) { 20 | if (currentFrame > 0) { 21 | currentFrame--; 22 | undoing = true 23 | store.load(states[currentFrame]) 24 | undoing = false 25 | } 26 | } 27 | 28 | export function nextState(store) { 29 | if (currentFrame < states.length -1) { 30 | currentFrame++; 31 | undoing = true 32 | store.load(states[currentFrame]) 33 | undoing = false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testURL": "http://localhost", 3 | "transform": { 4 | "^.+\\.jsx?$": "babel-jest", 5 | "^.+\\.tsx?$": "ts-jest" 6 | }, 7 | "testRegex": "/tests/[^/]*.spec.[jt]sx?$", 8 | "moduleNameMapper": { 9 | "^@r-val/(.*)": "/pkgs/$1/$1.ts" 10 | }, 11 | "globals": { 12 | "ts-jest": { 13 | "diagnostics": false, 14 | "tsConfig": "tsconfig.json" 15 | } 16 | }, 17 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"], 18 | "modulePathIgnorePatterns": ["node_modules", "\\.git"] 19 | } 20 | -------------------------------------------------------------------------------- /jest.config.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "testURL": "http://localhost", 3 | "transform": { 4 | "^.+\\.jsx?$": "babel-jest", 5 | "^.+\\.tsx?$": "ts-jest" 6 | }, 7 | "testRegex": "/tests/[^/]*.spec.[jt]sx?$", 8 | "moduleNameMapper": { 9 | "^@r-val/(.*)": "/pkgs/$1" 10 | }, 11 | "globals": { 12 | "ts-jest": { 13 | "diagnostics": false, 14 | "tsConfig": "tsconfig.json" 15 | } 16 | }, 17 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"] 18 | } 19 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "pkgs/*" 4 | ], 5 | "version": "0.0.4" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rval", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "", 6 | "repository": "git+https://github.com/mweststrate/rval.git", 7 | "main": "core/index.js", 8 | "umd:main": "core/index.umd.js", 9 | "module": "core/index.module.js", 10 | "jsnext:main": "core/index.module.js", 11 | "react-native": "core/index.module.js", 12 | "types": "core/index.d.ts", 13 | "scripts": { 14 | "build": "node scripts/build.js", 15 | "link-examples": "node scripts/link.js", 16 | "watch": "jest --config jest.config.json --watch", 17 | "test": "jest --config jest.config.test.json", 18 | "test:perf": "PERSIST=true time node --expose-gc pkgs/core/tests/perf/index.js", 19 | "coverage": "jest --runInBand --coverage --config jest.config.json && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 20 | "docz:dev": "docz dev", 21 | "docz:build": "docz build", 22 | "release": "yarn build && lerna publish && yarn docz:build && git add docs && git commit -m 'regenned docs' && git push" 23 | }, 24 | "author": "Michel Weststrate", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/jest": "^23.3.9", 28 | "@types/node": "^10.12.17", 29 | "@types/react": "^16.7.17", 30 | "@types/react-dom": "^16.0.11", 31 | "coveralls": "^3.0.2", 32 | "cross-env": "^5.2.0", 33 | "docz": "^0.13.4", 34 | "docz-theme-default": "^0.13.4", 35 | "immer": "^1.9.3", 36 | "jest": "^23.6.0", 37 | "lerna": "^3.8.0", 38 | "microbundle": "^0.9.0", 39 | "prettier": "^1.15.2", 40 | "ramda": "^0.26.1", 41 | "react": "^16.7.0-alpha.2", 42 | "react-dom": "^16.7.0-alpha.2", 43 | "react-testing-library": "^5.4.0", 44 | "rimraf": "^2.6.2", 45 | "rollup": "^0.68.0", 46 | "rollup-plugin-filesize": "^5.0.1", 47 | "rollup-plugin-terser": "^3.0.0", 48 | "rollup-plugin-typescript2": "^0.18.0", 49 | "sarcastic": "^1.5.0", 50 | "tape": "^4.9.2", 51 | "ts-jest": "^23.10.5", 52 | "tslib": "^1.9.3", 53 | "typescript": "^3.2.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkgs/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/core", 3 | "private": false, 4 | "version": "0.0.4", 5 | "description": "minimalistic, transparent reactive programming library", 6 | "main": "dist/core.js", 7 | "umd:main": "dist/core.umd.js", 8 | "unpkg": "dist/core.umd.js", 9 | "module": "dist/core.mjs", 10 | "jsnext:main": "dist/core.mjs", 11 | "react-native": "dist/core.mjs", 12 | "browser": { 13 | "./dist/core.js": "./dist/core.js", 14 | "./dist/core.mjs": "./dist/core.mjs" 15 | }, 16 | "source": "core.ts", 17 | "scripts": {}, 18 | "author": "Michel Weststrate", 19 | "license": "MIT", 20 | "files": [ 21 | "*.ts", 22 | "dist" 23 | ], 24 | "reserved": [ 25 | "global", 26 | "log", 27 | "warn", 28 | "configurable", 29 | "enumerable", 30 | "writable", 31 | "value", 32 | "$RVal", 33 | "rval", 34 | "defaultInstance", 35 | "defaultRValInstance", 36 | "isVal", 37 | "isDrv", 38 | "_deepfreeze", 39 | "_once", 40 | "_isPlainObject", 41 | "val", 42 | "drv", 43 | "sub", 44 | "run", 45 | "act", 46 | "effect", 47 | "configure", 48 | "autoFreeze", 49 | "fireImmediately" 50 | ], 51 | "mangle": { 52 | "reserved": [ 53 | "global", 54 | "log", 55 | "warn", 56 | "configurable", 57 | "enumerable", 58 | "writable", 59 | "value", 60 | "$RVal", 61 | "rval", 62 | "defaultInstance", 63 | "defaultRValInstance", 64 | "isVal", 65 | "isDrv", 66 | "_deepfreeze", 67 | "_once", 68 | "_isPlainObject", 69 | "val", 70 | "drv", 71 | "sub", 72 | "run", 73 | "act", 74 | "effect", 75 | "configure", 76 | "autoFreeze", 77 | "fireImmediately" 78 | ] 79 | }, 80 | "gitHead": "a824fe9bd83228fb126c8c6188ef7b85fc2ffdd7", 81 | "peerDependencies": { 82 | "@r-val/core": "*" 83 | } 84 | } -------------------------------------------------------------------------------- /pkgs/core/tests/config.spec.ts: -------------------------------------------------------------------------------- 1 | import { val, sub, drv, rval } from '@r-val/core' 2 | 3 | test('auto freeze', () => { 4 | { 5 | debugger 6 | const v = val({ x: 1 }) 7 | expect(() => v().y = 1).toThrow(/object is not extensible/) 8 | 9 | v({ x: { y: 1 }}) 10 | expect(() => v().x.y = 1).toThrow(/Cannot assign to read only property 'y'/) 11 | } 12 | { 13 | const newContext = rval() 14 | const v = newContext.val({ x: 1 }) 15 | expect(() => v().y = 1).toThrow(/object is not extensible/) 16 | newContext.configure({ 17 | autoFreeze: false 18 | }) 19 | expect(v({ x: 1})) 20 | expect(() => v().x = 2).not.toThrow() 21 | expect(v().x).toBe(2) 22 | expect(() => v().y = 2).not.toThrow() 23 | expect(v().y).toBe(2) 24 | } 25 | }) -------------------------------------------------------------------------------- /pkgs/core/tests/object.spec.ts: -------------------------------------------------------------------------------- 1 | import { val, sub, drv, act } from '@r-val/core' 2 | 3 | test('some basic stuff', () => { 4 | const events: any = [] 5 | const summer = { 6 | a: val(3), 7 | b: val(2), 8 | sum: drv(() => summer.a() + summer.b()), 9 | } 10 | 11 | sub(summer.sum, val => events.push(val)) 12 | 13 | expect(summer.sum()).toBe(5) 14 | summer.a(2) 15 | expect(events).toEqual([4]) 16 | }) 17 | 18 | describe('todos', () => { 19 | interface ITodo { 20 | id: string 21 | title: string 22 | done: boolean 23 | } 24 | 25 | function Todo(initial: ITodo) { 26 | const title = val(initial.title) 27 | const done = val(initial.done) 28 | const toggle = act(() => done(!done())) 29 | return { 30 | id: initial.id, 31 | title, 32 | done, 33 | toggle, 34 | } 35 | } 36 | 37 | function TodoList(initialState: ITodo[]) { 38 | const todos = val(initialState.map(Todo)) 39 | const completedCount = drv(() => todos().filter(todo => todo.done()).length) 40 | return { 41 | todos, 42 | add(todo: ITodo) { 43 | todos([...todos(), Todo(todo)]) 44 | }, 45 | remove(id) { 46 | todos(todos().filter(todo => todo.id !== id)) 47 | }, 48 | completedCount, 49 | } 50 | } 51 | 52 | const initialState = [ 53 | { 54 | id: 'a', 55 | title: 'make coffee', 56 | done: false, 57 | }, 58 | { 59 | id: 'b', 60 | title: 'grab cookie', 61 | done: true, 62 | }, 63 | ] 64 | 65 | test('updates count', () => { 66 | const events: number[] = [] 67 | const l = TodoList(initialState) 68 | sub(l.completedCount, c => events.push(c)) 69 | expect(l.completedCount()).toBe(1) 70 | l.todos()[0].title("No effect") 71 | l.todos()[1].toggle() 72 | l.add({ id: "x", title: "test", done: true}) 73 | l.remove("a") 74 | l.remove("x") 75 | expect(events).toEqual([ 76 | 0, 77 | 1, 78 | 0 79 | ]) 80 | }) 81 | 82 | test("direct manipulation should fail", () => { 83 | const l = TodoList(initialState) 84 | expect(() => { 85 | l.todos()[0].done = 3 as any 86 | }).toThrow("Cannot assign to read only property 'done'") 87 | expect(() => { 88 | l.todos().push(Todo({ id: "bla", title: "bla", done: false })) 89 | }).toThrow("object is not extensible") 90 | }) 91 | }) 92 | 93 | describe("using prototype", () => { 94 | interface ITodo { 95 | id: string 96 | title: string 97 | done: boolean 98 | } 99 | 100 | const TodoProto = { 101 | toggle(this: any) { 102 | this.done(!this.done()) 103 | } 104 | } 105 | 106 | const TodoListProto = { 107 | add(this: any, todo: ITodo) { 108 | this.todos([...this.todos(), Todo(todo)]) 109 | }, 110 | remove(this: any, id) { 111 | this.todos(this.todos().filter(todo => todo.id !== id)) 112 | }, 113 | } 114 | 115 | function Todo(initial: ITodo) { 116 | return Object.assign( 117 | Object.create(TodoProto), 118 | { 119 | id: initial.id, 120 | title: val(initial.title), 121 | done: val(initial.done) 122 | } 123 | ) 124 | } 125 | 126 | function TodoList(initialState: ITodo[]) { 127 | let self // used by drv, as 'this' is not available 128 | return self = Object.assign( 129 | Object.create(TodoListProto), { 130 | todos: val(initialState.map(Todo)), 131 | completedCount: drv(() => { 132 | return self.todos().filter(todo => todo.done()).length 133 | }) 134 | }) 135 | } 136 | 137 | const initialState = [ 138 | { 139 | id: 'a', 140 | title: 'make coffee', 141 | done: false, 142 | }, 143 | { 144 | id: 'b', 145 | title: 'grab cookie', 146 | done: true, 147 | }, 148 | ] 149 | 150 | test('updates count', () => { 151 | const events: number[] = [] 152 | const l = TodoList(initialState) 153 | sub(l.completedCount, c => events.push(c)) 154 | expect(l.completedCount()).toBe(1) 155 | l.todos()[0].title("No effect") 156 | l.todos()[1].toggle() 157 | l.add({ id: "x", title: "test", done: true}) 158 | l.remove("a") 159 | l.remove("x") 160 | expect(events).toEqual([ 161 | 0, 162 | 1, 163 | 0 164 | ]) 165 | }) 166 | 167 | test("direct manipulation should fail", () => { 168 | const l = TodoList(initialState) 169 | expect(() => { 170 | l.todos()[0].done = 3 as any 171 | }).toThrow("Cannot assign to read only property 'done'") 172 | expect(() => { 173 | l.todos().push(Todo({ id: "bla", title: "bla", done: false })) 174 | }).toThrow("object is not extensible") 175 | }) 176 | 177 | }) 178 | 179 | 180 | describe("using class", () => { 181 | interface ITodo { 182 | id: string 183 | title: string 184 | done: boolean 185 | } 186 | 187 | class Todo { 188 | readonly id 189 | readonly done = val(false) 190 | readonly title = val("") 191 | 192 | constructor(initial: ITodo) { 193 | this.id = initial.id 194 | this.done(initial.done) 195 | this.title(initial.title) 196 | } 197 | toggle() { 198 | this.done(!this.done()) 199 | } 200 | } 201 | 202 | class TodoList { 203 | readonly todos = val([]) 204 | 205 | constructor(initial) { 206 | this.todos(initial.map(t => new Todo(t))) 207 | } 208 | 209 | readonly completedCount= drv(() => { 210 | return this.todos().filter(todo => todo.done()).length 211 | }) 212 | 213 | add(this: any, todo: ITodo) { 214 | this.todos([...this.todos(), new Todo(todo)]) 215 | } 216 | 217 | remove(this: any, id) { 218 | this.todos(this.todos().filter(todo => todo.id !== id)) 219 | } 220 | } 221 | 222 | const initialState = [ 223 | { 224 | id: 'a', 225 | title: 'make coffee', 226 | done: false, 227 | }, 228 | { 229 | id: 'b', 230 | title: 'grab cookie', 231 | done: true, 232 | }, 233 | ] 234 | 235 | test('updates count', () => { 236 | const events: number[] = [] 237 | const l = new TodoList(initialState) 238 | sub(l.completedCount, c => events.push(c)) 239 | expect(l.completedCount()).toBe(1) 240 | l.todos()[0].title("No effect") 241 | l.todos()[1].toggle() 242 | l.add({ id: "x", title: "test", done: true}) 243 | l.remove("a") 244 | l.remove("x") 245 | expect(events).toEqual([ 246 | 0, 247 | 1, 248 | 0 249 | ]) 250 | }) 251 | 252 | test("direct manipulation should fail", () => { 253 | const l = new TodoList(initialState) 254 | expect(() => { 255 | (l.todos()[0] as any).done = 3 256 | }).toThrow("Cannot assign to read only property 'done'") 257 | expect(() => { 258 | l.todos().push(new Todo({ id: "bla", title: "bla", done: false })) 259 | }).toThrow("object is not extensible") 260 | }) 261 | 262 | }) 263 | -------------------------------------------------------------------------------- /pkgs/core/tests/perf/index.js: -------------------------------------------------------------------------------- 1 | var start = Date.now() 2 | 3 | if (process.env.PERSIST) { 4 | var fs = require("fs") 5 | var logFile = __dirname + "/perf.txt" 6 | // clear previous results 7 | if (fs.existsSync(logFile)) fs.unlinkSync(logFile) 8 | 9 | exports.logMeasurement = function(msg) { 10 | console.log(msg) 11 | fs.appendFileSync(logFile, "\n" + msg, "utf8") 12 | } 13 | } else { 14 | exports.logMeasurement = function(msg) { 15 | console.log(msg) 16 | } 17 | } 18 | 19 | require("./perf.js") 20 | 21 | // This test runs last.. 22 | require("tape")(t => { 23 | exports.logMeasurement( 24 | "\n\nCompleted performance suite in " + (Date.now() - start) / 1000 + " sec." 25 | ) 26 | t.end() 27 | }) 28 | -------------------------------------------------------------------------------- /pkgs/core/tests/perf/perf.txt: -------------------------------------------------------------------------------- 1 | 2 | One observers many observes one - Started/Updated in 46/54 ms. 3 | 500 props observing sibling - Started/Updated in 2/3 ms. 4 | Late dependency change - Updated in 829ms. 5 | Unused computables - Updated in 0 ms. 6 | Unused observables - Updated in 59 ms. 7 | Array reduce - Started/Updated in 314/603 ms. 8 | Array loop - Started/Updated in 361/670 ms. 9 | Order system batched: false tracked: true Started/Updated in 136/78 ms. 10 | Order system batched: true tracked: true Started/Updated in 120/65 ms. 11 | Order system batched: false tracked: false Started/Updated in 119/36 ms. 12 | Order system batched: true tracked: false Started/Updated in 109/45 ms. 13 | 14 | Create array - Created in 239ms. 15 | 16 | Create array (non-recursive) Created in 239ms. 17 | Observable with many observers + dispose: 4937ms 18 | expensive sort: created 2963 19 | expensive sort: updated 25460 20 | expensive sort: disposed210 21 | native plain sort: updated 1083 22 | computed memoization 1ms 23 | Initilizing 100000 maps: 84 ms. 24 | Looking up 100 map properties 1000 times: 20 ms. 25 | Setting and deleting 100 map properties 1000 times: 5136 ms. 26 | 27 | 28 | Completed performance suite in 44.588 sec. -------------------------------------------------------------------------------- /pkgs/core/tests/scheduling.spec.ts: -------------------------------------------------------------------------------- 1 | import { val, sub, drv, effect, $RVal } from '@r-val/core' 2 | 3 | function getDeps(thing) { 4 | return Array.from(thing[$RVal].listeners) 5 | } 6 | 7 | async function delay(time) { 8 | return new Promise(r => { 9 | setTimeout(() => r(), time) 10 | }) 11 | } 12 | 13 | test("scheduling 1", async () => { 14 | const x = val(3) 15 | const y = drv(() => { 16 | events.push("compute y") 17 | return Math.round(x()) 18 | }) 19 | 20 | let events: any[] = [] 21 | let p 22 | 23 | const d = effect( 24 | () => { 25 | events.push("compute") 26 | return y() * 2 27 | }, 28 | (hasChanged, pull) => { 29 | p = pull 30 | events.push("invalidate") 31 | setTimeout(() => { 32 | if (hasChanged()) { 33 | events.push("changed") 34 | setTimeout(() => { 35 | events.push("pulling") 36 | pull() // doesn't recompute if not dirty! 37 | events.push("got: "+ pull()) 38 | }, 10) 39 | } else { 40 | events.push("not changed") 41 | events.push("got: "+ pull()) 42 | } 43 | }, 10) 44 | } 45 | ) 46 | 47 | expect(events.splice(0)).toEqual([ 48 | "invalidate", 49 | ]) 50 | 51 | await delay(100) 52 | expect(events.splice(0)).toEqual([ 53 | "changed", 54 | "pulling", 55 | "compute", 56 | "compute y", 57 | "got: " + 6 58 | ]) 59 | 60 | expect(p()).toBe(6) 61 | 62 | await delay(50) 63 | expect(events.splice(0)).toEqual([ ]) 64 | 65 | if (y[$RVal].markDirty) { 66 | // only check on non-minified build 67 | // should be exactly one dependency 68 | expect(getDeps(x)).toEqual([y[$RVal].markDirty]) 69 | } 70 | x(4) 71 | expect(events.splice(0)).toEqual([ 72 | "invalidate" 73 | ]) 74 | 75 | await delay(50) 76 | 77 | expect(events.splice(0)).toEqual([ 78 | "compute y", 79 | "changed", 80 | "pulling", 81 | "compute", 82 | "got: " + 8 83 | ]) 84 | 85 | x(4.2) 86 | expect(events.splice(0)).toEqual([ 87 | "invalidate", 88 | ]) 89 | 90 | await delay(50) 91 | expect(events.splice(0)).toEqual([ 92 | "compute y", 93 | "not changed", 94 | "got: " + 8 95 | ]) 96 | 97 | d() 98 | x(5) 99 | await delay(50) 100 | 101 | expect(events.splice(0)).toEqual([ 102 | ]) 103 | }) 104 | 105 | test("drv are temporarily kept alive", async () => { 106 | let count = 0 107 | const x = val(1) 108 | const y = drv(() => { 109 | count++ 110 | return x() * 2 111 | }) 112 | 113 | expect(y()).toBe(2) 114 | expect(count).toBe(1) 115 | 116 | // kept alaive 117 | expect(y()).toBe(2) 118 | expect(count).toBe(1) 119 | 120 | x(2) 121 | expect(y()).toBe(4) 122 | expect(count).toBe(2) 123 | 124 | await delay(1000) 125 | expect(y()).toBe(4) 126 | expect(count).toBe(3) 127 | expect(y()).toBe(4) 128 | expect(count).toBe(3) 129 | 130 | x(3) 131 | expect(y()).toBe(6) 132 | expect(count).toBe(4) 133 | 134 | x(4) 135 | expect(count).toBe(4) // y() wasn't called 136 | }) 137 | -------------------------------------------------------------------------------- /pkgs/models/README.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Models 3 | order: 3 4 | menu: API 5 | route: /api/models 6 | --- 7 | 8 | # @r-val/models 9 | -------------------------------------------------------------------------------- /pkgs/models/models.ts: -------------------------------------------------------------------------------- 1 | import { isVal, _deepfreeze, PreProcessor, Val, Drv, drv } from "@r-val/core" 2 | import { toJS, keepAlive } from "@r-val/utils"; 3 | 4 | const $factory = Symbol('$factory') 5 | 6 | type SnapshotType = { 7 | [K in keyof T]?: T[K] extends Val 8 | ? X | S 9 | : T[K] extends Drv 10 | ? never 11 | : T[K] extends Function 12 | ? never 13 | : T[K] extends (string | number | boolean) 14 | ? T[K] 15 | : SnapshotType 16 | } 17 | 18 | // TODO: pass bae value as first arg to factory? or call res.afterCreate() ? and what about other hooks? parents? 19 | // TODO: pass RvalFactories in as second arg to factory? 20 | // TODO: make sure the return value is made readonly, typewise! 21 | export function model(factory: () => T, key?: keyof T): PreProcessor> 22 | export function model(factory, key?) { 23 | return Object.assign( 24 | function modelPreProcessor(newValue, currentValue?) { 25 | if (newValue == null) return newValue 26 | if (typeof newValue !== 'object') throw new Error('Model expects null, undefined or an object') 27 | if (newValue[$factory]) { 28 | if (newValue[$factory] !== factory) throw new Error(`Factory mismatch`) 29 | return newValue 30 | } 31 | if (key && newValue[key] === undefined) throw new Error(`Attribute '${key}' is required`) 32 | const reconcilable = currentValue && (!key || newValue[key] === currentValue[key]) 33 | let base 34 | if (reconcilable) 35 | base = currentValue 36 | else { 37 | const fromFactory = factory() 38 | const snapshot = drv(() => toJS(fromFactory)) 39 | keepAlive(snapshot) 40 | base = Object.assign({ 41 | [$factory]: factory, 42 | toJS: snapshot 43 | }, fromFactory) // Optimization: swapping args would probalby lot faster 44 | } 45 | // TODO: factory should set debug names 46 | // update props from the provided initial snapshot 47 | for (let prop in newValue) { 48 | if (isVal(base[prop])) { 49 | base[prop](newValue[prop]) 50 | } else if (!reconcilable) { 51 | if (prop in base) base[prop] = newValue[prop] 52 | else throw new Error(`Property '${prop}' has not been declared in the model`) 53 | } else if (prop !== key) throw new Error(`Property '${prop}' cannot be updated`) 54 | } 55 | return _deepfreeze(base) 56 | }, 57 | { key } 58 | ) 59 | } 60 | 61 | export function mapOf(model: PreProcessor): PreProcessor<{ [key: string]: T }, { [key: string]: T | S }> 62 | export function mapOf(model) { 63 | return function mapPreProcessor(newValue, currentValue) { 64 | const res = {} 65 | if (newValue) 66 | for (let key in newValue) { 67 | res[key] = model(newValue[key], currentValue && currentValue[key]) 68 | } 69 | return res 70 | } 71 | } 72 | 73 | export function arrayOf(model: PreProcessor): PreProcessor 74 | export function arrayOf(model) { 75 | return function arrayPreProcessor(newValue, currentValue) { 76 | if (!newValue) return [] 77 | const { key } = model 78 | if (!key || !currentValue || !currentValue.length) return newValue.map(v => model(v)) 79 | const cache = new Map() 80 | currentValue.forEach(v => { 81 | if (v) cache.set(v[key], v) 82 | }) 83 | return newValue.map(v => (v ? model(v, cache.get(v[key])) : v)) 84 | } 85 | } 86 | 87 | export function invariant(predicate: (v: T) => boolean): PreProcessor 88 | export function invariant(predicate: (v) => boolean) { 89 | return function predicatePreProcessor(newValue) { 90 | if (!predicate(newValue)) throw new Error(`Invariant failed for value '${newValue}'`) 91 | return newValue 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pkgs/models/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/models", 3 | "private": false, 4 | "version": "0.0.4", 5 | "description": "", 6 | "main": "dist/models.js", 7 | "umd:main": "dist/models.umd.js", 8 | "unpkg": "dist/models.umd.js", 9 | "module": "dist/models.mjs", 10 | "jsnext:main": "dist/models.mjs", 11 | "react-native": "dist/models.mjs", 12 | "browser": { 13 | "./dist/models.js": "./dist/models.js", 14 | "./dist/models.mjs": "./dist/models.mjs" 15 | }, 16 | "source": "models.ts", 17 | "scripts": {}, 18 | "author": "Michel Weststrate", 19 | "license": "MIT", 20 | "files": [ 21 | "*.ts", 22 | "dist" 23 | ], 24 | "peerDependencies": { 25 | "@r-val/core": "*" 26 | } 27 | } -------------------------------------------------------------------------------- /pkgs/models/tests/model.spec.ts: -------------------------------------------------------------------------------- 1 | import { val } from "@r-val/core" 2 | import { model, mapOf, arrayOf, invariant } from "@r-val/models" 3 | 4 | test('simple model', () => { 5 | const Todo = model(() => ({ 6 | title: val('test'), 7 | })) 8 | 9 | expect(() => { 10 | Todo(3 as any) 11 | }).toThrow('Model expects null, undefined or an object') 12 | expect(Todo(null as any)).toBe(null) 13 | expect(Todo(undefined as any)).toBe(undefined) 14 | expect(Todo({}).title()).toBe('test') 15 | expect(Todo({ title: 'hello' }).title()).toBe('hello') 16 | expect(() => { 17 | Todo({ title: 'test', bla: 3 } as any) 18 | }).toThrow('bla') 19 | expect(Todo(null as any, {} as any)).toBe(null) 20 | expect(Todo({ title: 'xx' }, undefined).title()).toEqual('xx') 21 | 22 | const t1 = Todo({ title: 'hello' }) 23 | const t2 = Todo({ title: 'world' }, t1) 24 | expect(t1).toBe(t2) 25 | expect(t2.title()).toBe('world') 26 | 27 | const t3 = Todo({ title: 'hello' }) 28 | const t4 = Todo(Todo({ title: 'world' }), t3) 29 | expect(t3).not.toBe(t4) 30 | expect(t3.title()).toBe('hello') 31 | expect(t4.title()).toBe('world') 32 | }) 33 | 34 | test('simple model - with key', () => { 35 | const Todo = model( 36 | () => ({ 37 | id: 0, 38 | title: val('test'), 39 | }), 40 | 'id' 41 | ) 42 | 43 | expect(() => { 44 | Todo({}) 45 | }).toThrow('required') 46 | 47 | { 48 | const t1 = Todo({ id: 0, title: 'hello' }) 49 | const t2 = Todo({ id: 0, title: 'world' }, t1) 50 | expect(t1).toBe(t2) 51 | expect(t2.title()).toBe('world') 52 | } 53 | { 54 | const t1 = Todo({ id: 0, title: 'hello' }) 55 | const t2 = Todo({ id: 1, title: 'world' }, t1) 56 | expect(t1).not.toBe(t2) 57 | expect(t1.title()).toBe('hello') 58 | expect(t2.title()).toBe('world') 59 | } 60 | { 61 | const t1 = Todo({ id: 0, title: 'hello' }) 62 | const t2 = Todo(Todo({ id: 0, title: 'world' }), t1) 63 | expect(t1).not.toBe(t2) 64 | expect(t1.title()).toBe('hello') 65 | expect(t2.title()).toBe('world') 66 | } 67 | }) 68 | 69 | test('invariant', () => { 70 | const Bool = invariant(x => typeof x === "boolean") 71 | const bool = val(false, Bool) 72 | bool(true) 73 | expect(bool()).toBe(true) 74 | expect(() => bool(0)).toThrow("Invariant failed") 75 | expect(bool()).toBe(true) 76 | expect(() => val(3, Bool)).toThrow("Invariant failed ") 77 | }) 78 | 79 | describe('todostore', () => { 80 | function toggle(this: any) { 81 | this.done(!this.done()) 82 | } 83 | const Todo = model( 84 | () => ({ 85 | id: 0, 86 | title: val('test'), 87 | done: val(false), 88 | toggle, 89 | }), 90 | 'id' 91 | ) 92 | 93 | const Store = model(() => { 94 | const todos = val([], arrayOf(Todo)) 95 | return { 96 | todos, 97 | } 98 | }) 99 | 100 | it('basics', () => { 101 | 102 | const s = Store({ 103 | todos: [ 104 | { 105 | id: 1, 106 | title: 'hello', 107 | done: true, 108 | }, 109 | ], 110 | }) 111 | const t1 = s.todos()[0] 112 | expect(t1.title()).toBe('hello') 113 | expect(t1.done()).toBe(true) 114 | expect(t1.id).toBe(1) 115 | 116 | t1.toggle() 117 | expect(t1.done()).toBe(false) 118 | }) 119 | 120 | it('reconciliation', () => { 121 | const s = Store({ 122 | todos: [ 123 | { 124 | id: 1, 125 | title: 'hello', 126 | done: true, 127 | }, 128 | ], 129 | }) 130 | const x = s.todos()[0] 131 | s.todos([ 132 | { 133 | id: 2, 134 | title: 'boe', 135 | }, 136 | ...s.todos(), 137 | ]) 138 | const [t1, t2] = s.todos() 139 | expect(t1.title()).toBe('boe') 140 | expect(t2.title()).toBe('hello') 141 | expect(x).toBe(t2) 142 | 143 | s.todos([{ id: 3, title: 'hey' }, { id: 1, done: true }, Todo({ id: 2, done: true })]) 144 | const [u1, u2, u3] = s.todos() 145 | expect(u1.title()).toBe('hey') 146 | expect(u2.title()).toBe('hello') 147 | expect(u2.done()).toBe(true) 148 | expect(u2).toBe(t2) 149 | expect(u3.title()).toBe('test') 150 | expect(u3.done()).toBe(true) 151 | expect(u3).not.toBe(t1) 152 | }) 153 | }) 154 | 155 | 156 | describe('todostore - with map', () => { 157 | function toggle(this: any) { 158 | this.done(!this.done()) 159 | } 160 | const Todo = model( 161 | () => ({ 162 | id: "", 163 | title: val('test'), 164 | done: val(false), 165 | toggle, 166 | }), 167 | 'id' 168 | ) 169 | 170 | const Store = model(() => { 171 | const todos = val({}, mapOf(Todo)) 172 | return { 173 | todos, 174 | } 175 | }) 176 | 177 | it('basics', () => { 178 | const s = Store({ 179 | todos: { 180 | "a": { 181 | id: "a", 182 | title: 'hello', 183 | done: true, 184 | }, 185 | }, 186 | }) 187 | const t1 = s.todos().a 188 | expect(t1.title()).toBe('hello') 189 | expect(t1.done()).toBe(true) 190 | expect(t1.id).toBe("a") 191 | t1.toggle() 192 | expect(t1.done()).toBe(false) 193 | }) 194 | 195 | it('reconciliation', () => { 196 | const s = Store({ 197 | todos: { 198 | a: { 199 | id: "a", 200 | title: 'hello', 201 | done: true, 202 | }, 203 | }, 204 | }) 205 | const x = s.todos().a 206 | s.todos({ 207 | "b": { 208 | id: "b", 209 | title: 'boe', 210 | }, 211 | ...s.todos(), 212 | }) 213 | { 214 | const {a, b} = s.todos() 215 | expect(a.title()).toBe('hello') 216 | expect(b.title()).toBe('boe') 217 | expect(x).toBe(a) 218 | } 219 | { 220 | const oldA = s.todos().a 221 | const oldB = s.todos().b 222 | s.todos({ 223 | c: { id: 'c', title: 'hey' }, 224 | a: { id: "a", done: true }, b: Todo({ id: "b", done: true })}) 225 | const {a,b,c} = s.todos() 226 | expect(c.title()).toBe('hey') 227 | expect(a.title()).toBe('hello') 228 | expect(a.done()).toBe(true) 229 | expect(a).toBe(oldA) 230 | expect(b.title()).toBe('test') 231 | expect(b.done()).toBe(true) 232 | expect(b).not.toBe(oldB) 233 | } 234 | }) 235 | }) 236 | 237 | 238 | 239 | describe('todostore - with parent', () => { 240 | function toggle(this: any) { 241 | this.done(!this.done()) 242 | } 243 | const Todo = parent => model( 244 | () => ({ 245 | id: 0, 246 | title: val('test'), 247 | done: val(false), 248 | toggle, 249 | remove() { 250 | parent.todos(parent.todos().filter(t => t !== this)) 251 | } 252 | }), 253 | 'id' 254 | ) 255 | 256 | const Store = model(() => { 257 | const self = {} 258 | const todos = val([], arrayOf(Todo(self))) 259 | return Object.assign(self, { 260 | todos, 261 | }) 262 | }) 263 | 264 | it('basics', () => { 265 | const s = val({ 266 | todos: [ 267 | { 268 | id: 1, 269 | title: 'hello', 270 | done: true, 271 | }, 272 | ], 273 | }, Store) 274 | const t1 = s().todos()[0] 275 | s({todos: [{ 276 | id: 1, title: "world" 277 | }]}) 278 | 279 | expect(s().toJS()).toEqual({ 280 | todos: [ 281 | { done: true, id: 0, title: "world" } 282 | ] 283 | }) 284 | 285 | expect(t1.title()).toBe('world') 286 | expect(t1.done()).toBe(true) 287 | expect(t1).toBe(s().todos()[0]) 288 | 289 | t1.remove() 290 | expect(s().todos().length).toBe(0) 291 | 292 | }) 293 | }) 294 | 295 | // TODO: store with drv, check liveleness of toJS -------------------------------------------------------------------------------- /pkgs/react/README.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: React bindings 3 | order: 1 4 | menu: API 5 | route: /api/react 6 | --- 7 | 8 | # @r-val/react 9 | 10 | ## useVal 11 | 12 | **Signature** 13 | 14 | `useVal(reactiveValue) -> currentValue` 15 | 16 | **Description** 17 | 18 | _This function is a React hook, currently, it can only be used with React 16.7.0-alpha. use `rview` in all other cases_ 19 | 20 | Given a reactive value, subscribes the current React function component to the value. 21 | It returns the current state, and make sure the component is re-rendered on the next change of the value. 22 | 23 | Example ([try online](https://codesandbox.io/s/m297j6w38)): 24 | 25 | ```javascript 26 | import React from "react" 27 | import { render } from "react-dom" 28 | import { val } from "@r-val/core" 29 | import { useVal } from "@r-val/react" 30 | 31 | const counter = val(0) 32 | 33 | const Counter = () => { 34 | const c = useVal(counter) 35 | return (<> 36 | {c} 37 | 40 | ) 41 | } 42 | 43 | setInterval(() => { 44 | counter(c => c + 1) 45 | }, 1000); 46 | 47 | render(, document.body) 48 | ``` 49 | 50 | ## rview 51 | 52 | **Signature** 53 | 54 | `rview(render: () => ReactNode, memo?, rvalInstance?) -> ReactElement` 55 | 56 | **Description** 57 | 58 | `rview` can be used inside the render part of any React component, to render the given `render` function 59 | and track which reactive values it is using. 60 | `rview` will make sure that every time that reactive values involved in the rendering change, the function is called again. 61 | 62 | Example ([try online](https://codesandbox.io/s/m297j6w38)): 63 | 64 | ```javascript 65 | import React from "react" 66 | import { render } from "react-dom" 67 | import { val } from "@r-val/core" 68 | import { useVal } from "@r-val/react" 69 | 70 | const counter = val(0) 71 | 72 | const Counter = () => rview(() => ( 73 | <> 74 | {counter()} 75 | 78 | 79 | )) 80 | 81 | setInterval(() => { 82 | counter(c => c + 1) 83 | }, 1000); 84 | 85 | render(, document.body) 86 | ``` 87 | 88 | The `memo` argument can be used to further optimize the rendering, and takes three possible values: 89 | * `false` (the default). If `memo` is `false`, the `rview` will _also_ re-render if the owning component re-renders. The reason for that is that a new render function is passed in to the `rview`, which might be referring variables in it's closure. (For example state or calbacks defined in the owining component). 90 | * `true`. Use `true` if the render function is pure and doesn't rely on anything from it's closure, but only on reactive values (which are tracked). 91 | * `[inputs]`. Use an array of inputs (similarly to for example the `inputs` arguments of React's `useEffect` hook. This will cache the rendering of the `rview`, until either some reactive value used changes, or some of the inputs are not pointer equal. 92 | 93 | For more details: see the [unit tests](https://github.com/mweststrate/rval/blob/2a861ebfbfcc359b130269d286b00183abae2ef1/pkgs/react/tests/rview.spec.tsx#L141-L319) 94 | 95 | The `rvalInstance` argument can be used to use a different `RValInstance` for tracking. See also the [`@r-val/core`](#/api/core) documentation. 96 | 97 | ## useLocalVal 98 | - context 99 | 100 | **Signature** 101 | 102 | `useLocalVal(initialValue, rvalInstance?) -> reactiveValue` 103 | 104 | **Description** 105 | 106 | _This function is a React hook, currently, it can only be used with React 16.7.0-alpha. use `rview` in all other cases_ 107 | 108 | `useLocalVal` creates a `val` that is used to store state in a reactive value that is owned by the current function component. 109 | The component will always re-render if a new value is pushed into the reactive value. 110 | 111 | 112 | ## useLocalDrv 113 | 114 | **Signature** 115 | 116 | `useLocalDrv(derivation: () -> value, inputs?, rvalInstance?) -> currentValue` 117 | 118 | **Description** 119 | 120 | _This function is a React hook, currently, it can only be used with React 16.7.0-alpha. use `rview` in all other cases_ 121 | 122 | Creates a `drv` which is local to the current component, subscribes to it, and returns it current value. 123 | Normally, the `drv` instance will be the same for the entire life-cycle of the component. 124 | So, if the output of the `drv` depends on non-rval state or props, normally the derivation wouldn't re-evaluate. 125 | The `inputs` argument can be used to make sure changes in the inputs are reflected as well. 126 | Example: 127 | 128 | ```javascript 129 | function CounterMultiplier(({ multiplier })) => { 130 | const counter = useLocalVal(0) 131 | // The derivation will automatically react to counter(), as it is a reactive value, 132 | // however, multiplier is a plain input, so we have to pass it as 'input' to make sure 133 | // the derivation updates as well if that one changes 134 | const total = useLocalDrv(() => { 135 | return counter() * multiplier 136 | }, [multiplier]) 137 | 138 | return
{total}
139 | } 140 | 141 | ReactDOM.render(, document.body) -------------------------------------------------------------------------------- /pkgs/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/react", 3 | "private": false, 4 | "version": "0.0.4", 5 | "description": "", 6 | "main": "dist/react.js", 7 | "umd:main": "dist/react.umd.js", 8 | "unpkg": "dist/react.umd.js", 9 | "module": "dist/react.mjs", 10 | "jsnext:main": "dist/react.mjs", 11 | "react-native": "dist/react.mjs", 12 | "browser": { 13 | "./dist/react.js": "./dist/react.js", 14 | "./dist/react.mjs": "./dist/react.mjs" 15 | }, 16 | "source": "react.ts", 17 | "scripts": {}, 18 | "author": "Michel Weststrate", 19 | "license": "MIT", 20 | "peerDependencies": { 21 | "react": "^16.0.0", 22 | "@r-val/core": "*" 23 | }, 24 | "files": [ 25 | "*.ts", 26 | "dist" 27 | ], 28 | "gitHead": "a824fe9bd83228fb126c8c6188ef7b85fc2ffdd7" 29 | } -------------------------------------------------------------------------------- /pkgs/react/react.ts: -------------------------------------------------------------------------------- 1 | import { Observable, isVal, isDrv, defaultInstance, rval, RValInstance, Val } from '@r-val/core' 2 | import { useState, useEffect, useMemo, ReactNode, ReactElement, createElement, Component } from 'react' 3 | 4 | export function useVal(observable: Observable): T { 5 | if (!isVal(observable) && !isDrv(observable)) throw new Error('useval - expected val or drv') 6 | const [_, setTick] = useState(0) 7 | const disposer = useMemo(() => 8 | rval(observable).sub(observable, () => setTick(tick => tick + 1)) 9 | , [observable]) 10 | useEffect(() => disposer, [observable]) 11 | return observable() 12 | } 13 | 14 | export function useLocalVal(initial: T, rvalInstance = defaultInstance): Val { 15 | const val = useMemo(() => rvalInstance.val(initial), []) 16 | useVal(val) 17 | return val 18 | } 19 | 20 | export function useLocalDrv(derivation: () => T, inputs: any[] = [], rvalInstance = defaultInstance): T { 21 | const drv = useMemo(() => rvalInstance.drv(derivation), inputs) 22 | return useVal(drv) 23 | } 24 | 25 | export function rview(render: () => ReactNode, memo?: any[] | boolean, rvalInstance?: RValInstance): ReactElement | null { 26 | // TODO: or should rview be a HOC? 27 | // return props => {() => render(props)} 28 | return createElement(RView, { 29 | rvalInstance, memo, children: render 30 | }) 31 | } 32 | 33 | class RView extends Component<{ 34 | children?: () => ReactNode 35 | memo?: any[] | boolean, 36 | rvalInstance?: RValInstance 37 | }, {}> { 38 | 39 | static defaultProps = { 40 | rvalInstance: defaultInstance, 41 | memo: false 42 | } 43 | 44 | disposer?: () => void 45 | renderPuller?: () => ReactNode 46 | inputsAtom = this.props.rvalInstance!.val(0) 47 | 48 | shouldComponentUpdate() { 49 | return false; 50 | } 51 | 52 | inputsChanged(nextProps) { 53 | // memo: 54 | // - true: always memoize! children only depends on reactive vals, nothing else (equals memo=[]) 55 | // - falsy: never memoize: if we get a new children func, re-render (equals memo=[children]) 56 | // - array: re-render if any of the given inputs change 57 | const { memo } = nextProps 58 | if (memo === true) 59 | return false 60 | const newInputs = Array.isArray(memo) ? memo : [nextProps.children] 61 | const currentInputs = Array.isArray(this.props.memo) ? this.props.memo : [this.props.children] 62 | if (newInputs.length !== currentInputs.length) 63 | return true 64 | for (let i = 0; i < newInputs.length; i++) 65 | if (currentInputs![i] !== newInputs[i]) 66 | return true 67 | return false 68 | } 69 | 70 | componentWillReceiveProps(nextProps) { 71 | // Yes, unsafe solution! Either this, or migrate to _RView as defined below 72 | if (this.inputsChanged(nextProps)) 73 | this.inputsAtom(this.inputsAtom() + 1) 74 | } 75 | 76 | render() { 77 | if (!this.disposer) { 78 | this.disposer = this.props.rvalInstance!.effect(() => { 79 | this.inputsAtom(); 80 | return this.props.children!() 81 | }, (didChange, pull) => { 82 | this.renderPuller = pull 83 | if (didChange() && this.disposer) { // this.disposer = false? -> first rendering 84 | this.forceUpdate() 85 | } 86 | }) 87 | } 88 | return this.renderPuller!() 89 | } 90 | 91 | componentWillUnmount() { 92 | this.disposer && this.disposer() 93 | } 94 | } 95 | 96 | // Hook based implementation: 97 | // export function _RView({ 98 | // children, 99 | // rvalInstance = defaultContext, 100 | // inputs = [children], 101 | // }: { 102 | // children?: () => ReactNode 103 | // rvalInstance?: RValFactories 104 | // inputs?: any[] 105 | // }): ReactElement | null { 106 | // if (typeof children !== 'function') throw new Error('RVal expected function as children') 107 | // const [tick, setTick] = useState(0) 108 | // const { render, dispose } = useMemo( 109 | // () => { 110 | // let render 111 | // const dispose = rvalInstance.effect(children!, (didChange, pull) => { 112 | // render = pull 113 | // if (didChange()) { 114 | // setTick(tick + 1) 115 | // } 116 | // }) 117 | // return { render, dispose } 118 | // }, 119 | // inputs 120 | // ) 121 | // useEffect(() => dispose, inputs) 122 | // return render() 123 | // } 124 | -------------------------------------------------------------------------------- /pkgs/react/tests/useval.spec.tsx: -------------------------------------------------------------------------------- 1 | import { val, drv, rval } from '@r-val/core' 2 | import { useVal, useLocalVal, useLocalDrv } from '@r-val/react' 3 | import * as React from 'react' 4 | import { render, waitForElement } from 'react-testing-library' 5 | 6 | function delay(time) { 7 | return new Promise(resolve => { 8 | setTimeout(resolve, time) 9 | }) 10 | } 11 | 12 | test('useVal - 1 ', async () => { 13 | const counter = val(0) 14 | const Comp = () => { 15 | const c = useVal(counter) 16 | return

{c}

17 | } 18 | 19 | const re = render() 20 | expect(re.container.innerHTML).toEqual('

0

') 21 | 22 | counter(counter() + 1) 23 | await waitForElement(() => re.container.innerHTML === '

1

') 24 | 25 | counter(counter() + 1) 26 | await waitForElement(() => re.container.innerHTML === '

2

') 27 | }) 28 | 29 | test('useVal - mimimum computations - 1', async () => { 30 | const counter = val(0) 31 | let called = 0 32 | const doubler = drv(() => { 33 | called++ 34 | return counter() * 2 35 | }) 36 | 37 | const Comp = () => { 38 | const c = useVal(doubler) 39 | return

{c}

40 | } 41 | 42 | const re = render() 43 | expect(re.container.innerHTML).toEqual('

0

') 44 | expect(called).toBe(1) 45 | 46 | counter(counter() + 1) 47 | await waitForElement(() => re.container.innerHTML === '

2

') 48 | expect(called).toBe(2) 49 | 50 | counter(counter() + 1) 51 | await waitForElement(() => re.container.innerHTML === '

4

') 52 | expect(called).toBe(3) 53 | }) 54 | 55 | test('useVal - mimimum computations - 2', async () => { 56 | const counter = val(0) 57 | let called = 0 58 | const doubler = drv(() => { 59 | called++ 60 | return counter() * 2 61 | }) 62 | 63 | const Comp = () => { 64 | const c = useVal(doubler) 65 | return

{c}

66 | } 67 | 68 | const { container, unmount } = render() 69 | expect(container.innerHTML).toEqual('

0

') 70 | 71 | await delay(100) 72 | expect(called).toBe(1) // and not 2! 73 | 74 | expect(doubler()).toBe(0) 75 | expect(called).toBe(1) // still hot 76 | 77 | unmount() 78 | expect(doubler()).toBe(0) 79 | expect(called).toBe(2) // and not 1, as the doubler is not hot anymore after unmount 80 | }) 81 | 82 | test('useVal - custom rval ', async () => { 83 | const myRval = rval() 84 | const counter = myRval.val(0) 85 | const Comp = () => { 86 | const c = useVal(counter) 87 | return

{c}

88 | } 89 | 90 | const re = render() 91 | expect(re.container.innerHTML).toEqual('

0

') 92 | 93 | counter(counter() + 1) 94 | await waitForElement(() => re.container.innerHTML === '

1

') 95 | 96 | counter(counter() + 1) 97 | await waitForElement(() => re.container.innerHTML === '

2

') 98 | }) 99 | 100 | test('useLocalVal - 1 ', async () => { 101 | let updater 102 | const Comp = () => { 103 | const counter = useLocalVal(0) 104 | updater = counter 105 | return

{counter()}

106 | } 107 | 108 | const re = render() 109 | expect(re.container.innerHTML).toEqual('

0

') 110 | 111 | updater(updater() + 1) 112 | await waitForElement(() => re.container.innerHTML === '

1

') 113 | 114 | updater(updater() + 1) 115 | await waitForElement(() => re.container.innerHTML === '

2

') 116 | }) 117 | 118 | test('useLocalDrv - 1 ', async () => { 119 | const counter = val(0) 120 | const Comp = () => { 121 | const doubled = useLocalDrv(() => 2 * counter()) 122 | return

{doubled}

123 | } 124 | 125 | const re = render() 126 | expect(re.container.innerHTML).toEqual('

0

') 127 | 128 | counter(counter() + 1) 129 | await waitForElement(() => re.container.innerHTML === '

2

') 130 | 131 | counter(counter() + 1) 132 | await waitForElement(() => re.container.innerHTML === '

4

') 133 | }) 134 | 135 | 136 | test('useLocalDrv - with state & inputs ', async () => { 137 | const counter = val(1) 138 | let updater 139 | const Comp = () => { 140 | const [multiplier, setM] = React.useState(2) 141 | updater = setM 142 | const doubled = useLocalDrv(() => multiplier * counter(), [multiplier]) 143 | return

{doubled}

144 | } 145 | 146 | const re = render() 147 | expect(re.container.innerHTML).toEqual('

2

') 148 | 149 | updater(3) 150 | delay(10) 151 | expect(re.container.innerHTML).toEqual('

3

') 152 | 153 | counter(counter() + 1) 154 | delay(10) 155 | expect(re.container.innerHTML).toEqual('

6

') 156 | 157 | updater(1) 158 | delay(10) 159 | expect(re.container.innerHTML).toEqual('

2

') 160 | 161 | }) 162 | -------------------------------------------------------------------------------- /pkgs/types/README.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Type preprocessors 3 | order: 3 4 | menu: API 5 | route: /api/types 6 | --- 7 | 8 | # @r-val/types 9 | -------------------------------------------------------------------------------- /pkgs/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/types", 3 | "private": false, 4 | "version": "0.0.4", 5 | "description": "", 6 | "main": "dist/types.js", 7 | "umd:main": "dist/types.umd.js", 8 | "unpkg": "dist/types.umd.js", 9 | "module": "dist/types.mjs", 10 | "jsnext:main": "dist/types.mjs", 11 | "react-native": "dist/types.mjs", 12 | "browser": { 13 | "./dist/types.js": "./dist/types.js", 14 | "./dist/types.mjs": "./dist/types.mjs" 15 | }, 16 | "source": "types.ts", 17 | "scripts": {}, 18 | "author": "Michel Weststrate", 19 | "license": "MIT", 20 | "files": [ 21 | "*.ts", 22 | "dist" 23 | ], 24 | "peerDependencies": { 25 | "@r-val/core": "*" 26 | } 27 | } -------------------------------------------------------------------------------- /pkgs/types/tests/__snapshots__/sarcastic.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`isShape complex 1`] = `"[object Object].todos must be an array"`; 4 | 5 | exports[`isShape complex 2`] = `"[object Object].todos[1] must be an object"`; 6 | 7 | exports[`isShape simple 1`] = `"[object Object].done must be a boolean"`; 8 | 9 | exports[`isShape simple 2`] = `"[object Object].title must be a string"`; 10 | -------------------------------------------------------------------------------- /pkgs/types/tests/__snapshots__/types.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`isShape complex 1`] = ` 4 | "Typecheck failed, expected 'Object<{ 5 | todos: Array> 9 | }>', got: (object) '{}'" 10 | `; 11 | 12 | exports[`isShape complex 2`] = ` 13 | "Typecheck failed, expected 'Object<{ 14 | todos: Array> 18 | }>', got: (object) '{\\"todos\\":[{\\"done\\":false,\\"title\\":\\"get coffee\\"},3]}'" 19 | `; 20 | 21 | exports[`isShape simple 1`] = ` 22 | "Typecheck failed, expected 'Object<{ 23 | done: boolean, 24 | title: string 25 | }>', got: (object) '{}'" 26 | `; 27 | 28 | exports[`isShape simple 2`] = ` 29 | "Typecheck failed, expected 'Object<{ 30 | done: boolean, 31 | title: string 32 | }>', got: (object) '{\\"done\\":true,\\"title\\":3}'" 33 | `; 34 | -------------------------------------------------------------------------------- /pkgs/types/tests/sarcastic.spec.ts: -------------------------------------------------------------------------------- 1 | import { val } from "@r-val/core" 2 | 3 | import * as is from "sarcastic" 4 | 5 | 6 | test('isNumber', () => { 7 | const v = val(3, is.number) 8 | 9 | expect(() => v("4")).toThrow("3 must be a number") 10 | expect(v()).toBe(3) 11 | v(4) 12 | expect(v()).toBe(4) 13 | }) 14 | 15 | 16 | describe('isShape', () => { 17 | const todoShape = is.shape({ 18 | done: is.boolean, 19 | title: is.string 20 | }) 21 | 22 | const todoStoreShape = is.shape({ 23 | todos: is.arrayOf(todoShape) 24 | }) 25 | 26 | test('simple', () => { 27 | const t1 = val({ done: false, title: "get coffee"}, todoShape) 28 | expect(() => t1({} as any)).toThrowErrorMatchingSnapshot() 29 | expect(() => t1({ done: true, title: 3 } as any)).toThrowErrorMatchingSnapshot() 30 | t1({ 31 | extra: 7, 32 | done: false, 33 | title: "stuff" 34 | } as any) 35 | }) 36 | 37 | test('complex', () => { 38 | const store = val({ 39 | todos: [{ done: false, title: "get coffee"}] 40 | }, todoStoreShape) 41 | 42 | expect(() => store({} as any)).toThrowErrorMatchingSnapshot() 43 | store({ 44 | todos: [] 45 | }) 46 | expect(() => store({ 47 | todos: [{ done: false, title: "get coffee"}, 3] 48 | } as any)).toThrowErrorMatchingSnapshot() 49 | }) 50 | }) -------------------------------------------------------------------------------- /pkgs/types/tests/types.spec.ts: -------------------------------------------------------------------------------- 1 | import { val } from "@r-val/core" 2 | import * as t from "@r-val/types" 3 | 4 | test('isLiteral', () => { 5 | const v = val(3, t.isLiteral(3)) 6 | 7 | expect(() => v(4)).toThrow("Typecheck failed, expected '3', got: (number) '4'") 8 | expect(v()).toBe(3) 9 | v(3) 10 | expect(v()).toBe(3) 11 | }) 12 | 13 | 14 | test('isNull', () => { 15 | const v = val(null, t.isNull) 16 | 17 | expect(() => v(undefined)).toThrow("Typecheck failed, expected 'null', got: (undefined) 'undefined'") 18 | expect(v()).toBe(null) 19 | v(null) 20 | expect(v()).toBe(null) 21 | }) 22 | 23 | test('isUndefined', () => { 24 | const v = val(undefined, t.isUndefined) 25 | 26 | expect(() => v(null)).toThrow("Typecheck failed, expected 'undefined', got: (object) 'null'") 27 | expect(v()).toBe(undefined) 28 | v(undefined) 29 | expect(v()).toBe(undefined) 30 | }) 31 | 32 | test('isNumber', () => { 33 | const v = val(3, t.isNumber) 34 | 35 | expect(() => v("4")).toThrow("Typecheck failed, expected 'number', got: (string) '4'") 36 | expect(v()).toBe(3) 37 | v(4) 38 | expect(v()).toBe(4) 39 | }) 40 | 41 | test('isString', () => { 42 | const v = val("3", t.isString) 43 | 44 | expect(() => v(4)).toThrow("Typecheck failed, expected 'string', got: (number) '4'") 45 | expect(v()).toBe("3") 46 | v("4") 47 | expect(v()).toBe("4") 48 | }) 49 | 50 | test('isBoolean', () => { 51 | const v = val(false, t.isBoolean) 52 | 53 | expect(() => v(4)).toThrow("Typecheck failed, expected 'boolean', got: (number) '4'") 54 | expect(v()).toBe(false) 55 | v(true) 56 | expect(v()).toBe(true) 57 | }) 58 | 59 | test('isFunction', () => { 60 | const v = val(() => {}, t.isFunction) 61 | 62 | expect(() => v(4)).toThrow("Typecheck failed, expected 'function', got: (number) '4'") 63 | expect(v()).toBeInstanceOf(Function) 64 | v(() => () => {}) 65 | }) 66 | 67 | test('isInstanceOf', () => { 68 | class X { 69 | 70 | } 71 | 72 | const v = val(new X, t.isInstanceOf(X)) 73 | 74 | expect(() => v({})).toThrow("Typecheck failed, expected 'X', got: (object) '{}'") 75 | expect(v()).toBeInstanceOf(X) 76 | v(new X) 77 | }) 78 | 79 | test('isDate', () => { 80 | const v = val(new Date, t.isDate) 81 | 82 | expect(() => v(4)).toThrow("Typecheck failed, expected 'Date', got: (number) '4'") 83 | expect(v()).toBeInstanceOf(Date) 84 | v(new Date) 85 | }) 86 | 87 | test('isMaybeDate', () => { 88 | const v = val(new Date, t.isMaybe(t.isDate)) 89 | 90 | expect(() => v(4)).toThrow("Typecheck failed, expected 'Date | undefined | null', got: (number) '4'") 91 | expect(v()).toBeInstanceOf(Date) 92 | v(new Date) 93 | expect(v()).toBeInstanceOf(Date) 94 | v(undefined) 95 | expect(v()).toBe(undefined) 96 | v(null) 97 | expect(v()).toBe(null) 98 | }) 99 | 100 | test('isEnum', () => { 101 | const v = val("stop", t.isEnum(["stop", "start"])) 102 | 103 | expect(() => v(4)).toThrow("Typecheck failed, expected 'stop | start', got: (number) '4'") 104 | expect(v()).toBe("stop") 105 | v("start") 106 | expect(v()).toBe("start") 107 | }) 108 | 109 | test('isArray', () => { 110 | const v = val(["stop"], t.isArray(t.isEnum(["stop", "start"]))) 111 | 112 | expect(() => v(["start", 4])).toThrow(`Typecheck failed, expected 'Array', got: (object) '["start",4]'`) 113 | expect(v()).toEqual(["stop"]) 114 | v(["stop", "start"]) 115 | expect(v()).toEqual(["stop", "start"]) 116 | v([]) 117 | }) 118 | 119 | test('isMap', () => { 120 | const v = val({x:"stop"}, t.isMap(t.isEnum(["stop", "start"]))) 121 | 122 | expect(() => v({y: "stop", x: 4})).toThrow(`Typecheck failed, expected 'Map', got: (object) '{"y":"stop","x":4}'`) 123 | expect(v()).toEqual({x:"stop"}) 124 | v({y: "stop", x: "stop"}) 125 | expect(v()).toEqual({y: "stop", x: "stop"}) 126 | v({}) 127 | }) 128 | 129 | describe('isShape', () => { 130 | const todoShape = t.isShape({ 131 | done: t.isBoolean, 132 | title: t.isString 133 | }) 134 | 135 | const todoStoreShape = t.isShape({ 136 | todos: t.isArray(todoShape) 137 | }) 138 | 139 | test('simple', () => { 140 | const t1 = val({ done: false, title: "get coffee"}, todoShape) 141 | expect(() => t1({} as any)).toThrowErrorMatchingSnapshot() 142 | expect(() => t1({ done: true, title: 3 } as any)).toThrowErrorMatchingSnapshot() 143 | t1({ 144 | extra: 7, 145 | done: false, 146 | title: "stuff" 147 | } as any) 148 | }) 149 | 150 | test('complex', () => { 151 | const store = val({ 152 | todos: [{ done: false, title: "get coffee"}] 153 | }, todoStoreShape) 154 | 155 | expect(() => store({} as any)).toThrowErrorMatchingSnapshot() 156 | store({ 157 | todos: [] 158 | }) 159 | expect(() => store({ 160 | todos: [{ done: false, title: "get coffee"}, 3] 161 | } as any)).toThrowErrorMatchingSnapshot() 162 | }) 163 | }) 164 | 165 | test('invariant', () => { 166 | const x = val(3, t.invariant(p => p > 0)) 167 | 168 | x(2) 169 | expect(() => x(-1)).toThrow("Typecheck failed, expected 'invariant 'p => p > 0'', got: (number) '-1'") 170 | }) 171 | -------------------------------------------------------------------------------- /pkgs/types/types.ts: -------------------------------------------------------------------------------- 1 | import { _isPlainObject } from "@r-val/core"; 2 | 3 | export type TypeChecker = (value: any) => T 4 | 5 | function createTypeChecker(predicate: (v: any) => boolean, typeName: string): TypeChecker { 6 | return Object.assign( 7 | function(v) { 8 | if (!predicate(v)) { 9 | const vAsStr = _isPlainObject(v) || Array.isArray(v) ? JSON.stringify(v) : v 10 | throw new Error(`Typecheck failed, expected '${typeName}', got: (${typeof v}) '${vAsStr}'`) 11 | } 12 | return v 13 | }, 14 | { 15 | typeName: typeName, 16 | } 17 | ) 18 | } 19 | 20 | function getTypeName(fn) { 21 | if (fn.typeName) return fn.typeName 22 | throw new Error('Not a type-checker function:' + fn) 23 | } 24 | 25 | function isA(typeChecker, value) { 26 | try { 27 | typeChecker(value) 28 | return true 29 | } catch (e) { 30 | return false 31 | } 32 | } 33 | 34 | export const isLiteral = (value: T) => createTypeChecker(v => v === value, "" + value) 35 | export const isNull = isLiteral(null) 36 | export const isUndefined = isLiteral(undefined) 37 | export const isNumber = createTypeChecker(v => typeof v === 'number', 'number') 38 | export const isString = createTypeChecker(v => typeof v === 'string', 'string') 39 | export const isBoolean = createTypeChecker(v => typeof v === 'boolean', 'boolean') 40 | export const isFunction = createTypeChecker(v => typeof v === 'function', 'function') 41 | export const isDate = isInstanceOf(Date) 42 | 43 | export function isInstanceOf(type: new (...args: any[]) => T): TypeChecker { 44 | return createTypeChecker(v => v instanceof type, type.name) 45 | } 46 | 47 | // Could use typings, but requires a lot of overloads 48 | export function isUnion(...types: TypeChecker[]): TypeChecker { 49 | return createTypeChecker( 50 | v => types.some(t => isA(t, v)), 51 | types.map(getTypeName).join(" | ") 52 | ) 53 | } 54 | 55 | export function isMaybe(subTypeChecker: TypeChecker): TypeChecker { 56 | return isUnion(subTypeChecker, isUndefined, isNull) 57 | } 58 | 59 | // Could use typings, but requires a lot of overloads 60 | export function isEnum(values: T[]): TypeChecker { 61 | return isUnion(...values.map(isLiteral)) 62 | } 63 | 64 | export function isArray(subTypeChecker: TypeChecker): TypeChecker { 65 | return createTypeChecker( 66 | v => Array.isArray(v) && v.every(i => isA(subTypeChecker, i)), 67 | `Array<${getTypeName(subTypeChecker)}>` 68 | ) 69 | } 70 | 71 | export function isMap(subTypeChecker: TypeChecker): TypeChecker<{[key: string]: T}> { 72 | return createTypeChecker( 73 | v => _isPlainObject(v) && Object.keys(v).every(key => isA(subTypeChecker, v[key])), 74 | `Map<${getTypeName(subTypeChecker)}>` 75 | ) 76 | } 77 | 78 | export function isShape }>(shape: T): TypeChecker { 79 | const typeString = "Object<{\n\t" 80 | + Object.keys(shape).map(key => `${key}: ${getTypeName(shape[key])}`).join(",\n\t") 81 | + "\n}>"; 82 | return createTypeChecker( 83 | v => Object.keys(shape).every(key => isA(shape[key], v[key])), 84 | typeString 85 | ) 86 | } 87 | 88 | export function invariant(predicate: (v: T) => boolean): TypeChecker { 89 | return createTypeChecker(predicate, `invariant '${predicate.toString()}'`) 90 | } 91 | -------------------------------------------------------------------------------- /pkgs/updaters/README.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Updaters 3 | order: 1 4 | menu: API 5 | route: /api/updaters 6 | --- 7 | 8 | # @r-val/updaters 9 | 10 | ## inc1 -------------------------------------------------------------------------------- /pkgs/updaters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/updaters", 3 | "private": false, 4 | "version": "0.0.4", 5 | "description": "", 6 | "main": "dist/updaters.js", 7 | "umd:main": "dist/updaters.umd.js", 8 | "unpkg": "dist/updaters.umd.js", 9 | "module": "dist/updaters.mjs", 10 | "jsnext:main": "dist/updaters.mjs", 11 | "react-native": "dist/updaters.mjs", 12 | "browser": { 13 | "./dist/updaters.js": "./dist/updaters.js", 14 | "./dist/updaters.mjs": "./dist/updaters.mjs" 15 | }, 16 | "source": "updaters.ts", 17 | "scripts": {}, 18 | "author": "Michel Weststrate", 19 | "license": "MIT", 20 | "files": [ 21 | "*.ts", 22 | "dist" 23 | ], 24 | "reserved": [ 25 | "toggle", 26 | "inc1", 27 | "inc", 28 | "dec", 29 | "dec1", 30 | "replace", 31 | "set", 32 | "unset", 33 | "push", 34 | "shift", 35 | "unshift", 36 | "splice", 37 | "assign", 38 | "pop", 39 | "removeBy", 40 | "removeValue" 41 | ], 42 | "mangle": { 43 | "reserved": [ 44 | "global", 45 | "log", 46 | "warn", 47 | "configurable", 48 | "enumerable", 49 | "writable", 50 | "value", 51 | "$RVal", 52 | "rval", 53 | "defaultInstance", 54 | "defaultRValInstance", 55 | "isVal", 56 | "isDrv", 57 | "_deepfreeze", 58 | "_once", 59 | "_isPlainObject", 60 | "val", 61 | "drv", 62 | "sub", 63 | "run", 64 | "act", 65 | "effect", 66 | "configure", 67 | "autoFreeze", 68 | "fireImmediately", 69 | "toggle", 70 | "inc1", 71 | "inc", 72 | "dec", 73 | "dec1", 74 | "replace", 75 | "set", 76 | "unset", 77 | "push", 78 | "shift", 79 | "unshift", 80 | "splice", 81 | "assign", 82 | "pop", 83 | "removeBy", 84 | "removeValue" 85 | ] 86 | }, 87 | "gitHead": "a824fe9bd83228fb126c8c6188ef7b85fc2ffdd7", 88 | "peerDependencies": { 89 | "@r-val/core": "*" 90 | } 91 | } -------------------------------------------------------------------------------- /pkgs/updaters/tests/updaters.spec.ts: -------------------------------------------------------------------------------- 1 | import { val, drv, sub, act, _deepfreeze } from "@r-val/core" 2 | import { toggle, inc, dec1, inc1, dec, replace, set, push, splice, shift, unshift, pop, assign, unset, removeBy, removeValue } from "@r-val/updaters" 3 | import { append } from "ramda" 4 | import produce from "immer"; 5 | 6 | test('toggle', () => { 7 | const x = val(false) 8 | x(toggle) 9 | expect(x()).toBe(true) 10 | }) 11 | 12 | test("inc/ dec", () => { 13 | const x = val(2) 14 | x(inc(3)) 15 | expect(x()).toBe(5) 16 | 17 | x(inc1) 18 | expect(x()).toBe(6) 19 | x(dec1) 20 | expect(x()).toBe(5) 21 | 22 | x(inc(3)) 23 | expect(x()).toBe(8) 24 | x(dec(4)) 25 | expect(x()).toBe(4) 26 | }) 27 | 28 | test("replace", () => { 29 | const inc = i => i + 1 30 | 31 | const a = val(3) 32 | a(inc) 33 | expect(a()).toBe(4) 34 | 35 | const b = val<() => number>(() => 3) 36 | const fn = function anotherFunction() { 37 | return 4 38 | } 39 | b(() => fn) 40 | expect(b()).toBe(fn) 41 | expect(b()()).toBe(4) 42 | 43 | b(replace(() => 5)) 44 | expect(b()()).toBe(5) 45 | }) 46 | 47 | test("set", () => { 48 | const ar = [1,2] 49 | const obj = { 50 | a: 1, 51 | b: 2 52 | } 53 | 54 | expect(set("b", 3)(obj)).toEqual({ a: 1, b: 3}) 55 | expect(set("b", 2)(obj)).toBe(obj) 56 | 57 | expect(set(1, 3)(ar)).toEqual([1, 3]) 58 | expect(set(1, 2)(ar)).toBe(ar) 59 | }) 60 | 61 | test("unset", () => { 62 | const ar = [1,2] 63 | const obj = { 64 | a: 1, 65 | b: 2 66 | } 67 | 68 | expect(unset("b")(obj)).toEqual({ a: 1}) 69 | expect(unset("b")(obj)).not.toBe(obj) 70 | expect(unset("c")(obj)).toBe(obj) 71 | 72 | expect(unset(0)(ar)).toEqual([2]) 73 | expect(unset(0)(ar)).not.toBe(ar) 74 | expect(unset(5)(ar)).toBe(ar) 75 | }) 76 | 77 | test("push", () => { 78 | const x = [] 79 | expect(push(3)(x)).not.toBe(x) 80 | expect(push(3)([1])).toEqual([1, 3]) 81 | expect(push(3,4)([1])).toEqual([1, 3, 4]) 82 | expect(push()(x)).toBe(x) 83 | }) 84 | 85 | test("splice", () => { 86 | const x = [1,2] 87 | expect(splice()(x)).toBe(x) 88 | expect(splice(0)(x)).toEqual([]) 89 | expect(splice(0)(x)).not.toBe(x) 90 | expect(splice(0,1)(x)).toEqual([2]) 91 | expect(splice(5,1)(x)).toBe(x) 92 | expect(splice(5,1,3,4)(x)).toEqual([1,2,3,4]) 93 | expect(splice(0,0,3,4)(x)).toEqual([3,4,1,2]) 94 | expect(splice(0,1,3,4)(x)).toEqual([3,4,2]) 95 | }) 96 | 97 | test("shift", () => { 98 | const x = [] 99 | const y = [1, 2] 100 | expect(shift(x)).toBe(x) 101 | expect(shift(y)).not.toBe(y) 102 | expect(shift(y)).toEqual([2]) 103 | }) 104 | 105 | test("unshift", () => { 106 | const y = [1, 2] 107 | expect(unshift()(y)).toBe(y) 108 | expect(unshift(1)(y)).not.toBe(y) 109 | expect(unshift(3,4)(y)).toEqual([3,4,1,2]) 110 | }) 111 | 112 | test("pop", () => { 113 | const x = [] 114 | const y = [1, 2] 115 | expect(pop(x)).toBe(x) 116 | expect(pop(y)).not.toBe(y) 117 | expect(pop(y)).toEqual([1]) 118 | }) 119 | 120 | test("assign", () => { 121 | const base = { 122 | x: 1, 123 | y: 2 124 | } 125 | 126 | expect(assign({})(base)).toBe(base) 127 | expect(assign(null as any)(base)).toBe(base) 128 | expect(assign({x: 1, y: 2})(base)).toBe(base) 129 | expect(assign({x: 2, y: 2})(base)).not.toBe(base) 130 | expect(assign({x: 2, y: 2})(base)).toEqual({ x: 2, y: 2 }) 131 | expect(assign({x: 2 })(base)).toEqual({ x: 2, y: 2 }) 132 | expect(assign({x: 2, z: 2 })(base)).toEqual({ x: 2, y: 2, z: 2 }) 133 | }) 134 | 135 | test("unset / removeBy / removeValue", () => { 136 | const todo1 = { 137 | id: "1", 138 | title: "get coffee", 139 | done: false 140 | } 141 | const todo2 = { 142 | id: "2", 143 | title: "get cookie", 144 | done: false 145 | } 146 | 147 | const ar = [todo1, todo2] 148 | const obj = { todo1, todo2 } 149 | _deepfreeze(ar) 150 | _deepfreeze(obj) 151 | 152 | expect(unset(0)(ar)).toEqual([todo2]) 153 | expect(unset("todo1")(obj)).toEqual({todo2}) 154 | 155 | expect(removeBy(v => v.title.startsWith("get"))(ar)).toEqual([]) 156 | expect(removeBy(v => v.title.startsWith("get"))(obj)).toEqual({}) 157 | 158 | expect(removeBy(v => false)(ar)).toBe(ar) 159 | expect(removeBy(v => false)(obj)).toBe(obj) 160 | 161 | expect(removeBy("id", "2")(ar)).toEqual([todo1]) 162 | expect(removeBy("id", "2")(obj)).toEqual({todo1}) 163 | 164 | expect(removeBy("id", "3")(ar)).toBe(ar) 165 | expect(removeBy("id", "3")(obj)).toBe(obj) 166 | 167 | expect(removeValue({})(ar)).toBe(ar) 168 | expect(removeValue({})(obj)).toBe(obj) 169 | 170 | expect(removeValue(todo1)(ar)).toEqual([todo2]) 171 | expect(removeValue(todo1)(obj)).toEqual({ todo2 }) 172 | }) 173 | 174 | test("it should append with ramda", () => { 175 | const numbers = val([1,2]) 176 | numbers(append(3)) 177 | expect(numbers()).toEqual([1,2,3]) 178 | }) 179 | 180 | test("it should produce with immer", () => { 181 | const numbers = val([1,2]) 182 | numbers(produce(draft => { draft.push(3) })) 183 | expect(numbers()).toEqual([1,2,3]) 184 | }) 185 | -------------------------------------------------------------------------------- /pkgs/updaters/updaters.ts: -------------------------------------------------------------------------------- 1 | export type KVMap = { 2 | [key: string]: T 3 | } 4 | 5 | export const toggle = (val: boolean) => !val 6 | 7 | export const inc = (by: number) => (val: number) => val + by 8 | 9 | export const inc1 = inc(1) 10 | export const dec1 = inc(-1) 11 | export const dec = (by: number) => inc(-by) 12 | 13 | export const replace = (newVal: T) => _ => newVal 14 | 15 | export function set(key: number, value: T): (o: T[]) => T[] 16 | export function set(key: K, value: T[K]): (o: T) => T 17 | export function set>(key: string, value: V): (o: T) => T 18 | export function set(key, value) { 19 | return o => { 20 | if (o[key] === value) return o // no-op 21 | if (Array.isArray(o)) { 22 | const res = o.slice() 23 | res[key] = value 24 | return res 25 | } 26 | return { ...o, [key]: value } 27 | } 28 | } 29 | 30 | export function unset(key: number): (o: T[]) => T[] 31 | export function unset(key: K): (o: T) => T 32 | export function unset>(key: string): (o: T) => T 33 | export function unset(key) { 34 | return o => { 35 | if (Array.isArray(o)) return splice(key, 1)(o) 36 | if (!(key in o)) return o // Noop delete 37 | const res = {...o} 38 | delete res[key] 39 | return res 40 | } 41 | } 42 | 43 | export function push(...values: T[]): (o: T[]) => T[] 44 | export function push(...values) { 45 | return o => { 46 | if (!values.length) return o 47 | const res = o.slice() 48 | res.push(...values) 49 | return res 50 | } 51 | } 52 | 53 | export function splice(idx?: number | undefined, deleteCount?: number | undefined, ...toAdd: T[]): (o: T[]) => T[] 54 | export function splice(idx?: any, deleteCount?: any, ...toAdd: any[]): any { 55 | return o => { 56 | if (!arguments.length || ((deleteCount === 0 || idx >= o.length) && !toAdd.length)) return o // no changes 57 | const res = o.slice() 58 | res.splice.apply(res, arguments) 59 | return res 60 | } 61 | } 62 | 63 | export function shift(val: T[]): T[] { 64 | if (!val.length) return val 65 | const res = val.slice() 66 | res.shift() 67 | return res 68 | } 69 | 70 | export function unshift(...items: T[]): (o: T[]) => T[] 71 | export function unshift(...items: T[]): (o: T[]) => T[] { 72 | return o => { 73 | if (!items.length) return o 74 | const res = o.slice() 75 | res.unshift(...items) 76 | return res 77 | } 78 | } 79 | 80 | export function pop(val: T[]): T[] { 81 | if (!val.length) return val 82 | const res = val.slice() 83 | res.pop() 84 | return res 85 | } 86 | 87 | export const assign = (v:A) => (o: B) => { 88 | if (!v) return o 89 | let change = false 90 | for (const key in v) if ((o as any) [key] !== v[key]) { 91 | change = true 92 | break 93 | } 94 | return (change ? Object.assign({}, o, v) : o) as (A & B) 95 | } 96 | 97 | export function removeBy(key: string, value: any): (o: B) => B 98 | export function removeBy(predicate: (val: any) => boolean): (o: B) => B 99 | export function removeBy(arg1, arg2?) { 100 | if (typeof arg1 !== "function") 101 | return removeBy(v => v[arg1] === arg2) 102 | return o => { 103 | if (Array.isArray(o)) { 104 | const res = o.filter(v => !arg1(v)) 105 | return res.length === o.length ? o: res 106 | } 107 | let match = false 108 | const res = {} 109 | Object.keys(o).forEach(k => { 110 | if (!arg1(o[k])) 111 | res[k] = o[k] 112 | else 113 | match = true 114 | }) 115 | return match ? res : o 116 | } 117 | } 118 | 119 | export function removeValue(value: any): (o: B) => B 120 | export function removeValue(value) { 121 | return removeBy(v => v === value) 122 | } 123 | 124 | -------------------------------------------------------------------------------- /pkgs/utils/README.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | name: Utils 3 | order: 3 4 | menu: API 5 | route: /api/utils 6 | --- 7 | 8 | # @r-val/utils 9 | -------------------------------------------------------------------------------- /pkgs/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/utils", 3 | "private": false, 4 | "version": "0.0.4", 5 | "description": "", 6 | "main": "dist/utils.js", 7 | "umd:main": "dist/utils.umd.js", 8 | "unpkg": "dist/utils.umd.js", 9 | "module": "dist/utils.mjs", 10 | "jsnext:main": "dist/utils.mjs", 11 | "react-native": "dist/utils.mjs", 12 | "browser": { 13 | "./dist/utils.js": "./dist/utils.js", 14 | "./dist/utils.mjs": "./dist/utils.mjs" 15 | }, 16 | "source": "utils.ts", 17 | "scripts": {}, 18 | "author": "Michel Weststrate", 19 | "license": "MIT", 20 | "files": [ 21 | "*.ts", 22 | "dist" 23 | ], 24 | "peerDependencies": { 25 | "@r-val/core": "*" 26 | } 27 | } -------------------------------------------------------------------------------- /pkgs/utils/tests/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { val, drv, sub, act } from "@r-val/core" 2 | import {toJS, assignVals, keepAlive, when } from "@r-val/utils" 3 | import { delay } from "q"; 4 | 5 | test("toJS", () => { 6 | expect(toJS(val(3))).toBe(3) 7 | 8 | expect(toJS(drv(() => 3))).toBe(3) 9 | 10 | expect(toJS(val({x: 3 }))).toEqual({x: 3}) 11 | 12 | expect(toJS(val({x: 3, toJS() { return 2 } }))).toEqual(2) 13 | 14 | expect(toJS(val([3]))).toEqual([3]) 15 | 16 | expect(toJS(val({x: val(3) }))).toEqual({x: 3}) 17 | 18 | { 19 | const x = {x: 3} 20 | expect(toJS(x)).not.toBe(x) 21 | } 22 | }) 23 | 24 | test("assignVals", () => { 25 | const x = { 26 | a: val(3), 27 | b: 2, 28 | c: drv(() => x.b * 2, v => { x.b = v}), 29 | } 30 | 31 | assignVals(x, { 32 | a: 2, 33 | }, { 34 | a: 4, 35 | c: 5 36 | }) 37 | 38 | expect(toJS(x)).toEqual({ 39 | a: 4, 40 | b: 5 41 | }) 42 | expect(x.c()).toBe(10) 43 | }) 44 | 45 | test("keepAlive", async () => { 46 | let calc = 0 47 | const x = val(1) 48 | const y = drv(() => { 49 | calc++ 50 | return x() 51 | }) 52 | 53 | await delay(10) 54 | const d = keepAlive(y) 55 | 56 | expect(calc).toBe(0) 57 | expect(y()).toBe(1) 58 | expect(calc).toBe(1) 59 | 60 | x(2) 61 | await delay(10) 62 | x(3) 63 | expect(calc).toBe(1) 64 | expect(y()).toBe(3) 65 | expect(calc).toBe(2) 66 | expect(y()).toBe(3) 67 | expect(calc).toBe(2) 68 | 69 | d() 70 | await delay(10) 71 | expect(y()).toBe(3) 72 | expect(calc).toBe(3) 73 | 74 | await delay(10) 75 | expect(y()).toBe(3) 76 | expect(calc).toBe(4) 77 | }) 78 | 79 | 80 | test("when - val", async () => { 81 | const x = val(0) 82 | 83 | setTimeout(() => { 84 | x(1) 85 | }, 10) 86 | 87 | await when(x) 88 | }) 89 | 90 | 91 | test("when - drv", async () => { 92 | let calcs = 0 93 | const x = val(0) 94 | const y = drv(() => { 95 | calcs++ 96 | return x() 97 | }) 98 | 99 | setTimeout(() => { 100 | x(1) 101 | }, 100) 102 | 103 | const p = when(y) 104 | expect(calcs).toBe(1) 105 | 106 | await p 107 | expect(calcs).toBe(2) 108 | }) 109 | 110 | test("when - short circuit", async () => { 111 | let calcs = 0 112 | const x = val(1) 113 | const y = drv(() => { 114 | calcs++ 115 | return x() 116 | }) 117 | 118 | const p = when(y, 100) 119 | expect(calcs).toBe(1) 120 | 121 | await p 122 | expect(calcs).toBe(1) 123 | }) 124 | 125 | test("when - timeout", async () => { 126 | const x = val(0) 127 | 128 | await expect(when(x, 100)).rejects.toMatchObject({ message: "TIMEOUT" }) 129 | }) 130 | -------------------------------------------------------------------------------- /pkgs/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { isVal, isDrv, Drv, Disposer, rval, _once, _isPlainObject, Val, Observable, drv, act } from '@r-val/core' 2 | 3 | // TODO: add typings! 4 | // TODO: support custom serializer function as second argument? 5 | export function toJS(thing) { 6 | if (!thing) return thing 7 | if (typeof thing.toJS === 'function') return thing.toJS() 8 | if (isVal(thing) || isDrv(thing)) return toJS(thing()) 9 | if (Array.isArray(thing)) return thing.map(toJS) 10 | if (_isPlainObject(thing)) { 11 | const res = {} 12 | for (const key in thing) 13 | if (typeof thing[key] !== "function" || isVal(thing[key])) 14 | res[key] = toJS(thing[key]) 15 | return res 16 | } 17 | return thing 18 | } 19 | 20 | type AssignVals = { 21 | [K in keyof T]?: T[K] extends Val ? T | S : T[K] extends Drv ? T : never 22 | } 23 | 24 | export function assignVals(target: T, vals: AssignVals, ...moreVals: AssignVals[]) 25 | export function assignVals(target, vals, ...moreVals) { 26 | if (moreVals.length) vals = Object.assign(vals, ...moreVals) 27 | act(() => { 28 | for (const key in vals) { 29 | if (isVal(target[key]) || isDrv(target[key])) target[key](vals[key]) 30 | else throw new Error(`[assignVals] value at key "${key}" is not a 'val' or 'drv'`) 31 | } 32 | })() 33 | return target 34 | } 35 | 36 | export function keepAlive(target: Drv): Disposer { 37 | return rval(target).effect( 38 | target, 39 | _once((didChange, pull) => { 40 | didChange() // we never have to pull, we only detect for changes once, so that the target becomes hot 41 | }) 42 | ) 43 | } 44 | 45 | export async function when(goal: Observable, timeout = 0): Promise { 46 | return new Promise((resolve, reject) => { 47 | if (goal()) return void resolve() // short-circuit 48 | const timeoutHandle = timeout && 49 | setTimeout(() => { 50 | disposer() 51 | reject(new Error("TIMEOUT")) 52 | }, timeout) 53 | const disposer = rval(goal).sub(goal, v => { 54 | if (v) { 55 | clearTimeout(timeoutHandle as any) 56 | resolve() 57 | disposer() 58 | } 59 | }) 60 | }) 61 | } 62 | 63 | export function debug(condition? : any, startDebuggerOrMessage?: any) { 64 | // if functioon 65 | // if nothing 66 | // if value 67 | // TODO: 68 | (new Function("debugger")) 69 | } 70 | 71 | export function logChanges(source: Observable, prefix = "rval: changed") { 72 | return rval(source).sub(source, (current, previous) => { 73 | console.log(`[${prefix}] ${previous} -> ${current}`) 74 | }) 75 | } 76 | 77 | // export function actNow(fn: () => R): R { 78 | // return act(fn)() 79 | // } 80 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const child_process = require('child_process') 3 | const os = require('os') 4 | 5 | const binFolder = child_process 6 | .execSync('yarn bin') 7 | .toString() 8 | .replace(/(\s+$)/g, '') 9 | const microbundle = binFolder + '/microbundle' 10 | 11 | const projects = fs.readdirSync('pkgs/') 12 | const templatePackageJsonSource = fs.readFileSync('scripts/package-template.json', 'utf8') 13 | 14 | const buildCommand = 15 | microbundle + 16 | ' --compress ' + 17 | // ' --no-compress ' + 18 | ' --source-map --entry $pkg.ts --name rval_$pkg --strict --format es,cjs,umd --globals ' + 19 | projects.map(pkg => '@r-val/' + pkg + '=rval_' + pkg).join(',') + 20 | ' --external ' + projects.map(pkg => '@r-val/' + pkg).join(',') + 21 | ' && mv dist/$pkg/*.d.ts dist/ ' + 22 | ' && rimraf dist/$pkg .rts2* mangle.json' 23 | 24 | const corePkgJson = JSON.parse(fs.readFileSync(__dirname + '/../pkgs/core/package.json', 'utf8')) 25 | 26 | projects.forEach(pkg => { 27 | const pkgJson = JSON.parse(fs.readFileSync(__dirname + '/../pkgs/' + pkg + '/package.json', 'utf8')) 28 | const templateJson = JSON.parse(templatePackageJsonSource.replace(/\$pkg/g, pkg)) 29 | 30 | // If reserved is set, mangle all the property names! 31 | // property names as defined in core shouldn't be mangled either 32 | if (Array.isArray(pkgJson.reserved)) { 33 | pkgJson.mangle = { 34 | reserved: Array.from(new Set([...corePkgJson.reserved, ...pkgJson.reserved])), 35 | } 36 | } 37 | 38 | for (const key in templateJson) if (!(key in pkgJson)) 39 | pkgJson[key] = templateJson[key] 40 | 41 | fs.writeFileSync(__dirname + '/../pkgs/' + pkg + '/package.json', JSON.stringify(pkgJson, null, ' '), 'utf8') 42 | const command = buildCommand.replace(/\$pkg/g, pkg) 43 | console.log('Running ' + command) 44 | child_process.execSync(command, { 45 | cwd: __dirname + '/../pkgs/' + pkg, 46 | stdio: [0, 1, 2], 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /scripts/link.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const child_process = require('child_process') 3 | const os = require('os') 4 | 5 | const binFolder = child_process 6 | .execSync('yarn bin') 7 | .toString() 8 | .replace(/(\s+$)/g, '') 9 | 10 | const basePath = __dirname + "/../" 11 | 12 | const projects = fs.readdirSync(basePath + 'pkgs/') 13 | 14 | projects.forEach(pkg => { 15 | child_process.execSync("yarn link", { 16 | cwd: basePath + 'pkgs/' + pkg, 17 | stdio: [0, 1, 2], 18 | }) 19 | }) 20 | 21 | const examples = fs.readdirSync(basePath + "examples/") 22 | 23 | examples.forEach(pkg => { 24 | pkgJson = JSON.parse(fs.readFileSync(basePath + "examples/" + pkg + "/package.json")) 25 | Object.keys(pkgJson.dependencies) 26 | .filter(dep => dep.startsWith("@r-val/")) 27 | .forEach(dep => { 28 | child_process.execSync("yarn link " + dep, { 29 | cwd: basePath + 'examples/' + pkg, 30 | stdio: [0, 1, 2], 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /scripts/package-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@r-val/$pkg", 3 | "main": "dist/$pkg.js", 4 | "umd:main": "dist/$pkg.umd.js", 5 | "unpkg": "dist/$pkg.umd.js", 6 | "module": "dist/$pkg.mjs", 7 | "jsnext:main": "dist/$pkg.mjs", 8 | "react-native": "dist/$pkg.mjs", 9 | "source": "$pkg.ts", 10 | "author": "Michel Weststrate", 11 | "license": "MIT", 12 | "files": ["*.ts", "dist"], 13 | "peerDependencies": { 14 | "@r-val/core": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "jsx": "react", 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist/" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true /* Enable all strict type-checking options. */, 26 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 30 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 31 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 32 | 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | 39 | /* Module Resolution Options */ 40 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | "paths": { 43 | "@r-val/core": ["pkgs/core/core"], 44 | "@r-val/updaters": ["pkgs/updaters/updaters"], 45 | "@r-val/types": ["pkgs/types/types"], 46 | "@r-val/utils": ["pkgs/utils/utils"], 47 | "@r-val/react": ["pkgs/react/react"], 48 | "@r-val/models": ["pkgs/models/models"], 49 | "*": ["node_modules/*"] 50 | } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | // "types": [], /* Type declaration files to be included in compilation. */ 54 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 56 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | } 68 | } 69 | --------------------------------------------------------------------------------