├── .editorconfig ├── .gitignore ├── .jshintignore ├── .travis.yml ├── .zuul.yml ├── CONTRIBUTION.md ├── LICENCE ├── README.md ├── ROADMAP.md ├── bin ├── browserify-editor.js ├── build.js ├── dist.js ├── example-server.js ├── example-tasks.js └── modules-docs.js ├── dist └── mercury.js ├── docs ├── faq.md ├── hooks.md ├── introduction.md ├── life-cycles.md ├── mercury-component.md ├── modules │ ├── README.md │ ├── dom-delegator.md │ ├── geval.md │ ├── main-loop.md │ ├── observ-array.md │ ├── observ-struct.md │ ├── observ-varhash.md │ ├── observ.md │ ├── value-event.md │ ├── vdom-thunk.md │ ├── vdom.md │ ├── virtual-dom.md │ ├── virtual-hyperscript.md │ └── vtree.md ├── thunks.md └── widgets.md ├── examples ├── 2048 │ ├── render.js │ └── style.css ├── async-state.js ├── bmi-counter.js ├── canvas.js ├── count.js ├── examples-styles.css ├── geometry │ ├── browser.js │ ├── lib │ │ └── drag-handler.js │ └── shapes.js ├── hot-reload │ ├── README.md │ ├── browser.js │ ├── index.html │ ├── package.json │ └── render.js ├── index.html ├── lib │ ├── focus-hook.js │ ├── reset-hook.js │ ├── router │ │ ├── anchor.js │ │ ├── index.js │ │ └── view.js │ ├── safe-hook.js │ └── weakmap-event.js ├── login-form │ ├── browser.js │ ├── login-component-render.js │ ├── login-component.js │ └── styles.js ├── markdown │ ├── app.js │ ├── browser.js │ ├── component │ │ ├── inlineMdEditor.js │ │ ├── mdRender.js │ │ ├── sideBySideMdEditor.js │ │ └── textarea.js │ └── style.css ├── number-input │ ├── browser.js │ └── number-component.js ├── real-dom.js ├── server-rendering │ ├── browser.js │ ├── render.js │ ├── server.js │ └── test.js ├── shared-state.js ├── todomvc │ ├── bg.png │ ├── browser.js │ ├── lib │ │ └── raf-listen.js │ ├── style.css │ ├── todo-app.js │ └── todo-item.js └── unidirectional │ ├── README.md │ ├── backbone │ ├── browser.js │ └── observ-backbone.js │ ├── immutable │ └── browser.js │ └── jsx │ ├── browser.js │ ├── package.json │ └── render.jsx ├── index.js ├── package.json ├── svg.js ├── test ├── bmi-counter.js ├── count.js ├── index.js ├── lib │ ├── embed-component.js │ └── load-hook.js ├── package.json ├── shared-state.js ├── ssr.js ├── synthetic-events.js ├── time-travel.js └── transforms │ └── intercept-mercury-app.js └── time-travel.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style = space 3 | indent_size = 4 4 | insert_final_newline = true 5 | max_line_length = 80 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .monitor 3 | .*.swp 4 | .nodemonignore 5 | releases 6 | *.log 7 | *.err 8 | fleet.json 9 | public/browserify 10 | bin/*.json 11 | .bin 12 | build 13 | compile 14 | .lock-wscript 15 | coverage 16 | node_modules 17 | disc.html 18 | TODO.md 19 | *.iml 20 | .idea 21 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | concepts/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - iojs 6 | script: npm run travis-test 7 | env: 8 | global: 9 | - secure: Eaf4v0L7GrNeQU2MtrFSVBuD0QrvGaqHRxwsrKU1Og+7x21cXmmQdbtWG2efWgxuutPT4UYPA1tStLc3rh20c6vdiMjvDrqcDvzT82ge6N8KHh8kUZ9jBLVOs6gubQyhd7qkMT6adFqCgUenQy+oYLU3WvYe4kg/Qh+gAdGtKuo= 10 | - secure: GE44G8Ru8cbULLX/B/eMVM5TuRqn2LSMDszOC402PfEE7HlQEv73mMHHNQbCq2roz/cOSNHlUvUbIY9JEhFofkR0/vaEhFVjAa3kMspoBACL2M/R1hZjOo44P3jm6dpKDTbfTjSfqO6KpwSFx87V1RUPLMmR3jb73ZqY2ICgHAU= 11 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: tape 2 | concurrency: 1 3 | browsers: 4 | - name: chrome 5 | version: latest 6 | - name: ie 7 | version: 9..latest 8 | - name: firefox 9 | version: 25..latest 10 | - name: opera 11 | version: 11..latest 12 | - name: safari 13 | version: 6..latest 14 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # This is an OPEN Open Source Project 2 | 3 | ## What? 4 | 5 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 6 | 7 | ## Rules 8 | 9 | There are a few basic ground-rules for contributors: 10 | 11 | - No `--force` pushes or modifying the Git history in any way. 12 | - Non-master branches ought to be used for ongoing work. 13 | - External API changes and significant modifications ought to be subject to an internal pull-request to solicit feedback from other contributors. 14 | - Internal pull-requests to solicit feedback are encouraged for any other non-trivial contribution but left to the discretion of the contributor. 15 | - For significant changes wait a full 24 hours before merging so that active contributors who are distributed throughout the world have a chance to weigh in. 16 | - Contributors should attempt to adhere to the prevailing code-style. 17 | 18 | ## Releases 19 | 20 | Declaring formal releases requires peer review. 21 | 22 | - A reviewer of a pull request should recommend a new version number (patch, minor or major). 23 | - Once your change is merged feel free to bump the version as recommended by the reviewer. 24 | - A new version number should not be cut without peer review unless done by the project maintainer. 25 | 26 | ## Want to contribute? 27 | 28 | Even though collaborators may contribute as they see fit, if you are not sure what to do, here's a suggested process: 29 | 30 | ### Cutting a new version 31 | 32 | - Get your branch merged on master 33 | - Run `npm version major` or `npm version minor` or `npm version patch` 34 | - `git push origin master --tags` 35 | - If you are a project owner, then `npm publish` 36 | 37 | ### If you want to have a bug fixed or a feature added: 38 | 39 | - Check open issues for what you want. 40 | - If there is an open issue, comment on it, otherwise open an issue describing your bug or feature with use cases. 41 | - Discussion happens on the issue about how to solve your problem. 42 | - You or a core contributor opens a pull request solving the issue with tests and documentation. 43 | - The pull requests gets reviewed and then merged. 44 | - A new release version get's cut. 45 | - (Disclaimer: Your feature might get rejected.) 46 | 47 | ### Changes to this arrangement 48 | 49 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 50 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Raynos. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mercury 2 | 3 | # Deprecated: Not actively being worked on. 4 | 5 | Instead [`tonic`](https://github.com/optoolco/tonic) and https://tonic.technology/ are actively being worked on. 6 | 7 | `mercury` has some interesting ideas but they are not very practical at scale. 8 | `tonic` is a lightweigth component system on top of web components that leverages 9 | the browsers HTML parser for the heavy lifting instead of `virtual-dom`. 10 | 11 | It comes with a set of [`components`](https://github.com/optoolco/components) which help with building 12 | apps more quickly by having some re-usable components out of the box. 13 | 14 | ## Description 15 | 16 | A truly modular frontend framework 17 | 18 | To understand what I mean by truly modular just [read the source](https://github.com/Raynos/mercury/blob/master/index.js) 19 | 20 | 21 | ## Examples 22 | 23 | ### Hello world 24 | 25 | ```js 26 | 'use strict'; 27 | 28 | var document = require('global/document'); 29 | var hg = require('mercury'); 30 | var h = require('mercury').h; 31 | 32 | function App() { 33 | return hg.state({ 34 | value: hg.value(0), 35 | channels: { 36 | clicks: incrementCounter 37 | } 38 | }); 39 | } 40 | 41 | function incrementCounter(state) { 42 | state.value.set(state.value() + 1); 43 | } 44 | 45 | App.render = function render(state) { 46 | return h('div.counter', [ 47 | 'The state ', h('code', 'clickCount'), 48 | ' has value: ' + state.value + '.', h('input.button', { 49 | type: 'button', 50 | value: 'Click me!', 51 | 'ev-click': hg.send(state.channels.clicks) 52 | }) 53 | ]); 54 | }; 55 | 56 | hg.app(document.body, App(), App.render); 57 | ``` 58 | 59 | ### Basic Examples 60 | 61 | - [count](examples/count.js) 62 | - [shared-state](examples/shared-state.js) 63 | - [bmi-counter](examples/bmi-counter.js) 64 | - [canvas](examples/canvas.js) 65 | - [async-state](examples/async-state.js) 66 | - [real-dom](examples/real-dom.js) 67 | 68 | ### Intermediate Examples 69 | 70 | - [TodoMVC](examples/todomvc) 71 | - [markdown editor](examples/markdown) 72 | - [number-input](examples/number-input) 73 | - [serverside rendering](examples/server-rendering) 74 | - [login form](examples/login-form) 75 | - [geometry](examples/geometry) 76 | - [2048 (wip)](https://github.com/Raynos/mercury/tree/2048-wip/examples/2048) 77 | - [github issues (wip)](https://github.com/Raynos/mercury/tree/github-issues/examples/github-issues-viewer) 78 | - [hot reloading with browserify or webpack](examples/hot-reload) 79 | 80 | ### Unidirectional examples 81 | 82 | The following examples demonstrate how you can mix & match 83 | mercury with other frameworks. This is possible because mercury 84 | is fundamentally modular. 85 | 86 | **Disclaimer:** The following are neither "good" nor "bad" ideas. 87 | Your milage may vary on using these ideas 88 | 89 | - [Backbone + Mercury](examples/unidirectional/backbone) 90 | - [Immutable + Mercury](examples/unidirectional/immutable) 91 | - [JSX + Mercury](examples/unidirectional/jsx) 92 | 93 | ## Motivation 94 | 95 | ### Mercury vs React 96 | 97 | `mercury` is similar to react, however it's larger in scope, 98 | it is better compared against [`om`][om] or 99 | [`quiescent`][quiescent] 100 | 101 | - mercury leverages [`virtual-dom`][virtual-dom] which uses 102 | an immutable vdom structure 103 | - mercury comes with [`observ-struct`][observ-struct] which uses 104 | immutable data for your state atom 105 | - mercury is truly modular, you can trivially swap out 106 | subsets of it for other modules 107 | - mercury source code itself is maintainable, the modules it 108 | uses are all small, well tested and well documented. 109 | You should not be afraid to use mercury in production 110 | as it's easy to maintain & fix. 111 | - mercury encourages zero dom manipulation in your application code. As far as your application is concerned 112 | elements do not exist. This means you don't need to reference DOM elements when rendering or when handling 113 | events 114 | - mercury is compact, it's 11kb min.gzip.js, that's smaller than backbone. 115 | - mercury strongly encourages FRP techniques and discourages local mutable state. 116 | - mercury is highly performant, it's faster than React / Om / ember+htmlbars in multiple benchmarks 117 | [TodoMVC benchmark](http://matt-esch.github.io/mercury-perf/)\ 118 | [animation benchmark](http://jsfiddle.net/sVPQL/11/) 119 | [TodoMVC benchmark source](https://github.com/matt-esch/mercury-perf) 120 | - mercury comes with FP features like time-travel / easy undo out of the box. 121 | - mercury is lean, it's an weekend's read at 2.5kloc. (virtual-dom is 1.1kloc, an evening's read.) 122 | compared to react which is almost 20kloc (a month's read) 123 | 124 | ## Modules 125 | 126 | `mercury` is a small glue layer that composes a set of modules 127 | that solves a subset of the frontend problem. 128 | 129 | If `mercury` is not ideal for your needs, you should check out 130 | the individual modules and see if you can re-use something. 131 | 132 | Alternatively if the default set of modules in `mercury` doesn't 133 | work for you, you can just require other modules. It's possible 134 | to for example, swap out [`vtree`][vtree] with 135 | [`react`][react] or swap out [`observ-struct`][observ-struct] 136 | with [`backbone`][backbone] 137 | 138 | See [the modules README](docs/modules/README.md) for more 139 | information. 140 | 141 | ## Documentation 142 | 143 | See the [documentation folder](docs) 144 | 145 | ### FAQ 146 | 147 | See the [FAQ document](docs/faq.md) 148 | 149 | ### API 150 | 151 | WIP. In lieu of documentation please see examples :( 152 | 153 | ## Installation 154 | 155 | `npm install mercury` 156 | 157 | ## Development 158 | 159 | If you want to develop on `mercury` you can clone the code 160 | 161 | ```sh 162 | git clone git@github.com:Raynos/mercury 163 | cd mercury 164 | npm install 165 | npm test 166 | ``` 167 | 168 | ### npm run tasks 169 | 170 | - `npm test` runs the tests 171 | - `npm run jshint` will run jshint on the code 172 | - `npm run disc` will open discify (if globally installed) 173 | - `npm run build` will build the html assets for gh-pages 174 | - `npm run examples` will start a HTTP server that shows examples 175 | - `npm run dist` will create a distributed version of mercury 176 | - `npm run modules-docs` will (re)generate docs of mercury modules 177 | 178 | ## Inspirations 179 | 180 | A lot of the philosophy and design of `mercury` is inspired by 181 | the following: 182 | 183 | - [`react`][react] for documenting and explaining the concept 184 | of a virtual DOM and its diffing algorithm 185 | - [`om`][om] for explaining the concept and benefits of 186 | immutable state and time travel. 187 | - [`elm`][elm] for explaining the concept of FRP and having a 188 | reference implementation of FRP in JavaScript. I wrote a 189 | pre-cursor to `mercury` that was literally a 190 | re-implementation of [`elm`][elm] in javascript 191 | ([`graphics`][graphics]) 192 | - [`reflex`][reflex] for demonstrating the techniques used to 193 | implement dynamic inputs. 194 | 195 | ## Contributors 196 | 197 | - Raynos 198 | - Matt-Esch 199 | - neonstalwart 200 | - parshap 201 | - nrw 202 | - kumavis 203 | 204 | ## MIT Licenced 205 | 206 | [1]: https://secure.travis-ci.org/Raynos/mercury.svg 207 | [2]: https://travis-ci.org/Raynos/mercury 208 | [3]: https://badge.fury.io/js/mercury.svg 209 | [4]: https://badge.fury.io/js/mercury 210 | [5]: http://img.shields.io/coveralls/Raynos/mercury.svg 211 | [6]: https://coveralls.io/r/Raynos/mercury 212 | [7]: https://gemnasium.com/Raynos/mercury.png 213 | [8]: https://gemnasium.com/Raynos/mercury 214 | [9]: https://david-dm.org/Raynos/mercury.svg 215 | [10]: https://david-dm.org/Raynos/mercury 216 | [11]: https://img.shields.io/badge/GITTER-join%20chat-green.svg 217 | [12]: https://gitter.im/Raynos/mercury 218 | [13]: https://badge-size.herokuapp.com/Raynos/mercury/master/dist/mercury.js 219 | [14]: https://badge-size.herokuapp.com/Raynos/mercury/master/dist/mercury.js 220 | 221 | [graphics]: https://github.com/Raynos/graphics 222 | [elm]: https://github.com/elm-lang/Elm 223 | [react]: https://github.com/facebook/react 224 | [om]: https://github.com/swannodette/om 225 | [reflex]: https://github.com/Gozala/reflex 226 | [backbone]: https://github.com/jashkenas/backbone 227 | [quiescent]: https://github.com/levand/quiescent 228 | [virtual-dom]: https://github.com/Matt-Esch/virtual-dom 229 | [vtree]: https://github.com/Matt-Esch/virtual-dom/tree/master/vtree 230 | [vdom]: https://github.com/Matt-Esch/virtual-dom/tree/master/vdom 231 | [vdom-create-element]: https://github.com/Matt-Esch/virtual-dom/blob/master/vdom/create-element.js 232 | [vdom-patch]: https://github.com/Matt-Esch/virtual-dom/blob/master/vdom/patch.js 233 | [min-document]: https://github.com/Raynos/min-document 234 | [virtual-hyperscript]: https://github.com/Matt-Esch/virtual-dom/tree/master/virtual-hyperscript 235 | [main-loop]: https://github.com/Raynos/main-loop 236 | [vdom-thunk]: https://github.com/Raynos/vdom-thunk 237 | [observ]: https://github.com/Raynos/observ 238 | [observ-computed]: https://github.com/Raynos/observ/blob/master/computed.js 239 | [observ-struct]: https://github.com/Raynos/observ-struct 240 | [observ-array]: https://github.com/Raynos/observ-array 241 | [geval]: https://github.com/Raynos/geval 242 | [dom-delegator]: https://github.com/Raynos/dom-delegator 243 | [value-event]: https://github.com/Raynos/value-event 244 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Initial 2 | 3 | ## Goal 4 | 5 | Write web applications in simple and expressive way using FP techniques 6 | 7 | ## Feature summary 8 | 9 | - Guide / tutorial 10 | - Documentation 11 | - Motivation & Philosophy 12 | - Benchmarks 13 | - Website / logo thing ? 14 | 15 | - VDOM thunks 16 | - 2048 + github issues examples in 17 | - lazy observables 18 | - test debt 19 | 20 | ## Detailed summary 21 | 22 | ### Guide / tutorial 23 | 24 | Write a guide either similar to Om / React / Mithril or write 25 | a tutorial walking through authoring one of the exmaples 26 | 27 | ### Documentation 28 | 29 | We should write high quality docs for mercury AND all it's direct 30 | dependencies. 31 | 32 | ### Motivation & Philosophy 33 | 34 | This is hand wavey 35 | 36 | ### Benchmarks 37 | 38 | Mostly done, just need to mention them 39 | 40 | ### Websites / logo 41 | 42 | Would be nice to have a proper gh-pages or proper mercuryjs.com 43 | 44 | ### VDOM thunks 45 | 46 | Implement thunks in virtual-dom & update vdom-thunk to use thunks 47 | instead of widgets 48 | 49 | ### 2048 & github issues 50 | 51 | Implement these examples, 2048 is already a branch 52 | 53 | ### lazy observables 54 | 55 | Implement lazy data structures for performance 56 | 57 | ### test debt 58 | 59 | Deal with the fact that both mercury & some of its dependencies 60 | dont have any tests 61 | -------------------------------------------------------------------------------- /bin/browserify-editor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserify = require('browserify'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var cuid = require('cuid'); 7 | var process = require('process'); 8 | 9 | function addLink(href) { 10 | return '' + 11 | 'var link = document.createElement("link")\n' + 12 | 'link.rel = "stylesheet"\n' + 13 | 'link.href = "' + href + '"\n' + 14 | 'document.head.appendChild(link)\n'; 15 | } 16 | 17 | function floatElement(name, float) { 18 | return '' + 19 | name + '.style.float = "' + float + '"\n' + 20 | name + '.style.padding = 0\n' + 21 | name + '.style.margin = 0\n' + 22 | name + '.style.width = "50%"\n'; 23 | } 24 | 25 | function main(fileName) { 26 | var src = fs.readFileSync(path.resolve(fileName), 'utf8'); 27 | 28 | var code = '' + 29 | 'var container = document.createElement("div")\n' + 30 | floatElement('document.body', 'left') + 31 | floatElement('container', 'right') + 32 | 'var createEditor = require("javascript-editor")\n' + 33 | 'window.addEventListener("load", function () {\n' + 34 | ' var editor = createEditor({\n' + 35 | ' container: container,\n' + 36 | ' value: ' + JSON.stringify(src) + ',\n' + 37 | ' readOnly: true\n' + 38 | ' })\n' + 39 | ' container.childNodes[0].style.fontSize = "12px"\n' + 40 | ' editor.editor.refresh()\n' + 41 | '})\n' + 42 | 'document.documentElement.appendChild(container)\n' + 43 | addLink('https://cdn.rawgit.com/maxogden/javascript-editor/' + 44 | '1835d09bdfe83e5121befe2ff660bc336a520d9a/css/codemirror.css') + 45 | addLink('https://cdn.rawgit.com/maxogden/javascript-editor/' + 46 | '1835d09bdfe83e5121befe2ff660bc336a520d9a/css/theme.css'); 47 | 48 | var loc = path.join(__dirname, cuid() + '.js'); 49 | fs.writeFileSync(loc, code); 50 | 51 | var bundle = browserify(); 52 | bundle.add(path.resolve(fileName)); 53 | bundle.add(loc); 54 | var stream = bundle.bundle(); 55 | stream.on('end', function onEnd() { 56 | fs.unlinkSync(loc); 57 | }); 58 | return stream; 59 | } 60 | 61 | module.exports = main; 62 | 63 | if (require.main === module) { 64 | var file = process.argv[2]; 65 | main(file).pipe(process.stdout); 66 | } 67 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parallel = require('run-parallel'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var process = require('process'); 7 | var logger = require('console'); 8 | var examplesTasks = require('./example-tasks.js'); 9 | 10 | function main() { 11 | var tasks = examplesTasks.map(function writeTask(task) { 12 | return function thunk(cb) { 13 | logger.log('reading', task.src); 14 | 15 | task.createStream() 16 | .pipe(fs.createWriteStream(task.dest)) 17 | .on('finish', function onFinish() { 18 | logger.log('writing', task.dest); 19 | cb(); 20 | }); 21 | }; 22 | }); 23 | 24 | parallel(tasks, function onTasks(err) { 25 | if (err) { 26 | throw err; 27 | } 28 | 29 | var html = '\n'; 36 | 37 | fs.writeFileSync( 38 | path.join(process.cwd(), 'index.html'), html); 39 | 40 | logger.log('build finished'); 41 | }); 42 | } 43 | 44 | if (require.main === module) { 45 | main(); 46 | } 47 | -------------------------------------------------------------------------------- /bin/dist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserify = require('browserify'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var logger = require('console'); 7 | 8 | var version = require('../package.json').version; 9 | 10 | var bundle = browserify() 11 | .add(path.join(__dirname, '..', 'index.js')) 12 | .bundle({ standalone: 'mercury' }); 13 | 14 | var dest = fs.createWriteStream( 15 | path.join(__dirname, '..', 'dist', 'mercury.js') 16 | ); 17 | 18 | dest.write('// mercury @ ' + version + ' \n'); 19 | bundle.pipe(dest); 20 | 21 | dest.on('finish', function fini() { 22 | logger.log('OK done'); 23 | }); 24 | -------------------------------------------------------------------------------- /bin/example-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var fs = require('fs'); 5 | var HttpHashRouter = require('http-hash-router'); 6 | var path = require('path'); 7 | var st = require('st'); 8 | var logger = require('console'); 9 | 10 | var tasks = require('./example-tasks.js'); 11 | var tasksHash = tasks.reduce(function buildHash(acc, task) { 12 | acc[task.name] = task; 13 | return acc; 14 | }, {}); 15 | 16 | var router = HttpHashRouter(); 17 | 18 | var handler = function exampleHandler(req, res, opts) { 19 | var name = opts.params.name + ((opts.splat) ? '/' + opts.splat : ''); 20 | var task = tasksHash[name]; 21 | if (!task) { 22 | res.statusCode = 404; 23 | return res.end('Example ' + name + ' Not Found'); 24 | } 25 | 26 | res.setHeader('Content-Type', 'text/html'); 27 | var stream = task.createStream(); 28 | stream.on('error', function onError(error) { 29 | logger.log('error', error); 30 | res.end('(' + function throwError(err) { 31 | throw new Error(err); 32 | } + '(' + JSON.stringify(error.message) + '))'); 33 | }); 34 | stream.pipe(res); 35 | }; 36 | 37 | var buildList = function buildList(items) { 38 | var i = 0; 39 | var l = items.length; 40 | var list = '
    '; 41 | var name; 42 | 43 | if (!items || !items.length) { 44 | return ''; // return here if there are no items to render 45 | } 46 | 47 | for (; i < l; i++) { 48 | name = items[i].name; 49 | list += '
  1. ' + 51 | name + '
  2. '; // make a list item element 52 | } 53 | list += '
'; 54 | 55 | return list; 56 | }; 57 | 58 | // Routes handlers 59 | router.set('/', function index(req, res) { 60 | var filename = path.dirname(__dirname) + '/examples/index.html'; 61 | var buf = fs.readFileSync(filename, 'utf8'); 62 | 63 | res.setHeader('Content-Type', 'text/html'); 64 | 65 | buf = buf.replace('{{examples}}', buildList(tasks)); 66 | res.end(buf); 67 | }); 68 | router.set('/:name', handler); 69 | router.set('/:name/*', handler); 70 | router.set('/mercury/*', st({ 71 | path: path.dirname(__dirname), 72 | url: '/mercury', 73 | cache: false 74 | })); 75 | 76 | // Server implementation 77 | var server = http.createServer(function handler(req, res) { 78 | router(req, res, {}, onError); 79 | 80 | function onError(err) { 81 | if (err) { 82 | // use your own custom error serialization. 83 | res.statusCode = err.statusCode || 500; 84 | res.end(err.message); 85 | } 86 | } 87 | }); 88 | server.listen(8080); 89 | logger.log('listening on port 8080'); 90 | -------------------------------------------------------------------------------- /bin/example-tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var indexhtmlify = require('indexhtmlify'); 5 | var browserify = require('browserify'); 6 | 7 | var browserifyEditor = require('./browserify-editor'); 8 | 9 | var examplesDir = path.join(__dirname, '..', 'examples'); 10 | var examplesTasks = [ 11 | browserifyTask('geometry'), 12 | browserifyTask('todomvc'), 13 | browserifyTask('markdown'), 14 | browserifyTask('number-input'), 15 | browserifyTask(path.join( 16 | 'unidirectional', 17 | 'backbone' 18 | )), 19 | browserifyTask(path.join( 20 | 'unidirectional', 21 | 'jsx' 22 | )), 23 | browserifyTask(path.join( 24 | 'unidirectional', 25 | 'immutable' 26 | )), 27 | browserifyTask('login-form'), 28 | browserifyEditorTask('bmi-counter'), 29 | browserifyEditorTask('shared-state'), 30 | browserifyEditorTask('count'), 31 | browserifyEditorTask('canvas'), 32 | browserifyEditorTask('async-state'), 33 | browserifyEditorTask('real-dom') 34 | ]; 35 | 36 | module.exports = examplesTasks; 37 | 38 | function browserifyTask(folder) { 39 | var task = { 40 | src: path.join(examplesDir, folder, 'browser.js'), 41 | dest: path.join(examplesDir, folder, 'index.html'), 42 | type: 'browserify', 43 | name: folder, 44 | createStream: createStream 45 | }; 46 | 47 | return task; 48 | 49 | function createStream() { 50 | var stream = browserifyBundle(task.src); 51 | var result = stream.pipe(indexhtmlify({})); 52 | 53 | stream.on('error', function onError(err) { 54 | result.emit('error', err); 55 | }); 56 | 57 | return result; 58 | } 59 | } 60 | 61 | function browserifyEditorTask(file) { 62 | var task = { 63 | src: path.join(examplesDir, file + '.js'), 64 | dest: path.join(examplesDir, file + '.html'), 65 | type: 'browserify-editor', 66 | name: file, 67 | createStream: createStream 68 | }; 69 | 70 | return task; 71 | 72 | function createStream() { 73 | return browserifyEditor(task.src) 74 | .pipe(indexhtmlify({})); 75 | } 76 | } 77 | 78 | function browserifyBundle(source) { 79 | var b = browserify(); 80 | b.add(source); 81 | return b.bundle(); 82 | } 83 | -------------------------------------------------------------------------------- /bin/modules-docs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | var docsPath = path.join(__dirname, '..', 'docs', 'modules'); 7 | var modulesPath = path.join(__dirname, '..', 'node_modules'); 8 | var moduleReadmes = { 9 | 'geval': ['geval', 'README.md', 'Raynos/geval'], 10 | 'dom-delegator': ['dom-delegator', 'README.md', 'Raynos/dom-delegator'], 11 | 'value-event': ['value-event', 'README.md', 'Raynos/value-event'], 12 | 'observ-array': ['observ-array', 'README.md', 'Raynos/observ-array'], 13 | 'observ-varhash': ['observ-varhash', 'Readme.md', 'nrw/observ-varhash'], 14 | 'observ-struct': ['observ-struct', 'README.md', 'Raynos/observ-struct'], 15 | 'observ': ['observ', 'README.md', 'Raynos/observ'], 16 | 'virtual-dom': ['virtual-dom', 'README.md', 'Matt-Esch/virtual-dom'], 17 | 'vtree': ['virtual-dom', path.join('vtree', 'README.md'), 18 | 'Matt-Esch/virtual-dom'], 19 | 'vdom': ['virtual-dom', path.join('vdom', 'README.md'), 20 | 'Matt-Esch/virtual-dom'], 21 | 'virtual-hyperscript': ['virtual-dom', 22 | path.join('virtual-hyperscript', 'README.md'), 23 | 'Matt-Esch/virtual-dom'], 24 | 'vdom-thunk': ['vdom-thunk', 'README.md', 'Raynos/vdom-thunk'], 25 | 'main-loop': ['main-loop', 'README.md', 'Raynos/main-loop'] 26 | }; 27 | 28 | Object.keys(moduleReadmes).forEach(saveModuleDoc); 29 | 30 | function saveModuleDoc(moduleName) { 31 | var moduleData = moduleReadmes[moduleName]; 32 | var moduleReadmePath = path.join(modulesPath, moduleData[0], 33 | moduleData[1]); 34 | var moduleUrl = 'https://github.com/' + moduleData[2]; 35 | var modulePackagePath = path.join(modulesPath, moduleData[0], 36 | 'package.json'); 37 | var modulePackageInfo = JSON.parse(fs.readFileSync(modulePackagePath, 38 | 'utf8')); 39 | var moduleVersion = modulePackageInfo.version; 40 | 41 | var moduleDocPath = path.join(docsPath, moduleName + '.md'); 42 | var docFile = fs.createWriteStream(moduleDocPath); 43 | 44 | docFile.write('Auto generated from [' + moduleData[0] + '](' + moduleUrl + 45 | ') (version ' + moduleVersion + ').\n\n', 46 | function writeModuleDocFile() { 47 | var moduleReadmeFile = fs.createReadStream(moduleReadmePath); 48 | moduleReadmeFile.pipe(docFile); 49 | } 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | A vnode consists mainly of `{ tagName, properties, children }`. When you diff two vnodes you are applying a diff on the tagName, on the properties and the children. 4 | 5 | The properties normally consist of keys and values, where the values are strings or nested objects (e.g. style, attributes). 6 | 7 | Other than strings and nested objects you can also place hooks on the properties object. 8 | 9 | A hook is a `{ hook: function (domElement, propertyName) {} }` and it's used like: 10 | 11 | ```js 12 | h('div', { 13 | 'arbitrary-keyname': Object.create({ 14 | hook: function (elem, propname) { 15 | assert(elem.tagName === 'div'); 16 | assert(propname === 'arbitrary-keyname'); 17 | } 18 | } 19 | }) 20 | ``` 21 | 22 | When you place a hook in a virtual node vtree/diff will create a patch object saying the properties have changed. Because hooks are instances of prototypes they will always be a new instance on every `diff` call so the hook value in the properties object will be different between `prev` and `current`. 23 | 24 | The hook only gets executed in `vdom/create-element` and `vdom/patch`. Hooks get executed in key order of the properties object. Hooks also get executed together with property patches being applied. This means you probably want all your hook keys to be at the bottom of your properties object. 25 | 26 | So whenever vdom is patching the properties on a DOM element it will also invoke hooks synchronously. The hook gets called with the DOM element and the property name. 27 | 28 | The use case for hooks is to set properties on DOM nodes that cannot be set through the DOM property interface, for example focus has no property based declarative interface so we have a focus hook that calls the `.focus()` method. 29 | 30 | The other use case for hooks is to manage stateful properties. Hooks are stateful and will always get called even if nothing else has changed. If a vnode has hooks we will call them, this means that even if there are no differences or the thunk has not changed we will still return a set of patches that is "invoke these hooks". 31 | 32 | Because of the fact hooks get called on every diff/patch cycle we can use hook to manage stateful properties. For example if you say `h('input', { value: ValueHook('foo') })` we will set the value of the input to `'foo'` on every render phase because we know that the user can change it away from `'foo'` and thus the DOM and the virtual DOM get out of sync which is not something we want. 33 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | ## What is mercury. 2 | 3 | > Below is a comment stolen from a mercury vs cycle ( https://github.com/staltz/cycle/issues/49#issuecomment-68612757 ) comparison. 4 | 5 | Mercury is very component oriented at the root of it. Generally you write applications as a series of components that 6 | return a state (the component data at any given point) and provides a render function. The render function knows how the 7 | component should be rendered including the dom structure, attributes/values and events/event handlers (known as channels in Mercury). 8 | 9 | The following is a quick example that makes a div that can be clicked which will create a new one exactly the same except it 10 | has a different `data-foo-id` attribute value. It is created in the image of a Cycle.js example you can see here ( https://github.com/staltz/cycle/blob/master/examples/simple/simple.js ). 11 | 12 | 13 | ```js 14 | var hg = require('mercury'); 15 | var h = require('mercury').h; 16 | 17 | function Foo(initialState) { 18 | return hg.state({ 19 | bars: hg.array(initialState.bars, createBar), 20 | channels: { 21 | addBar: Foo.addBar 22 | } 23 | }); 24 | 25 | function createBar(x) { 26 | return hg.struct({ 27 | id: hg.value(x.id), 28 | bar: hg.value(x.bar) 29 | }) 30 | } 31 | } 32 | 33 | Foo.addBar = function addBar(state) { 34 | state.bars.push({ 35 | id: 2, 36 | bar: Math.round(Math.random() * 1000) 37 | }); 38 | }; 39 | 40 | Foo.render = function render(state) { 41 | return h('div', state.bars.map(renderBar)) 42 | 43 | function renderBar(bar) { 44 | return h('div', { 45 | 'attributes': { 46 | 'data-foo-id': bar.id 47 | }, 48 | 'style': { 49 | 'margin': '10px', 50 | 'background': '#ececec', 51 | 'padding': '5px', 52 | 'cursor': 'pointer', 53 | 'display': 'inline-block' 54 | }, 55 | 'ev-click': hg.send(state.channels.addBar) 56 | }); 57 | } 58 | }; 59 | 60 | function main() { 61 | hg.app(document.body, Foo({ 62 | bars: [{ id: 2, bar: 135 }] 63 | }), Foo.render); 64 | } 65 | 66 | main(); 67 | ``` 68 | 69 | Some of the core ideas are: 70 | 71 | - your entire view is a single complex vtree 72 | - your entire view state is a single complex immutable object. 73 | - your rendering function is pure, it takes just the view state (disclaimer: hooks & widgets are not pure). 74 | - you declare all user input as channels up front in your view state. 75 | - the view state supports cursors, you can nest components in components. 76 | 77 | Mercury is unidirectional because: 78 | 79 | - A DOM event triggers a value to be send to a channel 80 | - The listener for the channel updates the view state 81 | - An update to the view state triggers a re-render 82 | - A new vtree is created 83 | - diff() and patch() update the DOM. 84 | 85 | See more about life cycles here ( https://github.com/Raynos/mercury/blob/master/docs/life-cycles.md ). 86 | 87 | Mercury is also a hybrid of functional and imperative, the rendering logic is functional but the updating logic has an imperative updating interface (backed by a stream of immutable objects under the hood). 88 | 89 | This gives you an FRP style application written in a way that still feels like its javascript 90 | -------------------------------------------------------------------------------- /docs/life-cycles.md: -------------------------------------------------------------------------------- 1 | # The life cycles of a mercury app 2 | 3 | A well structure mercury app is reasonably deterministic about 4 | when certain code runs. 5 | 6 | There are a few main "phases" 7 | 8 | - Reacting to browser events from the event loop 9 | - Running application update logic 10 | - Running the rendering cycle in `requestAnimationFrame` 11 | 12 | ## Reacting to the event loop 13 | 14 | The only time JavaScript get's executed is when the event loop 15 | tells your code that a new thing happened. 16 | 17 | This includes the initial evaluation of your `browser.js` 18 | source code. 19 | 20 | On first evaluation you setup your state, render your view 21 | and insert it into the DOM. 22 | 23 | ### Reacting to DOM events 24 | 25 | All your handling of DOM events (i.e. `click`, `change`, ...) 26 | should go through `dom-delegator`. 27 | 28 | `dom-delegator` intercepts all events and sees if you have 29 | registered an event handler using the `ev-{{name}}` syntax 30 | in `h()` or have registered a global listener using 31 | `delegator.addGlobalEventListener()`. 32 | 33 | If it finds one it will invoke it. 34 | 35 | If you suspect there might be an issue or bug with an event not 36 | getting fired you should go into 37 | `node_modules/mercury/node_modules/dom-delegator` and edit 38 | your copy of dom-delegator to add print statements. 39 | 40 | One common issue might be that your event is not in the whitelist 41 | ( https://github.com/Raynos/dom-delegator/blob/master/index.js#L10-L16 ) 42 | you should call `delegator.listenTo(eventName)` to make sure 43 | the delegator registers a global event handler for it. 44 | 45 | ### Reacting to other entries from the event loop 46 | 47 | There are many other ways the event loop can notify you that 48 | something has changed. 49 | 50 | It's highly recommended that you take all other effects & 51 | entries to the event loop and wrap them in a `geval` interface. 52 | 53 | For example: 54 | 55 | - https://github.com/Raynos/mercury/blob/master/examples/todomvc/input.js#L15 56 | - https://github.com/Raynos/mercury/blob/github-issues/examples/github-issues-viewer/input.js#L11-L12 57 | 58 | The benefit is that once you've done this, you can now trace or 59 | debug `geval` to check all new events entering the event loop. 60 | 61 | The other benefit is isolating your actual core application logic 62 | that does not concern itself with effects from the code with 63 | side effects. 64 | 65 | If you put all the IO in one bucket and create a "seperate" 66 | part of your app that is just "on a geval event, run business 67 | logic and update state" then that second part is really simple 68 | to reason about and unit test. 69 | 70 | ## Running application logic 71 | 72 | ### `geval` event listeners 73 | 74 | Eventually a event from the event loop will trigger into a new 75 | discrete value being emitted on a `geval` Event instance. 76 | 77 | This discrete value should be a application specific value, you 78 | shouldn't have any dom events or raw xhr objects being emitted 79 | through geval but instead have already converted them into 80 | something specific to your application. 81 | 82 | ### Application logic 83 | 84 | At this point the listener to the `geval` Event is generally one 85 | of your applications `update` functions that has access to 86 | either the top level state or one of the nested states. 87 | 88 | This is your core application / business logic and you do some 89 | computation and update the state into a new state. 90 | 91 | ## The request animation frame rendering loop 92 | 93 | ### Scheduling a `requestAnimationFrame` 94 | 95 | Any time you do `state.set()` or `state.some.key.set()` the 96 | `main-loop` module will check if we have scheduled a rerender 97 | yet and if not will schedule a render on the next frame. 98 | 99 | If you suspect a `requestAnimationFrame` is not scheduled then 100 | add logging or tracing to `main-loop`. 101 | 102 | ### Entering a `requestAnimationFrame` 103 | 104 | The browser will enter the `main-loop` rendering phase on the 105 | next animation frame. 106 | 107 | #### Rendering the view to create a new virtual tree. 108 | 109 | `main-loop` will call the top level `render` function that you 110 | passed to `mercury.app(state, render)` and create a new virtual 111 | tree. 112 | 113 | One of the tricks that makes this fast is the fact that 114 | `mercury.partial` is used to avoid evaluating subtrees in 115 | `render` unless needed 116 | 117 | If you suspect this doesn't happen just add debug statements to 118 | your top level render function. 119 | 120 | #### Calling diff on the trees 121 | 122 | The next step is the diff phase, here `vtree` will diff the 123 | new and previous tree. At this point any `partial`'s that have 124 | actually changed will be evaluated and their respective 125 | rendering functions will be called. 126 | 127 | If you suspect anything is wrong here it's recommended you add 128 | a print to `main-loop` to inspect the `patches` returned by 129 | the `diff()` function and see if your expected changes are 130 | included. 131 | 132 | #### Calling patch on the DOM 133 | 134 | The final step is calling `patch()` on the actual DOM with your 135 | patches from `diff`. 136 | 137 | This is the only place in the life cycle of a mercury app where 138 | actual DOM manipulation happens. it happens at the end of 139 | any raf we have scheduled. Here all patches are applied and 140 | all hooks get called 141 | 142 | If you suspect something is wrong here it's recommended that you 143 | use DOM mutation observers with a helper like listenMutation 144 | ( https://github.com/Raynos/jsonml-stringify/blob/master/examples/lib/listen-mutation.js ) 145 | to inspect the actual mutations applied to the DOM and see if 146 | they are what you might expect. 147 | 148 | -------------------------------------------------------------------------------- /docs/mercury-component.md: -------------------------------------------------------------------------------- 1 | A component in mercury is two things 2 | 3 | - a "constructor" that takes some initialization arguments and 4 | returns state 5 | - a rendering function. 6 | 7 | The `state` that is returned from a component is a 8 | black box & a lens. You embed it into your "top level" state 9 | atom at some key. 10 | 11 | For example: 12 | 13 | ```js 14 | var appState = mercury.struct({ 15 | loginState: LoginComponent(...) 16 | }) 17 | ``` 18 | 19 | You then update your "top level" rendering logic to call the 20 | components rendering function with the state that you 21 | "embedded" at some key. 22 | 23 | For example: 24 | 25 | ```js 26 | function appRender(state) { 27 | return h('div', [ 28 | h('.header', [ 29 | h('.my-logo'), 30 | LoginComponent.render(state.loginState) 31 | ]) 32 | ]) 33 | } 34 | ``` 35 | 36 | A component also has events, this is similar to how you 37 | pass channels into a component in om except it's slightly 38 | less coupled. 39 | 40 | The events a component jas might be something like 41 | "login button pressed", i.e. a component is saying 42 | "I have a login button but have no idea how to handle login, 43 | please listen to my event and mutate the correct state somewhere". 44 | 45 | For example: 46 | 47 | ```js 48 | var loginComp = LoginComponent(...) 49 | 50 | var appState = mercury.struct({ 51 | loginState: loginComp 52 | }) 53 | 54 | LoginComponent.onLogin(loginComp, function (user) { 55 | /* do something with user. probably ajax, maybe redirect */ 56 | }) 57 | ``` 58 | 59 | Note that if you want to mutate the black box `state` of a 60 | component you must not mutate it directly, a component 61 | should expose a set of functions like `resetField(state)` 62 | exactly like how evan has demonstrated. 63 | 64 | For example: 65 | 66 | ```js 67 | LoginComponent.renderLogOutForm(loginComp) 68 | ``` 69 | 70 | The advantage of this technique is that you need to know nothing 71 | about how a component updates itself nor do you need to know 72 | anything about any events a component might have. 73 | 74 | Instead the component will internally broadcast any events 75 | or do the correct mutation of it's own state. 76 | Since the state it returns is a lens it will "mutate" the top 77 | level state that the caller of the component has embedded the 78 | component into thus triggering a redraw. 79 | 80 | Note: this article is written based on my reply to the Elm 81 | discuss thread about modularity, to see other techniques used 82 | by Elm & om, read more at https://groups.google.com/forum/#!msg/elm-discuss/sv7DVJ47QkE/VhLr9_V-5E4J 83 | -------------------------------------------------------------------------------- /docs/modules/README.md: -------------------------------------------------------------------------------- 1 | ### Input, State, Render and Output 2 | 3 | There are three pieces to mercury, Input (Controller), 4 | State (Model) and Render (View). 5 | 6 | In a normal mercury app you define your top level Input which 7 | is a finite list of events. 8 | 9 | You then define your top level state "atom". Generally you want 10 | a large fat state object for your entire application. We then 11 | wire all the events in Input up to some updating logic, i.e. 12 | every time an event occurs in Input we do some logic and then 13 | update the State. 14 | 15 | Finally we define our Rendering logic as a single function 16 | that takes the entire state of our application and returns a 17 | virtual DOM representation of our UI. Every time the state 18 | changes we just call render and then update the DOM. 19 | 20 | You may also need Output for your application, if we want to 21 | have some other side effect other then updating the UI, like 22 | sending a HTTP POST or writing to a websocket or persisting 23 | to indexedDB. Then we generally listen to changes in the state 24 | and have our side effect. Note that Render is just a specific 25 | subset of the Output of your application. 26 | 27 | ### Rendering modules (The view layer) 28 | 29 | For the view layer mercury uses a set of modules that come 30 | together and make it easy to work with a Virtual DOM. 31 | 32 | In `mercury` the view is just a function that takes your 33 | application state and returns a virtual DOM representation. 34 | This makes writing your view really easy, you just write it 35 | top to bottom. 36 | 37 | `mercury` then uses the following modules to make it really 38 | performant to use the virtual DOM to update the real DOM. 39 | 40 | #### [`vtree`][vtree] 41 | 42 | [`vtree`][vtree] is the module that contains the data 43 | structures for the virtual DOM. These are the primitive 44 | objects and values that the rendering functions in a 45 | mercury app will return. 46 | 47 | [`vtree`][vtree] also contains the diffing algorithm used in 48 | `mercury`. Mercury uses a diffing algorithm on a virtual DOM 49 | to compute a minimal set of `VPatch` records that it can apply to the DOM. 50 | 51 | #### [`vdom`][vdom] 52 | 53 | [`vdom`][vdom] is the module that contains the `create` and 54 | `patch` algorithm for turning the `vtree` data structures 55 | into real DOM objects. 56 | 57 | [`vdom/create-element`][vdom-create-element] is used to turn a 58 | virtual DOM into a real DOM. this is used for the initial 59 | rendering of your app. 60 | 61 | [`vdom/patch`][vdom-patch] is used to apply a series of `VPatch` 62 | records to a real DOM element. 63 | 64 | You can also use [`vdom`][vdom] and 65 | [`min-document`][min-document] together on the server to 66 | generate HTML strings. [`min-document`][min-document] is a 67 | minimal fake DOM for use on the server, you can pass 68 | [`vdom`][vdom] any `document` you want. In this case 69 | [`min-document`][min-document] contains the logic to convert 70 | its fake DOM into a HTML string. 71 | 72 | #### [`virtual-hyperscript`][virtual-hyperscript] 73 | 74 | [`virtual-hyperscript`][virtual-hyperscript] is a module that 75 | makes it easier to create `VTree` nodes. It basically exports 76 | a `h()` function that creates a DSL similar to `jade` 77 | (just more brackets ;)). 78 | 79 | [`virtual-hyperscript`][virtual-hyperscript] allows you to write 80 | your views in an expressive manner. 81 | 82 | #### [`vdom-thunk`][vdom-thunk] 83 | 84 | [`vdom-thunk`][vdom-thunk] is a module that increases the 85 | performance of building applications with a virtual DOM 86 | system. One of the important parts of using a virtual DOM 87 | and functional programming in general is to make extensive 88 | use of caching. 89 | 90 | You can use [`vdom-thunk`][vdom-thunk] to effectively memoize a 91 | function that returns a virtual DOM node. This means if you 92 | call it twice with the same arguments it will not re-evaluate 93 | the function. 94 | 95 | This basically means you only have to render that which has 96 | changed instead of rendering the entire virtual tree of your 97 | application. 98 | 99 | It should be noted that [`vdom-thunk`][vdom-thunk] assumes 100 | arguments are immutable and thus does an O(1) `===` check 101 | to see whether the arguments has changed. This will only 102 | work if your state is immutable. Thankfully, 103 | [`observ-struct`][observ-struct] is immutable 104 | 105 | #### [`main-loop`][main-loop] 106 | 107 | [`main-loop`][main-loop] is another optimization module for a 108 | virtual DOM system. Normally you would re-create the virtual 109 | tree every time your state changes. This is not optimum, 110 | with [`main-loop`][main-loop] you will only update your 111 | virtual tree at most once per request animation frame. 112 | 113 | [`main-loop`][main-loop] basically gives you batching of your 114 | virtual DOM changes, which means if you change your model 115 | multiple times it will be rendered once asynchronously on 116 | the next request animation frame. 117 | 118 | ### State modules (The model layer) 119 | 120 | In `mercury` we use immutable data structure primitives to 121 | represent our model. Using immutable data structures allows 122 | you to use the [`vdom-thunk`][vdom-thunk] optimization. 123 | 124 | `mercury` uses an observable state representation so that you 125 | can be notified of any changes. 126 | 127 | Generally applications built with mercury will have a single 128 | top level state "atom". i.e. there is one large state object 129 | for your application and child components do not have local or 130 | hidden state. However we can directly embed the state of a 131 | child component in our top level state "atom" to achieve 132 | composition. 133 | 134 | #### [`observ`][observ] 135 | 136 | [`observ`][observ] is the data structure used for observable 137 | data. It allows you to create a value for which you can 138 | listen for changes. 139 | 140 | [`observ`][observ] also comes with higher order functions 141 | like [`observ/computed`][observ-computed] that can be used to 142 | create new dependent observables. Generally these computed 143 | observables cannot be directly mutated but instead change 144 | when they data they rely on changes. 145 | 146 | [`observ`][observ] is basically an implementation of the 147 | `Signal` type that is normally used in FRP. 148 | 149 | #### [`observ-struct`][observ-struct] 150 | 151 | [`observ-struct`][observ-struct] is an observable that contains an 152 | object with a fixed number of keys. Generally the key-value 153 | pairs in [`observ-struct`][observ-struct] are themselves 154 | observables. You can change the value of any key in an 155 | [`observ-struct`][observ-struct] and the top level object 156 | will also change to be a new object with that key changed. 157 | 158 | [`observ-struct`][observ-struct] uses shallow extension to ensure 159 | that every time the struct changes you get a fresh immutable 160 | object. 161 | 162 | #### [`observ-array`][observ-array] 163 | 164 | [`observ-array`][observ-array] is an observable that contains 165 | an array of observables. It's generally recommended that this a 166 | heterogeneous array. You can change the value of any item in 167 | the array and the top level array will also change to be a 168 | new array. 169 | 170 | [`observ-array`][observ-array] uses shallow extension to ensure 171 | that every time the array changes (an item changes or an 172 | item is added or removed) you get a fresh immutable array. 173 | 174 | [`observ-array`][observ-array] has the benefit of being able 175 | to add or remove items from the array, where as 176 | [`observ-struct`][observ-struct] has a fixed number 177 | of keys and you cannot add more keys to an 178 | [`observ-struct`][observ-struct] 179 | 180 | ### Input modules (The controller layer) 181 | 182 | In `mercury` we model all the inputs to our application 183 | explicitly. We define an input object that contains a bunch of 184 | [`geval`][geval] Event instances. 185 | 186 | Somewhere else in our application we listen to the Input and 187 | run some logic and update our state when an event happens. 188 | 189 | #### [`geval`][geval] 190 | 191 | [`geval`][geval] is our data structure for Events. it gives us 192 | a way of listening to events and a way of publishing them. 193 | 194 | Most of the time you will either create a computed Event that 195 | emits events based on some raw source, like winddow scroll 196 | events or a websocket. Or you can create a mutable Event which 197 | you pass to the UI renderer so it can emit events for dynamically 198 | created UI components. 199 | 200 | [`geval`][geval] is basically an implementation of the 201 | `Event` type that is normally used in FRP. 202 | 203 | #### [`dom-delegator`][dom-delegator] 204 | 205 | [`dom-delegator`][dom-delegator] is an event delegator that 206 | allows you to seperate your event listeners from your 207 | event emitters. It sets up global event listeners and 208 | allow you to embed event handlers on your virtual DOM 209 | elements without having to manage adding or removing 210 | actual event listeners. 211 | 212 | #### [`value-event`][value-event] 213 | 214 | [`value-event`][value-event] allows you to create event 215 | handlers that you can embed in a virtual DOM. 216 | These event handlers work with both the native DOM and 217 | [`dom-delegator`][dom-delegator]. 218 | 219 | [`value-event`][value-event] contains a set of higher order 220 | functions that allows you to write to a value to a 221 | [`geval`][geval] Event when some user interaction occurs. 222 | 223 | Using the higher order functions defined in 224 | [`value-event`][value-event] (change, submit, etc. ) 225 | allows you to not have to write any DOM event handling 226 | code in your application. [`value-event`][value-event] 227 | takes care of all the reading from the DOM. 228 | 229 | 230 | 231 | 232 | [graphics]: https://github.com/Raynos/graphics 233 | [elm]: https://github.com/elm-lang/Elm 234 | [react]: https://github.com/facebook/react 235 | [om]: https://github.com/swannodette/om 236 | [reflex]: https://github.com/Gozala/reflex 237 | [backbone]: https://github.com/jashkenas/backbone 238 | [quiescent]: https://github.com/levand/quiescent 239 | [virtual-dom]: https://github.com/Matt-Esch/virtual-dom 240 | [vtree]: https://github.com/Matt-Esch/virtual-dom/tree/master/vtree 241 | [vdom]: https://github.com/Matt-Esch/virtual-dom/tree/master/vdom 242 | [vdom-create-element]: https://github.com/Matt-Esch/virtual-dom/blob/master/vdom/create-element.js 243 | [vdom-patch]: https://github.com/Matt-Esch/virtual-dom/blob/master/vdom/patch.js 244 | [min-document]: https://github.com/Raynos/min-document 245 | [virtual-hyperscript]: https://github.com/Matt-Esch/virtual-dom/tree/master/virtual-hyperscript 246 | [main-loop]: https://github.com/Raynos/main-loop 247 | [vdom-thunk]: https://github.com/Raynos/vdom-thunk 248 | [observ]: https://github.com/Raynos/observ 249 | [observ-computed]: https://github.com/Raynos/observ/blob/master/computed.js 250 | [observ-struct]: https://github.com/Raynos/observ-struct 251 | [observ-array]: https://github.com/Raynos/observ-array 252 | [geval]: https://github.com/Raynos/geval 253 | [dom-delegator]: https://github.com/Raynos/dom-delegator 254 | [value-event]: https://github.com/Raynos/value-event 255 | 256 | -------------------------------------------------------------------------------- /docs/modules/dom-delegator.md: -------------------------------------------------------------------------------- 1 | Auto generated from [dom-delegator](https://github.com/Raynos/dom-delegator) package (version 13.1.0). 2 | 3 | # dom-delegator 4 | 5 | 12 | 13 | 14 | 15 | Decorate elements with delegated events 16 | 17 | `dom-delegator` allows you to attach an `EventHandler` to 18 | a dom element. 19 | 20 | When event of the correct type occurs `dom-delegator` will 21 | invoke your `EventHandler` 22 | 23 | This allows you to seperate your event listeners from your 24 | event writers. Sprinkle your event writers in the template 25 | in one part of your codebase. Attach listeners to the event 26 | sources in some other part of the code base. 27 | 28 | This decouples the event definition in the DOM from your event 29 | listeners in your application code. 30 | 31 | Also see [`html-delegator`](https://github.com/Raynos/html-delegator) 32 | for the same idea using html `data-` attributes. 33 | 34 | ## Example 35 | 36 | ```html 37 |
38 |
bar
39 |
baz
40 |
41 | ``` 42 | 43 | ```js 44 | var document = require("global/document") 45 | var Delegator = require("dom-delegator") 46 | var EventEmitter = require("events").EventEmitter 47 | 48 | var del = Delegator() 49 | var emitter = EventEmitter() 50 | emitter.on('textClicked', function (value) { 51 | // either 'bar' or 'bar' depends on which 52 | // `
` was clicked 53 | console.log("doSomething", value.type) 54 | }) 55 | 56 | var elem = document.querySelector(".foo") 57 | 58 | // add individual elems. (in a different file?) 59 | del.addEventListener(elem.querySelector(".bar"), "click", function (ev) { 60 | emmitter.emit('textClicked', { type: 'bar' }) 61 | }) 62 | del.addEventListener(elem.querySelector(".baz"), "click", function (ev) { 63 | emitter.emit('textClicked', { type: 'baz' }) 64 | }) 65 | ``` 66 | 67 | ## Example (global listeners) 68 | 69 | Sometimes you don't want to add events bound to an element but 70 | instead listen to them globally. 71 | 72 | ```js 73 | var Delegator = require("dom-delegator") 74 | 75 | var d = Delegator() 76 | d.addGlobalEventListener("keydown", function (ev) { 77 | // hit for every global key press 78 | // can implement keyboard shortcuts 79 | 80 | 81 | }) 82 | 83 | d.addEventListener(document.documentElement, "keydown", function (ev) { 84 | // hit for every keydown that is not captured 85 | // by an element listener lower in the tree 86 | 87 | // by default dom-delegator does not bubble events up 88 | // to other listeners on parent nodes 89 | 90 | // you can use global event listeners to intercept everything 91 | // even if there are listeners lower in the tree 92 | }) 93 | ``` 94 | 95 | ## Installation 96 | 97 | `npm install dom-delegator` 98 | 99 | ## Contributors 100 | 101 | - Raynos 102 | 103 | ## MIT Licenced 104 | 105 | [1]: https://secure.travis-ci.org/Raynos/dom-delegator.png 106 | [2]: https://travis-ci.org/Raynos/dom-delegator 107 | [3]: https://badge.fury.io/js/dom-delegator.png 108 | [4]: https://badge.fury.io/js/dom-delegator 109 | [5]: https://coveralls.io/repos/Raynos/dom-delegator/badge.png 110 | [6]: https://coveralls.io/r/Raynos/dom-delegator 111 | [7]: https://gemnasium.com/Raynos/dom-delegator.png 112 | [8]: https://gemnasium.com/Raynos/dom-delegator 113 | [9]: https://david-dm.org/Raynos/dom-delegator.png 114 | [10]: https://david-dm.org/Raynos/dom-delegator 115 | [11]: https://ci.testling.com/Raynos/dom-delegator.png 116 | [12]: https://ci.testling.com/Raynos/dom-delegator 117 | -------------------------------------------------------------------------------- /docs/modules/geval.md: -------------------------------------------------------------------------------- 1 | Auto generated from [geval](https://github.com/Raynos/geval) package (version 2.1.1). 2 | 3 | # geval 4 | 5 | [![build status][1]][2] 6 | [![NPM version][3]][4] 7 | [![Coverage Status][5]][6] 8 | [![Davis Dependency status][9]][10] 9 | 10 | [![browser support][11]][12] 11 | 12 | An implementation of an event 13 | 14 | ## Example 15 | 16 | ```js 17 | var Event = require("geval") 18 | var document = require("global/document") 19 | 20 | var clicks = Event(function (broadcast) { 21 | document.addEventListener("click", function (ev) { 22 | broadcast(ev) 23 | }) 24 | }) 25 | 26 | var removeListener = clicks(function listener(ev) { 27 | console.log('click happened', ev) 28 | }) 29 | 30 | // later you can call `removeListener()` to stop listening to events 31 | ``` 32 | 33 | ## What about [`dominictarr/observable`](https://github.com/dominictarr/observable) ? 34 | 35 | Both `geval` and `observable` having a similar interface. 36 | 37 | - `thing(function (ev) { ... })` listens for new values. 38 | 39 | The main difference is that `geval` is an `Event`. For discrete 40 | events it doesn't make sense to call `thing()` to get the 41 | current state. Events do not have a notion of current state. 42 | 43 | So the `"click"` event doesn't have a `.get()` method because 44 | clicks do not have a notion of current state that makes sense 45 | 46 | However you should not make an `Event` of the windows current 47 | width & height. You should make an `observable` instead which 48 | internally listens on the `"resize"` event and sets the correct 49 | new width & height. 50 | 51 | ## Motivation 52 | 53 | EventEmitter's are complex. They are multiplexed events by default 54 | 55 | `Event` is the simpler version of an `EventEmitter` 56 | 57 | The main differences are: 58 | 59 | - just one event. 60 | - no implicit string based events 61 | - forces explicit interfaces with named properties that are 62 | `Event`'s 63 | - no inheritance, you don't have to inherit from `Event` like 64 | you have to inherit from `EventEmitter`. 65 | - `Event` interface only has public listening functionality, 66 | this gives a clear seperation between broadcast and listen 67 | 68 | Instead of something like 69 | 70 | ```js 71 | var EventEmitter = require('events').EventEmitter 72 | 73 | var stream = new EventEmitter() 74 | 75 | stream.on('data', onData) 76 | stream.on('end', onEnd) 77 | stream.on('close', onClose) 78 | ``` 79 | 80 | you can do: 81 | 82 | ```js 83 | var Event = require('geval') 84 | 85 | var stream = { 86 | ondata: Event(function () { ... }), 87 | onend: Event(function () { ... }), 88 | onclose: Event(function () { ... }) 89 | } 90 | 91 | stream.ondata(onData) 92 | stream.onend(onEnd) 93 | stream.onclose(onClose) 94 | ``` 95 | 96 | Here the benefits are: 97 | 98 | - `stream` is an object of your shape and choice, you can call 99 | the properties whatever you want. the `[[Prototype]]` can 100 | be whatever you want. 101 | - `stream` has three well named properties that can be inspected 102 | statically or at run time which means the consumer knows 103 | exactly what type of events are available. 104 | - A consumer of `stream` could pass the `ondata` event to 105 | another object or module without also passing all other 106 | events along. 107 | - the `ondata` event is a concrete value. This allows for 108 | calling higher order functions on the value and enables 109 | various types of reactive programming techniques. 110 | - there are no special `"error"` semantics. There is no magic 111 | integration with `domain` or `"uncaughtException"`. 112 | - there is no public `emit()` function on the `stream` interface 113 | It's impossible for the consumer to emit events that it 114 | should not be emitting, you know that all events that 115 | come out of `ondata` are coming from the actual `stream` 116 | implementation. 117 | 118 | ## Docs 119 | 120 | ### `var removeListener = ev(function listener(value) {})` 121 | 122 | ```js 123 | var Event = require("geval") 124 | 125 | var ev = Event(...) 126 | 127 | var removeListener = ev(function listener(value) { 128 | /* do something with the event value */ 129 | }) 130 | 131 | // call `removeListener()` when you are done with the `ev`. 132 | ``` 133 | 134 | A concrete `ev` is a function which you can pass a `listener` 135 | to. The `listener` you pass to `ev` will be called with 136 | an `value` each time an event occurs. 137 | 138 | When calling `ev` with a `listener` it will return a 139 | `removeListener` function. You can call `removeListener` to 140 | remove your `listener` function from the event. After you call 141 | it your listener function will not be called with any future 142 | values coming from the event. 143 | 144 | ### `var ev = Event(function broadcaster(broadcast) {})` 145 | 146 | ```js 147 | var Event = require("geval") 148 | 149 | var ev = Event(function broadcaster(broadcast) { 150 | /* call broadcast with a value */ 151 | }) 152 | ``` 153 | 154 | 155 | `Event` takes a broadcasting function and returns an `event` 156 | function. 157 | 158 | The `broadcasting` function takes one argument, the `broadcast` 159 | function. The broadcaster can call `broadcast` each time it 160 | wants to make an event occur. Each time you call `broadcast` 161 | with a `value`, all listeners that are registered with `ev` 162 | will be invoked with the `value` 163 | 164 | ## Installation 165 | 166 | `npm install geval` 167 | 168 | ## Contributors 169 | 170 | - Raynos 171 | 172 | ## MIT Licenced 173 | 174 | [1]: https://secure.travis-ci.org/Raynos/geval.png 175 | [2]: https://travis-ci.org/Raynos/geval 176 | [3]: https://badge.fury.io/js/geval.png 177 | [4]: https://badge.fury.io/js/geval 178 | [5]: https://coveralls.io/repos/Raynos/geval/badge.png 179 | [6]: https://coveralls.io/r/Raynos/geval 180 | [7]: https://gemnasium.com/Raynos/geval.png 181 | [8]: https://gemnasium.com/Raynos/geval 182 | [9]: https://david-dm.org/Raynos/geval.png 183 | [10]: https://david-dm.org/Raynos/geval 184 | [11]: https://ci.testling.com/Raynos/geval.png 185 | [12]: https://ci.testling.com/Raynos/geval 186 | -------------------------------------------------------------------------------- /docs/modules/main-loop.md: -------------------------------------------------------------------------------- 1 | Auto generated from [main-loop](https://github.com/Raynos/main-loop) package (version 3.1.0). 2 | 3 | # main-loop 4 | 5 | 12 | 13 | 14 | 15 | A rendering loop for diffable UIs 16 | 17 | ## Example 18 | 19 | ```js 20 | var mainLoop = require("main-loop") 21 | var h = require("virtual-dom/h") 22 | 23 | var initState = { fruits: ["apple", "banana"], name: "Steve" } 24 | 25 | function render(state) { 26 | return h("div", [ 27 | h("div", [ 28 | h("span", "hello "), 29 | h("span.name", state.name) 30 | ]), 31 | h("ul", state.fruits.map(renderFruit)) 32 | ]) 33 | 34 | function renderFruit(fruitName) { 35 | return h("li", [ 36 | h("span", fruitName) 37 | ]) 38 | } 39 | } 40 | 41 | // set up a loop 42 | var loop = mainLoop(initState, render, { 43 | create: require("virtual-dom/create-element"), 44 | diff: require("virtual-dom/diff"), 45 | patch: require("virtual-dom/patch") 46 | }) 47 | document.body.appendChild(loop.target) 48 | 49 | // update the loop with the new application state 50 | loop.update({ 51 | fruits: ["apple", "banana", "cherry"], 52 | name: "Steve" 53 | }) 54 | loop.update({ 55 | fruits: ["apple", "banana", "cherry"], 56 | name: "Stevie" 57 | }) 58 | ``` 59 | 60 | ## Installation 61 | 62 | `npm install main-loop` 63 | 64 | ## Contributors 65 | 66 | - Raynos 67 | 68 | ## MIT Licenced 69 | 70 | [1]: https://secure.travis-ci.org/Raynos/main-loop.png 71 | [2]: https://travis-ci.org/Raynos/main-loop 72 | [3]: https://badge.fury.io/js/main-loop.png 73 | [4]: https://badge.fury.io/js/main-loop 74 | [5]: https://coveralls.io/repos/Raynos/main-loop/badge.png 75 | [6]: https://coveralls.io/r/Raynos/main-loop 76 | [7]: https://gemnasium.com/Raynos/main-loop.png 77 | [8]: https://gemnasium.com/Raynos/main-loop 78 | [9]: https://david-dm.org/Raynos/main-loop.png 79 | [10]: https://david-dm.org/Raynos/main-loop 80 | [11]: https://ci.testling.com/Raynos/main-loop.png 81 | [12]: https://ci.testling.com/Raynos/main-loop 82 | -------------------------------------------------------------------------------- /docs/modules/observ-array.md: -------------------------------------------------------------------------------- 1 | Auto generated from [observ-array](https://github.com/Raynos/observ-array) package (version 3.1.0). 2 | 3 | # observ-array 4 | 5 | 12 | 13 | 14 | 15 | An array containing observable values 16 | 17 | ## Example 18 | 19 | An `ObservArray` is an observable version of an array, every 20 | mutation of the array or mutation of an observable element in 21 | the array will cause the `ObservArray` to emit a new changed 22 | plain javascript array. 23 | 24 | ```js 25 | var ObservArray = require("observ-array") 26 | var ObservStruct = require("observ-struct") 27 | var Observ = require("observ") 28 | var uuid = require("uuid") 29 | 30 | function createTodo(title) { 31 | return ObservStruct({ 32 | id: uuid(), 33 | title: Observ(title || ""), 34 | completed: Observ(false) 35 | }) 36 | } 37 | 38 | var state = ObservStruct({ 39 | todos: ObservArray([ 40 | createTodo("some todo"), 41 | createTodo("some other todo") 42 | ]) 43 | }) 44 | 45 | state(function (currState) { 46 | // currState.todos is a plain javascript todo 47 | // currState.todos[0] is a plain javascript value 48 | currState.todos.forEach(function (todo, index) { 49 | console.log("todo", todo.title, index) 50 | }) 51 | }) 52 | 53 | state.todos.get(0).title.set("some new title") 54 | state.todos.push(createTodo("another todo")) 55 | ``` 56 | 57 | ### Transactions 58 | 59 | Batch changes together with transactions. 60 | 61 | ```js 62 | var array = ObservArray([ Observ("foo"), Observ("bar") ]) 63 | 64 | var removeListener = array(handleChange) 65 | 66 | array.transaction(function(rawList) { 67 | rawList.push(Observ("foobar")) 68 | rawList.splice(1, 1, Observ("baz"), Observ("bazbar")) 69 | rawList.unshift(Observ("foobaz")) 70 | rawList[6] = Observ("foobarbaz") 71 | }) 72 | 73 | function handleChange(value) { 74 | // this will only be called once 75 | // changes are batched into a single diff 76 | value._diff //= [ [1,1,"baz","bazbar","foobar", , "foobarbaz"], 77 | // [0,0,"foobaz"] ] 78 | } 79 | ``` 80 | 81 | ## Installation 82 | 83 | `npm install observ-array` 84 | 85 | ## Contributors 86 | 87 | - Raynos 88 | - [Matt McKegg][13] 89 | 90 | ## MIT Licenced 91 | 92 | [1]: https://secure.travis-ci.org/Raynos/observ-array.png 93 | [2]: https://travis-ci.org/Raynos/observ-array 94 | [3]: https://badge.fury.io/js/observ-array.png 95 | [4]: https://badge.fury.io/js/observ-array 96 | [5]: https://coveralls.io/repos/Raynos/observ-array/badge.png 97 | [6]: https://coveralls.io/r/Raynos/observ-array 98 | [7]: https://gemnasium.com/Raynos/observ-array.png 99 | [8]: https://gemnasium.com/Raynos/observ-array 100 | [9]: https://david-dm.org/Raynos/observ-array.png 101 | [10]: https://david-dm.org/Raynos/observ-array 102 | [11]: https://ci.testling.com/Raynos/observ-array.png 103 | [12]: https://ci.testling.com/Raynos/observ-array 104 | [13]: https://github.com/mmckegg 105 | -------------------------------------------------------------------------------- /docs/modules/observ-struct.md: -------------------------------------------------------------------------------- 1 | Auto generated from [observ-struct](https://github.com/Raynos/observ-struct) package (version 5.0.1). 2 | 3 | # observ-struct 4 | 5 | 12 | 13 | 14 | 15 | An object with observable key value pairs 16 | 17 | ## Example 18 | 19 | An observable will emit a new immutable value whenever one of 20 | its keys changes. 21 | 22 | Nested keys will still be the same value if they were not changed 23 | in that particular `.set()` call. 24 | 25 | ```js 26 | var ObservStruct = require("observ-struct") 27 | var Observ = require("observ") 28 | var assert = require("assert") 29 | 30 | var state = ObservStruct({ 31 | fruits: ObservStruct({ 32 | apples: Observ(3), 33 | oranges: Observ(5) 34 | }), 35 | customers: Observ(5) 36 | }) 37 | 38 | state(function (current) { 39 | console.log("apples", current.fruit.apples) 40 | console.log("customers", current.customers) 41 | }) 42 | 43 | state.fruits(function (current) { 44 | console.log("apples", current.apples) 45 | }) 46 | 47 | var initialState = state() 48 | assert.equal(initialState.fruits.bananas, 5) 49 | assert.equal(initialState.customers, 5) 50 | 51 | state.fruits.oranges.set(6) 52 | state.customers.set(5) 53 | state.fruits.apples.set(4) 54 | ``` 55 | 56 | ## Docs 57 | 58 | ### `var obj = ObservStruct(opts)` 59 | 60 | `ObservStruct()` takes an object literal of string keys to either 61 | normal values or observable values. 62 | 63 | It returns an `Observ` instance `obj`. The value of `obj` is 64 | a plain javascript object where the value for each key is either 65 | the normal value passed in or the value of the observable for 66 | that key. 67 | 68 | Whenever one of the observables on a `key` changes the `obj` will 69 | emit a new object that's a shallow copy with that `key` set to 70 | the value of the appropiate observable on that `key`. 71 | 72 | ## Installation 73 | 74 | `npm install observ-struct` 75 | 76 | ## Contributors 77 | 78 | - Raynos 79 | 80 | ## MIT Licenced 81 | 82 | [1]: https://secure.travis-ci.org/Raynos/observ-struct.png 83 | [2]: https://travis-ci.org/Raynos/observ-struct 84 | [3]: https://badge.fury.io/js/observ-struct.png 85 | [4]: https://badge.fury.io/js/observ-struct 86 | [5]: https://coveralls.io/repos/Raynos/observ-struct/badge.png 87 | [6]: https://coveralls.io/r/Raynos/observ-struct 88 | [7]: https://gemnasium.com/Raynos/observ-struct.png 89 | [8]: https://gemnasium.com/Raynos/observ-struct 90 | [9]: https://david-dm.org/Raynos/observ-struct.png 91 | [10]: https://david-dm.org/Raynos/observ-struct 92 | [11]: https://ci.testling.com/Raynos/observ-struct.png 93 | [12]: https://ci.testling.com/Raynos/observ-struct 94 | -------------------------------------------------------------------------------- /docs/modules/observ-varhash.md: -------------------------------------------------------------------------------- 1 | Auto generated from [observ-varhash](https://github.com/nrw/observ-varhash) package (version 1.0.4). 2 | 3 | # observ-varhash [![build status][1]][2] 4 | 5 | An object with observable key value pairs that can be added and removed 6 | 7 | ## Example 8 | 9 | An `ObservVarhash` is a version of `observ-struct` that allows 10 | adding and removing keys. Mutation of an observable element in 11 | the hash will cause the `ObservVarhash` to emit a new changed 12 | plain javascript object. 13 | 14 | ```js 15 | var ObservVarhash = require("observ-varhash") 16 | var Observ = require("observ") 17 | 18 | var people = ObservVarhash({jack: 'Jack'}, function create (obj, key) { 19 | return Observ(obj) 20 | }) 21 | 22 | people.put('diane', 'Diane') 23 | 24 | console.log(people()) 25 | // plain javascript object {jack: 'Jack', diane: 'Diane'} 26 | ``` 27 | 28 | ## Installation 29 | 30 | `npm install observ-varhash` 31 | 32 | ## Contributors 33 | 34 | - Nicholas Westlake 35 | 36 | API based on [`observ-struct`](https://github.com/Raynos/observ-struct) 37 | 38 | ## MIT Licenced 39 | 40 | [1]: https://secure.travis-ci.org/nrw/observ-varhash.png 41 | [2]: https://travis-ci.org/nrw/observ-varhash 42 | -------------------------------------------------------------------------------- /docs/modules/observ.md: -------------------------------------------------------------------------------- 1 | Auto generated from [observ](https://github.com/Raynos/observ) package (version 0.2.0). 2 | 3 | # observ 4 | 5 | [![build status][1]][2] [![NPM version][3]][4] [![Davis Dependency status][9]][10] 6 | 7 | [![browser support][11]][12] 8 | 9 | [![NPM][13]][14] 10 | 11 | A observable value representation 12 | 13 | ## Example 14 | 15 | ```js 16 | var Observable = require("observ") 17 | 18 | var v = Observable("initial value") 19 | v(function onchange(newValue) { 20 | assert.equal(newValue, "new value") 21 | }) 22 | v.set("new value") 23 | 24 | var curr = v() 25 | assert.equal(curr, "new value") 26 | ``` 27 | 28 | 29 | ## What about `dominictarr/observable` ? 30 | 31 | Both `observ` & `observable` have the same interface of 32 | 33 | - `thing()` gets the value 34 | - `thing.set(...)` sets the value 35 | - `thing(function (value) { ... })` listens to the value. 36 | 37 | The way `observ` and `observable` differ is in listening. 38 | 39 | - `observ` will ONLY call the listener if `.set()` is invoked. 40 | - `observable` calls the listener IMMEDIATELY and calls it whenever 41 | `.set()` is invoked 42 | 43 | `observ` can be used in a similar fashion to `observable` by using 44 | `var watch = require("observ/watch")`. You can then just 45 | `watch(thing, function (value) { ... })` and it will call the 46 | listener immediately 47 | 48 | Both `observ` & `observable` have a computed method with the same 49 | interface. 50 | 51 | - `require("observable").compute` 52 | - `require("observ/computed")` 53 | 54 | ## Example computed 55 | 56 | ```js 57 | var Observable = require("observ") 58 | var computed = require("observ/computed") 59 | 60 | var one = Observable(1) 61 | var two = Observable(2) 62 | 63 | var together = computed([one, two], function (a, b) { 64 | return a + b 65 | }) 66 | 67 | assert.equal(together(), 3) 68 | two.set(5) 69 | assert.equal(together(), 7) 70 | ``` 71 | 72 | ## Docs 73 | 74 | ```ocaml 75 | type Observable := { 76 | () => A & 77 | (Function) => void, 78 | set: (A) => void 79 | } 80 | 81 | observ := (A) => Observable 82 | ``` 83 | 84 | 85 | ## Installation 86 | 87 | `npm install observ` 88 | 89 | ## Contributors 90 | 91 | - Raynos 92 | 93 | ## MIT Licenced 94 | 95 | [1]: https://secure.travis-ci.org/Raynos/observ.png 96 | [2]: https://travis-ci.org/Raynos/observ 97 | [3]: https://badge.fury.io/js/observ.png 98 | [4]: https://badge.fury.io/js/observ 99 | [5]: https://coveralls.io/repos/Raynos/observ/badge.png 100 | [6]: https://coveralls.io/r/Raynos/observ 101 | [7]: https://gemnasium.com/Raynos/observ.png 102 | [8]: https://gemnasium.com/Raynos/observ 103 | [9]: https://david-dm.org/Raynos/observ.png 104 | [10]: https://david-dm.org/Raynos/observ 105 | [11]: https://ci.testling.com/Raynos/observ.png 106 | [12]: https://ci.testling.com/Raynos/observ 107 | [13]: http://nodei.co/npm/observ.png 108 | [14]: http://nodei.co/npm/observ 109 | -------------------------------------------------------------------------------- /docs/modules/value-event.md: -------------------------------------------------------------------------------- 1 | Auto generated from [value-event](https://github.com/Raynos/value-event) package (version 5.0.0). 2 | 3 | # value-event 4 | 5 | 12 | 13 | 14 | 15 | Create DOM event handlers that write to listeners 16 | 17 | ## Example (event) 18 | 19 | ```html 20 |
21 |
Bob Steve
22 | 23 |
24 | ``` 25 | 26 | ```js 27 | var event = require('value-event/event') 28 | var listener = function (data) { 29 | console.log('data', data) 30 | } 31 | 32 | var elem = document.getElementById('foo') 33 | elem.querySelector('div.name') 34 | .addEventListener('click', event(listener, { 35 | clicked: true 36 | })) 37 | elem.querySelector('input.name') 38 | .addEventListener('keypress', event(listener, { 39 | changed: true 40 | })) 41 | ``` 42 | 43 | ## Example (change) 44 | 45 | The change event happens when form elements change 46 | 47 | For example: 48 | 49 | - someone types a character in an input field 50 | - someone checks or unchecks a checkbox 51 | 52 | ```html 53 |
54 | 55 |
56 | ``` 57 | 58 | ```js 59 | var changeEvent = require('value-event/change') 60 | var listener = function (data) { 61 | console.log('data', data.changed, data.foo) 62 | } 63 | 64 | var elem = document.getElementById('my-app') 65 | elem 66 | .addEventListener('input', changeEvent(listener, { 67 | changed: true 68 | })) 69 | ``` 70 | 71 | ## Example (submit) 72 | 73 | The submit event happens when form elements get submitted. 74 | 75 | For example: 76 | 77 | - a button gets clicked 78 | - someone hits ENTER in an input field 79 | 80 | ```html 81 |
82 | 83 |
84 | ``` 85 | 86 | ```js 87 | var submitEvent = require('value-event/submit') 88 | var listener = function (data) { 89 | console.log('data', data.changed, data.foo) 90 | } 91 | 92 | var elem = document.getElementById('my-app') 93 | elem 94 | .addEventListener('keypress', submitEvent(listener, { 95 | changed: true 96 | })) 97 | ``` 98 | 99 | ## Example (value) 100 | 101 | The value event happens whenever the event listener fires. 102 | It attaches input values just like `'submit'` and `'change'` 103 | except it doesn't have special semantics of what's a valid 104 | event. 105 | 106 | ```html 107 |
108 | 109 |
110 | ``` 111 | 112 | ```js 113 | var valueEvent = require('value-event/value') 114 | var listener = function (data) { 115 | // currentValues is { 'foo': 'bar' } 116 | console.log('data', data.changed, data.foo) 117 | } 118 | 119 | var elem = document.getElementById('my-app') 120 | elem.querySelector('input.name') 121 | .addEventListener('blur', valueEvent(listener, { 122 | changed: true 123 | })) 124 | ``` 125 | 126 | ## Installation 127 | 128 | `npm install value-event` 129 | 130 | ## Contributors 131 | 132 | - Raynos 133 | 134 | ## MIT Licenced 135 | 136 | [1]: https://secure.travis-ci.org/Raynos/value-event.png 137 | [2]: https://travis-ci.org/Raynos/value-event 138 | [3]: https://badge.fury.io/js/value-event.png 139 | [4]: https://badge.fury.io/js/value-event 140 | [5]: https://coveralls.io/repos/Raynos/value-event/badge.png 141 | [6]: https://coveralls.io/r/Raynos/value-event 142 | [7]: https://gemnasium.com/Raynos/value-event.png 143 | [8]: https://gemnasium.com/Raynos/value-event 144 | [9]: https://david-dm.org/Raynos/value-event.png 145 | [10]: https://david-dm.org/Raynos/value-event 146 | [11]: https://ci.testling.com/Raynos/value-event.png 147 | [12]: https://ci.testling.com/Raynos/value-event 148 | -------------------------------------------------------------------------------- /docs/modules/vdom-thunk.md: -------------------------------------------------------------------------------- 1 | Auto generated from [vdom-thunk](https://github.com/Raynos/vdom-thunk) package (version 3.0.0). 2 | 3 | # vdom-thunk 4 | 5 | 12 | 13 | 14 | 15 | A thunk optimization for virtual-dom 16 | 17 | ## Example 18 | 19 | Use partial when you want to avoid re-rendering subtrees. 20 | 21 | `partial` will only re-evaluate the subtree if the arguments 22 | you pass to it change. This means you should use an immutable 23 | data structure (like `observ-struct`) 24 | 25 | ```js 26 | var partial = require("vdom-thunk") 27 | 28 | function render(state) { 29 | return h('div', [ 30 | partial(header, state.head), 31 | main(), 32 | partial(footer, state.foot) 33 | ]) 34 | } 35 | 36 | function header(head) { ... } 37 | function main() { ... } 38 | function footer(foot) { ... } 39 | ``` 40 | 41 | ## Installation 42 | 43 | `npm install vdom-thunk` 44 | 45 | ## Contributors 46 | 47 | - Raynos 48 | 49 | ## MIT Licenced 50 | 51 | [1]: https://secure.travis-ci.org/Raynos/vdom-thunk.png 52 | [2]: https://travis-ci.org/Raynos/vdom-thunk 53 | [3]: https://badge.fury.io/js/vdom-thunk.png 54 | [4]: https://badge.fury.io/js/vdom-thunk 55 | [5]: https://coveralls.io/repos/Raynos/vdom-thunk/badge.png 56 | [6]: https://coveralls.io/r/Raynos/vdom-thunk 57 | [7]: https://gemnasium.com/Raynos/vdom-thunk.png 58 | [8]: https://gemnasium.com/Raynos/vdom-thunk 59 | [9]: https://david-dm.org/Raynos/vdom-thunk.png 60 | [10]: https://david-dm.org/Raynos/vdom-thunk 61 | [11]: https://ci.testling.com/Raynos/vdom-thunk.png 62 | [12]: https://ci.testling.com/Raynos/vdom-thunk 63 | -------------------------------------------------------------------------------- /docs/modules/vdom.md: -------------------------------------------------------------------------------- 1 | Auto generated from [virtual-dom](https://github.com/Matt-Esch/virtual-dom) package (version 1.3.0). 2 | 3 | # vdom 4 | 5 | A DOM render and patch algorithm for vtree 6 | 7 | ## Motivation 8 | 9 | Given a `vtree` structure representing a DOM structure, we would like to either 10 | render the structure to a DOM node using `vdom/create-element` or we would like 11 | to update the DOM using the results of `vtree/diff` by patching the DOM with 12 | `vdom/patch` 13 | 14 | ## Example 15 | 16 | ```js 17 | var h = require("virtual-dom/h") 18 | var diff = require("virtual-dom/diff") 19 | 20 | var createElement = require("virtual-dom/create-element") 21 | var patch = require("virtual-dom/patch") 22 | 23 | var leftNode = h("div") 24 | var rightNode = h("text") 25 | 26 | // Render the left node to a DOM node 27 | var rootNode = createElement(leftNode) 28 | document.body.appendChild(rootNode) 29 | 30 | // Update the DOM with the results of a diff 31 | var patches = diff(leftNode, rightNode) 32 | patch(rootNode, patches) 33 | ``` 34 | 35 | ## Installation 36 | 37 | `npm install virtual-dom` 38 | 39 | ## Contributors 40 | 41 | - Matt Esch 42 | 43 | ## MIT Licenced 44 | -------------------------------------------------------------------------------- /docs/modules/virtual-dom.md: -------------------------------------------------------------------------------- 1 | Auto generated from [virtual-dom](https://github.com/Matt-Esch/virtual-dom) package (version 1.3.0). 2 | 3 | # virtual-dom 4 | 5 | A JavaScript [DOM model](#dom-model) supporting [element creation](#element-creation), [diff computation](#diff-computation) and [patch operations](#patch-operations) for efficient re-rendering 6 | 7 | [![build status][1]][2] 8 | [![NPM version][3]][4] 9 | [![Coverage Status][5]][6] 10 | [![Davis Dependency status][7]][8] 11 | [![experimental](http://hughsk.github.io/stability-badges/dist/experimental.svg)](http://github.com/hughsk/stability-badges) 12 | 13 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/mattesch.svg)](https://saucelabs.com/u/mattesch) 14 | 15 | ## Motivation 16 | 17 | Manual DOM manipulation is messy and keeping track of the previous DOM state is hard. A solution to this problem is to write your code as if you were recreating the entire DOM whenever state changes. Of course, if you actually recreated the entire DOM every time your application state changed, your app would be very slow and your input fields would lose focus. 18 | 19 | `virtual-dom` is a collection of modules designed to provide a declarative way of representing the DOM for your app. So instead of updating the DOM when your application state changes, you simply create a virtual tree or `VTree`, which looks like the DOM state that you want. `virtual-dom` will then figure out how to make the DOM look like this efficiently without recreating all of the DOM nodes. 20 | 21 | `virtual-dom` allows you to update a view whenever state changes by creating a full `VTree` of the view and then patching the DOM efficiently to look exactly as you described it. This results in keeping manual DOM manipulation and previous state tracking out of your application code, promoting clean and maintainable rendering logic for web applications. 22 | 23 | ## Example 24 | 25 | ```javascript 26 | var h = require('virtual-dom/h'); 27 | var diff = require('virtual-dom/diff'); 28 | var patch = require('virtual-dom/patch'); 29 | var createElement = require('virtual-dom/create-element'); 30 | 31 | // 1: Create a function that declares what the DOM should look like 32 | function render(count) { 33 | return h('div', { 34 | style: { 35 | textAlign: 'center', 36 | lineHeight: (100 + count) + 'px', 37 | border: '1px solid red', 38 | width: (100 + count) + 'px', 39 | height: (100 + count) + 'px' 40 | } 41 | }, [String(count)]); 42 | } 43 | 44 | // 2: Initialise the document 45 | var count = 0; // We need some app data. Here we just store a count. 46 | 47 | var tree = render(count); // We need an initial tree 48 | var rootNode = createElement(tree); // Create an initial root DOM node ... 49 | document.body.appendChild(rootNode); // ... and it should be in the document 50 | 51 | // 3: Wire up the update logic 52 | setInterval(function () { 53 | count++; 54 | 55 | var newTree = render(count); 56 | var patches = diff(tree, newTree); 57 | rootNode = patch(rootNode, patches); 58 | tree = newTree; 59 | }, 1000); 60 | ``` 61 | [View on RequireBin](http://requirebin.com/?gist=5492847b9a9025e64bab) 62 | 63 | ## Documentation 64 | 65 | You can find the documentation for the seperate components 66 | in their READMEs 67 | 68 | - For `create-element.js` see the [vdom README](vdom/README.md) 69 | - For `diff.js` see the [vtree README](vtree/README.md) 70 | - For `h.js` see the [virtual-hyperscript README](virtual-hyperscript/README.md) 71 | - For `patch.js` see the [vdom README](vdom/README.md) 72 | 73 | For information about the type signatures of these modules feel 74 | free to read the [javascript signature definition](docs.jsig) 75 | 76 | ## DOM model 77 | 78 | `virtual-dom` exposes a set of objects designed for representing DOM nodes. A "Document Object Model Model" might seem like a strange term, but it is exactly that. It's a native JavaScript tree structure that represents a native DOM node tree. We call this a **VTree** 79 | 80 | We can create a VTree using the objects directly in a verbose manner, or we can use the more terse virtual-hyperscript. 81 | 82 | ### Example - creating a VTree using the objects directly 83 | 84 | ```javascript 85 | var VNode = require('virtual-dom/vtree/vnode'); 86 | var VText = require('virtual-dom/vtree/vtext') 87 | 88 | function render(data) { 89 | return new VNode('div', { 90 | className: "greeting" 91 | }, [ 92 | new VText("Hello " + String(data.name)); 93 | ]); 94 | } 95 | 96 | module.exports = render; 97 | ``` 98 | 99 | ### Example - creating a VTree using virtual-hyperscript 100 | 101 | ```javascript 102 | var h = require('virtual-dom/h'); 103 | 104 | function render(data) { 105 | return h('.greeting', ['Hello ' + data.name]); 106 | } 107 | 108 | module.exports = render; 109 | ``` 110 | 111 | The DOM model is designed to be efficient to create and read from. The reason why we don't just create a real DOM tree is that creating DOM nodes and reading the node properties is an expensive operation which is what we are trying to avoid. Reading some DOM node properties even causes side effects, so recreating the entire DOM structure with real DOM nodes simply isn't suitable for high performance rendering and it is not easy to reason about either. 112 | 113 | A `VTree` is designed to be equivalent to an immutable data structure. While it's not actually immutable, you can reuse the nodes in multiple places and the functions we have exposed that take VTrees as arguments never mutate the trees. We could freeze the objects in the model but don't for efficiency. (The benefits of an immutable-equivalent data structure will be documented in vtree or blog post at some point) 114 | 115 | 116 | 117 | ## Element creation 118 | 119 | ```haskell 120 | createElement(tree:VTree) -> DOMNode 121 | ``` 122 | 123 | Given that we have created a `VTree`, we need some way to translate this into a real DOM tree of some sort. This is provided by `create-element.js`. When rendering for the first time we would pass a complete `VTree` to create-element function to create the equivalent DOM node. 124 | 125 | ## Diff computation 126 | 127 | ```haskell 128 | diff(previous:VTree, current:VTree) -> PatchObject 129 | ``` 130 | 131 | The primary motivation behind virtual-dom is to allow us to write code indepentent of previous state. So when our application state changes we will generate a new `VTree`. The `diff` function creates a set of DOM patches that, based on the difference between the previous `VTree` and the current `VTree`, will update the previous DOM tree to match the new `VTree`. 132 | 133 | ## Patch operations 134 | 135 | ```haskell 136 | patch(rootNode:DOMNode, patches:PatchObject) -> DOMNode newRootNode 137 | ``` 138 | 139 | Once we have computed the set of patches required to apply to the DOM, we need a function that can apply those patches. This is provided by the `patch` function. Given a DOM root node and a set of DOM patches, the `patch` function will update the DOM. After applying the patches to the DOM, the DOM should look like the new `VTree`. 140 | 141 | 142 | ## Original motivation 143 | 144 | virtual-dom is heavily inspired by the inner workings of React by facebook. This project originated as a gist of ideas, which [we have linked to provide some background context](https://gist.github.com/Raynos/8414846). 145 | 146 | [1]: https://secure.travis-ci.org/Matt-Esch/virtual-dom.svg 147 | [2]: https://travis-ci.org/Matt-Esch/virtual-dom 148 | [3]: https://badge.fury.io/js/virtual-dom.svg 149 | [4]: https://badge.fury.io/js/virtual-dom 150 | [5]: http://img.shields.io/coveralls/Matt-Esch/virtual-dom.svg 151 | [6]: https://coveralls.io/r/Matt-Esch/virtual-dom 152 | [7]: https://david-dm.org/Matt-Esch/virtual-dom.svg 153 | [8]: https://david-dm.org/Matt-Esch/virtual-dom 154 | -------------------------------------------------------------------------------- /docs/modules/virtual-hyperscript.md: -------------------------------------------------------------------------------- 1 | Auto generated from [virtual-dom](https://github.com/Matt-Esch/virtual-dom) package (version 1.3.0). 2 | 3 | # virtual-hyperscript 4 | 5 | 12 | 13 | 14 | 15 | A DSL for creating virtual trees 16 | 17 | ## Example 18 | 19 | ```js 20 | var h = require('virtual-dom/h') 21 | 22 | var tree = h('div.foo#some-id', [ 23 | h('span', 'some text'), 24 | h('input', { type: 'text', value: 'foo' }) 25 | ]) 26 | ``` 27 | 28 | ## Docs 29 | 30 | See [hyperscript](https://github.com/dominictarr/hyperscript) which has the 31 | same interface. 32 | 33 | Except `virtual-hyperscript` returns a virtual DOM tree instead of a DOM 34 | element. 35 | 36 | ### `h(selector, properties, children)` 37 | 38 | `h()` takes a selector, an optional properties object and an 39 | optional array of children or a child that is a string. 40 | 41 | If you pass it a selector like `span.foo.bar#some-id` it will 42 | parse the selector and change the `id` and `className` 43 | properties of the `properties` object. 44 | 45 | If you pass it an array of `children` it will have child 46 | nodes, normally ou want to create children with `h()`. 47 | 48 | If you pass it a string it will create an array containing 49 | a single child node that is a text element. 50 | 51 | ### Special properties in `h()` 52 | 53 | #### `key` 54 | 55 | If you call `h` with `h('div', { key: someKey })` it will 56 | set a key on the return `VNode`. This `key` is not a normal 57 | DOM property but is a virtual-dom optimization hint. 58 | 59 | It basically tells virtual-dom to re-order DOM nodes instead of 60 | mutating them. 61 | 62 | #### `namespace` 63 | 64 | If you call `h` with `h('div', { namespace: "http://www.w3.org/2000/svg" })` 65 | it will set the namespace on the returned `VNode`. This 66 | `namespace` is not a normal DOM property, instead it will 67 | cause `vdom` to create a DOM element with a namespace. 68 | 69 | #### `ev-*` 70 | 71 | **Note:** You must create an instance of `dom-delegator` for `ev-*` to work. 72 | 73 | If you call `h` with `h('div', { ev-click: function (ev) { } })` it 74 | will store the event handler on the dom element. It will not 75 | set a property `'ev-foo'` on the DOM element. 76 | 77 | This means that `dom-delegator` will recognise the event handler 78 | on that element and correctly call your handler when an a click 79 | event happens. 80 | 81 | ## Installation 82 | 83 | `npm install virtual-dom` 84 | 85 | ## Contributors 86 | 87 | - Raynos 88 | - Matt Esch 89 | 90 | ## MIT Licenced 91 | 92 | [1]: https://secure.travis-ci.org/Raynos/virtual-hyperscript.png 93 | [2]: https://travis-ci.org/Raynos/virtual-hyperscript 94 | [3]: https://badge.fury.io/js/virtual-hyperscript.png 95 | [4]: https://badge.fury.io/js/virtual-hyperscript 96 | [5]: https://coveralls.io/repos/Raynos/virtual-hyperscript/badge.png 97 | [6]: https://coveralls.io/r/Raynos/virtual-hyperscript 98 | [7]: https://gemnasium.com/Raynos/virtual-hyperscript.png 99 | [8]: https://gemnasium.com/Raynos/virtual-hyperscript 100 | [9]: https://david-dm.org/Raynos/virtual-hyperscript.png 101 | [10]: https://david-dm.org/Raynos/virtual-hyperscript 102 | [11]: https://ci.testling.com/Raynos/virtual-hyperscript.png 103 | [12]: https://ci.testling.com/Raynos/virtual-hyperscript 104 | -------------------------------------------------------------------------------- /docs/modules/vtree.md: -------------------------------------------------------------------------------- 1 | Auto generated from [virtual-dom](https://github.com/Matt-Esch/virtual-dom) package (version 1.3.0). 2 | 3 | # vtree 4 | 5 | A realtime tree diffing algorithm 6 | 7 | ## Motivation 8 | 9 | `vtree` currently exists as part of `virtual-dom`. It is used for imitating 10 | diff operations between two `vnode` structures that imitate the structure of 11 | the active DOM node structure in the browser. 12 | 13 | ## Example 14 | 15 | ```js 16 | var h = require("virtual-dom/h") 17 | var diff = require("virtual-dom/diff") 18 | 19 | var leftNode = h("div") 20 | var rightNode = h("text") 21 | 22 | var patches = diff(leftNode, rightNode) 23 | /* 24 | -> { 25 | a: lefNode, 26 | 0: vpatch(rightNode) // a replace operation for the first node 27 | } 28 | */ 29 | ``` 30 | 31 | ## Installation 32 | 33 | `npm install virtual-dom` 34 | 35 | ## Contributors 36 | 37 | - Matt Esch 38 | 39 | ## MIT Licenced 40 | -------------------------------------------------------------------------------- /docs/thunks.md: -------------------------------------------------------------------------------- 1 | # Thunks 2 | 3 | vtree has a notion of a thunk build into the virtual tree data structure. A virtual tree can either be a virtual node, a virtual text node, a widget or a thunk. 4 | 5 | A thunk is an unevaluated virtual node.The purpose of it is to make the virtual tree data structure lazy, i.e. to create a new complex virtual tree data structure you do not have to build the entire thing top to bottom. 6 | 7 | Being lazy on it's own adds little value without also being able to add caching. 8 | 9 | The problem we are solving here is that creating 5000 virtual nodes from top to bottom every time something changes is expensive on GC churn. There are two solutions to this problem, you either make subsections of the virtual tree lazy and only evaluate them if you need to evaluate them or you do subtree re-rendering. 10 | 11 | As a brief aside, the subtree re-rendering technique (which is not implemented) says that recreating the entire virtual tree from the top to the bottom when anything changes is a bad idea. Instead if something changes we should try and only recreate the component that has changed (and it's children). This is not implemented because having a state <-> component mapping is hard to do whilst keeping purity, immutability & referential transparency. 12 | 13 | So thunks, allow us to only evaluate subsets of the virtual tree if they really need to be evaluated. Imagine that only one text node has really changed, and imagine this text node is 10 vnodes deep in the tree, i.e. has 10 parents. Ideally we only evaluate that node and it's 10 parents, the other 4989 nodes in the virtual tree do not have to be constantly recreated or constantly diffed at 60 frames per second. 14 | 15 | So how do thunks work. 16 | 17 | A thunk is an object that looks like 18 | 19 | ```js 20 | { 21 | type: "Thunk", 22 | render: function (previous) { 23 | return h('div', 'some text') 24 | }, 25 | vnode: null 26 | } 27 | ``` 28 | It's important to note that `render` is only allowed to return one vnode ever. So for every thunk instance we will only call `render()` once then cache the result on `thunk.vnode` and access `thunk.vnode` in the future. This means there is zero cache invalidation and this is because we assume a thunk is just an unevaluated vnode and any thunk instance only ever evaluates to a single vnode. 29 | 30 | When we see a thunk, if `thunk.vnode` exists we use that and don't call `.render()`. If `thunk.vnode` does not exist we call `.render(previousThunk)`. i.e. if we are doing a diff and we see a thunk object in the previous tree and a thunk object in the current tree we will call the current thunk with the previous thunk. 31 | 32 | The purpose of invoking a thunk instance in the current tree with the equivelant thunk instance in the previous tree is so that the implementor of the thunk can implement a better caching technique. 33 | 34 | For example 35 | 36 | ```js 37 | { 38 | type: "Thunk", 39 | isTextThunk: true, 40 | render: function (previous) { 41 | if (previous.text === this.text && previous.isTextThunk) { 42 | return prev.vnode 43 | } 44 | 45 | return h('div', this.text) 46 | }, 47 | text: 'some text', 48 | vnode: null 49 | } 50 | ``` 51 | 52 | Here we use the fact that all our thunk instances that `isTextThunk` will have a text property and the only thing that makes this thunk dynamic is the text. This means that if the previous thunk has the same text we know that the previous vnode is deep equivelant to the thunk we would have created so we can return it instead. 53 | 54 | This safes us a small amount of computation because we do not have to call `h()`. Also because we are return the previous vnode we know that the vtree/diff algorithm will go `prevVnode === currVnode` i.e. returning the same vnode instance means the diff algorithm can short circuit and does not have to recursively check stuff. 55 | 56 | Now imagine you wrapped a subtree containing 1000 virtual nodes behind a thunk, you would save having to recreate them if nothing changed and you would save having to diff them because diff can short circuit on strict equality of the root virtual node for that subtree. 57 | 58 | ## Partials 59 | 60 | `hg.partial` is a wrapper over a low-level [thunk implementation](https://github.com/Raynos/vdom-thunk). 61 | Call it with a render function and the arguments that will be passed to that render function: 62 | 63 | ```js 64 | App.render = function render(state) { 65 | return h('div.counters', [ 66 | hg.partial(renderCounter, state.counter1), 67 | hg.partial(renderCounter, state.counter2), 68 | ]); 69 | }; 70 | 71 | function renderCounter(counterValue) { 72 | return h('span', ''+counterValue); 73 | } 74 | ``` 75 | 76 | In this example, when the `counter1` state changes, the `counter2` partial will not be re-evaluated and its vdom tree will simply be reused from the last state. 77 | 78 | **Be careful**: partials are also re-evaluated when their rendering function changes. 79 | If the code above were changed to: 80 | 81 | ```js 82 | App.render = function render(state) { 83 | var renderCounterLocal = function(cv) { ... }; 84 | return h('div.counters', [ 85 | hg.partial(renderCounterLocal, state.aCounter), 86 | ... 87 | ``` 88 | 89 | then the counter's vdom would be re-evaluated every render, because the identity of `renderCounterLocal` changes every time the `render` function is called. 90 | This nullifies the benefits of using partials. 91 | -------------------------------------------------------------------------------- /docs/widgets.md: -------------------------------------------------------------------------------- 1 | # Widgets 2 | 3 | Widget is a data type in `virtual-dom` that allows you to get low level with the DOM 4 | and either optimize your code or handle finely detailed concerns. 5 | 6 | ## Example widget. 7 | 8 | ```js 9 | var createElement = require('virtual-dom/create-element.js'); 10 | var diff = require('virtual-dom/diff'); 11 | var patch = require('virtual-dom/patch'); 12 | var document = require('global/document'); 13 | 14 | module.exports = PureWrapperWidget; 15 | 16 | /* 17 | A PureWrapperWidget wraps a vnode in a container. 18 | 19 | It can do all kinds of DOM specific logic on the 20 | container if you wanted to. Like handling 21 | scroll and actual heights in the DOM. 22 | */ 23 | function PureWrapperWidget(vnode) { 24 | this.currVnode = vnode; 25 | } 26 | 27 | var proto = PureWrapperWidget.prototype; 28 | proto.type = 'Widget'; 29 | 30 | proto.init = function init() { 31 | var elem = createElement(this.currVnode); 32 | var container = document.createElement('div'); 33 | container.appendChild(elem); 34 | return container; 35 | }; 36 | 37 | proto.update = function update(prev, elem) { 38 | var prevVnode = prev.currVnode; 39 | var currVnode = this.currVnode; 40 | 41 | var patches = diff(prevVnode, currVnode); 42 | var rootNode = elem.childNodes[0]; 43 | var newNode = patch(rootNode, patches); 44 | if (newNode !== elem.childNodes[0]) { 45 | elem.replaceChild(newNode, elem.childNodes[0]); 46 | } 47 | }; 48 | ``` 49 | -------------------------------------------------------------------------------- /examples/2048/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var h = require('../../index.js').h; 4 | 5 | module.exports = render; 6 | 7 | function render(state) { 8 | return h('.2048-wrapper', [ 9 | h('link', { 10 | rel: 'stylesheet', 11 | href: 'https://rawgithub.com/raynos/mercury/' + 12 | 'master/examples/2048/style.css' 13 | }) 14 | ]); 15 | } 16 | -------------------------------------------------------------------------------- /examples/async-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../index.js'); 5 | var h = require('../index.js').h; 6 | var setTimeout = require('timers').setTimeout; 7 | 8 | function App() { 9 | var state = hg.state({ 10 | isUpdated: hg.value(false) 11 | }); 12 | // Arrange for state to be updated asynchronously 13 | setTimeout(function updateState() { 14 | state.isUpdated.set(true); 15 | }, 2000); 16 | return state; 17 | } 18 | 19 | App.render = function render(state) { 20 | return h('div', [ 21 | 'The state has been updated asynchronously: ' + state.isUpdated 22 | ]); 23 | }; 24 | 25 | hg.app(document.body, App(), App.render); 26 | -------------------------------------------------------------------------------- /examples/bmi-counter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../index.js'); 5 | var h = require('../index.js').h; 6 | 7 | var SafeHook = require('./lib/safe-hook.js'); 8 | 9 | function App() { 10 | return hg.state({ 11 | height: hg.value(180), 12 | weight: hg.value(80), 13 | bmi: hg.value(calcBmi(180, 80)), 14 | channels: { 15 | heightChange: updateData.bind(null, 'height'), 16 | weightChange: updateData.bind(null, 'weight'), 17 | bmiChange: updateData.bind(null, 'bmi') 18 | } 19 | }); 20 | } 21 | 22 | function updateData(type, state, data) { 23 | state[type].set(Number(data.slider)); 24 | 25 | if (type !== 'bmi') { 26 | state.bmi.set(calcBmi(state.height(), state.weight())); 27 | } else { 28 | state.weight.set(calcWeight(state.height(), state.bmi())); 29 | } 30 | } 31 | 32 | function calcWeight(height, bmi) { 33 | var meterz = height / 100; 34 | return meterz * meterz * bmi; 35 | } 36 | 37 | function calcBmi(height, weight) { 38 | var meterz = height / 100; 39 | return weight / (meterz * meterz); 40 | } 41 | 42 | App.render = function render(state) { 43 | var channels = state.channels; 44 | var color = state.bmi < 18.5 ? 'orange' : 45 | state.bmi < 25 ? 'inherit' : 46 | state.bmi < 30 ? 'orange' : 'red'; 47 | var diagnose = state.bmi < 18.5 ? 'underweight' : 48 | state.bmi < 25 ? 'normal' : 49 | state.bmi < 30 ? 'overweight' : 'obese'; 50 | 51 | return h('div', [ 52 | h('h3', 'BMI calculator'), 53 | h('div.weight', [ 54 | 'Weight: ' + ~~state.weight + 'kg', 55 | slider(state.weight, channels.weightChange, 30, 150) 56 | ]), 57 | h('div.height', [ 58 | 'Height: ' + ~~state.height + 'cm', 59 | slider(state.height, channels.heightChange, 100, 220) 60 | ]), 61 | h('div.bmi', [ 62 | 'BMI: ' + ~~state.bmi + ' ', 63 | h('span.diagnose', { 64 | style: { color: color } 65 | }, diagnose), 66 | slider(state.bmi, channels.bmiChange, 10, 50) 67 | ]) 68 | ]); 69 | }; 70 | 71 | function slider(value, channel, min, max) { 72 | return h('input.slider', { 73 | type: SafeHook('range'), // SafeHook for IE9 + type='range' 74 | min: min, max: max, value: String(value), 75 | style: { width: '100%' }, name: 'slider', 76 | 'ev-event': hg.sendChange(channel) 77 | }); 78 | } 79 | 80 | hg.app(document.body, App(), App.render); 81 | -------------------------------------------------------------------------------- /examples/canvas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../index.js'); 5 | var h = require('../index.js').h; 6 | 7 | function CanvasWidget(paint, data) { 8 | if (!(this instanceof CanvasWidget)) { 9 | return new CanvasWidget(paint, data); 10 | } 11 | 12 | this.data = data; 13 | this.paint = paint; 14 | } 15 | 16 | CanvasWidget.prototype.type = 'Widget'; 17 | 18 | CanvasWidget.prototype.init = function init() { 19 | var elem = document.createElement('canvas'); 20 | this.update(null, elem); 21 | return elem; 22 | }; 23 | 24 | CanvasWidget.prototype.update = function update(prev, elem) { 25 | var context = elem.getContext('2d'); 26 | 27 | this.paint(context, this.data); 28 | }; 29 | 30 | function App() { 31 | return hg.state({ 32 | color: hg.value('red'), 33 | channels: { 34 | changeColor: changeColor 35 | } 36 | }); 37 | } 38 | 39 | function changeColor(state, data) { 40 | state.color.set(data.color); 41 | } 42 | 43 | App.render = function renderColor(state) { 44 | var channels = state.channels; 45 | 46 | return h('div', [ 47 | h('div', [ 48 | h('span', state.color + ' '), 49 | h('input', { 50 | 'ev-event': hg.sendChange(channels.changeColor), 51 | value: state.color, 52 | name: 'color' 53 | }) 54 | ]), 55 | CanvasWidget(drawColor, state.color) 56 | ]); 57 | }; 58 | 59 | function drawColor(context, color) { 60 | context.fillStyle = color; 61 | context.fillRect(0, 0, 100, 100); 62 | } 63 | 64 | hg.app(document.body, App(), App.render); 65 | -------------------------------------------------------------------------------- /examples/count.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../index.js'); 5 | var h = require('../index.js').h; 6 | 7 | function App() { 8 | return hg.state({ 9 | value: hg.value(0), 10 | channels: { 11 | clicks: incrementCounter 12 | } 13 | }); 14 | } 15 | 16 | function incrementCounter(state) { 17 | state.value.set(state.value() + 1); 18 | } 19 | 20 | App.render = function render(state) { 21 | return h('div.counter', [ 22 | 'The state ', h('code', 'clickCount'), 23 | ' has value: ' + state.value + '.', h('input.button', { 24 | type: 'button', 25 | value: 'Click me!', 26 | 'ev-click': hg.send(state.channels.clicks) 27 | }) 28 | ]); 29 | }; 30 | 31 | hg.app(document.body, App(), App.render); 32 | -------------------------------------------------------------------------------- /examples/examples-styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 40px auto; 3 | width: 500px; 4 | font: 15px 'trebuchet MS', 'lucida sans'; 5 | } 6 | 7 | ol { 8 | counter-reset: li; /* Initiate a counter */ 9 | list-style: none; /* Remove default numbering */ 10 | *list-style: decimal; /* Keep using default numbering for IE6/7 */ 11 | padding: 0; 12 | margin-bottom: 4em; 13 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 14 | } 15 | 16 | ol ol { 17 | margin: 0 0 0 2em; /* Add some left margin for inner lists */ 18 | } 19 | 20 | 21 | .rounded-list a { 22 | position: relative; 23 | display: block; 24 | padding: .4em .4em .4em 2em; 25 | *padding: .4em; 26 | margin: .5em 0; 27 | background: #ddd; 28 | color: #444; 29 | text-decoration: none; 30 | border-radius: .3em; 31 | transition: all .3s ease-out; 32 | } 33 | 34 | .rounded-list a:hover{ 35 | background: #eee; 36 | } 37 | 38 | .rounded-list a:hover:before { 39 | transform: rotate(360deg); 40 | } 41 | 42 | .rounded-list a:before { 43 | content: counter(li); 44 | counter-increment: li; 45 | position: absolute; 46 | left: -1.3em; 47 | top: 50%; 48 | margin-top: -1.3em; 49 | background: #87ceeb; 50 | height: 2em; 51 | width: 2em; 52 | line-height: 2em; 53 | border: .3em solid #fff; 54 | text-align: center; 55 | font-weight: bold; 56 | border-radius: 2em; 57 | transition: all .3s ease-out; 58 | } 59 | -------------------------------------------------------------------------------- /examples/geometry/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../../index.js'); 5 | var svg = require('virtual-dom/virtual-hyperscript/svg'); 6 | 7 | var shapes = require('./shapes.js'); 8 | var dragEvent = require('./lib/drag-handler.js'); 9 | 10 | function App() { 11 | return hg.state({ 12 | p1: hg.value([100, 100]), 13 | p2: hg.value([200, 200]), 14 | p3: hg.value([100, 200]), 15 | c: hg.value([250, 250]), 16 | p: hg.value([250, 300]), 17 | width: hg.value(800), 18 | height: hg.value(600), 19 | channels: { 20 | movePoint: movePoint 21 | } 22 | }); 23 | } 24 | 25 | function movePoint(state, data) { 26 | var point = state[data.name](); 27 | 28 | state[data.name].set([ 29 | point[0] + data.x, 30 | point[1] + data.y 31 | ]); 32 | } 33 | 34 | App.render = function render(state) { 35 | return svg('svg', { 36 | 'width': state.width, 37 | 'height': state.height, 38 | 'style': { 'border': '1px solid black' } 39 | }, [ 40 | svg('text', { 41 | style: { 42 | '-webkit-user-select': 'none', 43 | '-moz-user-select': 'none' 44 | }, 45 | x: 20, 46 | y: 20, 47 | 'font-size': 20 48 | }, 'The points are draggable'), 49 | rootScene(state) 50 | ]); 51 | }; 52 | 53 | function rootScene(state) { 54 | return svg('g', [ 55 | shapes.triangle(state.p1, state.p2, state.p3), 56 | shapes.circle(state.p, state.c), 57 | shapes.segment(state.p, state.c), 58 | shapes.point({ 59 | cx: state.c[0], 60 | cy: state.c[1], 61 | 'ev-mousedown': dragEvent(state.channels.movePoint, { 62 | name: 'c' 63 | }) 64 | }), 65 | shapes.point({ 66 | cx: state.p[0], 67 | cy: state.p[1], 68 | 'ev-mousedown': dragEvent(state.channels.movePoint, { 69 | name: 'p' 70 | }) 71 | }), 72 | shapes.point({ 73 | cx: state.p1[0], 74 | cy: state.p1[1], 75 | 'ev-mousedown': dragEvent(state.channels.movePoint, { 76 | name: 'p1' 77 | }) 78 | }), 79 | shapes.point({ 80 | cx: state.p2[0], 81 | cy: state.p2[1], 82 | 'ev-mousedown': dragEvent(state.channels.movePoint, { 83 | name: 'p2' 84 | }) 85 | }), 86 | shapes.point({ 87 | cx: state.p3[0], 88 | cy: state.p3[1], 89 | 'ev-mousedown': dragEvent(state.channels.movePoint, { 90 | name: 'p3' 91 | }) 92 | }) 93 | ]); 94 | } 95 | 96 | hg.app(document.body, App(), App.render); 97 | -------------------------------------------------------------------------------- /examples/geometry/lib/drag-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../../index.js'); 4 | var extend = require('xtend'); 5 | 6 | module.exports = hg.BaseEvent(handleDrag); 7 | 8 | function handleDrag(ev, broadcast) { 9 | var data = this.data; 10 | var delegator = hg.Delegator(); 11 | 12 | var current = { 13 | x: ev.offsetX || ev.layerX, 14 | y: ev.offsetY || ev.layerY 15 | }; 16 | 17 | function onmove(ev2) { 18 | var previous = current; 19 | 20 | current = { 21 | x: ev2.offsetX || ev2.layerX, 22 | y: ev2.offsetY || ev2.layerY 23 | }; 24 | 25 | var delta = { 26 | x: current.x - previous.x, 27 | y: current.y - previous.y 28 | }; 29 | 30 | broadcast(extend(data, delta)); 31 | } 32 | 33 | function onup(ev2) { 34 | delegator.unlistenTo('mousemove'); 35 | delegator.removeGlobalEventListener('mousemove', onmove); 36 | delegator.removeGlobalEventListener('mouseup', onup); 37 | } 38 | 39 | delegator.listenTo('mousemove'); 40 | delegator.addGlobalEventListener('mousemove', onmove); 41 | delegator.addGlobalEventListener('mouseup', onup); 42 | } 43 | -------------------------------------------------------------------------------- /examples/geometry/shapes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var extend = require('xtend'); 4 | 5 | var svg = require('virtual-dom/virtual-hyperscript/svg'); 6 | 7 | var pointDefaults = { 8 | 'stroke': 'black', 9 | 'strokeWidth': '2', 10 | 'fill': 'blue', 11 | 'r': '5' 12 | }; 13 | 14 | var segmentDefaults = { 15 | 'stroke': 'black', 16 | 'stroke-width': '2' 17 | }; 18 | 19 | var circleDefaults = { 20 | 'fill': 'rgba(255, 0, 0, 0.1)', 21 | 'stroke': 'black', 22 | 'stroke-width': '2' 23 | }; 24 | 25 | module.exports = { 26 | point: point, 27 | segment: segment, 28 | triangle: triangle, 29 | circle: circle 30 | }; 31 | 32 | function point(opts) { 33 | return svg('circle', extend(pointDefaults, opts)); 34 | } 35 | 36 | function segment(start, end) { 37 | return svg('line', extend(segmentDefaults, { 38 | 'x1': start[0], 'y1': start[1], 39 | 'x2': end[0], 'y2': end[1] 40 | })); 41 | } 42 | 43 | function triangle(a, b, c) { 44 | return svg('g', [ 45 | segment(a, b), 46 | segment(b, c), 47 | segment(c, a) 48 | ]); 49 | } 50 | 51 | function circle(center, radius) { 52 | return svg('circle', extend(circleDefaults, { 53 | 'cx': center[0], 54 | 'cy': center[1], 55 | 'r': dist(center, radius) 56 | })); 57 | } 58 | 59 | function dist(p1, p2) { 60 | return Math.sqrt( 61 | Math.pow(p1[0] - p2[0], 2) + 62 | Math.pow(p1[1] - p2[1], 2) 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /examples/hot-reload/README.md: -------------------------------------------------------------------------------- 1 | # Hot module replacement with browserify/watchify, webpack or amok 2 | 3 | Using hot reloading while developing can improve your workflow. 4 | No more clicking through your application to a particular state each time you change the rendering code! 5 | 6 | ## Running the examples 7 | 8 | This example must be run standalone, instead of through the usual `npm run examples` in this repository's root. 9 | To install and run: 10 | 11 | npm install 12 | 13 | // for browserify 14 | npm run hot-browserify 15 | 16 | // for webpack 17 | npm run hot-webpack 18 | 19 | // for amok 20 | npm run hot-amok 21 | 22 | ### browserify and webpack 23 | Edit [package.json][] if you want to run on a different port. 24 | If you're running inside a container or VM, add `--host 0.0.0.0` (webpack) so you can access the dev server from your host machine. 25 | 26 | Try clicking to increment the counter, then edit [render.js][]! 27 | Your changes should instantly appear on the page without refreshing or upsetting the counter's value. 28 | Note that if you edit [browser.js][], your page will refresh and your state will be reset (only using webpack). 29 | 30 | ### amok 31 | Amok starts its own web server to provide hot reloading and also starts the browser. Live editing is not limited to the render function. It uses a V8 feature instead of eval and is limited to Chrome or Chromium browsers. Change the counter increment in `browser.js` for example. 32 | 33 | Edit [package.json][] to switch the web browser to use (Chrome or Chromium). 34 | 35 | [package.json]: ./package.json 36 | [render.js]: ./render.js 37 | [browser.js]: ./browser.js 38 | 39 | ## How it works 40 | 41 | ### webpack 42 | Running `webpack-dev-server` with the `--hot` argument enables [hot module replacement][], but to make use of it you need to add a little bit of code to your application. 43 | When webpack detects a file has changed, `module.hot.accept` gets a chance to 'claim' the reload and prevent the page from refreshing as it usually would. 44 | We use that opportunity to replace the function the app uses to render. 45 | We have to be careful to not reseat any references - so we pass a function that doesn't change to `hg.app` (i.e. `App.render`), but inside that function, we call a function which may change at runtime. 46 | 47 | ### browserify/watchify 48 | The main difference to webpack is that browserify does not come with a dev-server which is why we use [http-server][] to host our example. Other than that it works quite similar. Under the hood [browserify-hmr][] imitates webpack's hot module replacement. 49 | 50 | ### amok 51 | [Amok][] uses watchify to fire a `patch` event on the window object when a file is changed. Similarly to the webpack/browserify, we must change the state so Mercury re-renders automatically. Without this action, the changes in the code would only be seen after we clicked the button in the example. (The button click would change the state and trigger a re-render as well.) 52 | 53 | [hot module replacement]: https://github.com/webpack/docs/wiki/hot-module-replacement-with-webpack 54 | [http-server]: https://github.com/indexzero/http-server 55 | [browserify-hmr]: https://github.com/AgentME/browserify-hmr 56 | [Amok]: https://github.com/caspervonb/amok 57 | 58 | -------------------------------------------------------------------------------- /examples/hot-reload/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var document = require('global/document'); 5 | var window = require('global/window'); 6 | 7 | // Copied from examples/count.js 8 | function App() { 9 | return hg.state({ 10 | count: hg.value(0), 11 | _hotVersion: hg.value(0), // This is new - see below 12 | channels: { 13 | clicks: incrementCount 14 | } 15 | }); 16 | } 17 | 18 | function incrementCount(state) { 19 | state.count.set(state.count() + 1); 20 | } 21 | 22 | // This render function may be replaced by webpack and browserify! 23 | var render = require('./render.js'); 24 | App.render = function renderApp(state) { 25 | return render(state); 26 | }; 27 | 28 | // Need a reference to this below. 29 | var appState = App(); 30 | 31 | hg.app(document.body, appState, App.render); 32 | 33 | // Special sauce for webpack and browserify: 34 | // Detect changes to the rendering code and swap the rendering 35 | // function out without reloading the page. 36 | if (module.hot) { 37 | module.hot.accept('./render.js', function swapModule() { 38 | render = require('./render.js'); 39 | forceRerender(appState); 40 | return true; 41 | }); 42 | } 43 | 44 | // Otherwise, if using amok, an event is fired when a file changes. 45 | window.addEventListener('patch', function() { 46 | forceRerender(appState); 47 | }); 48 | 49 | // Force a re-render by changing the application state. 50 | function forceRerender(appState) { 51 | appState._hotVersion.set(appState._hotVersion() + 1); 52 | } 53 | -------------------------------------------------------------------------------- /examples/hot-reload/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hot reload example 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/hot-reload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "hot-browserify": "npm run http-server && npm run watchify-hmr", 4 | "hot-webpack": "webpack-dev-server --port 8080 --hot --progress --colors --inline ./browser.js", 5 | "http-server": "http-server -c 1 -a localhost &", 6 | "watchify-hmr": "watchify -p browserify-hmr browser.js -o bundle.js", 7 | "hot-amok": "npm run watchify & npm run amok-html", 8 | "watchify": "watchify browser.js -o bundle.js", 9 | "amok-html": "amok file://$PWD/index.html --browser chrome --hot" 10 | }, 11 | "devDependencies": { 12 | "amok": "^1.1.3", 13 | "browserify-hmr": "^0.2.2", 14 | "http-server": "^0.8.0", 15 | "watchify": "^3.4.0", 16 | "webpack-dev-server": "^1.10.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/hot-reload/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var h = hg.h; 5 | 6 | module.exports = function render(state) { 7 | return h('div.counter', [ 8 | 'The state ', h('code', 'count'), 9 | ' has value: ' + state.count + '.', h('input.button', { 10 | type: 'button', 11 | value: 'Click me!', 12 | 'ev-click': hg.send(state.channels.clicks) 13 | }) 14 | ]); 15 | }; 16 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mercury Examples 6 | 7 | 8 | 9 |

Mercury Examples

10 | {{examples}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/lib/focus-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var nextTick = require('next-tick'); 5 | 6 | module.exports = MutableFocusHook; 7 | 8 | function MutableFocusHook() { 9 | if (!(this instanceof MutableFocusHook)) { 10 | return new MutableFocusHook(); 11 | } 12 | } 13 | 14 | MutableFocusHook.prototype.hook = function hook(node, property) { 15 | nextTick(function onTick() { 16 | if (document.activeElement !== node) { 17 | node.focus(); 18 | } 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /examples/lib/reset-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var setTimeout = require('timers').setTimeout; 4 | 5 | function ResetHook(value, sink) { 6 | if (!(this instanceof ResetHook)) { 7 | return new ResetHook(value, sink); 8 | } 9 | 10 | this.value = value; 11 | this.sink = sink; 12 | } 13 | 14 | ResetHook.prototype.hook = function hook(elem, propName) { 15 | var self = this; 16 | elem[propName] = self.value; 17 | setTimeout(function lol() { 18 | self.sink(false); 19 | }, 0); 20 | }; 21 | 22 | module.exports = ResetHook; 23 | -------------------------------------------------------------------------------- /examples/lib/router/anchor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var h = require('../../../index.js').h; 4 | var sendClick = require('value-event/click.js'); 5 | 6 | var routeAtom = require('./index.js').atom; 7 | 8 | module.exports = anchor; 9 | 10 | function anchor(props, text) { 11 | var href = props.href; 12 | props.href = '#'; 13 | 14 | props['ev-click'] = sendClick(pushState, null, { 15 | ctrl: false, 16 | meta: false, 17 | rightClick: false, 18 | preventDefault: true 19 | }); 20 | 21 | return h('a', props, text); 22 | 23 | function pushState() { 24 | routeAtom.set(href); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/lib/router/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // circular dependency. 4 | module.exports = getRouter; 5 | 6 | var mercury = require('../../../index.js'); 7 | var source = require('geval/source'); 8 | var window = require('global/window'); 9 | var document = require('global/document'); 10 | 11 | var cachedRouter = Router(); 12 | 13 | getRouter.atom = cachedRouter; 14 | getRouter.anchor = require('./anchor.js'); 15 | getRouter.render = require('./view.js'); 16 | 17 | function getRouter() { 18 | return cachedRouter; 19 | } 20 | 21 | function Router() { 22 | var inPopState = false; 23 | var popstates = popstate(); 24 | var atom = mercury.value(String(document.location.pathname)); 25 | 26 | popstates(onPopState); 27 | atom(onRouteSet); 28 | 29 | return atom; 30 | 31 | function onPopState(uri) { 32 | inPopState = true; 33 | atom.set(uri); 34 | } 35 | 36 | function onRouteSet(uri) { 37 | if (inPopState) { 38 | inPopState = false; 39 | return; 40 | } 41 | 42 | pushHistoryState(uri); 43 | } 44 | } 45 | 46 | function pushHistoryState(uri) { 47 | window.history.pushState(undefined, document.title, uri); 48 | } 49 | 50 | function popstate() { 51 | return source(function broadcaster(broadcast) { 52 | window.addEventListener('popstate', onPopState); 53 | 54 | function onPopState() { 55 | broadcast(String(document.location.pathname)); 56 | } 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /examples/lib/router/view.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var routeMap = require('route-map'); 4 | 5 | module.exports = render; 6 | 7 | function render(atom, defn, opts) { 8 | if (opts && opts.base) { 9 | defn = Object.keys(defn) 10 | .reduce(function applyBase(acc, str) { 11 | acc[opts.base + str] = defn[str]; 12 | return acc; 13 | }, {}); 14 | } 15 | 16 | var match = routeMap(defn); 17 | 18 | var res = match(atom); 19 | if (!res) { 20 | throw new Error('router: no match found'); 21 | } 22 | 23 | res.params.url = res.url; 24 | return res.fn(res.params); 25 | } 26 | -------------------------------------------------------------------------------- /examples/lib/safe-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function SafeHook(value) { 4 | if (!(this instanceof SafeHook)) { 5 | return new SafeHook(value); 6 | } 7 | 8 | this.value = value; 9 | } 10 | 11 | SafeHook.prototype.hook = function hook(elem, propName) { 12 | // jscs:disable 13 | try { 14 | elem[propName] = this.value; 15 | } catch (error) { 16 | /* ignore */ 17 | } 18 | // jscs:enable 19 | }; 20 | 21 | module.exports = SafeHook; 22 | -------------------------------------------------------------------------------- /examples/lib/weakmap-event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Event = require('geval/event'); 4 | var extend = require('xtend'); 5 | var createStore = require('weakmap-shim/create-store'); 6 | 7 | module.exports = WeakmapEvent; 8 | 9 | function WeakmapEvent() { 10 | var store = createStore(); 11 | 12 | listen.asArray = listenAsArray; 13 | listen.asHash = listenAsHash; 14 | 15 | return { 16 | broadcast: broadcast, 17 | listen: listen 18 | }; 19 | 20 | function broadcast(obj, value) { 21 | getEvent(obj).broadcast(value, obj); 22 | } 23 | 24 | function listen(obj, fn) { 25 | getEvent(obj).listen(fn); 26 | } 27 | 28 | function getEvent(obj) { 29 | var privates = store(obj); 30 | privates.event = privates.event || Event(); 31 | return privates.event; 32 | } 33 | 34 | function listenAsArray(arr, fn) { 35 | throw new Error('Not Implemented.'); 36 | } 37 | 38 | function listenAsHash(hash, fn) { 39 | var current = extend(hash); 40 | 41 | Object.keys(hash()).forEach(function listenKey(k) { 42 | listen(hash[k], fn); 43 | }); 44 | 45 | hash(function onChange(newObj) { 46 | Object.keys(hash()).forEach(function listenKey(k) { 47 | if (current[k] !== hash[k]) { 48 | listen(hash[k], fn); 49 | } 50 | }); 51 | 52 | current = extend(hash); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/login-form/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index'); 4 | var h = require('../../index').h; 5 | var LoginComponent = require('./login-component.js'); 6 | var document = require('global/document'); 7 | 8 | var RCSS = require('rcss'); 9 | RCSS.injectAll(); 10 | 11 | function App() { 12 | var state = hg.state({ 13 | message: hg.value(''), 14 | loginDone: hg.value(false), 15 | loginComponent: LoginComponent() 16 | }); 17 | 18 | LoginComponent.onSuccess(state.loginComponent, onSuccess); 19 | 20 | return state; 21 | 22 | function onSuccess(opts) { 23 | state.loginDone.set(true); 24 | 25 | if (opts.type === 'login') { 26 | state.message.set('Congrats login' + 27 | 'user: ' + opts.user.email + ' password: ' + 28 | opts.user.password); 29 | } else if (opts.type === 'register') { 30 | state.message.set('Congrats register' + 31 | 'user: ' + opts.user.email + ' password: ' + 32 | opts.user.password); 33 | } 34 | } 35 | } 36 | 37 | App.render = function render(state) { 38 | return h('div', [ 39 | state.loginDone ? 40 | h('div', state.message) : 41 | LoginComponent.render(state.loginComponent) 42 | ]); 43 | }; 44 | 45 | hg.app(document.body, App(), App.render); 46 | -------------------------------------------------------------------------------- /examples/login-form/login-component-render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var h = require('../../index.js').h; 5 | var styles = require('./styles.js'); 6 | 7 | module.exports = render; 8 | 9 | function render(state) { 10 | return state.registerMode ? 11 | renderRegister(state) : 12 | renderLogin(state); 13 | } 14 | 15 | function renderLogin(state) { 16 | var channels = state.channels; 17 | 18 | return h('div', { 19 | 'ev-event': hg.sendSubmit(channels.login) 20 | }, [ 21 | h('fieldset', [ 22 | h('legend', 'Login Form'), 23 | labeledInput('Email: ', { 24 | name: 'email', 25 | error: state.emailError 26 | }), 27 | labeledInput('Password: ', { 28 | name: 'password', 29 | type: 'password' 30 | }), 31 | h('div', [ 32 | h('button', { 33 | 'ev-click': hg.send(channels.switchMode, 34 | !state.registerMode) 35 | }, 'Register new User'), 36 | h('button', 'Login') 37 | ]) 38 | ]) 39 | ]); 40 | } 41 | 42 | function renderRegister(state) { 43 | var channels = state.channels; 44 | 45 | return h('div', { 46 | 'ev-event': hg.sendSubmit(channels.register) 47 | }, [ 48 | h('fieldset', [ 49 | h('legend', 'Register Form'), 50 | labeledInput('Email: ', { 51 | name: 'email', 52 | error: state.emailError 53 | }), 54 | labeledInput('Password: ', { 55 | name: 'password', 56 | type: 'password', 57 | error: state.passwordError 58 | }), 59 | labeledInput('Repeat Password: ', { 60 | name: 'repeatPassword', 61 | type: 'password' 62 | }), 63 | h('div', [ 64 | h('button', { 65 | 'ev-click': hg.send(channels.switchMode, 66 | !state.registerMode) 67 | }, 'Login into existing User'), 68 | h('button', 'Register') 69 | ]) 70 | ]) 71 | ]); 72 | } 73 | 74 | function labeledInput(label, opts) { 75 | opts.className = opts.error ? 76 | styles.inputError.className : ''; 77 | 78 | return h('div', [ 79 | h('label', { 80 | className: opts.error ? styles.error.className : '' 81 | }, [ 82 | label, 83 | h('input', opts) 84 | ]), 85 | h('div', { 86 | className: styles.error.className 87 | }, [ 88 | opts.error 89 | ]) 90 | ]); 91 | } 92 | -------------------------------------------------------------------------------- /examples/login-form/login-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var validEmail = require('valid-email'); 4 | var hg = require('../../index.js'); 5 | var WeakmapEvent = require('../lib/weakmap-event.js'); 6 | 7 | var onSuccess = WeakmapEvent(); 8 | 9 | LoginComponent.render = require('./login-component-render.js'); 10 | LoginComponent.onSuccess = onSuccess.listen; 11 | 12 | module.exports = LoginComponent; 13 | 14 | function LoginComponent(options) { 15 | return hg.state({ 16 | emailError: hg.value(''), 17 | passwordError: hg.value(''), 18 | registerMode: hg.value(false), 19 | channels: { 20 | switchMode: switchMode, 21 | login: login, 22 | register: register 23 | } 24 | }); 25 | } 26 | 27 | function switchMode(state, registerMode) { 28 | state.registerMode.set(registerMode); 29 | } 30 | 31 | function login(state, user) { 32 | resetErrors(state); 33 | var email = user.email; 34 | 35 | if (!validEmail(email)) { 36 | return state.emailError.set('Invalid email'); 37 | } 38 | 39 | onSuccess.broadcast(state, { 40 | type: 'login', 41 | user: user 42 | }); 43 | } 44 | 45 | function register(state, user) { 46 | resetErrors(state); 47 | var email = user.email; 48 | 49 | if (!validEmail(email)) { 50 | return state.emailError.set('Invalid email'); 51 | } 52 | 53 | if (user.password !== user.repeatPassword) { 54 | return state.passwordError.set('Password not same'); 55 | } 56 | 57 | if (user.password.length <= 6) { 58 | return state.passwordError.set('Password too small'); 59 | } 60 | 61 | onSuccess.broadcast(state, { 62 | type: 'register', 63 | user: user 64 | }); 65 | } 66 | 67 | function resetErrors(state) { 68 | state.emailError.set(''); 69 | state.passwordError.set(''); 70 | } 71 | -------------------------------------------------------------------------------- /examples/login-form/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RCSS = require('rcss'); 4 | 5 | module.exports = { 6 | error: RCSS.registerClass({ 7 | color: 'red' 8 | }), 9 | inputError: RCSS.registerClass({ 10 | borderColor: 'red' 11 | }) 12 | }; 13 | -------------------------------------------------------------------------------- /examples/markdown/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../../index'); 4 | var h = mercury.h; 5 | var inlineMdEditor = require('./component/inlineMdEditor'); 6 | var sideBySideMdEditor = require('./component/sideBySideMdEditor'); 7 | 8 | app.render = appRender; 9 | 10 | module.exports = app; 11 | 12 | function app() { 13 | var state = mercury.struct({ 14 | inlineEditor: inlineMdEditor({ 15 | placeholder: 'Enter some markdown...' 16 | }), 17 | sideBySideEditor: sideBySideMdEditor({ 18 | placeholder: 'Enter some markdown...', 19 | value: [ 20 | '#Hello World', 21 | '', 22 | '* sample', 23 | '* bullet', 24 | '* points' 25 | ].join('\n') 26 | }) 27 | }); 28 | 29 | return state; 30 | } 31 | 32 | function appRender(state) { 33 | return h('.page', { 34 | style: { visibility: 'hidden' } 35 | }, [ 36 | h('link', { 37 | rel: 'stylesheet', 38 | href: '/mercury/examples/markdown/style.css' 39 | }), 40 | h('.content', [ 41 | h('h2', 'Side-by-side Markdown Editor'), 42 | h('p', 'Enter some markdown in the left pane and see it rendered ' + 43 | 'in the pane on the right.'), 44 | sideBySideMdEditor.render(state.sideBySideEditor), 45 | h('h2', 'Inline Markdown Editor'), 46 | h('p', 'Enter some markdown and click outside of the textarea to ' + 47 | 'see the parsed result. Click the output to edit again.'), 48 | inlineMdEditor.render(state.inlineEditor) 49 | ]) 50 | ]); 51 | } 52 | -------------------------------------------------------------------------------- /examples/markdown/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var mercury = require('../../index'); 5 | var app = require('./app'); 6 | var state = app(); 7 | 8 | mercury.app(document.body, state, app.render); 9 | -------------------------------------------------------------------------------- /examples/markdown/component/inlineMdEditor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../../../index'); 4 | var h = mercury.h; 5 | var textarea = require('./textarea'); 6 | var mdRender = require('./mdRender'); 7 | var update = { 8 | editorBlur: function editorBlur(state, text) { 9 | state.focusEditor.set(false); 10 | state.isEditing.set(!text); 11 | }, 12 | rendererClick: function rendererClick(state) { 13 | state.focusEditor.set(true); 14 | state.isEditing.set(true); 15 | } 16 | }; 17 | 18 | inlineMdEditor.render = inlineMdEditorRender; 19 | inlineMdEditor.update = update; 20 | 21 | module.exports = inlineMdEditor; 22 | 23 | function inlineMdEditor(options) { 24 | options = options || {}; 25 | 26 | var focusEditor = mercury.value(false); 27 | var editor = textarea({ 28 | value: options.value, 29 | placeholder: options.placeholder, 30 | title: options.title, 31 | shouldFocus: focusEditor 32 | }); 33 | var renderer = mdRender({ value: options.value }); 34 | var state = mercury.struct({ 35 | editor: editor, 36 | renderer: renderer, 37 | // if no initial value, show the editor 38 | isEditing: mercury.value(!options.value), 39 | focusEditor: focusEditor 40 | }); 41 | 42 | editor.value(renderer.value.set); 43 | 44 | editor.events.blur(update.editorBlur.bind(null, state)); 45 | renderer.events.click(update.rendererClick.bind(null, state)); 46 | 47 | return state; 48 | } 49 | 50 | function inlineMdEditorRender(state) { 51 | return h('.inlineMdEditor', [ 52 | state.isEditing ? 53 | textarea.render(state.editor) : 54 | mdRender.render(state.renderer) 55 | ]); 56 | } 57 | -------------------------------------------------------------------------------- /examples/markdown/component/mdRender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../../../index'); 4 | var h = mercury.h; 5 | var parseMarkdown = require('marked/lib/marked'); 6 | 7 | mdRender.render = mdRenderRender; 8 | mdRender.input = input; 9 | 10 | module.exports = mdRender; 11 | 12 | function mdRender(options) { 13 | var events = input(); 14 | var state = mercury.struct({ 15 | events: events, 16 | value: mercury.value(options.value || '') 17 | }); 18 | 19 | return state; 20 | } 21 | 22 | function input() { 23 | return mercury.input([ 'click' ]); 24 | } 25 | 26 | function mdRenderRender(state) { 27 | var events = state.events; 28 | 29 | return h('.markdown', { 30 | 'ev-click': events.click 31 | }, [ 32 | // using a nested node due to a bug with innerHTML in vtree 33 | h('', { innerHTML: parseMarkdown(state.value) }) 34 | ]); 35 | } 36 | -------------------------------------------------------------------------------- /examples/markdown/component/sideBySideMdEditor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../../../index'); 4 | var h = mercury.h; 5 | var textarea = require('./textarea'); 6 | var mdRender = require('./mdRender'); 7 | 8 | sideBySideMdEditor.render = sideBySideMdEditorRender; 9 | 10 | module.exports = sideBySideMdEditor; 11 | 12 | function sideBySideMdEditor(options) { 13 | options = options || {}; 14 | 15 | var editor = textarea({ 16 | value: options.value, 17 | placeholder: options.placeholder, 18 | title: options.title 19 | }); 20 | var renderer = mdRender({ value: options.value }); 21 | var state = mercury.struct({ 22 | editor: editor, 23 | renderer: renderer 24 | }); 25 | 26 | editor.value(renderer.value.set); 27 | 28 | return state; 29 | } 30 | 31 | function sideBySideMdEditorRender(state) { 32 | return h('.sideBySideMdEditor', [ 33 | textarea.render(state.editor), 34 | mdRender.render(state.renderer) 35 | ]); 36 | } 37 | -------------------------------------------------------------------------------- /examples/markdown/component/textarea.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../../../index'); 4 | var h = mercury.h; 5 | var doMutableFocus = require('../../lib/focus-hook.js'); 6 | var update = { 7 | // this needs to be input rather than change so that the pre expands as text 8 | // is entered into the textarea 9 | input: function input(state, e) { 10 | state.value.set(e.target.value); 11 | }, 12 | change: function change(state, e) { 13 | // trim the content on a change event 14 | state.value.set(e.target.value.trim()); 15 | } 16 | }; 17 | 18 | textarea.render = textareaRender; 19 | textarea.update = update; 20 | textarea.input = input; 21 | 22 | module.exports = textarea; 23 | 24 | function textarea(options) { 25 | options = options || {}; 26 | 27 | var events = input(); 28 | var state = mercury.struct({ 29 | events: events, 30 | value: mercury.value(options.value || ''), 31 | placeholder: mercury.value(options.placeholder || ''), 32 | title: mercury.value(options.title || ''), 33 | shouldFocus: options.shouldFocus 34 | }); 35 | 36 | events.input(update.input.bind(null, state)); 37 | events.change(update.change.bind(null, state)); 38 | 39 | return state; 40 | } 41 | 42 | function input() { 43 | return mercury.input([ 'blur', 'change', 'input' ]); 44 | } 45 | 46 | function textareaRender(state) { 47 | var events = state.events; 48 | 49 | return h('.textarea', [ 50 | h('textarea.expanding', { 51 | 'ev-blur': mercury.event(events.blur, state.value), 52 | 'ev-change': events.change, 53 | 'ev-input': events.input, 54 | 'ev-focus': state.shouldFocus ? doMutableFocus() : null, 55 | name: state.title, 56 | placeholder: state.placeholder, 57 | value: state.value 58 | }), 59 | h('pre', [ 60 | h('span', state.value), 61 | h('br') 62 | ]) 63 | ]); 64 | } 65 | -------------------------------------------------------------------------------- /examples/markdown/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Helvetica, Arial, sans-serif; 3 | } 4 | body { 5 | background: #f4f4f4; 6 | color: #333; 7 | font-size: 0.8125rem; 8 | line-height: 1rem; 9 | } 10 | html, 11 | body, 12 | .page { 13 | margin: 0; 14 | min-width: 320px; 15 | } 16 | *, 17 | *:before, 18 | *:after { 19 | -moz-box-sizing: border-box; 20 | -webkit-box-sizing: border-box; 21 | -webkit-box-sizing: border-box; 22 | -moz-box-sizing: border-box; 23 | box-sizing: border-box; 24 | } 25 | .page { 26 | visibility: visible !important; 27 | } 28 | .container { 29 | height: 100%; 30 | margin: 0 40px; 31 | overflow: auto; 32 | } 33 | h1, 34 | h2, 35 | h3, 36 | h4, 37 | h5, 38 | h6 { 39 | font-weight: normal; 40 | color: #000; 41 | margin: 0.5em 0; 42 | } 43 | h1 { 44 | font-size: 1.5rem; 45 | } 46 | h2 { 47 | font-size: 1.375rem; 48 | } 49 | h3 { 50 | font-size: 1.25rem; 51 | } 52 | h4 { 53 | font-size: 1.125rem; 54 | } 55 | h5, 56 | h6 { 57 | font-size: 0.875rem; 58 | } 59 | a { 60 | color: #007aff; 61 | text-decoration: none; 62 | } 63 | a:hover { 64 | text-decoration: underline; 65 | } 66 | p { 67 | margin-bottom: 0.75rem; 68 | } 69 | strong { 70 | font-weight: bold; 71 | } 72 | em { 73 | font-style: italic; 74 | } 75 | hr { 76 | border: 0; 77 | height: 0; 78 | border-top: 1px solid #ccc; 79 | margin: 0; 80 | } 81 | blockquote { 82 | color: #666; 83 | } 84 | blockquote:before { 85 | content: '“'; 86 | } 87 | blockquote:after { 88 | content: '”'; 89 | } 90 | .textarea { 91 | background: #fff; 92 | border: 1px solid #ccc; 93 | min-height: 3em; 94 | position: relative; 95 | } 96 | .textarea > textarea, 97 | .textarea > pre { 98 | background: transparent; 99 | border: none; 100 | font: inherit; 101 | padding: 5px; 102 | white-space: pre-wrap; 103 | word-wrap: break-word; 104 | } 105 | .textarea > textarea { 106 | height: 100%; 107 | left: 0; 108 | position: absolute; 109 | resize: none; 110 | top: 0; 111 | width: 100%; 112 | } 113 | .textarea > pre { 114 | visibility: hidden; 115 | } 116 | .inlineMdEditor { 117 | cursor: pointer; 118 | } 119 | .sideBySideMdEditor { 120 | height: 20rem; 121 | overflow: hidden; 122 | } 123 | .sideBySideMdEditor > div { 124 | float: left; 125 | height: 100%; 126 | width: 50%; 127 | } 128 | .markdown { 129 | overflow: auto; 130 | } 131 | -------------------------------------------------------------------------------- /examples/number-input/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var document = require('global/document'); 5 | var h = hg.h; 6 | 7 | var NumberInput = require('./number-component.js'); 8 | 9 | function App() { 10 | return hg.state({ 11 | red: NumberInput(), 12 | blue: NumberInput(), 13 | green: NumberInput() 14 | }); 15 | } 16 | 17 | App.render = function render(state) { 18 | function numberspanny(name, numberInput) { 19 | return h('span', [ 20 | name, 21 | NumberInput.render(numberInput) 22 | ]); 23 | } 24 | 25 | return h('div', [ 26 | numberspanny('red', state.red), 27 | numberspanny('blue', state.blue), 28 | numberspanny('green', state.green) 29 | ]); 30 | }; 31 | 32 | hg.app(document.body, App(), App.render); 33 | -------------------------------------------------------------------------------- /examples/number-input/number-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var h = hg.h; 5 | 6 | function NumberInput() { 7 | return hg.state({ 8 | value: hg.value(0), 9 | channels: { 10 | change: change, 11 | increase: increase, 12 | decrease: decrease 13 | } 14 | }); 15 | } 16 | 17 | function change(state, data) { 18 | state.value.set(parseInt(data.number, 10) || 0); 19 | } 20 | 21 | function increase(state) { 22 | state.value.set(state.value() + 1); 23 | } 24 | 25 | function decrease(state) { 26 | state.value.set(state.value() - 1); 27 | } 28 | 29 | NumberInput.render = function render(state) { 30 | return h('div', [ 31 | h('input', { 32 | type: 'text', 33 | name: 'number', 34 | value: String(state.value), 35 | 'ev-event': hg.sendChange(state.channels.change) 36 | }), 37 | h('input', { 38 | type: 'button', 39 | value: 'increase', 40 | 'ev-click': hg.send(state.channels.increase) 41 | }, 'increase'), 42 | h('input', { 43 | type: 'button', 44 | value: 'decrease', 45 | 'ev-click': hg.send(state.channels.decrease) 46 | }, 'decrease') 47 | ]); 48 | }; 49 | 50 | module.exports = NumberInput; 51 | -------------------------------------------------------------------------------- /examples/real-dom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../index.js'); 5 | var h = require('../index.js').h; 6 | var setTimeout = require('timers').setTimeout; 7 | 8 | function Hook() {} 9 | // Hook to be called when DOM node has been rendered 10 | Hook.prototype.hook = function hook(node) { 11 | // Note that you should only modify DOM node attributes in virtual-dom 12 | // hooks, anything else is unsafe 13 | node.style.color = 'red'; 14 | }; 15 | // Hook to be called when DOM node is removed 16 | Hook.prototype.unhook = function unhook(node) { 17 | node.style.color = null; 18 | }; 19 | 20 | function Widget() { 21 | this.type = 'Widget'; 22 | } 23 | 24 | Widget.prototype.init = function init() { 25 | var elem = document.createElement('div'); 26 | elem.innerHTML = 'Content set directly on real DOM node, by widget ' + 27 | 'before update.'; 28 | return elem; 29 | }; 30 | Widget.prototype.update = function update(prev, elem) { 31 | elem.innerHTML = 'Content set directly on real DOM node, by widget ' + 32 | 'after update.'; 33 | }; 34 | 35 | function App() { 36 | var state = hg.state({ 37 | updated: hg.value(false) 38 | }); 39 | setTimeout(function timer() { 40 | state.updated.set(true); 41 | }, 2000); 42 | return state; 43 | } 44 | 45 | App.render = function App(state) { 46 | return h('div', [ 47 | // Create virtual node with hook if !state.updated 48 | h('div', !state.updated ? {hook: new Hook()} : {}, [ 49 | 'Style attribute of real DOM node set by ', 50 | h('em', 'hook'), 51 | ' and unset by ', 52 | h('em', 'unhook') 53 | ]), 54 | // Create widget controlled node 55 | new Widget() 56 | ]); 57 | }; 58 | 59 | hg.app(document.body, App(), App.render); 60 | -------------------------------------------------------------------------------- /examples/server-rendering/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../../index.js'); 5 | var virtualize = require('vdom-virtualize'); 6 | var JSONGlobals = require('json-globals/get'); 7 | 8 | function App(initialState) { 9 | return hg.state({ 10 | description: hg.value(initialState.description || ''), 11 | items: hg.array(initialState.items || []), 12 | channels: { 13 | add: add 14 | } 15 | }); 16 | } 17 | 18 | function add(state, data) { 19 | state.items.push({ 20 | name: data.name 21 | }); 22 | } 23 | 24 | App.render = require('./render.js'); 25 | 26 | var app = App(JSONGlobals('state')); 27 | var targetElem = document.body.firstChild; 28 | var prevTree = virtualize(targetElem); 29 | 30 | hg.app(true, app, App.render, { 31 | initialTree: prevTree, 32 | target: targetElem 33 | }); 34 | 35 | app.set(app()); 36 | -------------------------------------------------------------------------------- /examples/server-rendering/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../../index.js'); 4 | var h = require('../../index.js').h; 5 | 6 | module.exports = render; 7 | 8 | function render(state) { 9 | return h('div', { 10 | 'ev-event': mercury.sendSubmit(state.channels.add) 11 | }, [ 12 | h('span', state.description), 13 | h('ul', state.items.map(function toItem(item) { 14 | return h('li', [ 15 | h('span', item.name) 16 | ]); 17 | })), 18 | h('input', { 19 | name: 'name', 20 | placeholder: 'name' 21 | }) 22 | ]); 23 | } 24 | -------------------------------------------------------------------------------- /examples/server-rendering/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var http = require('http'); 4 | var browserify = require('browserify'); 5 | var path = require('path'); 6 | var toHTML = require('vdom-to-html'); 7 | var JSONGlobals = require('json-globals'); 8 | var h = require('../../index.js').h; 9 | var logger = require('console'); 10 | 11 | var render = require('./render.js'); 12 | 13 | var server = http.createServer(function onReq(req, res) { 14 | if (req.url === '/bundle.js') { 15 | res.setHeader('Content-Type', 'application/javascript'); 16 | return browserify() 17 | .add(path.join(__dirname, 'browser.js')) 18 | .bundle() 19 | .pipe(res); 20 | } 21 | 22 | var state = { 23 | description: 'server description', 24 | channels: { add: {} }, 25 | items: [{ 26 | name: 'server item name' 27 | }] 28 | }; 29 | var content = render(state); 30 | var vtree = layout(content, state); 31 | 32 | res.setHeader('Content-Type', 'text/html'); 33 | res.end('' + toHTML(vtree)); 34 | }); 35 | 36 | server.listen(8000); 37 | logger.log('listening on port 8000'); 38 | 39 | function layout(content, state) { 40 | return h('html', [ 41 | h('head', [ 42 | h('title', 'Server side rendering') 43 | ]), 44 | h('body', [ 45 | content, 46 | h('script', JSONGlobals({ 47 | state: state 48 | })), 49 | h('script', { 50 | src: 'bundle.js' 51 | }) 52 | ]) 53 | ]); 54 | } 55 | -------------------------------------------------------------------------------- /examples/server-rendering/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var select = require('vtree-select'); 5 | var render = require('./render.js'); 6 | 7 | test('server-rendering vtree structure', function t(assert) { 8 | var state = { 9 | description: 'server description', 10 | events: { add: {} }, 11 | items: [{ 12 | name: 'server item name' 13 | }] 14 | }; 15 | var tree = render(state); 16 | 17 | // Assert description 18 | var spanText = select('div:root > span')(tree)[0].children[0].text; 19 | assert.equal(spanText, state.description); 20 | 21 | // Assert items 22 | var items = select('div ul li span')(tree); 23 | assert.equal(items.length, state.items.length); 24 | items.forEach(function isEqual(item, i) { 25 | assert.equal(item.children[0].text, state.items[i].name); 26 | }); 27 | 28 | // Assert name input 29 | assert.ok(select('input[name=name]')(tree)); 30 | assert.end(); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/shared-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var hg = require('../index.js'); 5 | var h = require('../index.js').h; 6 | 7 | function App() { 8 | return hg.state({ 9 | text: hg.value(''), 10 | channels: { 11 | change: setText 12 | } 13 | }); 14 | } 15 | 16 | function setText(state, data) { 17 | state.text.set(data.text); 18 | } 19 | 20 | App.render = function render(state) { 21 | return h('div', [ 22 | h('p.content', 'The value is now: ' + state.text), 23 | h('p', [ 24 | 'Change it here: ', 25 | inputBox(state.text, state.channels.change) 26 | ]) 27 | ]); 28 | }; 29 | 30 | function inputBox(value, sink) { 31 | return h('input.input', { 32 | value: value, 33 | name: 'text', 34 | type: 'text', 35 | 'ev-event': hg.sendChange(sink) 36 | }); 37 | } 38 | 39 | hg.app(document.body, App(), App.render); 40 | -------------------------------------------------------------------------------- /examples/todomvc/bg.png: -------------------------------------------------------------------------------- 1 | 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 2 | 0000 0064 0000 0064 0203 0000 000d 8c7d 3 | c700 0000 0c50 4c54 45ee eeee ebeb ebe6 4 | e6e6 dfdf dfad df36 7300 0007 fd49 4441 5 | 5478 5e75 96d1 6ae2 8a16 8697 41c5 48a7 6 | 2441 2509 ad98 6024 09d6 6768 bc9a 3d54 7 | a9b9 da53 aa18 efa6 549f 4103 6798 0e53 8 | 3161 ce30 1695 24cc 19da a279 863a b4a5 9 | f54a c54a f5aa 1595 c6a7 389e bbd9 7bf7 10 | 3cc1 cfe2 ffd6 b716 f4a6 97dd f162 f174 11 | f3af dbab f9ac 3b99 3f3b ce8b f36b 5a07 12 | 5af0 703e 91f7 226e ce85 a121 1147 f018 13 | 8679 791a deee 64ed bd8d bde6 bbb7 e1cb 14 | e34e 6323 b1d7 6993 27cd fa21 b098 ab50 15 | 930c 25cf 6079 0963 ca95 bc92 3774 ab8c 16 | c0ed 6878 3dbf 9eea abfe cdc3 e05e 7b9e 17 | dd97 578b d5f3 9305 9e58 0008 8167 512f 18 | 47bb c880 9b15 0417 c484 6810 fe3c da89 19 | 6f36 dbe4 c597 d6c5 59ae 75dc 6952 b944 20 | 62b3 1306 aba2 e54d c5cc 2b49 1967 2559 21 | 91f1 5a3e 636a 3a01 73e7 be3c bbfe 6e3e 22 | 7c5d a7fc bcee 8dcc 8775 f2e3 e310 00e7 23 | 3d3e 9a66 29a0 68c1 4772 1c47 d101 de1b 24 | 8ac1 86ed 4bef 244a 6fe3 179b 1bc5 543c 25 | bd97 ba48 7436 b2d9 2dd0 252c a3d4 3009 26 | 5170 a6a2 3079 4335 a0a0 5b11 0496 d3bb 27 | 417f 3c72 16e6 f56c 31fc 753d b999 8eeb 28 | 67df 571f 81f7 7362 080d ba08 4ea4 79d1 29 | 1313 2381 18e7 1522 31e8 1c6d 9f91 cd44 30 | c94e a477 36c3 c53f f636 f70e 1ab9 2f5f 31 | 2e40 d78c 4846 4b26 cbba eaca 4379 3faf 32 | c84c cdb5 abe6 6139 ba1a 3a2f b393 dbc5 33 | b4fb 38ba b61c a77b b574 e60f 4b10 6231 34 | 9ef7 c4a2 74c8 8d13 2c42 6334 1ef3 138c 35 | e887 37a9 cbb0 502c 554b 68b1 d416 8e9a 36 | ad66 b1f4 fe32 9c6d 404d d50b c99a aec9 37 | 1ac3 aa99 248b 48ba a411 4605 81bb 87cf 38 | eb7e 56cf f3d5 723a 980d b5de f0e7 60bc 39 | 5c39 7388 b2d1 08ed 1551 91c2 a202 4142 40 | 0c25 8351 d243 09d0 faf0 fe3c 7e56 eda4 41 | eb87 c56c a991 a3de 35c2 cdec 66aa 03b2 42 | 6c69 e58c 6cc2 feba a58c 5ccb af73 2349 43 | c495 81de ddf3 f2ec b373 395d 4e9f 6eeb 44 | b7b3 abd5 f8aa bb7a bafe 0644 2c8a f8b1 45 | 8837 40b8 a36e 9a10 314f 1047 7186 4561 46 | afd1 3ebe ec6c 35da 2707 cdd6 76fb 73eb 47 | fcb2 bd93 48d9 25c0 c02a 1bac c4aa d2ee 48 | 6ed9 a511 0a63 148c 4852 d2e0 d559 16df 49 | 1e57 40a3 2e9e 46bc 0282 9198 2812 b158 50 | 848c e080 8b1c 1c6f c437 77ec f069 16cd 51 | ed24 c271 3b95 48f0 8776 b15a 04ab 204b 52 | 8cc1 ea99 8291 ac55 76cb 4619 54ab 5043 53 | 30b8 598d cd89 7932 5cae 66f3 59ff 7939 54 | fa7a 35bf b9b8 79b9 032a e45e d38c 04d0 55 | 2827 88a2 4079 4234 1d0b e17c 14fe c2b2 56 | 7ffb e062 27bc 91c8 76fe b041 9255 9351 57 | f635 5696 a582 2659 86a2 ee33 3851 a8c1 58 | 6aa2 4dee 173f eab5 afa3 de60 dc5f 3e8e 59 | 5e4e e633 6730 07c6 c579 681f 4ff3 288e 60 | 4789 088d d031 018d a088 0fc2 e1f0 e6ab 61 | 6cc3 eb4b 6a68 162c 5fce 9d93 fbe1 6ada 62 | 739e 078f c3f1 74b9 9818 4b73 3105 d4e7 63 | 167c b497 a6c5 181e a018 4114 041a f150 64 | 8128 d887 f676 fb53 fca0 716e 1f35 4fc9 65 | 544b 3c4c d067 945d 85d7 9056 5543 8f40 66 | 7f35 1ed7 17ce 78e5 2c6f 26d6 6daf ff30 67 | 711e cd6e b70f 04ea c53d b418 f004 d980 68 | 800a 1e91 8bd1 4c88 c2dc d0da 3a7e d349 69 | 64f7 3ec4 9b87 8d7a f87d ae53 4d57 13a7 70 | f621 d40c b69c 0725 59ae e5b1 4ac1 444c 71 | 45d1 65ad 60e9 d07d bc1d af7a fdf1 c374 72 | f29f 9fbd c1ad d5bb 1f76 1ff4 f90d f8d1 73 | c0da 6aa4 27ea e650 5af0 63a8 cb4b 6184 74 | 0b0f c0b6 dd38 4b91 d962 834c 6d16 b785 75 | 930f c271 bbda de38 3a83 829c 9190 5d17 76 | c31a 8a81 5b38 8148 116b 5fc1 d50c 3c8c 77 | 06df befd e87f 5c0e 9753 f577 3940 d4e3 78 | 65a2 6828 f68f 3d82 d6c1 3bf2 e4a8 d4b9 79 | f8d4 f26f fa4f 4ba7 97d9 b787 a7e9 6a15 80 | 18c2 5010 4d2f efd7 ca6b de0c c5b4 2c05 81 | 9774 4287 bfb8 d399 dfd4 2b4e 6f3c fcde 82 | fb09 6428 28ae e761 79b7 1865 792f e109 83 | 9221 6068 8c85 62ba 75b0 954e 64e3 f154 84 | 23db f950 bfa8 9e97 5a3b c7a7 2d50 c0aa 85 | 5919 45ad 1032 ceb2 6a84 5d87 e6b1 82a6 86 | 40bd 3bec 7d5c 0c6f 968b e7c5 c9cb fc7e 87 | 31b9 b91b 77bb cb31 084c 30e6 0fc5 2811 88 | 1329 9c42 b928 8505 027e 3c84 41d5 b6df 89 | a02d bb99 fb9f 70aa 1dbb d9d9 3ace a5db 90 | d914 1849 422b d42a 5091 cd72 79df 6549 91 | 8466 d66a 8a89 c162 bcbc bf9f 8d06 77bf 92 | 86f3 fee2 da31 bb8f 8eb5 98dc 7f07 9e0b 93 | 8558 3e42 a344 c803 1c25 ba7d 6e0a 019a 94 | e5a1 feb6 59ea 7cc9 edbd 3b6c da8d ce46 95 | 63ab d87c c36f 53e9 0f90 d493 19b5 9c49 96 | ee03 a1ca 88cb 8aec 5a15 5d51 4d19 1e5f 97 | b485 39b4 ce2d d570 26cb caed c5b7 d10f 98 | eb76 f8b0 8075 4890 e562 ffd4 2924 2e4b 99 | cda3 76ce dea8 a78f 3e37 aba5 f81f e1c3 100 | d33f dbe9 3d60 f51a 8614 7449 2e64 4c8b 101 | b02c d9c8 9819 4453 30f8 351f 9eff f87a 102 | 7d77 3519 2c87 671f 57ea f57c 327a f976 103 | 3506 1cc5 636e 7f80 09ae 0547 9204 d094 104 | 9fe4 7db4 c0c0 dfcf 68f6 4baa 9dda 2e89 105 | 2750 ae58 3a96 7799 6c66 5789 68fb 358d 106 | 584b 554d aa2c 0c2f 9c65 6fe0 cc2e 7bdd 107 | a7c1 c5d3 487d 98ce afae ef47 3fe0 6ff2 108 | 1439 9189 b042 d443 41bc 2a34 4e8a b9ed 109 | 4ff1 5c63 eb34 8dda a58d f851 bc7d dc84 110 | 886e c8cc be6b b766 e42d 13ab 1915 59d3 111 | 7143 d50d 183f 0fff dd7f 9a2c 0663 bd7b 112 | 3578 b9b9 9b4e 6793 abc9 d401 9647 053f 113 | e971 b324 0fac 6f0d 1d4f 7188 5f24 fc60 114 | c76d 21dc 7925 0d32 65c9 90f0 025b 9159 115 | b9a0 170a 6a59 2e48 665e c261 f4f5 ff10 116 | 07a2 18c1 2851 080a 4228 c48b b440 e35e 117 | 4f54 f404 08e8 7c26 0fbf dbf6 59c2 3e6f 118 | 9cff 0e05 e03a a6b1 058b 4d32 08a1 48bb 119 | c8ae 5c93 cb65 3393 84b3 ab9b 1fa3 f35f 120 | 4fcb bbaf 7f3d 4510 1311 ce4b 732e 9fd7 121 | 4d90 219f 9ba1 fc94 8715 8204 54c9 e3ea 122 | 616e 2fbe d70a 27fe 3ccd 6653 d487 a394 123 | af54 4c80 49c8 8682 33a0 9793 b2a2 28bb 124 | f95d c4dc cfb8 a40c fcd6 4865 f5b3 37b9 125 | fa35 ef4d bb9f ea13 1070 36c4 f062 d0cf 126 | 0608 8e0e b10c 8404 37ed 0d21 701a 6fbe 127 | 6965 531b dfed f707 5bbf cb1b 0af9 b229 128 | edea 72de b240 3355 8391 1824 6230 960b 129 | 16b7 93e9 c374 e53c 8fef 9e67 ebb7 603a 130 | beeb f7e7 1f7b e7e0 059a e002 5e02 0da2 131 | 28ce 0545 32ca b33c cdf9 6828 7e2e b61a 132 | af49 0e88 4ae5 7525 fc17 6fb6 fde0 2eb9 133 | a790 0000 0000 4945 4e44 ae42 6082 134 | -------------------------------------------------------------------------------- /examples/todomvc/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var TimeTravel = require('../../time-travel.js'); 5 | var document = require('global/document'); 6 | var window = require('global/window'); 7 | var rafListen = require('./lib/raf-listen.js'); 8 | var localStorage = window.localStorage; 9 | 10 | var TodoApp = require('./todo-app.js'); 11 | 12 | function App() { 13 | // load from localStorage 14 | var storedState = localStorage.getItem('todos-mercury@11'); 15 | var initialState = storedState ? JSON.parse(storedState) : null; 16 | 17 | var todoApp = TodoApp(initialState); 18 | 19 | rafListen(todoApp, function onChange(value) { 20 | localStorage.setItem('todos-mercury@11', 21 | JSON.stringify(value)); 22 | }); 23 | 24 | return todoApp; 25 | } 26 | 27 | App.render = TodoApp.render; 28 | 29 | var app = window.app = App(); 30 | var history = TimeTravel(app); 31 | window.undo = history.undo; 32 | window.redo = history.redo; 33 | hg.app(document.body, app, App.render); 34 | 35 | module.exports = App; 36 | -------------------------------------------------------------------------------- /examples/todomvc/lib/raf-listen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var raf = require('raf'); 4 | 5 | module.exports = rafListen; 6 | 7 | function rafListen(observ, fn) { 8 | var sending = false; 9 | var currValue; 10 | 11 | return observ(onvalue); 12 | 13 | function onvalue(value) { 14 | currValue = value; 15 | if (sending) { 16 | return; 17 | } 18 | 19 | sending = true; 20 | raf(send); 21 | } 22 | 23 | function send() { 24 | var oldValue = currValue; 25 | fn(currValue); 26 | sending = false; 27 | 28 | if (oldValue !== currValue) { 29 | sending = true; 30 | raf(send); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/todomvc/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | color: inherit; 20 | -webkit-appearance: none; 21 | -ms-appearance: none; 22 | -o-appearance: none; 23 | appearance: none; 24 | } 25 | 26 | body { 27 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 28 | line-height: 1.4em; 29 | background: #eaeaea url('bg.png'); 30 | color: #4d4d4d; 31 | width: 550px; 32 | margin: 0 auto; 33 | -webkit-font-smoothing: antialiased; 34 | -moz-font-smoothing: antialiased; 35 | -ms-font-smoothing: antialiased; 36 | -o-font-smoothing: antialiased; 37 | font-smoothing: antialiased; 38 | } 39 | 40 | button, 41 | input[type="checkbox"] { 42 | outline: none; 43 | } 44 | 45 | #todoapp { 46 | background: #fff; 47 | background: rgba(255, 255, 255, 0.9); 48 | margin: 130px 0 40px 0; 49 | border: 1px solid #ccc; 50 | position: relative; 51 | border-top-left-radius: 2px; 52 | border-top-right-radius: 2px; 53 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 54 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 55 | } 56 | 57 | #todoapp:before { 58 | content: ''; 59 | border-left: 1px solid #f5d6d6; 60 | border-right: 1px solid #f5d6d6; 61 | width: 2px; 62 | position: absolute; 63 | top: 0; 64 | left: 40px; 65 | height: 100%; 66 | } 67 | 68 | #todoapp input::-webkit-input-placeholder { 69 | font-style: italic; 70 | } 71 | 72 | #todoapp input::-moz-placeholder { 73 | font-style: italic; 74 | color: #a9a9a9; 75 | } 76 | 77 | #todoapp h1 { 78 | position: absolute; 79 | top: -120px; 80 | width: 100%; 81 | font-size: 70px; 82 | font-weight: bold; 83 | text-align: center; 84 | color: #b3b3b3; 85 | color: rgba(255, 255, 255, 0.3); 86 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 87 | -webkit-text-rendering: optimizeLegibility; 88 | -moz-text-rendering: optimizeLegibility; 89 | -ms-text-rendering: optimizeLegibility; 90 | -o-text-rendering: optimizeLegibility; 91 | text-rendering: optimizeLegibility; 92 | } 93 | 94 | #header { 95 | padding-top: 15px; 96 | border-radius: inherit; 97 | } 98 | 99 | #header:before { 100 | content: ''; 101 | position: absolute; 102 | top: 0; 103 | right: 0; 104 | left: 0; 105 | height: 15px; 106 | z-index: 2; 107 | border-bottom: 1px solid #6c615c; 108 | background: #8d7d77; 109 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 110 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 111 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 112 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 113 | border-top-left-radius: 1px; 114 | border-top-right-radius: 1px; 115 | } 116 | 117 | #new-todo, 118 | .edit { 119 | position: relative; 120 | margin: 0; 121 | width: 100%; 122 | font-size: 24px; 123 | font-family: inherit; 124 | line-height: 1.4em; 125 | border: 0; 126 | outline: none; 127 | color: inherit; 128 | padding: 6px; 129 | border: 1px solid #999; 130 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 131 | -moz-box-sizing: border-box; 132 | -ms-box-sizing: border-box; 133 | -o-box-sizing: border-box; 134 | box-sizing: border-box; 135 | -webkit-font-smoothing: antialiased; 136 | -moz-font-smoothing: antialiased; 137 | -ms-font-smoothing: antialiased; 138 | -o-font-smoothing: antialiased; 139 | font-smoothing: antialiased; 140 | } 141 | 142 | #new-todo { 143 | padding: 16px 16px 16px 60px; 144 | border: none; 145 | background: rgba(0, 0, 0, 0.02); 146 | z-index: 2; 147 | box-shadow: none; 148 | } 149 | 150 | #main { 151 | position: relative; 152 | z-index: 2; 153 | border-top: 1px dotted #adadad; 154 | } 155 | 156 | label[for='toggle-all'] { 157 | display: none; 158 | } 159 | 160 | #toggle-all { 161 | position: absolute; 162 | top: -42px; 163 | left: -4px; 164 | width: 40px; 165 | text-align: center; 166 | /* Mobile Safari */ 167 | border: none; 168 | } 169 | 170 | #toggle-all:before { 171 | content: '»'; 172 | font-size: 28px; 173 | color: #d9d9d9; 174 | padding: 0 25px 7px; 175 | } 176 | 177 | #toggle-all:checked:before { 178 | color: #737373; 179 | } 180 | 181 | #todo-list { 182 | margin: 0; 183 | padding: 0; 184 | list-style: none; 185 | } 186 | 187 | #todo-list li { 188 | position: relative; 189 | font-size: 24px; 190 | border-bottom: 1px dotted #ccc; 191 | } 192 | 193 | #todo-list li:last-child { 194 | border-bottom: none; 195 | } 196 | 197 | #todo-list li.editing { 198 | border-bottom: none; 199 | padding: 0; 200 | } 201 | 202 | #todo-list li.editing .edit { 203 | display: block; 204 | width: 506px; 205 | padding: 13px 17px 12px 17px; 206 | margin: 0 0 0 43px; 207 | } 208 | 209 | #todo-list li.editing .view { 210 | display: none; 211 | } 212 | 213 | #todo-list li .toggle { 214 | text-align: center; 215 | width: 40px; 216 | /* auto, since non-WebKit browsers doesn't support input styling */ 217 | height: auto; 218 | position: absolute; 219 | top: 0; 220 | bottom: 0; 221 | margin: auto 0; 222 | /* Mobile Safari */ 223 | border: none; 224 | -webkit-appearance: none; 225 | -ms-appearance: none; 226 | -o-appearance: none; 227 | appearance: none; 228 | } 229 | 230 | #todo-list li .toggle:after { 231 | content: '✔'; 232 | /* 40 + a couple of pixels visual adjustment */ 233 | line-height: 43px; 234 | font-size: 20px; 235 | color: #d9d9d9; 236 | text-shadow: 0 -1px 0 #bfbfbf; 237 | } 238 | 239 | #todo-list li .toggle:checked:after { 240 | color: #85ada7; 241 | text-shadow: 0 1px 0 #669991; 242 | bottom: 1px; 243 | position: relative; 244 | } 245 | 246 | #todo-list li label { 247 | white-space: pre; 248 | word-break: break-word; 249 | padding: 15px 60px 15px 15px; 250 | margin-left: 45px; 251 | display: block; 252 | line-height: 1.2; 253 | -webkit-transition: color 0.4s; 254 | transition: color 0.4s; 255 | } 256 | 257 | #todo-list li.completed label { 258 | color: #a9a9a9; 259 | text-decoration: line-through; 260 | } 261 | 262 | #todo-list li .destroy { 263 | display: none; 264 | position: absolute; 265 | top: 0; 266 | right: 10px; 267 | bottom: 0; 268 | width: 40px; 269 | height: 40px; 270 | margin: auto 0; 271 | font-size: 22px; 272 | color: #a88a8a; 273 | -webkit-transition: all 0.2s; 274 | transition: all 0.2s; 275 | } 276 | 277 | #todo-list li .destroy:hover { 278 | text-shadow: 0 0 1px #000, 279 | 0 0 10px rgba(199, 107, 107, 0.8); 280 | -webkit-transform: scale(1.3); 281 | -ms-transform: scale(1.3); 282 | transform: scale(1.3); 283 | } 284 | 285 | #todo-list li .destroy:after { 286 | content: '✖'; 287 | } 288 | 289 | #todo-list li:hover .destroy { 290 | display: block; 291 | } 292 | 293 | #todo-list li .edit { 294 | display: none; 295 | } 296 | 297 | #todo-list li.editing:last-child { 298 | margin-bottom: -1px; 299 | } 300 | 301 | #footer { 302 | color: #777; 303 | padding: 0 15px; 304 | position: absolute; 305 | right: 0; 306 | bottom: -31px; 307 | left: 0; 308 | height: 20px; 309 | z-index: 1; 310 | text-align: center; 311 | } 312 | 313 | #footer:before { 314 | content: ''; 315 | position: absolute; 316 | right: 0; 317 | bottom: 31px; 318 | left: 0; 319 | height: 50px; 320 | z-index: -1; 321 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 322 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 323 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 324 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 325 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 326 | } 327 | 328 | #todo-count { 329 | float: left; 330 | text-align: left; 331 | } 332 | 333 | #filters { 334 | margin: 0; 335 | padding: 0; 336 | list-style: none; 337 | position: absolute; 338 | right: 0; 339 | left: 0; 340 | } 341 | 342 | #filters li { 343 | display: inline; 344 | } 345 | 346 | #filters li a { 347 | color: #83756f; 348 | margin: 2px; 349 | text-decoration: none; 350 | } 351 | 352 | #filters li a.selected { 353 | font-weight: bold; 354 | } 355 | 356 | #clear-completed { 357 | float: right; 358 | position: relative; 359 | line-height: 20px; 360 | text-decoration: none; 361 | background: rgba(0, 0, 0, 0.1); 362 | font-size: 11px; 363 | padding: 0 10px; 364 | border-radius: 3px; 365 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 366 | } 367 | 368 | #clear-completed:hover { 369 | background: rgba(0, 0, 0, 0.15); 370 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 371 | } 372 | 373 | #info { 374 | margin: 65px auto 0; 375 | color: #a6a6a6; 376 | font-size: 12px; 377 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 378 | text-align: center; 379 | } 380 | 381 | #info a { 382 | color: inherit; 383 | } 384 | 385 | /* 386 | Hack to remove background from Mobile Safari. 387 | Can't use it globally since it destroys checkboxes in Firefox and Opera 388 | */ 389 | 390 | @media screen and (-webkit-min-device-pixel-ratio:0) { 391 | #toggle-all, 392 | #todo-list li .toggle { 393 | background: none; 394 | } 395 | 396 | #todo-list li .toggle { 397 | height: 40px; 398 | } 399 | 400 | #toggle-all { 401 | top: -56px; 402 | left: -15px; 403 | width: 65px; 404 | height: 41px; 405 | -webkit-transform: rotate(90deg); 406 | -ms-transform: rotate(90deg); 407 | transform: rotate(90deg); 408 | -webkit-appearance: none; 409 | appearance: none; 410 | } 411 | } 412 | 413 | .hidden { 414 | display: none; 415 | } 416 | 417 | hr { 418 | margin: 20px 0; 419 | border: 0; 420 | border-top: 1px dashed #C5C5C5; 421 | border-bottom: 1px dashed #F7F7F7; 422 | } 423 | 424 | .learn a { 425 | font-weight: normal; 426 | text-decoration: none; 427 | color: #b83f45; 428 | } 429 | 430 | .learn a:hover { 431 | text-decoration: underline; 432 | color: #787e7e; 433 | } 434 | 435 | .learn h3, 436 | .learn h4, 437 | .learn h5 { 438 | margin: 10px 0; 439 | font-weight: 500; 440 | line-height: 1.2; 441 | color: #000; 442 | } 443 | 444 | .learn h3 { 445 | font-size: 24px; 446 | } 447 | 448 | .learn h4 { 449 | font-size: 18px; 450 | } 451 | 452 | .learn h5 { 453 | margin-bottom: 0; 454 | font-size: 14px; 455 | } 456 | 457 | .learn ul { 458 | padding: 0; 459 | margin: 0 0 30px 25px; 460 | } 461 | 462 | .learn li { 463 | line-height: 20px; 464 | } 465 | 466 | .learn p { 467 | font-size: 15px; 468 | font-weight: 300; 469 | line-height: 1.3; 470 | margin-top: 0; 471 | margin-bottom: 0; 472 | } 473 | 474 | .quote { 475 | border: none; 476 | margin: 20px 0 60px 0; 477 | } 478 | 479 | .quote p { 480 | font-style: italic; 481 | } 482 | 483 | .quote p:before { 484 | content: '“'; 485 | font-size: 50px; 486 | opacity: .15; 487 | position: absolute; 488 | top: -20px; 489 | left: 3px; 490 | } 491 | 492 | .quote p:after { 493 | content: '”'; 494 | font-size: 50px; 495 | opacity: .15; 496 | position: absolute; 497 | bottom: -42px; 498 | right: 3px; 499 | } 500 | 501 | .quote footer { 502 | position: absolute; 503 | bottom: -40px; 504 | right: 0; 505 | } 506 | 507 | .quote footer img { 508 | border-radius: 3px; 509 | } 510 | 511 | .quote footer a { 512 | margin-left: 5px; 513 | vertical-align: middle; 514 | } 515 | 516 | .speech-bubble { 517 | position: relative; 518 | padding: 10px; 519 | background: rgba(0, 0, 0, .04); 520 | border-radius: 5px; 521 | } 522 | 523 | .speech-bubble:after { 524 | content: ''; 525 | position: absolute; 526 | top: 100%; 527 | right: 30px; 528 | border: 13px solid transparent; 529 | border-top-color: rgba(0, 0, 0, .04); 530 | } 531 | 532 | .learn-bar > .learn { 533 | position: absolute; 534 | width: 272px; 535 | top: 8px; 536 | left: -300px; 537 | padding: 10px; 538 | border-radius: 5px; 539 | background-color: rgba(255, 255, 255, .6); 540 | -webkit-transition-property: left; 541 | transition-property: left; 542 | -webkit-transition-duration: 500ms; 543 | transition-duration: 500ms; 544 | } 545 | 546 | @media (min-width: 899px) { 547 | .learn-bar { 548 | width: auto; 549 | margin: 0 0 0 300px; 550 | } 551 | 552 | .learn-bar > .learn { 553 | left: 8px; 554 | } 555 | 556 | .learn-bar #todoapp { 557 | width: 550px; 558 | margin: 130px auto 40px auto; 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /examples/todomvc/todo-app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var h = require('../../index.js').h; 5 | var Router = require('../lib/router/'); 6 | var document = require('global/document'); 7 | 8 | var TodoItem = require('./todo-item.js'); 9 | 10 | var ROOT_URI = String(document.location.pathname); 11 | var COMPLETED_URI = ROOT_URI + '/completed'; 12 | var ACTIVE_URI = ROOT_URI + '/active'; 13 | 14 | module.exports = TodoApp; 15 | 16 | function TodoApp(opts) { 17 | opts = opts || {}; 18 | 19 | var state = hg.state({ 20 | todos: hg.varhash(opts.todos || {}, TodoItem), 21 | route: Router(), 22 | field: hg.struct({ 23 | text: hg.value(opts.field && opts.field.text || '') 24 | }), 25 | channels: { 26 | setTodoField: setTodoField, 27 | add: add, 28 | clearCompleted: clearCompleted, 29 | toggleAll: toggleAll, 30 | destroy: destroy 31 | } 32 | }); 33 | 34 | TodoItem.onDestroy.asHash(state.todos, function onDestroy(ev) { 35 | destroy(state, ev); 36 | }); 37 | 38 | return state; 39 | } 40 | 41 | function setTodoField(state, data) { 42 | state.field.text.set(data.newTodo); 43 | } 44 | 45 | function add(state, data) { 46 | if (data.newTodo.trim() === '') { 47 | return; 48 | } 49 | 50 | var todo = TodoItem({ 51 | title: data.newTodo.trim() 52 | }); 53 | state.todos.put(todo.id(), todo); 54 | state.field.text.set(''); 55 | } 56 | 57 | function clearCompleted(state) { 58 | Object.keys(state.todos).forEach(function clear(key) { 59 | if (TodoItem.isCompleted(state.todos[key])) { 60 | destroy(state, state.todos[key]()); 61 | } 62 | }); 63 | } 64 | 65 | function toggleAll(state, value) { 66 | Object.keys(state.todos).forEach(function toggle(key) { 67 | TodoItem.setCompleted(state.todos[key], value.toggle); 68 | }); 69 | } 70 | 71 | function destroy(state, opts) { 72 | state.todos.delete(opts.id); 73 | } 74 | 75 | TodoApp.render = function render(state) { 76 | return h('.todomvc-wrapper', { 77 | style: { visibility: 'hidden' } 78 | }, [ 79 | h('link', { 80 | rel: 'stylesheet', 81 | href: '/mercury/examples/todomvc/style.css' 82 | }), 83 | h('section#todoapp.todoapp', [ 84 | hg.partial(header, state.field, state.channels), 85 | hg.partial(mainSection, 86 | state.todos, state.route, state.channels), 87 | hg.partial(statsSection, 88 | state.todos, state.route, state.channels) 89 | ]), 90 | hg.partial(infoFooter) 91 | ]); 92 | }; 93 | 94 | function header(field, channels) { 95 | return h('header#header.header', { 96 | 'ev-event': [ 97 | hg.sendChange(channels.setTodoField), 98 | hg.sendSubmit(channels.add) 99 | ] 100 | }, [ 101 | h('h1', 'Todos'), 102 | h('input#new-todo.new-todo', { 103 | placeholder: 'What needs to be done?', 104 | autofocus: true, 105 | value: field.text, 106 | name: 'newTodo' 107 | }) 108 | ]); 109 | } 110 | 111 | function mainSection(todos, route, channels) { 112 | var todosList = objectToArray(todos); 113 | 114 | var allCompleted = todosList.every(function isComplete(todo) { 115 | return todo.completed; 116 | }); 117 | var visibleTodos = todosList.filter(function isVisible(todo) { 118 | return route === COMPLETED_URI && todo.completed || 119 | route === ACTIVE_URI && !todo.completed || 120 | route === ROOT_URI; 121 | }); 122 | 123 | return h('section#main.main', { hidden: !todosList.length }, [ 124 | h('input#toggle-all.toggle-all', { 125 | type: 'checkbox', 126 | name: 'toggle', 127 | checked: allCompleted, 128 | 'ev-change': hg.sendValue(channels.toggleAll) 129 | }), 130 | h('label', { htmlFor: 'toggle-all' }, 'Mark all as complete'), 131 | h('ul#todo-list.todolist', visibleTodos 132 | .map(function renderItem(todo) { 133 | return TodoItem.render(todo, channels); 134 | })) 135 | ]); 136 | } 137 | 138 | function statsSection(todos, route, channels) { 139 | var todosList = objectToArray(todos); 140 | var todosLeft = todosList.filter(function notComplete(todo) { 141 | return !todo.completed; 142 | }).length; 143 | var todosCompleted = todosList.length - todosLeft; 144 | 145 | return h('footer#footer.footer', { 146 | hidden: !todosList.length 147 | }, [ 148 | h('span#todo-count.todo-count', [ 149 | h('strong', String(todosLeft)), 150 | todosLeft === 1 ? ' item' : ' items', 151 | ' left' 152 | ]), 153 | h('ul#filters.filters', [ 154 | link(ROOT_URI, 'All', route === ROOT_URI), 155 | link(ACTIVE_URI, 'Active', route === ACTIVE_URI), 156 | link(COMPLETED_URI, 'Completed', 157 | route === COMPLETED_URI) 158 | ]), 159 | h('button.clear-completed#clear-completed', { 160 | hidden: todosCompleted === 0, 161 | 'ev-click': hg.send(channels.clearCompleted) 162 | }, 'Clear completed (' + String(todosCompleted) + ')') 163 | ]); 164 | } 165 | 166 | function link(uri, text, isSelected) { 167 | return h('li', [ 168 | Router.anchor({ 169 | className: isSelected ? 'selected' : '', 170 | href: uri 171 | }, text) 172 | ]); 173 | } 174 | 175 | function infoFooter() { 176 | return h('footer#info.info', [ 177 | h('p', 'Double-click to edit a todo'), 178 | h('p', [ 179 | 'Written by ', 180 | h('a', { href: 'https://github.com/Raynos' }, 'Raynos') 181 | ]), 182 | h('p', [ 183 | 'Part of ', 184 | h('a', { href: 'http://todomvc.com' }, 'TodoMVC') 185 | ]) 186 | ]); 187 | } 188 | 189 | function objectToArray(obj) { 190 | return Object.keys(obj).map(function toItem(k) { 191 | return obj[k]; 192 | }); 193 | } 194 | -------------------------------------------------------------------------------- /examples/todomvc/todo-item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var hg = require('../../index.js'); 4 | var h = require('../../index.js').h; 5 | var cuid = require('cuid'); 6 | var WeakmapEvent = require('../lib/weakmap-event.js'); 7 | 8 | var FocusHook = require('../lib/focus-hook.js'); 9 | 10 | var DestroyEvent = WeakmapEvent(); 11 | var ESCAPE = 27; 12 | 13 | TodoItem.onDestroy = DestroyEvent.listen; 14 | TodoItem.setCompleted = function setCompleted(s, x) { 15 | s.completed.set(x); 16 | }; 17 | TodoItem.isCompleted = function isCompleted(s) { 18 | return s.completed(); 19 | }; 20 | 21 | module.exports = TodoItem; 22 | 23 | function TodoItem(item) { 24 | item = item || {}; 25 | 26 | return hg.state({ 27 | id: hg.value(item.id || cuid()), 28 | title: hg.value(item.title || ''), 29 | editing: hg.value(item.editing || false), 30 | completed: hg.value(item.completed || false), 31 | channels: { 32 | toggle: toggle, 33 | startEdit: startEdit, 34 | cancelEdit: cancelEdit, 35 | finishEdit: finishEdit 36 | } 37 | }); 38 | } 39 | 40 | function toggle(state, data) { 41 | state.completed.set(data.completed); 42 | } 43 | 44 | function startEdit(state) { 45 | state.editing.set(true); 46 | } 47 | 48 | function finishEdit(state, data) { 49 | if (state.editing() === false) { 50 | return; 51 | } 52 | 53 | state.editing.set(false); 54 | state.title.set(data.title); 55 | 56 | if (data.title.trim() === '') { 57 | DestroyEvent.broadcast(state, { 58 | id: state.id() 59 | }); 60 | } 61 | } 62 | 63 | function cancelEdit(state) { 64 | state.editing.set(false); 65 | } 66 | 67 | var sneakyGlobal = {}; 68 | 69 | TodoItem.render = function render(todo, parentHandles) { 70 | var className = (todo.completed ? 'completed ' : '') + 71 | (todo.editing ? 'editing' : ''); 72 | 73 | sneakyGlobal[todo.id] = todo; 74 | 75 | return h('li', { className: className, key: todo.id }, [ 76 | h('.view', [ 77 | h('input.toggle', { 78 | type: 'checkbox', 79 | checked: todo.completed, 80 | 'ev-change': hg.send(todo.channels.toggle, { 81 | completed: !todo.completed 82 | }) 83 | }), 84 | h('label', { 85 | 'ev-dblclick': hg.send(todo.channels.startEdit) 86 | }, todo.title), 87 | h('button.destroy', { 88 | 'ev-click': hg.send(parentHandles.destroy, { 89 | id: todo.id 90 | }) 91 | }) 92 | ]), 93 | h('input.edit', { 94 | value: todo.title, 95 | name: 'title', 96 | // when we need an RPC invocation we add a 97 | // custom mutable operation into the tree to be 98 | // invoked at patch time 99 | 'ev-focus': todo.editing ? FocusHook() : null, 100 | 'ev-keydown': hg.sendKey( 101 | todo.channels.cancelEdit, null, {key: ESCAPE}), 102 | 'ev-event': hg.sendSubmit(todo.channels.finishEdit), 103 | 'ev-blur': hg.sendValue(todo.channels.finishEdit) 104 | }) 105 | ]); 106 | 107 | }; 108 | -------------------------------------------------------------------------------- /examples/unidirectional/README.md: -------------------------------------------------------------------------------- 1 | # Building your own unidirectional framework 2 | 3 | `mercury` contains a set of modules that you can use to build 4 | you own framework. 5 | 6 | This folder contains a set of examples that demonstrate using 7 | a subset of mercury with other existing modules to build your 8 | own opinion about doing unidirectional apps. 9 | 10 | **Disclaimer:** These examples are for demonstration purpose 11 | only, I am neutral as to whether these examples are a "good" or 12 | "bad" idea, use at your own risk. 13 | -------------------------------------------------------------------------------- /examples/unidirectional/backbone/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var window = require('global/window'); 5 | var mercury = require('../../../index.js'); 6 | var backbone = require('backbone'); 7 | var h = mercury.h; 8 | 9 | var toObserv = require('./observ-backbone.js'); 10 | 11 | var Item = backbone.Model.extend({ 12 | defaults: { 13 | name: '', 14 | value: '', 15 | color: '' 16 | } 17 | }); 18 | 19 | var Items = backbone.Collection.extend({ 20 | model: Item 21 | }); 22 | 23 | var AppState = backbone.Model.extend({ 24 | defaults: { 25 | description: '', 26 | events: {}, 27 | items: [] 28 | } 29 | }); 30 | 31 | var events = mercury.input(['add']); 32 | 33 | var appState = new AppState({ 34 | description: 'app state description', 35 | events: events, 36 | items: new Items([ 37 | { name: 'one', value: 'first', color: 'red' }, 38 | { name: 'two', value: 'second', color: 'blue' } 39 | ]) 40 | }); 41 | 42 | events.add(function add(data) { 43 | appState.get('items').add(data); 44 | }); 45 | 46 | window.state = appState; 47 | 48 | mercury.app(document.body, toObserv(appState), render); 49 | 50 | function render(state) { 51 | return h('div', [ 52 | h('h2', state.description), 53 | h('ul', state.items.map(function toItem(item) { 54 | return h('li', { 55 | key: item.cid, 56 | style: { color: item.color } 57 | }, [ 58 | h('span', item.name), 59 | h('input', { value: item.value }) 60 | ]); 61 | })), 62 | h('div', { 63 | 'ev-event': mercury.submitEvent(state.events.add) 64 | }, [ 65 | h('input', { 66 | name: 'name', 67 | placeholder: 'name' 68 | }), 69 | h('input', { 70 | name: 'value', 71 | placeholder: 'value' 72 | }), 73 | h('input', { 74 | name: 'color', 75 | placeholder: 'color' 76 | }), 77 | h('button', 'add') 78 | ]) 79 | ]); 80 | } 81 | -------------------------------------------------------------------------------- /examples/unidirectional/backbone/observ-backbone.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* This is a reference implementation of how to recursively 4 | observ a Backbone.Model. 5 | 6 | A proper implementation is going to need a bunch of 7 | performance optimizations 8 | 9 | */ 10 | module.exports = toObserv; 11 | 12 | // given some model returns an observable of the model state 13 | function toObserv(model) { 14 | // return an obect 15 | return function observ(listener) { 16 | // observ() with no args must return state 17 | if (!listener) { 18 | return serialize(model); 19 | } 20 | 21 | // observ(listener) should notify the listener on 22 | // every change 23 | listen(model, function serializeModel() { 24 | listener(serialize(model)); 25 | }); 26 | }; 27 | } 28 | 29 | // convert a Backbone model to JSON 30 | function serialize(model) { 31 | var data = model.toJSON(); 32 | Object.keys(data).forEach(function serializeRecur(key) { 33 | var value = data[key]; 34 | // if any value can be serialized toJSON() then do it 35 | if (value && value.toJSON) { 36 | data[key] = data[key].toJSON(); 37 | } 38 | }); 39 | return data; 40 | } 41 | 42 | // listen to a Backbone model 43 | function listen(model, listener) { 44 | model.on('change', listener); 45 | 46 | model.values().forEach(function listenRecur(value) { 47 | var isCollection = value && value._byId; 48 | 49 | if (!isCollection) { 50 | return; 51 | } 52 | 53 | // for each collection listen to it 54 | // console.log('listenCollection') 55 | listenCollection(value, listener); 56 | }); 57 | } 58 | 59 | // listen to a Backbone collection 60 | function listenCollection(collection, listener) { 61 | collection.forEach(function listenModel(model) { 62 | listen(model, listener); 63 | }); 64 | 65 | collection.on('add', function onAdd(model) { 66 | listen(model, listener); 67 | listener(); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /examples/unidirectional/immutable/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var mercury = require('../../../index.js'); 5 | var h = mercury.h; 6 | 7 | var Immutable = require('immutable'); 8 | var Cursor = require('immutable/contrib/cursor'); 9 | 10 | var events = mercury.input(['add']); 11 | 12 | var data = Immutable.fromJS({ 13 | description: 'app state description', 14 | items: [ 15 | { name: 'one', value: 'first', color: 'red' }, 16 | { name: 'two', value: 'second', color: 'blue' } 17 | ], 18 | events: events 19 | }); 20 | 21 | var appState = Cursor.from(data); 22 | 23 | events.add(function add(newItem) { 24 | appState.update('items', function pushItem(items) { 25 | return items.push(Immutable.fromJS(newItem)); 26 | }); 27 | }); 28 | 29 | function render(state) { 30 | return h('div', [ 31 | h('h2', state.get('description')), 32 | h('ul', state.get('items').map(function toitem(item) { 33 | return h('li', { 34 | key: item.cid, 35 | style: { color: item.get('color') } 36 | }, [ 37 | h('span', item.get('name')), 38 | h('input', { value: item.get('value') }) 39 | ]); 40 | }).toArray()), 41 | h('div', { 42 | 'ev-event': mercury.submitEvent(state.getIn(['events', 'add'])) 43 | }, [ 44 | h('input', { 45 | name: 'name', 46 | placeholder: 'name' 47 | }), 48 | h('input', { 49 | name: 'value', 50 | placeholder: 'value' 51 | }), 52 | h('input', { 53 | name: 'color', 54 | placeholder: 'color' 55 | }), 56 | h('button', 'add') 57 | ]) 58 | ]); 59 | } 60 | 61 | function toObserv(cursor) { 62 | return function observ(listener) { 63 | if (!listener) { 64 | return cursor.deref(); 65 | } 66 | 67 | cursor._onChange = function onChange(newData) { 68 | cursor._rootData = newData; 69 | listener(newData); 70 | }; 71 | }; 72 | } 73 | 74 | mercury.app(document.body, toObserv(appState), render); 75 | -------------------------------------------------------------------------------- /examples/unidirectional/jsx/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var mercury = require('../../../index.js'); 5 | var cuid = require('cuid'); 6 | 7 | var Render = require('./render.jsx'); 8 | 9 | var events = mercury.input(['add', 'changeText', 'toggle']); 10 | 11 | var state = mercury.struct({ 12 | description: mercury.value(''), 13 | list: mercury.array([]), 14 | events: events 15 | }); 16 | 17 | events.add(function onAdd(description) { 18 | state.list.push(mercury.struct({ 19 | id: cuid(), 20 | description: mercury.value(description), 21 | done: mercury.value(false) 22 | })); 23 | }); 24 | 25 | events.changeText(function onChangeText(data) { 26 | state.description.set(data.description); 27 | }); 28 | 29 | events.toggle(function onToggle(data) { 30 | state.list.some(function toggleItem(item) { 31 | if (item.id === data.id) { 32 | item.done.set(!item.done()); 33 | return true; 34 | } 35 | }); 36 | }); 37 | 38 | mercury.app(document.body, state, Render); 39 | -------------------------------------------------------------------------------- /examples/unidirectional/jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unidirectional-jsx-demo", 3 | "version": "1.0.0", 4 | "browserify": { 5 | "transform": [ "mercury-jsxify" ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/unidirectional/jsx/render.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | 'use strict'; 3 | 4 | var mercury = require("../../../index.js"); 5 | var h = mercury.h; 6 | 7 | module.exports = render; 8 | 9 | function render(state) { 10 | return
11 | 16 | 19 | 20 | {state.list.map(renderTask)} 21 |
22 |
23 | 24 | function renderTask(item) { 25 | return 26 | 27 | 34 | 35 | 36 | {item.description} 37 | 38 | 39 | } 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SingleEvent = require('geval/single'); 4 | var MultipleEvent = require('geval/multiple'); 5 | var extend = require('xtend'); 6 | 7 | /* 8 | Pro tip: Don't require `mercury` itself. 9 | require and depend on all these modules directly! 10 | */ 11 | var mercury = module.exports = { 12 | // Entry 13 | main: require('main-loop'), 14 | app: app, 15 | 16 | // Base 17 | BaseEvent: require('value-event/base-event'), 18 | 19 | // Input 20 | Delegator: require('dom-delegator'), 21 | // deprecated: use hg.channels instead. 22 | input: input, 23 | // deprecated: use hg.channels instead. 24 | handles: channels, 25 | channels: channels, 26 | // deprecated: use hg.send instead. 27 | event: require('value-event/event'), 28 | send: require('value-event/event'), 29 | // deprecated: use hg.sendValue instead. 30 | valueEvent: require('value-event/value'), 31 | sendValue: require('value-event/value'), 32 | // deprecated: use hg.sendSubmit instead. 33 | submitEvent: require('value-event/submit'), 34 | sendSubmit: require('value-event/submit'), 35 | // deprecated: use hg.sendChange instead. 36 | changeEvent: require('value-event/change'), 37 | sendChange: require('value-event/change'), 38 | // deprecated: use hg.sendKey instead. 39 | keyEvent: require('value-event/key'), 40 | sendKey: require('value-event/key'), 41 | // deprecated use hg.sendClick instead. 42 | clickEvent: require('value-event/click'), 43 | sendClick: require('value-event/click'), 44 | 45 | // State 46 | // remove from core: favor hg.varhash instead. 47 | array: require('observ-array'), 48 | struct: require('observ-struct'), 49 | // deprecated: use hg.struct instead. 50 | hash: require('observ-struct'), 51 | varhash: require('observ-varhash'), 52 | value: require('observ'), 53 | state: state, 54 | 55 | // Render 56 | diff: require('virtual-dom/vtree/diff'), 57 | patch: require('virtual-dom/vdom/patch'), 58 | partial: require('vdom-thunk'), 59 | create: require('virtual-dom/vdom/create-element'), 60 | h: require('virtual-dom/virtual-hyperscript'), 61 | 62 | // Utilities 63 | // remove from core: require computed directly instead. 64 | computed: require('observ/computed'), 65 | // remove from core: require watch directly instead. 66 | watch: require('observ/watch') 67 | }; 68 | 69 | function input(names) { 70 | if (!names) { 71 | return SingleEvent(); 72 | } 73 | 74 | return MultipleEvent(names); 75 | } 76 | 77 | function state(obj) { 78 | var copy = extend(obj); 79 | var $channels = copy.channels; 80 | var $handles = copy.handles; 81 | 82 | if ($channels) { 83 | copy.channels = mercury.value(null); 84 | } else if ($handles) { 85 | copy.handles = mercury.value(null); 86 | } 87 | 88 | var observ = mercury.struct(copy); 89 | if ($channels) { 90 | observ.channels.set(mercury.channels($channels, observ)); 91 | } else if ($handles) { 92 | observ.handles.set(mercury.channels($handles, observ)); 93 | } 94 | return observ; 95 | } 96 | 97 | function channels(funcs, context) { 98 | return Object.keys(funcs).reduce(createHandle, {}); 99 | 100 | function createHandle(acc, name) { 101 | var handle = mercury.Delegator.allocateHandle( 102 | funcs[name].bind(null, context)); 103 | 104 | acc[name] = handle; 105 | return acc; 106 | } 107 | } 108 | 109 | function app(elem, observ, render, opts) { 110 | if (!elem) { 111 | throw new Error( 112 | 'Element does not exist. ' + 113 | 'Mercury cannot be initialized.'); 114 | } 115 | 116 | mercury.Delegator(opts); 117 | var loop = mercury.main(observ(), render, extend({ 118 | diff: mercury.diff, 119 | create: mercury.create, 120 | patch: mercury.patch 121 | }, opts)); 122 | 123 | // This allows `true` to be passed meaning that the app was 124 | // prerendered 125 | if (elem !== true) { 126 | elem.appendChild(loop.target); 127 | } 128 | 129 | return observ(loop.update); 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercury", 3 | "version": "14.2.0", 4 | "description": "A truly modular frontend framework", 5 | "keywords": [ 6 | "framework", 7 | "frontend", 8 | "virtual", 9 | "react", 10 | "modular", 11 | "web" 12 | ], 13 | "author": "Raynos ", 14 | "repository": "git://github.com/Raynos/mercury.git", 15 | "main": "index.js", 16 | "homepage": "https://github.com/Raynos/mercury", 17 | "contributors": [ 18 | { 19 | "name": "Raynos" 20 | }, 21 | { 22 | "name": "Matt-Esch" 23 | }, 24 | { 25 | "name": "neonstalwart" 26 | }, 27 | { 28 | "name": "parshap" 29 | }, 30 | { 31 | "name": "nrw" 32 | } 33 | ], 34 | "bugs": { 35 | "url": "https://github.com/Raynos/mercury/issues", 36 | "email": "raynos2@gmail.com" 37 | }, 38 | "dependencies": { 39 | "dom-delegator": "^13.0.1", 40 | "geval": "^2.1.1", 41 | "main-loop": "^3.1.0", 42 | "observ": "^0.2.0", 43 | "observ-array": "^3.1.0", 44 | "observ-struct": "^5.0.1", 45 | "observ-varhash": "^1.0.2", 46 | "value-event": "^5.0.0", 47 | "vdom-thunk": "^3.0.0", 48 | "virtual-dom": "^2.1.1", 49 | "xtend": "^4.0.0", 50 | "http-hash-router": "~1.1.0" 51 | }, 52 | "devDependencies": { 53 | "backbone": "^1.1.2", 54 | "browserify": "^3.38.0", 55 | "callify": "^0.2.0", 56 | "coveralls": "^2.11.1", 57 | "cuid": "^1.2.1", 58 | "disc": "^1.3.0", 59 | "function-bind": "^0.1.0", 60 | "global": "^4.2.1", 61 | "hash-router": "^0.4.0", 62 | "immutable": "^3.6.2", 63 | "indexhtmlify": "^1.2.0", 64 | "istanbul": "^0.2.16", 65 | "javascript-editor": "^1.0.0", 66 | "jquery": "^2.1.4", 67 | "json-globals": "^0.2.1", 68 | "lint-trap": "^1.0.1", 69 | "marked": "^0.3.2", 70 | "mercury-jsxify": "^0.14.0", 71 | "min-document": "^2.9.0", 72 | "next-tick": "^0.2.2", 73 | "node-hook": "^0.1.0", 74 | "opn": "^1.0.1", 75 | "pre-commit": "0.0.7", 76 | "process": "^0.7.0", 77 | "raf": "^2.0.1", 78 | "rcss": "^0.1.5", 79 | "require-modify": "^0.1.0", 80 | "rimraf": "^2.2.8", 81 | "route-map": "^0.1.0", 82 | "run-browser": "^1.3.1", 83 | "run-parallel": "^1.0.0", 84 | "run-series": "^1.0.2", 85 | "st": "^0.4.1", 86 | "synthetic-dom-events": "git://github.com/Raynos/synthetic-dom-events", 87 | "tap-spec": "^0.2.0", 88 | "tape": "^2.13.2", 89 | "valid-email": "0.0.1", 90 | "vdom-to-html": "~2.2.0", 91 | "vdom-virtualize": "0.0.5", 92 | "vtree-select": "^1.0.1", 93 | "weakmap-shim": "^1.1.0", 94 | "zuul": "^1.9.0" 95 | }, 96 | "licenses": [ 97 | { 98 | "type": "MIT", 99 | "url": "http://github.com/Raynos/mercury/raw/master/LICENSE" 100 | } 101 | ], 102 | "scripts": { 103 | "disc": "browserify index.js --full-paths | discify > disc.html && opn disc.html", 104 | "lint": "lint-trap", 105 | "test": "npm run lint && node test/index.js | tap-spec", 106 | "travis-test": "npm run phantom && npm run cover && istanbul report lcov && ((cat coverage/lcov.info | coveralls) || exit 0) && zuul -- test/index.js", 107 | "phantom": "run-browser test/index.js -b | tap-spec", 108 | "browser": "run-browser test/index.js", 109 | "cover": "istanbul cover --report html --print detail ./test/index.js", 110 | "view-cover": "istanbul report html && opn coverage/index.html", 111 | "build": "node bin/build.js", 112 | "examples": "node bin/example-server.js", 113 | "dist": "node bin/dist.js", 114 | "dist-publish": "npm run dist && git add dist/mercury.js && git commit -m 'dist' && npm publish", 115 | "modules-docs": "node bin/modules-docs.js" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /svg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('virtual-dom/virtual-hyperscript/svg'); 4 | -------------------------------------------------------------------------------- /test/bmi-counter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var raf = require('raf'); 4 | var test = require('tape'); 5 | var document = require('global/document'); 6 | var event = require('synthetic-dom-events'); 7 | 8 | if (typeof window === 'undefined') { 9 | require('./lib/load-hook.js'); 10 | } 11 | 12 | var embedComponent = require('./lib/embed-component.js'); 13 | var bmiCounter = require('../examples/bmi-counter.js'); 14 | 15 | test('bmi state', function t(assert) { 16 | var state = bmiCounter.state(); 17 | 18 | assert.equal(state.height, 180); 19 | assert.equal(state.weight, 80); 20 | assert.equal(state.bmi.toFixed(2), '24.69'); 21 | 22 | assert.end(); 23 | }); 24 | 25 | test('render bmi', function t(assert) { 26 | var comp = embedComponent(bmiCounter); 27 | 28 | var sliders = document.getElementsByClassName('slider'); 29 | var diagnose = document 30 | .getElementsByClassName('diagnose')[0]; 31 | 32 | assert.equal(sliders.length, 3); 33 | assert.equal(sliders[0].value, '80'); 34 | assert.equal(sliders[1].value, '180'); 35 | assert.equal(Number(sliders[2].value).toFixed(0), '25'); 36 | 37 | assert.equal(diagnose.childNodes[0].data, 'normal'); 38 | assert.equal(diagnose.style.color, 'inherit'); 39 | 40 | comp.destroy(); 41 | assert.end(); 42 | }); 43 | 44 | test('update weight', function t(assert) { 45 | var comp = embedComponent(bmiCounter); 46 | 47 | var slider = document.getElementsByClassName('slider')[0]; 48 | 49 | slider.value = '120'; 50 | slider.dispatchEvent(event('input')); 51 | 52 | raf(function afterRender() { 53 | assert.equal(comp.state().weight, 120); 54 | 55 | var sliders = document 56 | .getElementsByClassName('slider'); 57 | var diagnose = document 58 | .getElementsByClassName('diagnose')[0]; 59 | 60 | assert.equal(sliders[0].value, '120'); 61 | assert.equal(sliders[1].value, '180'); 62 | assert.equal(Number(sliders[2].value).toFixed(0), '37'); 63 | 64 | assert.equal(diagnose.childNodes[0].data, 'obese'); 65 | assert.equal(diagnose.style.color, 'red'); 66 | 67 | comp.destroy(); 68 | assert.end(); 69 | }); 70 | }); 71 | 72 | test('update height', function t(assert) { 73 | var comp = embedComponent(bmiCounter); 74 | 75 | var slider = document.getElementsByClassName('slider')[1]; 76 | 77 | slider.value = '210'; 78 | slider.dispatchEvent(event('input')); 79 | 80 | raf(function afterRender() { 81 | assert.equal(comp.state().height, 210); 82 | assert.equal(comp.state().weight, 80); 83 | 84 | var sliders = document 85 | .getElementsByClassName('slider'); 86 | var diagnose = document 87 | .getElementsByClassName('diagnose')[0]; 88 | 89 | assert.equal(sliders[0].value, '80'); 90 | assert.equal(sliders[1].value, '210'); 91 | assert.equal(Number(sliders[2].value).toFixed(0), '18'); 92 | 93 | assert.equal(diagnose.childNodes[0].data, 'underweight'); 94 | assert.equal(diagnose.style.color, 'orange'); 95 | 96 | comp.destroy(); 97 | assert.end(); 98 | }); 99 | }); 100 | 101 | test('update bmi', function t(assert) { 102 | var comp = embedComponent(bmiCounter); 103 | 104 | var slider = document.getElementsByClassName('slider')[2]; 105 | 106 | slider.value = '27'; 107 | slider.dispatchEvent(event('input')); 108 | 109 | raf(function afterRender() { 110 | assert.equal(comp.state().height, 180); 111 | assert.equal(comp.state().weight, 87.48); 112 | 113 | var sliders = document 114 | .getElementsByClassName('slider'); 115 | var diagnose = document 116 | .getElementsByClassName('diagnose')[0]; 117 | 118 | assert.equal(Number(sliders[0].value).toFixed(0), '87'); 119 | assert.equal(sliders[1].value, '180'); 120 | assert.equal(Number(sliders[2].value).toFixed(0), '27'); 121 | 122 | assert.equal(diagnose.childNodes[0].data, 'overweight'); 123 | assert.equal(diagnose.style.color, 'orange'); 124 | 125 | comp.destroy(); 126 | assert.end(); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/count.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var event = require('synthetic-dom-events'); 5 | var document = require('global/document'); 6 | var raf = require('raf'); 7 | 8 | if (typeof window === 'undefined') { 9 | require('./lib/load-hook.js'); 10 | } 11 | 12 | var embedComponent = require('./lib/embed-component.js'); 13 | var count = require('../examples/count.js'); 14 | 15 | test('count state is a number', function t(assert) { 16 | assert.equal(typeof count.state().value, 'number'); 17 | 18 | assert.end(); 19 | }); 20 | 21 | test('count increments on click', function t(assert) { 22 | var comp = embedComponent(count); 23 | 24 | var button = document.getElementsByClassName('button')[0]; 25 | 26 | button.dispatchEvent(event('click')); 27 | button.dispatchEvent(event('click')); 28 | 29 | assert.equal(count.state().value, 2); 30 | 31 | raf(afterRender); 32 | 33 | function afterRender() { 34 | var elem = comp.target.childNodes[0].childNodes[2]; 35 | 36 | assert.equal(elem.data, ' has value: 2.'); 37 | 38 | comp.destroy(); 39 | 40 | assert.end(); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | 5 | var mercury = require('../index'); 6 | 7 | // FFFfffff--- phantomJS. 8 | if (!Function.prototype.bind) { 9 | /*eslint no-extend-native: 0*/ 10 | Function.prototype.bind = require('function-bind'); 11 | } 12 | 13 | test('mercury is a object', function t(assert) { 14 | assert.equal(typeof mercury, 'object'); 15 | assert.end(); 16 | }); 17 | 18 | // jscs:disable disallowKeywords 19 | test('missing element prevents app init', function t(assert) { 20 | try { 21 | assert.throws(mercury); 22 | mercury.app(null); 23 | } catch (exception) { 24 | assert.equal(exception.message, 25 | 'Element does not exist. Mercury cannot be initialized.'); 26 | assert.end(); 27 | } 28 | }); 29 | // jscs:enable disallowKeywords 30 | 31 | require('./synthetic-events.js'); 32 | require('./count.js'); 33 | require('./shared-state.js'); 34 | require('./bmi-counter.js'); 35 | require('./time-travel.js'); 36 | require('./ssr.js'); 37 | -------------------------------------------------------------------------------- /test/lib/embed-component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | 5 | var mercury = require('../../index.js'); 6 | 7 | module.exports = embedComponent; 8 | 9 | function embedComponent(component) { 10 | var div = document.createElement('div'); 11 | document.body.appendChild(div); 12 | 13 | var startState = component.state(); 14 | 15 | var remove = mercury.app(div, component.state, component.render); 16 | 17 | return { 18 | destroy: destroy, 19 | state: component.state, 20 | render: component.render, 21 | target: div 22 | }; 23 | 24 | function destroy() { 25 | component.state.set(startState); 26 | document.body.removeChild(div); 27 | remove(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/lib/load-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Module = require('module'); 4 | 5 | var appRegex = /hg\.app\([\w\.]+,\s?([\w\(\)]+),\s?([\w\.]+)\)/; 6 | 7 | var _compile = Module.prototype._compile; 8 | Module.prototype._compile = transformSource; 9 | 10 | function transformSource(source, fileName) { 11 | if (fileName.indexOf('/examples/') === -1) { 12 | return _compile.call(this, source, fileName); 13 | } 14 | 15 | source = source.replace(appRegex, replacer); 16 | 17 | return _compile.call(this, source, fileName); 18 | } 19 | 20 | function replacer(match, state, render) { 21 | return 'module.exports = {\n' + 22 | ' state: ' + state + ',\n' + 23 | ' render: ' + render + '\n' + 24 | '}'; 25 | } 26 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercury.test", 3 | "version": "1.0.0", 4 | "browser": { 5 | "./lib/load-example.js": false 6 | }, 7 | "browserify": { 8 | "transform": [ 9 | "./transforms/intercept-mercury-app.js" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/shared-state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var event = require('synthetic-dom-events'); 5 | var document = require('global/document'); 6 | var raf = require('raf'); 7 | 8 | if (typeof window === 'undefined') { 9 | require('./lib/load-hook.js'); 10 | } 11 | 12 | var embedComponent = require('./lib/embed-component.js'); 13 | var shared = require('../examples/shared-state.js'); 14 | 15 | test('shared state is a string', function t(assert) { 16 | assert.equal(typeof shared.state().text, 'string'); 17 | 18 | assert.end(); 19 | }); 20 | 21 | test('syncing state to DOM', function t(assert) { 22 | var comp = embedComponent(shared); 23 | 24 | comp.state.text.set('foobar'); 25 | 26 | raf(function afterRender() { 27 | var content = document.getElementsByClassName('content')[0]; 28 | 29 | assert.equal(content.childNodes[0].data, 30 | 'The value is now: foobar'); 31 | 32 | var input = document.getElementsByClassName('input')[0]; 33 | 34 | assert.equal(input.value, 'foobar'); 35 | 36 | comp.destroy(); 37 | 38 | assert.end(); 39 | }); 40 | }); 41 | 42 | test('syncing from the DOM', function t(assert) { 43 | var comp = embedComponent(shared); 44 | 45 | var input = document.getElementsByClassName('input')[0]; 46 | input.value = 'foobar'; 47 | 48 | input.dispatchEvent(event('input', { 49 | bubbles: true 50 | })); 51 | 52 | assert.equal(comp.state().text, 'foobar'); 53 | 54 | raf(function afterRender() { 55 | var content = document.getElementsByClassName('content')[0]; 56 | 57 | assert.equal(content.childNodes[0].data, 58 | 'The value is now: foobar'); 59 | 60 | comp.destroy(); 61 | 62 | assert.end(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/ssr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../index.js'); 4 | var h = mercury.h; 5 | var document = require('global/document'); 6 | var test = require('tape'); 7 | var virtualize = require('vdom-virtualize'); 8 | var raf = require('raf'); 9 | 10 | test('Can reattach in the client', function t(assert) { 11 | var div = document.createElement('div'); 12 | div.appendChild(document.createTextNode('Hello world')); 13 | document.body.appendChild(div); 14 | 15 | function render(name) { 16 | return h('div', 'Hello ' + name); 17 | } 18 | 19 | var state = mercury.value('venus'); 20 | 21 | mercury.app(true, state, render, { 22 | target: div, 23 | initialTree: virtualize(div) 24 | }); 25 | state.set(state()); 26 | 27 | raf(function afterRender() { 28 | var tn = document.body.childNodes[0].childNodes[0]; 29 | 30 | assert.equal(tn.data, 'Hello venus', 'Element was updated'); 31 | 32 | document.body.removeChild(document.body.childNodes[0]); 33 | assert.end(); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/synthetic-events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mercury = require('../index.js'); 4 | var h = mercury.h; 5 | var event = require('synthetic-dom-events'); 6 | var document = require('global/document'); 7 | var test = require('tape'); 8 | 9 | test('events happen', function t(assert) { 10 | var callCount = 0; 11 | 12 | var click = mercury.input(); 13 | 14 | click(function onClick() { 15 | callCount++; 16 | }); 17 | 18 | function render(onClick) { 19 | return h('button', { 20 | 'ev-click': mercury.event(onClick) 21 | }, 'Click Me'); 22 | } 23 | 24 | var div = document.createElement('div'); 25 | mercury.app(div, mercury.value(click), render, { 26 | document: document 27 | }); 28 | 29 | document.body.appendChild(div); 30 | 31 | // action 32 | div.childNodes[0].dispatchEvent( 33 | event('click', {bubbles: true}) 34 | ); 35 | 36 | // assert 37 | assert.equal(callCount, 1); 38 | 39 | document.body.removeChild(div); 40 | assert.end(); 41 | }); 42 | -------------------------------------------------------------------------------- /test/time-travel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var mercury = require('../index.js'); 5 | var TimeTravel = require('../time-travel.js'); 6 | 7 | test('change', function t(assert) { 8 | var state = mercury.struct({ label: mercury.value(0) }); 9 | state.label.set(1); 10 | assert.equal(state().label, 1); 11 | assert.end(); 12 | }); 13 | 14 | test('undo', function t(assert) { 15 | var state = mercury.struct({ label: mercury.value(0) }); 16 | var history = TimeTravel(state); 17 | var undo = history.undo; 18 | state.label.set(1); 19 | undo(); 20 | assert.equal(state().label, 0); 21 | assert.end(); 22 | }); 23 | 24 | test('undo then redo', function t(assert) { 25 | var state = mercury.struct({ label: mercury.value(0) }); 26 | var history = TimeTravel(state); 27 | var undo = history.undo; 28 | var redo = history.redo; 29 | state.label.set(1); 30 | undo(); 31 | redo(); 32 | assert.equal(state().label, 1); 33 | assert.end(); 34 | }); 35 | 36 | test('undo at beginning', function t(assert) { 37 | var state = mercury.struct({ label: mercury.value(0) }); 38 | var history = TimeTravel(state); 39 | var undo = history.undo; 40 | state.label.set(1); 41 | undo(); 42 | undo(); 43 | assert.equal(state().label, 0); 44 | assert.end(); 45 | }); 46 | 47 | test('redo at end', function t(assert) { 48 | var state = mercury.struct({ label: mercury.value(0) }); 49 | var history = TimeTravel(state); 50 | var redo = history.redo; 51 | state.label.set(1); 52 | redo(); 53 | assert.equal(state().label, 1); 54 | assert.end(); 55 | }); 56 | 57 | test('undo then change', function t(assert) { 58 | var state = mercury.struct({ label: mercury.value(0) }); 59 | var history = TimeTravel(state); 60 | var undo = history.undo; 61 | state.label.set(1); 62 | undo(); 63 | state.label.set(2); 64 | assert.equal(state().label, 2); 65 | assert.end(); 66 | }); 67 | 68 | test('undo then change then redo', function t(assert) { 69 | var state = mercury.struct({ label: mercury.value(0) }); 70 | var history = TimeTravel(state); 71 | var undo = history.undo; 72 | var redo = history.redo; 73 | state.label.set(1); 74 | undo(); 75 | state.label.set(2); 76 | redo(); 77 | assert.equal(state().label, 2); 78 | assert.end(); 79 | }); 80 | -------------------------------------------------------------------------------- /test/transforms/intercept-mercury-app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var callify = require('callify'); 4 | 5 | module.exports = callify({ 6 | '../index.js': function transformNode(node, params) { 7 | if (params.file.indexOf('test') !== -1) { 8 | return; 9 | } 10 | 11 | if (params.calls[0] === 'hg' && 12 | params.calls[1] === 'app' 13 | ) { 14 | node.update(replacer(node)); 15 | } 16 | } 17 | }); 18 | 19 | function replacer(node) { 20 | return 'module.exports = {\n' + 21 | ' state: ' + node.arguments[1].source() + ',\n' + 22 | ' render: ' + node.arguments[2].source() + '\n' + 23 | '}'; 24 | } 25 | -------------------------------------------------------------------------------- /time-travel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = TimeTravel; 4 | 5 | function TimeTravel(state) { 6 | var history = [state()]; 7 | 8 | // Tracks the current position in history. 9 | var cursor = 0; 10 | 11 | var isRedoOrUndo = false; 12 | 13 | state(function recordState(newState) { 14 | 15 | // This function gets called whenever there is a state change. 16 | // State changes happen due to events being handled, or due to 17 | // undo/redo. 18 | 19 | // If we are replaying items in the history, 20 | // we don't want to re-add them to the end of the history. 21 | // Just quit. 22 | if (isRedoOrUndo) { 23 | return; 24 | } 25 | 26 | // If we've made it this far, `newState` is due to a new action, 27 | // not due to undo/redo. 28 | 29 | // If we've called `undo` a bunch of times, 30 | // the cursor won't be at the end. 31 | // Any states past the cursor should be cut off. 32 | history.splice(cursor + 1); 33 | 34 | // Add the new item to the history 35 | history.push(newState); 36 | 37 | cursor = history.length - 1; 38 | }); 39 | 40 | return { undo: undo, redo: redo }; 41 | 42 | function undo() { 43 | if (cursor < 1) { 44 | // Don't move before the beginning of time 45 | return undefined; 46 | } 47 | 48 | cursor--; 49 | isRedoOrUndo = true; 50 | state.set(history[cursor]); 51 | isRedoOrUndo = false; 52 | return history[cursor]; 53 | } 54 | 55 | function redo() { 56 | if (cursor + 1 >= history.length) { 57 | // Don't move past the end of time 58 | return undefined; 59 | } 60 | 61 | cursor++; 62 | isRedoOrUndo = true; 63 | state.set(history[cursor]); 64 | isRedoOrUndo = false; 65 | return history[cursor]; 66 | } 67 | } 68 | --------------------------------------------------------------------------------