├── html.js ├── .gitignore ├── a-can-of-kanye └── screenshot.png ├── lib ├── expose.js ├── persist.js └── logger.js ├── LICENSE ├── package.json ├── README.md ├── index.js └── example.js /html.js: -------------------------------------------------------------------------------- 1 | module.exports = require('bel') 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | dist/ 5 | npm-debug.log* 6 | .DS_Store 7 | .nyc_output 8 | -------------------------------------------------------------------------------- /a-can-of-kanye/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoshuawuyts/playground-nanoframework/HEAD/a-can-of-kanye/screenshot.png -------------------------------------------------------------------------------- /lib/expose.js: -------------------------------------------------------------------------------- 1 | module.exports = expose 2 | 3 | function expose () { 4 | return function (state, bus) { 5 | window.choo = {} 6 | window.choo.state = state 7 | window.choo.emit = function (eventName, data) { 8 | bus.emit(eventName, data) 9 | } 10 | 11 | window.choo.on = function (eventName, listener) { 12 | bus.on(eventName, listener) 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /lib/persist.js: -------------------------------------------------------------------------------- 1 | var mutate = require('xtend/mutable') 2 | var key = 'choo-todomvc' 3 | 4 | module.exports = persist 5 | 6 | function persist () { 7 | return function (state, bus) { 8 | var savedState = JSON.parse(window.localStorage.getItem('choo-todomvc')) 9 | mutate(state, savedState) 10 | 11 | bus.on('*', function (eventName, data) { 12 | window.localStorage.setItem(key, JSON.stringify(state)) 13 | }) 14 | 15 | bus.on('clear', function () { 16 | window.localStorage.setItem(key, '{}') 17 | bus.emit('log:warn', 'Wiping localStorage') 18 | }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var nanologger = require('nanologger') 2 | 3 | module.exports = logger 4 | 5 | function logger () { 6 | return function (state, bus) { 7 | var log = nanologger('choo') 8 | 9 | bus.on('*', function (eventName, data) { 10 | if (!/^log:\w{4,5}/.test(eventName)) { 11 | log.info(eventName, data) 12 | } 13 | 14 | var listeners = bus.listeners(eventName) 15 | if (eventName === 'pushState') return 16 | if (eventName === 'DOMContentLoaded') return 17 | if (!listeners.length) { 18 | log.error('No listeners for ' + eventName) 19 | } 20 | }) 21 | 22 | bus.on('log:debug', function (message, data) { 23 | log.debug(message, data) 24 | }) 25 | 26 | bus.on('log:info', function (message, data) { 27 | log.info(message, data) 28 | }) 29 | 30 | bus.on('log:warn', function (message, data) { 31 | log.warn(message, data) 32 | }) 33 | 34 | bus.on('log:error', function (message, data) { 35 | log.error(message, data) 36 | }) 37 | 38 | bus.on('log:fatal', function (message, data) { 39 | log.fatal(message, data) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-nanoframework", 3 | "description": "Building tiny frameworks yo", 4 | "repository": "yoshuawuyts/playground-nanoframework", 5 | "version": "1.0.0", 6 | "private": true, 7 | "scripts": { 8 | "deps": "dependency-check . && dependency-check . --extra --no-dev", 9 | "start": "bankai example --open", 10 | "test": "standard && npm run deps && nyc node test.js", 11 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov" 12 | }, 13 | "dependencies": { 14 | "bel": "^4.5.1", 15 | "document-ready": "^1.0.3", 16 | "morphdom": "^2.3.1", 17 | "nanobus": "^1.0.0", 18 | "nanologger": "^1.0.0", 19 | "nanomorph": "^3.0.0", 20 | "nanomount": "^1.0.0", 21 | "nanoraf": "^3.0.0", 22 | "nanorouter": "^1.0.0", 23 | "nanotick": "^1.1.6", 24 | "sheetify": "^6.0.1", 25 | "todomvc-app-css": "^2.0.6", 26 | "todomvc-common": "^1.0.3", 27 | "xtend": "^4.0.1" 28 | }, 29 | "devDependencies": { 30 | "bankai": "^5.4.0", 31 | "dependency-check": "^2.8.0", 32 | "nyc": "^10.1.2", 33 | "standard": "^8.6.0", 34 | "tape": "^4.6.3", 35 | "uglifyify": "^3.0.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playground-nanoframework 2 | Building tiny frameworks yo. Fiddling around with cool new architectury things. 3 | Mainly trying to figure out how to solve the same problems `reselect` tries to 4 | solve but like, you know - differently. 5 | 6 | Anyway. I'm sharing this because I believe developing in the open is cool. 7 | Sharing ideas and / or failed experiments is even cooler. I'd appreciate it if 8 | you let me do my thing here tho. 9 | 10 | ![kanye west quote about that silly uehhhhh, yeah creative process stuff. Dang, 11 | I should be ashamed I thinkg - but I'm not. I like that 12 | tweet](./a-can-of-kanye/screenshot.png) 13 | 14 | ## Usage 15 | ```js 16 | var html = require('./html') 17 | var choo = require('./') 18 | 19 | var app = choo() 20 | 21 | app.use(function logger (state, bus) { 22 | bus.on('*', function (messageName, data) { 23 | console.log('event', messageName, data) 24 | }) 25 | }) 26 | 27 | app.use(function counterModel (state, bus) { 28 | state.count = 0 29 | bus.on('increment', function (count) { 30 | state.count += count 31 | bus.emit('render') 32 | }) 33 | }) 34 | 35 | app.router([ '/', mainView ]) 36 | app.mount('body') 37 | 38 | function mainView (state, emit) { 39 | return html` 40 | 41 |

count is ${state.count}

42 | 43 | 44 | ` 45 | 46 | function onclick () { 47 | emit('increment', 1) 48 | } 49 | } 50 | ``` 51 | 52 | ## API 53 | ### app = framework() 54 | Create a new instance 55 | 56 | ### app.use(callback(state, emitter)) 57 | Call a function and pass it a `state` and `emitter`. `emitter` is an instance 58 | of [nanobus](https://github.com/yoshuawuyts/nanobus/). You can listen to 59 | messages by calling `bus.on()` and emit messages by calling `bus.emit()`. 60 | 61 | Choo fires messages when certain events happen: 62 | - __DOMContentLoaded:__ when the DOM has succesfully finished loading 63 | - __render:__ when the DOM re-renders 64 | 65 | ### app.router([opts], routes) 66 | Register a router 67 | 68 | ### html = app.start() 69 | Start the application 70 | 71 | ### app.mount(selector) 72 | Start the application and mount it on the given `querySelector` 73 | 74 | ### domNode = app.start() 75 | Start the application. Returns a DOM node that can be appended to the DOM. 76 | 77 | ### app.toString(location, [state]) 78 | Render the application to a string. Useful for rendering on the server 79 | 80 | ### framework/html 81 | Exposes [bel](https://github.com/shama/bel) 82 | 83 | ## License 84 | [MIT](https://tldrlegal.com/license/mit-license) 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var onHistoryChange = require('nanorouter/history') 2 | var documentReady = require('document-ready') 3 | var walkRouter = require('nanorouter/walk') 4 | var onHref = require('nanorouter/href') 5 | var nanorouter = require('nanorouter') 6 | var mutate = require('xtend/mutable') 7 | var nanomount = require('nanomount') 8 | var nanomorph = require('nanomorph') 9 | var nanotick = require('nanotick') 10 | var nanoraf = require('nanoraf') 11 | var nanobus = require('nanobus') 12 | var assert = require('assert') 13 | 14 | module.exports = Framework 15 | 16 | function Framework (opts) { 17 | opts = opts || {} 18 | 19 | var tick = nanotick() 20 | var bus = nanobus() 21 | var rerender = null 22 | var router = null 23 | var tree = null 24 | var state = {} 25 | 26 | return { 27 | router: createRouter, 28 | toString: toString, 29 | use: register, 30 | mount: mount, 31 | start: start 32 | } 33 | 34 | function createRouter (opts, routes) { 35 | if (!routes) { 36 | routes = opts 37 | opts = {} 38 | } 39 | 40 | var routerOpts = mutate({ thunk: 'match' }, opts) 41 | router = nanorouter(routerOpts, routes) 42 | 43 | walkRouter(router, function (route, handler) { 44 | return function chooWrap (params) { 45 | return function (state, emit) { 46 | state.params = params 47 | return handler(state, emit) 48 | } 49 | } 50 | }) 51 | } 52 | 53 | function register (cb) { 54 | cb(state, bus) 55 | } 56 | 57 | function start () { 58 | tree = router(createLocation(), state, tick(emit)) 59 | rerender = nanoraf(function () { 60 | var newTree = router(createLocation(), state, tick(emit)) 61 | tree = nanomorph(tree, newTree) 62 | }) 63 | 64 | bus.on('render', rerender) 65 | 66 | if (opts.history !== false) { 67 | onHistoryChange(function (href) { 68 | bus.emit('pushState', window.location.href) 69 | scrollIntoView() 70 | }) 71 | 72 | if (opts.href !== false) { 73 | onHref(function (location) { 74 | var href = location.href 75 | var currHref = window.location.href 76 | if (href === currHref) return 77 | window.history.pushState({}, null, href) 78 | bus.emit('pushState', window.location.href) 79 | bus.emit('render') 80 | scrollIntoView() 81 | }) 82 | } 83 | } 84 | 85 | documentReady(function () { 86 | bus.emit('DOMContentLoaded') 87 | }) 88 | 89 | return tree 90 | 91 | function emit (eventName, data) { 92 | bus.emit(eventName, data) 93 | } 94 | } 95 | 96 | function mount (selector) { 97 | var newTree = start() 98 | documentReady(function () { 99 | var root = document.querySelector(selector) 100 | assert.ok(root, 'could not query selector: ' + selector) 101 | nanomount(root, newTree) 102 | tree = root 103 | }) 104 | } 105 | 106 | function toString (location, state) { 107 | state = state || {} 108 | return router(location, state) 109 | } 110 | } 111 | 112 | function scrollIntoView () { 113 | var hash = window.location.hash 114 | if (hash) { 115 | try { 116 | var el = document.querySelector(hash) 117 | if (el) el.scrollIntoView(true) 118 | } catch (e) {} 119 | } 120 | } 121 | 122 | function createLocation () { 123 | var pathname = window.location.pathname.replace(/\/$/, '') 124 | var hash = window.location.hash.replace(/^#/, '/') 125 | return pathname + hash 126 | } 127 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var persist = require('./lib/persist') 2 | var mutate = require('xtend/mutable') 3 | var expose = require('./lib/expose') 4 | var logger = require('./lib/logger') 5 | var css = require('sheetify') 6 | var html = require('bel') 7 | var choo = require('./') 8 | 9 | css('todomvc-common/base.css') 10 | css('todomvc-app-css/index.css') 11 | 12 | var app = choo() 13 | app.use(persist()) 14 | app.use(expose()) 15 | app.use(logger()) 16 | app.use(todosModel()) 17 | 18 | app.router([ 19 | ['/', mainView], 20 | ['#active', mainView], 21 | ['#completed', mainView] 22 | ]) 23 | app.mount('body') 24 | 25 | function mainView (state, emit) { 26 | emit('log:debug', 'Rendering main view') 27 | return html` 28 | 29 |
30 | ${Header(state, emit)} 31 | ${TodoList(state, emit)} 32 | ${Footer(state, emit)} 33 |
34 | 39 | 40 | ` 41 | } 42 | 43 | function todosModel () { 44 | return function (state, bus) { 45 | var localState = state.todos 46 | 47 | if (!localState) { 48 | localState = state.todos = {} 49 | 50 | localState.active = [] 51 | localState.done = [] 52 | localState.all = [] 53 | 54 | localState.idCounter = 0 55 | } 56 | 57 | bus.on('DOMContentLoaded', function () { 58 | bus.emit('log:debug', 'Loading todos model') 59 | 60 | // CRUD 61 | bus.on('todos:create', create) 62 | bus.on('todos:update', update) 63 | bus.on('todos:delete', del) 64 | 65 | // Shorthand 66 | bus.on('todos:edit', edit) 67 | bus.on('todos:unedit', unedit) 68 | bus.on('todos:toggle', toggle) 69 | bus.on('todos:toggleAll', toggleAll) 70 | bus.on('todos:deleteCompleted', deleteCompleted) 71 | }) 72 | 73 | function create (data) { 74 | var item = { 75 | id: localState.idCounter, 76 | name: data.name, 77 | editing: false, 78 | done: false 79 | } 80 | 81 | localState.idCounter += 1 82 | localState.active.push(item) 83 | localState.all.push(item) 84 | bus.emit('render') 85 | } 86 | 87 | function edit (id) { 88 | localState.all.forEach(function (todo) { 89 | if (todo.id === id) todo.editing = true 90 | }) 91 | bus.emit('render') 92 | } 93 | 94 | function unedit (id) { 95 | localState.all.forEach(function (todo) { 96 | if (todo.id === id) todo.editing = false 97 | }) 98 | bus.emit('render') 99 | } 100 | 101 | function update (newTodo) { 102 | var todo = localState.all.filter(function (todo) { 103 | return todo.id === newTodo.id 104 | })[0] 105 | var isChanged = newTodo.done === todo.done 106 | var isDone = todo.done 107 | mutate(todo, newTodo) 108 | 109 | if (isChanged) { 110 | var arr = isDone ? localState.done : localState.active 111 | var target = isDone ? localState.active : localState.done 112 | var index = arr.indexOf[todo] 113 | arr.splice(index, 1) 114 | target.push(todo) 115 | } 116 | bus.emit('render') 117 | } 118 | 119 | function del (id) { 120 | var i = null 121 | var todo = null 122 | state.todos.all.forEach(function (_todo, j) { 123 | if (_todo.id === id) { 124 | i = j 125 | todo = _todo 126 | } 127 | }) 128 | state.todos.all.splice(i, 1) 129 | 130 | if (todo.done) { 131 | var done = localState.done 132 | var doneIndex = done[todo] 133 | done.splice(doneIndex, 1) 134 | } else { 135 | var active = localState.active 136 | var activeIndex = active[todo] 137 | active.splice(activeIndex, 1) 138 | } 139 | bus.emit('render') 140 | } 141 | 142 | function deleteCompleted (data) { 143 | var done = localState.done 144 | done.forEach(function (todo) { 145 | var index = state.todos.all.indexOf(todo) 146 | state.todos.all.splice(index, 1) 147 | }) 148 | localState.done = [] 149 | bus.emit('render') 150 | } 151 | 152 | function toggle (id) { 153 | var todo = localState.all.filter(function (todo) { 154 | return todo.id === id 155 | })[0] 156 | var done = todo.done 157 | todo.done = !done 158 | var arr = done ? localState.done : localState.active 159 | var target = done ? localState.active : localState.done 160 | var index = arr.indexOf[todo] 161 | arr.splice(index, 1) 162 | target.push(todo) 163 | bus.emit('render') 164 | } 165 | 166 | function toggleAll (data) { 167 | var todos = localState.all 168 | var allDone = localState.all.length && 169 | localState.done.length === localState.all.length 170 | 171 | todos.forEach(function (todo) { 172 | todo.done = !allDone 173 | }) 174 | 175 | if (allDone) { 176 | localState.done = localState.all 177 | localState.active = [] 178 | } else { 179 | localState.done = [] 180 | localState.active = localState.all 181 | } 182 | 183 | bus.emit('render') 184 | } 185 | } 186 | } 187 | 188 | function Footer (state, emit) { 189 | var filter = window.location.hash.replace(/^#/, '') 190 | var activeCount = state.todos.active.length 191 | var hasDone = state.todos.done.length 192 | 193 | return html` 194 | 206 | ` 207 | 208 | function filterButton (name, filter, currentFilter, emit) { 209 | var filterClass = filter === currentFilter 210 | ? 'selected' 211 | : '' 212 | 213 | var uri = '#' + name.toLowerCase() 214 | if (uri === '#all') uri = '/' 215 | return html` 216 |
  • 217 | 218 | ${name} 219 | 220 |
  • 221 | ` 222 | } 223 | 224 | function deleteCompleted (emit) { 225 | return html` 226 | 229 | ` 230 | 231 | function deleteAllCompleted () { 232 | emit('todos:deleteCompleted') 233 | } 234 | } 235 | } 236 | 237 | function Header (todos, emit) { 238 | return html` 239 |
    240 |

    todos

    241 | 245 |
    246 | ` 247 | 248 | function createTodo (e) { 249 | if (e.keyCode === 13) { 250 | emit('todos:create', { name: e.target.value }) 251 | e.target.value = '' 252 | } 253 | } 254 | } 255 | 256 | function TodoItem (todo, emit) { 257 | return html` 258 |
  • 259 |
    260 | 265 | 266 | 270 |
    271 | 276 |
  • 277 | ` 278 | 279 | function toggle (e) { 280 | emit('todos:toggle', todo.id) 281 | } 282 | 283 | function edit (e) { 284 | emit('todos:edit', todo.id) 285 | } 286 | 287 | function destroy (e) { 288 | emit('todos:delete', todo.id) 289 | } 290 | 291 | function update (e) { 292 | emit('todos:update', { 293 | id: todo.id, 294 | editing: false, 295 | name: e.target.value 296 | }) 297 | } 298 | 299 | function handleEditKeydown (e) { 300 | if (e.keyCode === 13) update(e) // Enter 301 | else if (e.code === 27) emit('todos:unedit') // Escape 302 | } 303 | 304 | function classList (classes) { 305 | var str = '' 306 | var keys = Object.keys(classes) 307 | for (var i = 0, len = keys.length; i < len; i++) { 308 | var key = keys[i] 309 | var val = classes[key] 310 | if (val) str += (key + ' ') 311 | } 312 | return str 313 | } 314 | } 315 | 316 | function TodoList (state, emit) { 317 | var filter = window.location.hash.replace(/^#/, '') 318 | var items = filter === 'completed' 319 | ? state.todos.done 320 | : filter === 'active' 321 | ? state.todos.active 322 | : state.todos.all 323 | 324 | var allDone = state.todos.done.length === state.todos.all.length 325 | 326 | var nodes = items.map(function (todo) { 327 | return TodoItem(todo, emit) 328 | }) 329 | 330 | return html` 331 |
    332 | 337 | 340 | 343 |
    344 | ` 345 | 346 | function toggleAll () { 347 | emit('todos:toggleAll') 348 | } 349 | } 350 | --------------------------------------------------------------------------------