├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENCE ├── README.md ├── docs.mli ├── index.js ├── package.json └── test └── index.js /.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 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxdepth": 4, 3 | "maxstatements": 200, 4 | "maxcomplexity": 12, 5 | "maxlen": 80, 6 | "maxparams": 5, 7 | 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "latedef": false, 12 | "noarg": true, 13 | "noempty": true, 14 | "nonew": true, 15 | "undef": true, 16 | "unused": "vars", 17 | "trailing": true, 18 | 19 | "quotmark": true, 20 | "expr": true, 21 | "asi": true, 22 | 23 | "browser": false, 24 | "esnext": true, 25 | "devel": false, 26 | "node": false, 27 | "nonstandard": false, 28 | 29 | "predef": ["require", "module", "__dirname", "__filename"] 30 | } 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - "0.10" 5 | before_script: 6 | - npm install 7 | - npm install istanbul coveralls 8 | script: npm run travis-test 9 | -------------------------------------------------------------------------------- /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 | # main-loop 2 | 3 | 10 | 11 | 12 | 13 | A rendering loop for diffable UIs 14 | 15 | `main-loop` is an optimization module for a virtual DOM system. Normally you would re-create the virtual tree every time your state changes. This is not optimum, with main-loop you will only update your virtual tree at most once per request animation frame. 16 | 17 | `main-loop` basically gives you batching of your virtual DOM changes, which means if you change your model multiple times it will be rendered once asynchronously on the next request animation frame. 18 | 19 | ## Example 20 | 21 | ```js 22 | var mainLoop = require("main-loop") 23 | var h = require("virtual-dom/h") 24 | 25 | var initState = { fruits: ["apple", "banana"], name: "Steve" } 26 | 27 | function render(state) { 28 | return h("div", [ 29 | h("div", [ 30 | h("span", "hello "), 31 | h("span.name", state.name) 32 | ]), 33 | h("ul", state.fruits.map(renderFruit)) 34 | ]) 35 | 36 | function renderFruit(fruitName) { 37 | return h("li", [ 38 | h("span", fruitName) 39 | ]) 40 | } 41 | } 42 | 43 | // set up a loop 44 | var loop = mainLoop(initState, render, { 45 | create: require("virtual-dom/create-element"), 46 | diff: require("virtual-dom/diff"), 47 | patch: require("virtual-dom/patch") 48 | }) 49 | document.body.appendChild(loop.target) 50 | 51 | // update the loop with the new application state 52 | loop.update({ 53 | fruits: ["apple", "banana", "cherry"], 54 | name: "Steve" 55 | }) 56 | loop.update({ 57 | fruits: ["apple", "banana", "cherry"], 58 | name: "Stevie" 59 | }) 60 | ``` 61 | 62 | ## var loop = mainLoop(initState, render, opts) 63 | 64 | Create a loop object with some initial state, a render function, and some 65 | options. Your `function render (state) {}` receives the current state as its 66 | argument and must return a virtual-dom object. 67 | 68 | You must supply: `opts.diff`, `opts.patch`, and `opts.create`. These can be 69 | obtained directly from `require("virtual-dom")`. 70 | 71 | Optionally supply an `opts.target` and `opts.initialTree`. 72 | 73 | ## loop.target 74 | 75 | The main-loop root DOM element. Insert this element to the page. 76 | 77 | ## loop.update(newState) 78 | 79 | Update the page state, automatically re-rendering the page as necessary. 80 | 81 | ## loop.state 82 | 83 | Read the current main-loop state. To modify the loop state, use `loop.update()`. 84 | 85 | ## Installation 86 | 87 | `npm install main-loop` 88 | 89 | ## Contributors 90 | 91 | - Raynos 92 | 93 | ## MIT Licenced 94 | 95 | [1]: https://secure.travis-ci.org/Raynos/main-loop.png 96 | [2]: https://travis-ci.org/Raynos/main-loop 97 | [3]: https://badge.fury.io/js/main-loop.png 98 | [4]: https://badge.fury.io/js/main-loop 99 | [5]: https://coveralls.io/repos/Raynos/main-loop/badge.png 100 | [6]: https://coveralls.io/r/Raynos/main-loop 101 | [7]: https://gemnasium.com/Raynos/main-loop.png 102 | [8]: https://gemnasium.com/Raynos/main-loop 103 | [9]: https://david-dm.org/Raynos/main-loop.png 104 | [10]: https://david-dm.org/Raynos/main-loop 105 | [11]: https://ci.testling.com/Raynos/main-loop.png 106 | [12]: https://ci.testling.com/Raynos/main-loop 107 | -------------------------------------------------------------------------------- /docs.mli: -------------------------------------------------------------------------------- 1 | import { VElem, VPatch } from "vtree" 2 | import { DOMElement } from "jsig.dom" 3 | 4 | type MainLoop : ( 5 | initialState: T, 6 | view: (T) => VElem, 7 | opts?: { 8 | create?: (VElem, opts: Object) => DOMElement, 9 | diff?: (prev: VElem, curr: VElem, opts: Object) => Array, 10 | patch?: ( 11 | target: DOMElement, 12 | patches: Array, 13 | opts: Object 14 | ) => void, 15 | initialTree?: VElem, 16 | target?: DOMElement, 17 | createOnly?: Boolean 18 | } 19 | ) => { 20 | target: DOMElement, 21 | update: (T) => void 22 | } 23 | 24 | main-loop : MainLoop 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var raf = require("raf") 2 | var TypedError = require("error/typed") 3 | 4 | var InvalidUpdateInRender = TypedError({ 5 | type: "main-loop.invalid.update.in-render", 6 | message: "main-loop: Unexpected update occurred in loop.\n" + 7 | "We are currently rendering a view, " + 8 | "you can't change state right now.\n" + 9 | "The diff is: {stringDiff}.\n" + 10 | "SUGGESTED FIX: find the state mutation in your view " + 11 | "or rendering function and remove it.\n" + 12 | "The view should not have any side effects.\n" + 13 | "This may also have happened if rendering did not complete due to an error.\n", 14 | diff: null, 15 | stringDiff: null 16 | }) 17 | 18 | module.exports = main 19 | 20 | function main(initialState, view, opts) { 21 | opts = opts || {} 22 | 23 | var currentState = initialState 24 | var create = opts.create 25 | var diff = opts.diff 26 | var patch = opts.patch 27 | var redrawScheduled = false 28 | 29 | var tree = opts.initialTree || view(currentState, 0); 30 | var target = opts.target || create(tree, opts) 31 | var inRenderingTransaction = false 32 | 33 | currentState = null 34 | 35 | var loop = { 36 | state: initialState, 37 | target: target, 38 | update: update 39 | } 40 | return loop 41 | 42 | function update(state) { 43 | if (inRenderingTransaction) { 44 | throw InvalidUpdateInRender({ 45 | diff: state._diff, 46 | stringDiff: JSON.stringify(state._diff) 47 | }) 48 | } 49 | 50 | if (currentState === null && !redrawScheduled) { 51 | redrawScheduled = true 52 | raf(redraw) 53 | } 54 | 55 | currentState = state 56 | loop.state = state 57 | } 58 | 59 | function redraw(time) { 60 | redrawScheduled = false 61 | if (currentState === null) { 62 | return 63 | } 64 | 65 | inRenderingTransaction = true 66 | var newTree = view(currentState, time) 67 | 68 | if (opts.createOnly) { 69 | inRenderingTransaction = false 70 | create(newTree, opts) 71 | } else { 72 | var patches = diff(tree, newTree, opts) 73 | inRenderingTransaction = false 74 | target = patch(target, patches, opts) 75 | } 76 | 77 | tree = newTree 78 | currentState = null 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "main-loop", 3 | "version": "3.4.0", 4 | "description": "A rendering loop for diffable UIs", 5 | "keywords": [], 6 | "author": "Raynos ", 7 | "repository": "git://github.com/Raynos/main-loop.git", 8 | "main": "index", 9 | "homepage": "https://github.com/Raynos/main-loop", 10 | "contributors": [ 11 | { 12 | "name": "Raynos" 13 | } 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/Raynos/main-loop/issues", 17 | "email": "raynos2@gmail.com" 18 | }, 19 | "dependencies": { 20 | "error": "^4.1.1", 21 | "raf": "^2.0.1" 22 | }, 23 | "devDependencies": { 24 | "global": "^3.0.0", 25 | "istanbul": "^0.3.0", 26 | "tape": "^2.13.3", 27 | "virtual-dom": "0.0.23" 28 | }, 29 | "licenses": [ 30 | { 31 | "type": "MIT", 32 | "url": "http://github.com/Raynos/main-loop/raw/master/LICENSE" 33 | } 34 | ], 35 | "scripts": { 36 | "test": "node ./test/index.js", 37 | "start": "node ./index.js", 38 | "watch": "nodemon -w ./index.js index.js", 39 | "travis-test": "istanbul cover ./test/index.js && ((cat coverage/lcov.info | coveralls) || exit 0)", 40 | "cover": "istanbul cover --report none --print detail ./test/index.js", 41 | "view-cover": "istanbul report html && google-chrome ./coverage/index.html", 42 | "test-browser": "testem-browser ./test/browser/index.js", 43 | "testem": "testem-both -b=./test/browser/index.js" 44 | }, 45 | "testling": { 46 | "files": "test/index.js", 47 | "browsers": [ 48 | "ie/8..latest", 49 | "firefox/16..latest", 50 | "firefox/nightly", 51 | "chrome/22..latest", 52 | "chrome/canary", 53 | "opera/12..latest", 54 | "opera/next", 55 | "safari/5.1..latest", 56 | "ipad/6.0..latest", 57 | "iphone/6.0..latest", 58 | "android-browser/4.2..latest" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require("tape") 2 | var h = require("virtual-dom/virtual-hyperscript") 3 | var document = require("global/document") 4 | var raf = require("raf") 5 | 6 | var mainLoop = require("../index") 7 | 8 | test("mainLoop is a function", function (assert) { 9 | assert.equal(typeof mainLoop, "function") 10 | assert.end() 11 | }) 12 | 13 | test("can set up main loop", function (assert) { 14 | var initState = { fruits: ["apple", "banana"], name: "Steve" } 15 | 16 | function render(state) { 17 | return h("div", [ 18 | h("div", [ 19 | h("span", "hello "), 20 | h("span.name", state.name) 21 | ]), 22 | h("ul", state.fruits.map(renderFruit)) 23 | ]) 24 | 25 | function renderFruit(fruitName) { 26 | return h("li", [ 27 | h("span", fruitName) 28 | ]) 29 | } 30 | } 31 | 32 | // set up a loop 33 | var loop = mainLoop(initState, render, { 34 | document: document, 35 | create: require("virtual-dom/create-element"), 36 | diff: require("virtual-dom/diff"), 37 | patch: require("virtual-dom/patch") 38 | }) 39 | document.body.appendChild(loop.target) 40 | 41 | var div = loop.target 42 | var span = div.childNodes[0].childNodes[1] 43 | var ul = div.childNodes[1] 44 | 45 | assert.equal(div.tagName, "DIV") 46 | assert.equal(span.childNodes[0].data, "Steve") 47 | assert.equal(ul.childNodes.length, 2) 48 | 49 | // update the loop with the new application state 50 | loop.update({ 51 | fruits: ["apple", "banana", "cherry"], 52 | name: "Steve" 53 | }) 54 | 55 | raf(function () { 56 | assert.equal(ul.childNodes.length, 3) 57 | 58 | loop.update({ 59 | fruits: ["apple", "banana", "cherry"], 60 | name: "Stevie" 61 | }) 62 | 63 | raf(function () { 64 | assert.equal(span.childNodes[0].data, "Stevie") 65 | 66 | document.body.removeChild(loop.target) 67 | assert.end() 68 | }) 69 | }) 70 | }) 71 | 72 | test("loop.state exposed", function (assert) { 73 | var loop = mainLoop({ n: 0 }, render, { 74 | document: document, 75 | create: require("virtual-dom/create-element"), 76 | diff: require("virtual-dom/diff"), 77 | patch: require("virtual-dom/patch") 78 | }) 79 | assert.equal(loop.state.n, 0) 80 | loop.update({ n: 4 }) 81 | assert.equal(loop.state.n, 4) 82 | assert.end() 83 | 84 | function render(state) { 85 | return h('div', String(state.n)) 86 | } 87 | }) 88 | 89 | test("render called with monotonically increasing times", function(assert){ 90 | assert.plan(2) 91 | 92 | var times = [] 93 | var loop = mainLoop({ n: 0 }, render, { 94 | document: document, 95 | create: require("virtual-dom/create-element"), 96 | diff: require("virtual-dom/diff"), 97 | patch: require("virtual-dom/patch") 98 | }) 99 | 100 | 101 | function render(state, time) { 102 | times.push(time); 103 | return h('div', String(state.n)) 104 | } 105 | 106 | 107 | raf(function () { 108 | 109 | loop.update({ n: 1}) 110 | 111 | raf(function () { 112 | loop.update({ n: 2}) 113 | 114 | assert.equal(times.length, 2) 115 | assert.ok(times[0] < times[1], "should be increasing") 116 | assert.end() 117 | }) 118 | }) 119 | }); 120 | --------------------------------------------------------------------------------