├── 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 | 
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 |
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 |
344 | `
345 |
346 | function toggleAll () {
347 | emit('todos:toggleAll')
348 | }
349 | }
350 |
--------------------------------------------------------------------------------