├── .gitignore ├── .npmrc ├── .travis.yml ├── examples ├── choo-example │ ├── .npmrc │ ├── index.js │ ├── package.json │ ├── store.js │ ├── test.js │ └── view.js ├── with-inferno │ ├── .npmrc │ ├── index.html │ ├── index.js │ └── package.json ├── with-lit-html │ ├── .npmrc │ ├── index.html │ ├── index.js │ └── package.json ├── with-nanomorph │ ├── .npmrc │ ├── index.js │ └── package.json ├── with-preact │ ├── .npmrc │ ├── index.html │ ├── index.js │ └── package.json ├── with-react-jsx │ ├── .npmrc │ ├── index.html │ ├── index.js │ └── package.json ├── with-react │ ├── .npmrc │ ├── index.html │ ├── index.js │ └── package.json └── with-vue-jsx │ ├── .npmrc │ ├── index.html │ ├── index.js │ └── package.json ├── index.js ├── package.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | generate -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - '7' 3 | - '8' 4 | - '9' 5 | sudo: false 6 | language: node_js 7 | env: 8 | - CXX=g++-4.8 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-4.8 15 | script: npm run test -------------------------------------------------------------------------------- /examples/choo-example/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/choo-example/index.js: -------------------------------------------------------------------------------- 1 | var css = require('sheetify') 2 | var choo = require('../../') 3 | var nanomorph = require('nanomorph') 4 | 5 | css('todomvc-common/base.css') 6 | css('todomvc-app-css/index.css') 7 | 8 | var app = choo({ 9 | mount: nanomorph, 10 | render: nanomorph, 11 | toString: function (tree) { 12 | return tree.toString() 13 | } 14 | }) 15 | 16 | if (process.env.NODE_ENV !== 'production') { 17 | app.use(require('choo-devtools')()) 18 | } 19 | app.use(require('./store')) 20 | 21 | app.route('/', require('./view')) 22 | app.route('#active', require('./view')) 23 | app.route('#completed', require('./view')) 24 | app.route('*', require('./view')) 25 | 26 | if (module.parent) module.exports = app 27 | else app.mount('body') 28 | -------------------------------------------------------------------------------- /examples/choo-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "choo-todomvc-example", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "budo index.js -l -P -- -t sheetify", 7 | "test": "standard && node test.js" 8 | }, 9 | "dependencies": { 10 | "choo-devtools": "2.0.0", 11 | "nanohtml": "^1.2.6", 12 | "nanomorph": "^5.1.3", 13 | "sheetify": "^6.0.1", 14 | "todomvc-app-css": "^2.0.6", 15 | "todomvc-common": "^1.0.3" 16 | }, 17 | "devDependencies": { 18 | "budo": "^11.5.0", 19 | "spok": "^0.9.1", 20 | "standard": "^9.0.1", 21 | "tape": "^4.9.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/choo-example/store.js: -------------------------------------------------------------------------------- 1 | module.exports = todoStore 2 | 3 | function todoStore (state, emitter) { 4 | if (!state.todos) { 5 | state.todos = {} 6 | 7 | state.todos.active = [] 8 | state.todos.done = [] 9 | state.todos.all = [] 10 | 11 | state.todos.idCounter = 0 12 | } 13 | 14 | // Always reset when application boots 15 | state.todos.input = '' 16 | 17 | // Register emitters after DOM is loaded to speed up DOM loading 18 | emitter.on('DOMContentLoaded', function () { 19 | // CRUD 20 | emitter.on('todos:create', create) 21 | emitter.on('todos:update', update) 22 | emitter.on('todos:delete', del) 23 | 24 | // Special 25 | emitter.on('todos:input', oninput) 26 | 27 | // Shorthand 28 | emitter.on('todos:edit', edit) 29 | emitter.on('todos:unedit', unedit) 30 | emitter.on('todos:toggle', toggle) 31 | emitter.on('todos:toggleAll', toggleAll) 32 | emitter.on('todos:deleteCompleted', deleteCompleted) 33 | }) 34 | 35 | function oninput (text) { 36 | state.todos.input = text 37 | } 38 | 39 | function create (name) { 40 | var item = { 41 | id: state.todos.idCounter, 42 | editing: false, 43 | done: false, 44 | name: name 45 | } 46 | 47 | state.todos.idCounter += 1 48 | state.todos.active.push(item) 49 | state.todos.all.push(item) 50 | emitter.emit('render') 51 | } 52 | 53 | function edit (id) { 54 | state.todos.all.forEach(function (todo) { 55 | if (todo.id === id) todo.editing = true 56 | }) 57 | emitter.emit('render') 58 | } 59 | 60 | function unedit (id) { 61 | state.todos.all.forEach(function (todo) { 62 | if (todo.id === id) todo.editing = false 63 | }) 64 | emitter.emit('render') 65 | } 66 | 67 | function update (newTodo) { 68 | var todo = state.todos.all.filter(function (todo) { 69 | return todo.id === newTodo.id 70 | })[0] 71 | 72 | if (newTodo.done && todo.done === false) { 73 | state.todos.active.splice(state.todos.active.indexOf(todo), 1) 74 | state.todos.done.push(todo) 75 | } else if (newTodo.done === false && todo.done) { 76 | state.todos.done.splice(state.todos.done.indexOf(todo), 1) 77 | state.todos.active.push(todo) 78 | } 79 | 80 | Object.assign(todo, newTodo) 81 | emitter.emit('render') 82 | } 83 | 84 | function del (id) { 85 | var i = null 86 | var todo = null 87 | state.todos.all.forEach(function (_todo, j) { 88 | if (_todo.id === id) { 89 | i = j 90 | todo = _todo 91 | } 92 | }) 93 | state.todos.all.splice(i, 1) 94 | 95 | if (todo.done) { 96 | var done = state.todos.done 97 | var doneIndex 98 | done.forEach(function (_todo, j) { 99 | if (_todo.id === id) { 100 | doneIndex = j 101 | } 102 | }) 103 | done.splice(doneIndex, 1) 104 | } else { 105 | var active = state.todos.active 106 | var activeIndex 107 | active.forEach(function (_todo, j) { 108 | if (_todo.id === id) { 109 | activeIndex = j 110 | } 111 | }) 112 | active.splice(activeIndex, 1) 113 | } 114 | emitter.emit('render') 115 | } 116 | 117 | function deleteCompleted (data) { 118 | var done = state.todos.done 119 | done.forEach(function (todo) { 120 | var index = state.todos.all.indexOf(todo) 121 | state.todos.all.splice(index, 1) 122 | }) 123 | state.todos.done = [] 124 | emitter.emit('render') 125 | } 126 | 127 | function toggle (id) { 128 | var todo = state.todos.all.filter(function (todo) { 129 | return todo.id === id 130 | })[0] 131 | var done = todo.done 132 | todo.done = !done 133 | var arr = done ? state.todos.done : state.todos.active 134 | var target = done ? state.todos.active : state.todos.done 135 | var index = arr.indexOf(todo) 136 | arr.splice(index, 1) 137 | target.push(todo) 138 | emitter.emit('render') 139 | } 140 | 141 | function toggleAll (data) { 142 | var todos = state.todos.all 143 | var allDone = state.todos.all.length && 144 | state.todos.done.length === state.todos.all.length 145 | 146 | todos.forEach(function (todo) { 147 | todo.done = !allDone 148 | }) 149 | 150 | if (allDone) { 151 | state.todos.done = [] 152 | state.todos.active = state.todos.all 153 | } else { 154 | state.todos.done = state.todos.all 155 | state.todos.active = [] 156 | } 157 | 158 | emitter.emit('render') 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /examples/choo-example/test.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter 2 | var spok = require('spok') 3 | var tape = require('tape') 4 | 5 | var todoStore = require('./store') 6 | 7 | tape('should initialize empty state', function (t) { 8 | var emitter = new EventEmitter() 9 | var state = {} 10 | todoStore(state, emitter) 11 | spok(t, state, { 12 | todos: { 13 | idCounter: 0, 14 | active: spok.arrayElements(0), 15 | done: spok.arrayElements(0), 16 | all: spok.arrayElements(0) 17 | } 18 | }) 19 | t.end() 20 | }) 21 | 22 | tape('restore previous state', function (t) { 23 | var emitter = new EventEmitter() 24 | var state = { 25 | todos: { 26 | idCounter: 100, 27 | active: [], 28 | done: [], 29 | all: [] 30 | } 31 | } 32 | todoStore(state, emitter) 33 | spok(t, state, { 34 | todos: { 35 | idCounter: 100 36 | } 37 | }) 38 | t.end() 39 | }) 40 | 41 | tape('todos:create', function (t) { 42 | var emitter = new EventEmitter() 43 | var state = {} 44 | todoStore(state, emitter) 45 | emitter.emit('DOMContentLoaded') 46 | 47 | emitter.emit('todos:create', 'same as it ever was') 48 | spok(t, state.todos, { 49 | all: spok.arrayElements(1), 50 | active: spok.arrayElements(1), 51 | done: spok.arrayElements(0) 52 | }) 53 | spok(t, state.todos.all[0], { 54 | name: 'same as it ever was', 55 | editing: false, 56 | done: false, 57 | id: 0 58 | }) 59 | 60 | emitter.emit('todos:create', 'and another one down') 61 | spok(t, state.todos, { 62 | all: spok.arrayElements(2), 63 | active: spok.arrayElements(2), 64 | done: spok.arrayElements(0) 65 | }) 66 | spok(t, state.todos.all[1], { 67 | name: 'and another one down', 68 | editing: false, 69 | done: false, 70 | id: 1 71 | }) 72 | 73 | t.end() 74 | }) 75 | 76 | tape('todos:update', function (t) { 77 | var emitter = new EventEmitter() 78 | var state = {} 79 | todoStore(state, emitter) 80 | emitter.emit('DOMContentLoaded') 81 | 82 | emitter.emit('todos:create', 'same as it ever was') 83 | emitter.emit('todos:create', 'and another one down') 84 | 85 | emitter.emit('todos:update', { 86 | id: 0, 87 | editing: true, 88 | name: 'been here all along' 89 | }) 90 | spok(t, state.todos.all[0], { 91 | name: 'been here all along', 92 | editing: true, 93 | done: false, 94 | id: 0 95 | }) 96 | 97 | emitter.emit('todos:update', { 98 | done: true, 99 | id: 1 100 | }) 101 | spok(t, state.todos, { 102 | all: spok.arrayElements(2), 103 | active: spok.arrayElements(1), 104 | done: spok.arrayElements(1) 105 | }) 106 | 107 | emitter.emit('todos:update', { 108 | done: false, 109 | id: 1 110 | }) 111 | spok(t, state.todos, { 112 | all: spok.arrayElements(2), 113 | active: spok.arrayElements(2), 114 | done: spok.arrayElements(0) 115 | }) 116 | 117 | t.end() 118 | }) 119 | 120 | // tape('todos:delete') 121 | // tape('todos:edit') 122 | // tape('todos:unedit') 123 | // tape('todos:toggle') 124 | // tape('todos:toggleAll') 125 | // tape('todos:deleteCompleted') 126 | -------------------------------------------------------------------------------- /examples/choo-example/view.js: -------------------------------------------------------------------------------- 1 | var html = require('nanohtml') 2 | 3 | module.exports = mainView 4 | 5 | function mainView (state, emit) { 6 | emit('log:debug', 'Rendering main view') 7 | return html` 8 | 9 |
10 | ${Header(state, emit)} 11 | ${TodoList(state, emit)} 12 | ${Footer(state, emit)} 13 |
14 | 19 | 20 | ` 21 | } 22 | 23 | function Header (state, emit) { 24 | return html` 25 |
26 |

todos

27 | 32 |
33 | ` 34 | 35 | function createTodo (e) { 36 | var value = e.target.value 37 | if (!value) return 38 | if (e.keyCode === 13) { 39 | emit('todos:input', '') 40 | emit('todos:create', value) 41 | } else { 42 | emit('todos:input', value) 43 | } 44 | } 45 | } 46 | 47 | function Footer (state, emit) { 48 | var filter = state.href.replace(/^\//, '') || '' 49 | var activeCount = state.todos.active.length 50 | var hasDone = state.todos.done.length 51 | 52 | return html` 53 | 65 | ` 66 | 67 | function filterButton (name, filter, currentFilter, emit) { 68 | var filterClass = filter === currentFilter 69 | ? 'selected' 70 | : '' 71 | 72 | var uri = '#' + name.toLowerCase() 73 | if (uri === '#all') uri = '/' 74 | return html` 75 |
  • 76 | 77 | ${name} 78 | 79 |
  • 80 | ` 81 | } 82 | 83 | function deleteCompleted (emit) { 84 | return html` 85 | 88 | ` 89 | 90 | function deleteAllCompleted () { 91 | emit('todos:deleteCompleted') 92 | } 93 | } 94 | } 95 | 96 | function TodoItem (todo, emit) { 97 | var clx = classList({ completed: todo.done, editing: todo.editing }) 98 | return html` 99 |
  • 100 |
    101 | 106 | 107 | 111 |
    112 | 117 |
  • 118 | ` 119 | 120 | function toggle (e) { 121 | emit('todos:toggle', todo.id) 122 | } 123 | 124 | function edit (e) { 125 | emit('todos:edit', todo.id) 126 | } 127 | 128 | function destroy (e) { 129 | emit('todos:delete', todo.id) 130 | } 131 | 132 | function update (e) { 133 | emit('todos:update', { 134 | id: todo.id, 135 | editing: false, 136 | name: e.target.value 137 | }) 138 | } 139 | 140 | function handleEditKeydown (e) { 141 | if (e.keyCode === 13) update(e) // Enter 142 | else if (e.code === 27) emit('todos:unedit') // Escape 143 | } 144 | 145 | function classList (classes) { 146 | var str = '' 147 | var keys = Object.keys(classes) 148 | for (var i = 0, len = keys.length; i < len; i++) { 149 | var key = keys[i] 150 | var val = classes[key] 151 | if (val) str += (key + ' ') 152 | } 153 | return str 154 | } 155 | } 156 | 157 | function TodoList (state, emit) { 158 | var filter = state.href.replace(/^\//, '') || '' 159 | var items = filter === 'completed' 160 | ? state.todos.done 161 | : filter === 'active' 162 | ? state.todos.active 163 | : state.todos.all 164 | 165 | var allDone = state.todos.done.length === state.todos.all.length 166 | 167 | var nodes = items.map(function (todo) { 168 | return TodoItem(todo, emit) 169 | }) 170 | 171 | return html` 172 |
    173 | 178 | 181 | 184 |
    185 | ` 186 | 187 | function toggleAll () { 188 | emit('todos:toggleAll') 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /examples/with-inferno/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-inferno/index.html: -------------------------------------------------------------------------------- 1 | budo
    -------------------------------------------------------------------------------- /examples/with-inferno/index.js: -------------------------------------------------------------------------------- 1 | var Inferno = require('inferno') 2 | var hyperx = require('hyperx') 3 | var html = hyperx(require('inferno-create-element').createElement) 4 | var monoapp = require('../../index') 5 | var devtools = require('choo-devtools') 6 | 7 | var app = monoapp() 8 | 9 | app.use(withInferno) 10 | app.use(devtools()) 11 | app.use(countStore) 12 | app.route('/', mainView) 13 | app.mount('#app') 14 | 15 | function withInferno (state, emitter, app) { 16 | app._mount = (tree, newTree, root) => Inferno.render(newTree, tree) 17 | app._render = (tree, newTree, root) => Inferno.render(newTree, tree) 18 | } 19 | 20 | function mainView (state, emit) { 21 | return html` 22 |
    23 |

    count is ${state.count}

    24 | 25 |
    26 | ` 27 | 28 | function onclick () { 29 | emit('increment', 1) 30 | } 31 | } 32 | 33 | function countStore (state, emitter) { 34 | state.count = 0 35 | emitter.on('increment', function (count) { 36 | state.count += count 37 | emitter.emit('render') 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /examples/with-inferno/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-inferno", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "budo": "^11.5.0" 14 | }, 15 | "dependencies": { 16 | "choo-devtools": "^2.5.1", 17 | "hyperx": "^2.4.0", 18 | "inferno": "^6.2.1", 19 | "inferno-create-element": "^6.2.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-lit-html/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-lit-html/index.html: -------------------------------------------------------------------------------- 1 | budo
    -------------------------------------------------------------------------------- /examples/with-lit-html/index.js: -------------------------------------------------------------------------------- 1 | var { html, render } = require('lit-html') 2 | var monoapp = require('../../index') 3 | var devtools = require('choo-devtools') 4 | 5 | var app = monoapp() 6 | 7 | app.use(withLit) 8 | app.use(devtools()) 9 | app.use(countStore) 10 | app.route('/', mainView) 11 | app.mount('#app') 12 | 13 | function withLit (state, emitter, app) { 14 | app._mount = (tree, newTree, root) => render(newTree, tree) 15 | app._render = (tree, newTree, root) => render(newTree, tree) 16 | } 17 | 18 | function mainView (state, emit) { 19 | return html` 20 |
    21 |

    count is ${state.count}

    22 | 23 |
    24 | ` 25 | 26 | function onclick () { 27 | emit('increment', 1) 28 | } 29 | } 30 | 31 | function countStore (state, emitter) { 32 | state.count = 0 33 | emitter.on('increment', function (count) { 34 | state.count += count 35 | emitter.emit('render') 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /examples/with-lit-html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-lit-html", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P -- -p esmify" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "budo": "^11.5.0", 14 | "esmify": "^2.0.0" 15 | }, 16 | "dependencies": { 17 | "choo-devtools": "^2.5.1", 18 | "lit-html": "^0.13.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-nanomorph/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-nanomorph/index.js: -------------------------------------------------------------------------------- 1 | var html = require('nanohtml') 2 | var nanomorph = require('nanomorph') 3 | var monoapp = require('../../index') 4 | var devtools = require('choo-devtools') 5 | 6 | var app = monoapp() 7 | 8 | app.use(withNanomorph) 9 | app.use(devtools()) 10 | app.use(countStore) 11 | app.route('/', mainView) 12 | app.mount('body') 13 | 14 | function withNanomorph (state, emitter, app) { 15 | app._mount = nanomorph 16 | app._render = nanomorph 17 | } 18 | 19 | function mainView (state, emit) { 20 | return html` 21 | 22 |

    count is ${state.count}

    23 | 24 | 25 | ` 26 | 27 | function onclick () { 28 | emit('increment', 1) 29 | } 30 | } 31 | 32 | function countStore (state, emitter) { 33 | state.count = 0 34 | emitter.on('increment', function (count) { 35 | state.count += count 36 | emitter.emit('render') 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /examples/with-nanomorph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-nanomorph", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "budo": "^11.5.0" 14 | }, 15 | "dependencies": { 16 | "choo-devtools": "^2.5.1", 17 | "nanohtml": "^1.2.6", 18 | "nanomorph": "^5.1.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-preact/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-preact/index.html: -------------------------------------------------------------------------------- 1 | budo
    -------------------------------------------------------------------------------- /examples/with-preact/index.js: -------------------------------------------------------------------------------- 1 | var preact = require('preact') 2 | var hyperx = require('hyperx') 3 | var html = hyperx(preact.h) 4 | var monoapp = require('../../index') 5 | var devtools = require('choo-devtools') 6 | 7 | var app = monoapp() 8 | 9 | app.use(withPreact) 10 | app.use(devtools()) 11 | app.use(countStore) 12 | app.route('/', mainView) 13 | app.mount('#app') 14 | 15 | function withPreact (state, emitter, app) { 16 | app._mount = (tree, newTree, root) => preact.render(newTree, tree) 17 | app._render = (tree, newTree, root) => preact.render(newTree, tree, root) 18 | } 19 | 20 | function mainView (state, emit) { 21 | return html` 22 |
    23 |

    count is ${state.count}

    24 | 25 |
    26 | ` 27 | 28 | function onclick () { 29 | emit('increment', 1) 30 | } 31 | } 32 | 33 | function countStore (state, emitter) { 34 | state.count = 0 35 | emitter.on('increment', function (count) { 36 | state.count += count 37 | emitter.emit('render') 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /examples/with-preact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-preact", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "budo": "^11.5.0" 14 | }, 15 | "dependencies": { 16 | "choo-devtools": "^2.5.1", 17 | "hyperx": "^2.4.0", 18 | "preact": "^8.3.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/with-react-jsx/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-react-jsx/index.html: -------------------------------------------------------------------------------- 1 | budo
    -------------------------------------------------------------------------------- /examples/with-react-jsx/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var ReactDOM = require('react-dom') 3 | var monoapp = require('../../index') 4 | var devtools = require('choo-devtools') 5 | 6 | var app = monoapp() 7 | 8 | app.use(withReact) 9 | app.use(devtools()) 10 | app.use(countStore) 11 | app.route('/', mainView) 12 | app.mount('#app') 13 | 14 | function withReact (state, emitter, app) { 15 | app._mount = (tree, newTree) => ReactDOM.render(newTree, tree) 16 | app._render = (tree, newTree) => ReactDOM.render(newTree, tree) 17 | } 18 | 19 | function mainView (state, emit) { 20 | return ( 21 |
    22 |

    count is {state.count}

    23 | 24 |
    25 | ) 26 | 27 | function onclick () { 28 | emit('increment', 1) 29 | } 30 | } 31 | 32 | function countStore (state, emitter) { 33 | state.count = 0 34 | emitter.on('increment', function (count) { 35 | state.count += count 36 | emitter.emit('render') 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /examples/with-react-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-react-jsx", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P -- -t [ babelify --presets [ @babel/preset-react ] ]" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.1.5", 14 | "@babel/preset-react": "^7.0.0", 15 | "babelify": "^10.0.0", 16 | "budo": "^11.5.0" 17 | }, 18 | "dependencies": { 19 | "choo-devtools": "^2.5.1", 20 | "react": "^16.6.1", 21 | "react-dom": "^16.6.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/with-react/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-react/index.html: -------------------------------------------------------------------------------- 1 | budo
    -------------------------------------------------------------------------------- /examples/with-react/index.js: -------------------------------------------------------------------------------- 1 | var React = require('react') 2 | var ReactDOM = require('react-dom') 3 | var hyperx = require('hyperx') 4 | var html = hyperx(React.createElement) 5 | var monoapp = require('../../index') 6 | var devtools = require('choo-devtools') 7 | 8 | var app = monoapp() 9 | 10 | app.use(withReact) 11 | app.use(devtools()) 12 | app.use(countStore) 13 | app.route('/', mainView) 14 | app.mount('#app') 15 | 16 | function withReact (state, emitter, app) { 17 | app._mount = (tree, newTree) => ReactDOM.render(newTree, tree) 18 | app._render = (tree, newTree) => ReactDOM.render(newTree, tree) 19 | } 20 | 21 | function mainView (state, emit) { 22 | return html` 23 |
    24 |

    count is ${state.count}

    25 | 26 |
    27 | ` 28 | 29 | function onclick () { 30 | emit('increment', 1) 31 | } 32 | } 33 | 34 | function countStore (state, emitter) { 35 | state.count = 0 36 | emitter.on('increment', function (count) { 37 | state.count += count 38 | emitter.emit('render') 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /examples/with-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "budo": "^11.5.0" 14 | }, 15 | "dependencies": { 16 | "choo-devtools": "^2.5.1", 17 | "hyperx": "^2.4.0", 18 | "react": "^16.6.1", 19 | "react-dom": "^16.6.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/with-vue-jsx/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/with-vue-jsx/index.html: -------------------------------------------------------------------------------- 1 | budo
    -------------------------------------------------------------------------------- /examples/with-vue-jsx/index.js: -------------------------------------------------------------------------------- 1 | var Vue = require('vue') 2 | var monoapp = require('../../index') 3 | var devtools = require('choo-devtools') 4 | 5 | var app = monoapp() 6 | 7 | app.use(withVue) 8 | app.use(devtools()) 9 | app.use(countStore) 10 | app.route('/', mainView) 11 | app.mount('#app') 12 | 13 | function withVue (state, emitter, app) { 14 | app._mount = (tree, newTree, root) => { 15 | return new Vue({ 16 | el: tree, 17 | data: { 18 | ViewComponent: newTree 19 | }, 20 | render (h) { 21 | return h(this.ViewComponent) 22 | } 23 | }) 24 | } 25 | app._render = (tree, newTree, root) => { 26 | root.ViewComponent = newTree 27 | return root 28 | } 29 | } 30 | 31 | function mainView (state, emit) { 32 | return { 33 | render () { 34 | return ( 35 |
    36 |

    count is {state.count}

    37 | 38 |
    39 | ) 40 | } 41 | } 42 | 43 | function onclick () { 44 | emit('increment', 1) 45 | } 46 | } 47 | 48 | function countStore (state, emitter) { 49 | state.count = 0 50 | emitter.on('increment', function (count) { 51 | state.count += count 52 | emitter.emit('render') 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /examples/with-vue-jsx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "with-vue", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js -l -P -- -t [ babelify --presets [ @vue/babel-preset-jsx ] ]" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.1.5", 14 | "@vue/babel-preset-jsx": "^0.1.0", 15 | "babelify": "^10.0.0", 16 | "budo": "^11.5.0" 17 | }, 18 | "dependencies": { 19 | "choo-devtools": "^2.5.1", 20 | "vue": "^2.5.17" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var scrollToAnchor = require('scroll-to-anchor') 2 | var documentReady = require('document-ready') 3 | var nanotiming = require('nanotiming') 4 | var nanorouter = require('nanorouter') 5 | var nanoquery = require('nanoquery') 6 | var nanohref = require('nanohref') 7 | var nanoraf = require('nanoraf') 8 | var nanobus = require('nanobus') 9 | var assert = require('assert') 10 | var xtend = require('xtend') 11 | 12 | module.exports = Monoapp 13 | 14 | var HISTORY_OBJECT = {} 15 | 16 | function Monoapp (opts) { 17 | if (!(this instanceof Monoapp)) return new Monoapp(opts) 18 | opts = opts || {} 19 | 20 | assert.equal(typeof opts, 'object', 'choo: opts should be type object') 21 | 22 | var self = this 23 | 24 | // define events used by choo 25 | this._events = { 26 | DOMCONTENTLOADED: 'DOMContentLoaded', 27 | DOMTITLECHANGE: 'DOMTitleChange', 28 | REPLACESTATE: 'replaceState', 29 | PUSHSTATE: 'pushState', 30 | NAVIGATE: 'navigate', 31 | POPSTATE: 'popState', 32 | RENDER: 'render' 33 | } 34 | 35 | // properties for internal use only 36 | this._historyEnabled = opts.history === undefined ? true : opts.history 37 | this._hrefEnabled = opts.href === undefined ? true : opts.href 38 | this._hashEnabled = opts.hash === undefined ? true : opts.hash 39 | this._hasWindow = typeof window !== 'undefined' 40 | this._loaded = false 41 | this._stores = [] 42 | this._tree = null 43 | this._treeref = null 44 | this._mount = opts.mount 45 | this._render = opts.render 46 | this._toString = opts.toString 47 | 48 | // state 49 | var _state = { 50 | events: this._events, 51 | components: {} 52 | } 53 | if (this._hasWindow) { 54 | this.state = window.initialState 55 | ? xtend(window.initialState, _state) 56 | : _state 57 | delete window.initialState 58 | } else { 59 | this.state = _state 60 | } 61 | 62 | // properties that are part of the API 63 | this.router = nanorouter({ curry: true }) 64 | this.emitter = nanobus('choo.emit') 65 | this.emit = this.emitter.emit.bind(this.emitter) 66 | 67 | // listen for title changes; available even when calling .toString() 68 | if (this._hasWindow) this.state.title = document.title 69 | this.emitter.prependListener(this._events.DOMTITLECHANGE, function (title) { 70 | assert.equal(typeof title, 'string', 'events.DOMTitleChange: title should be type string') 71 | self.state.title = title 72 | if (self._hasWindow) document.title = title 73 | }) 74 | } 75 | 76 | Monoapp.prototype.route = function (route, handler) { 77 | assert.equal(typeof route, 'string', 'choo.route: route should be type string') 78 | assert.equal(typeof handler, 'function', 'choo.handler: route should be type function') 79 | this.router.on(route, handler) 80 | } 81 | 82 | Monoapp.prototype.use = function (cb) { 83 | assert.equal(typeof cb, 'function', 'choo.use: cb should be type function') 84 | var self = this 85 | this._stores.push(function (state) { 86 | var msg = 'choo.use' 87 | msg = cb.storeName ? msg + '(' + cb.storeName + ')' : msg 88 | var endTiming = nanotiming(msg) 89 | cb(state, self.emitter, self) 90 | endTiming() 91 | }) 92 | } 93 | 94 | Monoapp.prototype.start = function () { 95 | assert.equal(typeof window, 'object', 'choo.start: window was not found. .start() must be called in a browser, use .toString() if running in Node') 96 | 97 | var self = this 98 | if (this._historyEnabled) { 99 | this.emitter.prependListener(this._events.NAVIGATE, function () { 100 | self._matchRoute() 101 | if (self._loaded) { 102 | self.emitter.emit(self._events.RENDER) 103 | setTimeout(scrollToAnchor.bind(null, window.location.hash), 0) 104 | } 105 | }) 106 | 107 | this.emitter.prependListener(this._events.POPSTATE, function () { 108 | self.emitter.emit(self._events.NAVIGATE) 109 | }) 110 | 111 | this.emitter.prependListener(this._events.PUSHSTATE, function (href) { 112 | assert.equal(typeof href, 'string', 'events.pushState: href should be type string') 113 | window.history.pushState(HISTORY_OBJECT, null, href) 114 | self.emitter.emit(self._events.NAVIGATE) 115 | }) 116 | 117 | this.emitter.prependListener(this._events.REPLACESTATE, function (href) { 118 | assert.equal(typeof href, 'string', 'events.replaceState: href should be type string') 119 | window.history.replaceState(HISTORY_OBJECT, null, href) 120 | self.emitter.emit(self._events.NAVIGATE) 121 | }) 122 | 123 | window.onpopstate = function () { 124 | self.emitter.emit(self._events.POPSTATE) 125 | } 126 | 127 | if (self._hrefEnabled) { 128 | nanohref(function (location) { 129 | var href = location.href 130 | var hash = location.hash 131 | if (href === window.location.href) { 132 | if (!self._hashEnabled && hash) scrollToAnchor(hash) 133 | return 134 | } 135 | self.emitter.emit(self._events.PUSHSTATE, href) 136 | }) 137 | } 138 | } 139 | 140 | this._stores.forEach(function (initStore) { 141 | initStore(self.state) 142 | }) 143 | 144 | this._matchRoute() 145 | this._tree = this._prerender(this.state) 146 | assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href) 147 | assert.equal(typeof this._mount, 'function', 'choo: choo._mount should be a function') 148 | assert.equal(typeof this._render, 'function', 'choo: choo._render should be a function') 149 | 150 | this.emitter.prependListener(self._events.RENDER, nanoraf(function () { 151 | var renderTiming = nanotiming('choo.render') 152 | var newTree = self._prerender(self.state) 153 | assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + self.state.href) 154 | 155 | var morphTiming = nanotiming('choo.morph') 156 | self._treeref = self._render(self._tree, newTree, self._treeref) 157 | morphTiming() 158 | 159 | renderTiming() 160 | })) 161 | 162 | documentReady(function () { 163 | self.emitter.emit(self._events.DOMCONTENTLOADED) 164 | self._loaded = true 165 | }) 166 | 167 | return this._tree 168 | } 169 | 170 | Monoapp.prototype.mount = function mount (selector) { 171 | if (typeof window !== 'object') { 172 | assert.ok(typeof selector === 'string', 'choo.mount: selector should be type String') 173 | this.selector = selector 174 | return this 175 | } 176 | 177 | assert.ok(typeof selector === 'string' || typeof selector === 'object', 'choo.mount: selector should be type String or HTMLElement') 178 | 179 | var self = this 180 | 181 | documentReady(function () { 182 | var renderTiming = nanotiming('choo.render') 183 | var newTree = self.start() 184 | if (typeof selector === 'string') { 185 | self._tree = document.querySelector(selector) 186 | } else { 187 | self._tree = selector 188 | } 189 | 190 | assert.ok(self._tree, 'choo.mount: could not query selector: ' + selector) 191 | 192 | var morphTiming = nanotiming('choo.morph') 193 | self._treeref = self._mount(self._tree, newTree, self._tree.lastChild) 194 | morphTiming() 195 | 196 | renderTiming() 197 | }) 198 | } 199 | 200 | Monoapp.prototype.toString = function (location, state) { 201 | this.state = xtend(this.state, state || {}) 202 | 203 | assert.notEqual(typeof window, 'object', 'choo.mount: window was found. .toString() must be called in Node, use .start() or .mount() if running in the browser') 204 | assert.equal(typeof location, 'string', 'choo.toString: location should be type string') 205 | assert.equal(typeof this.state, 'object', 'choo.toString: state should be type object') 206 | assert.equal(typeof this._toString, 'function', 'choo: choo._toString should be a function') 207 | 208 | var self = this 209 | this._stores.forEach(function (initStore) { 210 | initStore(self.state) 211 | }) 212 | 213 | this._matchRoute(location) 214 | var html = this._prerender(this.state) 215 | assert.ok(html, 'choo.toString: no valid value returned for the route ' + location) 216 | assert(!Array.isArray(html), 'choo.toString: return value was an array for the route ' + location) 217 | return typeof html.outerHTML === 'string' ? html.outerHTML : this._toString(html) 218 | } 219 | 220 | Monoapp.prototype._matchRoute = function (locationOverride) { 221 | var location, queryString 222 | if (locationOverride) { 223 | location = locationOverride.replace(/\?.+$/, '').replace(/\/$/, '') 224 | if (!this._hashEnabled) location = location.replace(/#.+$/, '') 225 | queryString = locationOverride 226 | } else { 227 | location = window.location.pathname.replace(/\/$/, '') 228 | if (this._hashEnabled) location += window.location.hash.replace(/^#/, '/') 229 | queryString = window.location.search 230 | } 231 | var matched = this.router.match(location) 232 | this._handler = matched.cb 233 | this.state.href = location 234 | this.state.query = nanoquery(queryString) 235 | this.state.route = matched.route 236 | this.state.params = matched.params 237 | return this.state 238 | } 239 | 240 | Monoapp.prototype._prerender = function (state) { 241 | var routeTiming = nanotiming("choo.prerender('" + state.route + "')") 242 | var res = this._handler(state, this.emit) 243 | routeTiming() 244 | return res 245 | } 246 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monoapp", 3 | "version": "3.0.0", 4 | "description": "choo architecture without a renderer", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "mkdir -p dist/ && browserify index -s Monoapp -p bundle-collapser/plugin > dist/bundle.js && browserify index -s Monoapp -p tinyify > dist/bundle.min.js && cat dist/bundle.min.js | gzip --best --stdout | wc -c | pretty-bytes", 8 | "inspect": "browserify --full-paths index -p tinyify | discify --open", 9 | "prepublish": "npm run build", 10 | "deps": "dependency-check . && dependency-check . --extra --no-dev", 11 | "test": "standard --fix && standard && npm run deps" 12 | }, 13 | "repository": "jongacnik/monoapp", 14 | "keywords": [ 15 | "choo", 16 | "client", 17 | "frontend", 18 | "framework", 19 | "minimal", 20 | "composable", 21 | "tiny" 22 | ], 23 | "author": "Jon Gacnik ", 24 | "license": "MIT", 25 | "dependencies": { 26 | "document-ready": "^2.0.1", 27 | "nanobus": "^4.2.0", 28 | "nanohref": "^3.0.0", 29 | "nanoquery": "^1.1.0", 30 | "nanoraf": "^3.0.0", 31 | "nanorouter": "^4.0.0", 32 | "nanotiming": "^7.0.0", 33 | "scroll-to-anchor": "^1.0.0", 34 | "xtend": "^4.0.1" 35 | }, 36 | "devDependencies": { 37 | "browserify": "^16.2.3", 38 | "bundle-collapser": "^1.3.0", 39 | "dependency-check": "^4.0.0", 40 | "discify": "^1.6.3", 41 | "pretty-bytes-cli": "^2.0.0", 42 | "spok": "^1.0.0", 43 | "standard": "^13.0.1", 44 | "tape": "^5.0.0", 45 | "tinyify": "^2.4.3", 46 | "uglifyify": "^5.0.1", 47 | "uglifyjs": "^2.4.11", 48 | "unassertify": "^2.1.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # monoapp 2 | 3 | [![API stability](https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square)](https://nodejs.org/api/documentation.html#documentation_stability_index) 4 | [![NPM version](https://img.shields.io/npm/v/monoapp.svg?style=flat-square)](https://npmjs.org/package/monoapp) 5 | [![Standard](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com) 6 | ![Size](https://img.shields.io/badge/size-3.88kB-yellow.svg?style=flat-square) 7 | 8 | [choo](https://github.com/choojs/choo) architecture without a renderer. Bring-your-own view layer. 9 | 10 | ## Overview 11 | 12 | `monoapp` is an opinionated fork of `choo`, a small frontend framework with a simple, functional architecture. Read-up on the [choo documentation](https://github.com/choojs/choo#api) for details on routing, events, and the architecture in general. 13 | 14 | In `monoapp`, we have removed the modules used to render the dom ([nanohtml](https://github.com/choojs/nanohtml) and [nanomorph](https://github.com/choojs/nanomorph)), and made these pluggable instead. This allows us to build apps using `choo` architecture, but render views and components however we would like. See the [examples directory](https://github.com/jongacnik/monoapp/tree/master/examples/) for using with [react](https://github.com/jongacnik/monoapp/tree/master/examples/with-react), [lit-html](https://github.com/jongacnik/monoapp/tree/master/examples/with-lit-html), [vue](https://github.com/jongacnik/monoapp/tree/master/examples/with-vue-jsx), [nanomorph](https://github.com/jongacnik/monoapp/tree/master/examples/with-nanomorph), etc. 15 | 16 | ## Example 17 | 18 | Clone of the [choo example](https://github.com/choojs/choo#example), but we bring [nanohtml](https://github.com/choojs/nanohtml) and [nanomorph](https://github.com/choojs/nanomorph) ourselves. 19 | 20 | ```js 21 | var html = require('nanohtml') 22 | var morph = require('nanomorph') 23 | var monoapp = require('monoapp') 24 | var devtools = require('choo-devtools') 25 | 26 | var app = monoapp({ 27 | mount: morph, 28 | render: morph 29 | }) 30 | 31 | app.use(devtools()) 32 | app.use(countStore) 33 | app.route('/', mainView) 34 | app.mount('body') 35 | 36 | function mainView (state, emit) { 37 | return html` 38 | 39 |

    count is ${state.count}

    40 | 41 | 42 | ` 43 | 44 | function onclick () { 45 | emit('increment', 1) 46 | } 47 | } 48 | 49 | function countStore (state, emitter) { 50 | state.count = 0 51 | emitter.on('increment', function (count) { 52 | state.count += count 53 | emitter.emit('render') 54 | }) 55 | } 56 | ``` 57 | 58 | You could also choose to define `mount` and `render` using a simple plugin, rather than passing as options: 59 | 60 | ```js 61 | app.use(withNanomorph) 62 | 63 | function withNanomorph (state, emitter, app) { 64 | app._mount = morph 65 | app._render = morph 66 | } 67 | ``` 68 | 69 | ## API 70 | 71 | The only items documented here are the methods to implement. These can be defined as options when creating a `monoapp` instance, or can be set with a plugin. Refer to the [choo documentation](https://github.com/choojs/choo#api) for anything related to app architecture (routing, state, and events). 72 | 73 | ### `app._mount(tree, newTree, root)`* 74 | 75 | Mount tree onto the root: 76 | 77 | ```js 78 | app._mount = (tree, newTree, root) => nanomorph(tree, newTree) 79 | ``` 80 | 81 | ### `app._render(tree, newTree, root)`* 82 | 83 | Render new tree: 84 | 85 | ```js 86 | app._render = (tree, newTree, root) => nanomorph(tree, newTree) 87 | ``` 88 | 89 | ### `app._toString(tree)` 90 | 91 | Convert tree to string. This method is useful for ssr: 92 | 93 | ```js 94 | app._toString = (tree) => tree.toString() 95 | ``` 96 | 97 | \*Required 98 | 99 | ## Plugins 100 | 101 | Some plugins to use with `monoapp` which take care of common configs: 102 | 103 | - [monoapp-react](https://github.com/jongacnik/monoapp-react) 104 | - ~~monoapp-lit-html~~ soon 105 | - ~~monoapp-nanomorph~~ soon 106 | 107 | ## More Examples 108 | 109 | - [with-react](https://github.com/jongacnik/monoapp/tree/master/examples/with-react) 110 | - [with-react-jsx](https://github.com/jongacnik/monoapp/tree/master/examples/with-react-jsx) 111 | - [with-lit-html](https://github.com/jongacnik/monoapp/tree/master/examples/with-lit-html) 112 | - [with-vue-jsx](https://github.com/jongacnik/monoapp/tree/master/examples/with-vue-jsx) 113 | - [with-nanomorph](https://github.com/jongacnik/monoapp/tree/master/examples/with-nanomorph) 114 | - [with-preact](https://github.com/jongacnik/monoapp/tree/master/examples/with-preact) 115 | - [with-inferno](https://github.com/jongacnik/monoapp/tree/master/examples/with-inferno) 116 | 117 | ## Why does this exist? 118 | 119 | `choo` is really calm and we like to build apps using it. That said, sometimes `nanohtml` and `nanomorph` aren't the best tools for the job. We wanted to be able to build apps using `choo` architecture but swap out the view layer and make use of other component ecosystems when a project calls for it. 120 | 121 | ## Notes 122 | 123 | `monoapp` is currently feature-matched to choo 6.13.1 --------------------------------------------------------------------------------