choo components (#639)
* add components * example: componentize header * fix choo SSR * example: componentize footer * example: fix footer * update component cache asserts * componentize todos * reorder example dir * fix example tests * fix dep check test * Add garbage collection of unused components * Apply args when calling identity * update to new component preview * update nanocomponent * fix cache err name * remove static methods from example * restore app.emit() function * public API tests * offset component cache iteration by 2 * whitelist contents of component folder (#643) * allow lru number arg * fix lint typo
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
var assert = require('assert')
|
||||||
|
var LRU = require('nanolru')
|
||||||
|
|
||||||
|
module.exports = ChooComponentCache
|
||||||
|
|
||||||
|
function ChooComponentCache (state, emit, lru) {
|
||||||
|
assert.ok(this instanceof ChooComponentCache, 'ChooComponentCache should be created with `new`')
|
||||||
|
|
||||||
|
assert.equal(typeof state, 'object', 'ChooComponentCache: state should be type object')
|
||||||
|
assert.equal(typeof emit, 'function', 'ChooComponentCache: emit should be type function')
|
||||||
|
|
||||||
|
if (typeof lru === 'number') this.cache = new LRU(lru)
|
||||||
|
else this.cache = lru || new LRU(100)
|
||||||
|
this.state = state
|
||||||
|
this.emit = emit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get & create component instances.
|
||||||
|
ChooComponentCache.prototype.render = function (Component, id) {
|
||||||
|
assert.equal(typeof Component, 'function', 'ChooComponentCache.render: Component should be type function')
|
||||||
|
assert.ok(typeof id === 'string' || typeof id === 'number', 'ChooComponentCache.render: id should be type string or type number')
|
||||||
|
|
||||||
|
var el = this.cache.get(id)
|
||||||
|
if (!el) {
|
||||||
|
var args = []
|
||||||
|
for (var i = 2, len = arguments.length; i < len; i++) {
|
||||||
|
args.push(arguments[i])
|
||||||
|
}
|
||||||
|
args.unshift(Component, id, this.state, this.emit)
|
||||||
|
el = newCall.apply(newCall, args)
|
||||||
|
this.cache.set(id, el)
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because you can't call `new` and `.apply()` at the same time. This is a mad
|
||||||
|
// hack, but hey it works so we gonna go for it. Whoop.
|
||||||
|
function newCall (Cls) {
|
||||||
|
return new (Cls.bind.apply(Cls, arguments)) // eslint-disable-line
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
module.exports = require('nanocomponent')
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
module.exports = deleteCompleted
|
||||||
|
|
||||||
|
function deleteCompleted (emit) {
|
||||||
|
return html`
|
||||||
|
<button class="clear-completed" onclick=${deleteAllCompleted}>
|
||||||
|
Clear completed
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
|
||||||
|
function deleteAllCompleted () {
|
||||||
|
emit('todos:deleteCompleted')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
module.exports = filterButton
|
||||||
|
|
||||||
|
function filterButton (name, filter, currentFilter, emit) {
|
||||||
|
var filterClass = filter === currentFilter
|
||||||
|
? 'selected'
|
||||||
|
: ''
|
||||||
|
|
||||||
|
var uri = '#' + name.toLowerCase()
|
||||||
|
if (uri === '#all') uri = '/'
|
||||||
|
|
||||||
|
return html`<li>
|
||||||
|
<a href=${uri} class=${filterClass}>
|
||||||
|
${name}
|
||||||
|
</a>
|
||||||
|
</li>`
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
var Component = require('../../../component')
|
||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
var clearButton = require('./clear-button')
|
||||||
|
var filterButton = require('./filter-button')
|
||||||
|
|
||||||
|
module.exports = class Footer extends Component {
|
||||||
|
constructor (name, state, emit) {
|
||||||
|
super(name)
|
||||||
|
this.state = state
|
||||||
|
this.emit = emit
|
||||||
|
|
||||||
|
this.local = this.state.components.footer = {}
|
||||||
|
this.setState()
|
||||||
|
}
|
||||||
|
|
||||||
|
setState () {
|
||||||
|
this.local.rawTodos = this.state.todos.clock
|
||||||
|
this.local.rawHref = this.state.href
|
||||||
|
|
||||||
|
this.local.filter = this.state.href.replace(/^\//, '') || ''
|
||||||
|
this.local.activeCount = this.state.todos.active.length
|
||||||
|
this.local.hasDone = this.state.todos.done.length || null
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
if (this.local.rawTodos !== this.state.todos.clock ||
|
||||||
|
this.local.rawHref !== this.state.href) {
|
||||||
|
this.setState()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createElement () {
|
||||||
|
return html`<footer class="footer">
|
||||||
|
<span class="todo-count">
|
||||||
|
<strong>${this.local.activeCount}</strong>
|
||||||
|
item${this.state.todos.all === 1 ? '' : 's'} left
|
||||||
|
</span>
|
||||||
|
<ul class="filters">
|
||||||
|
${filterButton('All', '', this.local.filter, this.emit)}
|
||||||
|
${filterButton('Active', 'active', this.local.filter, this.emit)}
|
||||||
|
${filterButton('Completed', 'completed', this.local.filter, this.emit)}
|
||||||
|
</ul>
|
||||||
|
${this.local.hasDone && clearButton(this.emit)}
|
||||||
|
</footer>`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
var Component = require('../../component')
|
||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
module.exports = class Header extends Component {
|
||||||
|
constructor (name, state, emit) {
|
||||||
|
super(name)
|
||||||
|
this.state = state
|
||||||
|
this.emit = emit
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
createElement () {
|
||||||
|
return html`<header class="header">
|
||||||
|
<h1>todos</h1>
|
||||||
|
<input class="new-todo"
|
||||||
|
autofocus
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
onkeydown=${this.createTodo.bind(this)} />
|
||||||
|
</header>`
|
||||||
|
}
|
||||||
|
|
||||||
|
createTodo (e) {
|
||||||
|
var value = e.target.value
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
e.target.value = ''
|
||||||
|
this.emit('todos:create', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
var Component = require('../../component')
|
||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
module.exports = class Info extends Component {
|
||||||
|
update () {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
createElement () {
|
||||||
|
return html`<footer class="info">
|
||||||
|
<p>Double-click to edit a todo</p>
|
||||||
|
<p>choo by <a href="https://yoshuawuyts.com/">Yoshua Wuyts</a></p>
|
||||||
|
<p>Created by <a href="http://shuheikagawa.com">Shuhei Kagawa</a></p>
|
||||||
|
</footer>`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
var Component = require('../../../component')
|
||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
var Todo = require('./todo')
|
||||||
|
|
||||||
|
module.exports = class Header extends Component {
|
||||||
|
constructor (name, state, emit) {
|
||||||
|
super(name)
|
||||||
|
this.state = state
|
||||||
|
this.emit = emit
|
||||||
|
this.local = this.state.components[name] = {}
|
||||||
|
this.setState()
|
||||||
|
}
|
||||||
|
|
||||||
|
setState () {
|
||||||
|
this.local.rawTodos = this.state.todos.clock
|
||||||
|
this.local.rawHref = this.state.href
|
||||||
|
|
||||||
|
this.local.allDone = this.state.todos.done.length === this.state.todos.all.length
|
||||||
|
this.local.filter = this.state.href.replace(/^\//, '') || ''
|
||||||
|
this.local.todos = this.local.filter === 'completed'
|
||||||
|
? this.state.todos.done
|
||||||
|
: this.local.filter === 'active'
|
||||||
|
? this.state.todos.active
|
||||||
|
: this.state.todos.all
|
||||||
|
}
|
||||||
|
|
||||||
|
update () {
|
||||||
|
if (this.local.rawTodos !== this.state.todos.clock ||
|
||||||
|
this.local.rawHref !== this.state.href) {
|
||||||
|
this.setState()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createElement () {
|
||||||
|
return html`<section class="main">
|
||||||
|
<input
|
||||||
|
class="toggle-all"
|
||||||
|
type="checkbox"
|
||||||
|
checked=${this.local.allDone}
|
||||||
|
onchange=${() => this.toggleAll()}/>
|
||||||
|
<label for="toggle-all" style="display: none;">
|
||||||
|
Mark all as done
|
||||||
|
</label>
|
||||||
|
<ul class="todo-list">
|
||||||
|
${this.local.todos.map(todo => Todo(todo, this.emit))}
|
||||||
|
</ul>
|
||||||
|
</section>`
|
||||||
|
}
|
||||||
|
|
||||||
|
createTodo (e) {
|
||||||
|
var value = e.target.value
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
e.target.value = ''
|
||||||
|
this.emit('todos:create', value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAll () {
|
||||||
|
this.emit('todos:toggleAll')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
var html = require('bel')
|
||||||
|
|
||||||
|
module.exports = Todo
|
||||||
|
|
||||||
|
function Todo (todo, emit) {
|
||||||
|
var clx = classList({ completed: todo.done, editing: todo.editing })
|
||||||
|
return html`
|
||||||
|
<li id=${todo.id} class=${clx}>
|
||||||
|
<div class="view">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle"
|
||||||
|
checked="${todo.done}"
|
||||||
|
onchange=${toggle} />
|
||||||
|
<label ondblclick=${edit}>${todo.name}</label>
|
||||||
|
<button
|
||||||
|
class="destroy"
|
||||||
|
onclick=${destroy}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
class="edit"
|
||||||
|
value=${todo.name}
|
||||||
|
onkeydown=${handleEditKeydown}
|
||||||
|
onblur=${update} />
|
||||||
|
</li>
|
||||||
|
`
|
||||||
|
|
||||||
|
function toggle (e) {
|
||||||
|
emit('todos:toggle', todo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit (e) {
|
||||||
|
emit('todos:edit', todo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy (e) {
|
||||||
|
emit('todos:delete', todo.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function update (e) {
|
||||||
|
emit('todos:update', {
|
||||||
|
id: todo.id,
|
||||||
|
editing: false,
|
||||||
|
name: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditKeydown (e) {
|
||||||
|
if (e.keyCode === 13) update(e) // Enter
|
||||||
|
else if (e.code === 27) emit('todos:unedit') // Escape
|
||||||
|
}
|
||||||
|
|
||||||
|
function classList (classes) {
|
||||||
|
var str = ''
|
||||||
|
var keys = Object.keys(classes)
|
||||||
|
for (var i = 0, len = keys.length; i < len; i++) {
|
||||||
|
var key = keys[i]
|
||||||
|
var val = classes[key]
|
||||||
|
if (val) str += (key + ' ')
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-5
@@ -8,11 +8,11 @@ var app = choo()
|
|||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
app.use(require('choo-devtools')())
|
app.use(require('choo-devtools')())
|
||||||
}
|
}
|
||||||
app.use(require('./store'))
|
app.use(require('./stores/todos'))
|
||||||
|
|
||||||
app.route('/', require('./view'))
|
app.route('/', require('./views/main'))
|
||||||
app.route('#active', require('./view'))
|
app.route('#active', require('./views/main'))
|
||||||
app.route('#completed', require('./view'))
|
app.route('#completed', require('./views/main'))
|
||||||
app.route('*', require('./view'))
|
app.route('*', require('./views/main'))
|
||||||
|
|
||||||
module.exports = app.mount('body')
|
module.exports = app.mount('body')
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
module.exports = todoStore
|
module.exports = todoStore
|
||||||
|
|
||||||
function todoStore (state, emitter) {
|
function todoStore (state, emitter) {
|
||||||
if (!state.todos) {
|
state.todos = {
|
||||||
state.todos = {}
|
clock: 0,
|
||||||
|
idCounter: 0,
|
||||||
state.todos.active = []
|
active: [],
|
||||||
state.todos.done = []
|
done: [],
|
||||||
state.todos.all = []
|
all: []
|
||||||
|
|
||||||
state.todos.idCounter = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always reset when application boots
|
|
||||||
state.todos.input = ''
|
|
||||||
|
|
||||||
// Register emitters after DOM is loaded to speed up DOM loading
|
|
||||||
emitter.on('DOMContentLoaded', function () {
|
emitter.on('DOMContentLoaded', function () {
|
||||||
// CRUD
|
|
||||||
emitter.on('todos:create', create)
|
emitter.on('todos:create', create)
|
||||||
emitter.on('todos:update', update)
|
emitter.on('todos:update', update)
|
||||||
emitter.on('todos:delete', del)
|
emitter.on('todos:delete', del)
|
||||||
|
|
||||||
// Special
|
|
||||||
emitter.on('todos:input', oninput)
|
|
||||||
|
|
||||||
// Shorthand
|
|
||||||
emitter.on('todos:edit', edit)
|
emitter.on('todos:edit', edit)
|
||||||
emitter.on('todos:unedit', unedit)
|
emitter.on('todos:unedit', unedit)
|
||||||
emitter.on('todos:toggle', toggle)
|
emitter.on('todos:toggle', toggle)
|
||||||
@@ -32,10 +20,6 @@ function todoStore (state, emitter) {
|
|||||||
emitter.on('todos:deleteCompleted', deleteCompleted)
|
emitter.on('todos:deleteCompleted', deleteCompleted)
|
||||||
})
|
})
|
||||||
|
|
||||||
function oninput (text) {
|
|
||||||
state.todos.input = text
|
|
||||||
}
|
|
||||||
|
|
||||||
function create (name) {
|
function create (name) {
|
||||||
var item = {
|
var item = {
|
||||||
id: state.todos.idCounter,
|
id: state.todos.idCounter,
|
||||||
@@ -47,21 +31,21 @@ function todoStore (state, emitter) {
|
|||||||
state.todos.idCounter += 1
|
state.todos.idCounter += 1
|
||||||
state.todos.active.push(item)
|
state.todos.active.push(item)
|
||||||
state.todos.all.push(item)
|
state.todos.all.push(item)
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit (id) {
|
function edit (id) {
|
||||||
state.todos.all.forEach(function (todo) {
|
state.todos.all.forEach(function (todo) {
|
||||||
if (todo.id === id) todo.editing = true
|
if (todo.id === id) todo.editing = true
|
||||||
})
|
})
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function unedit (id) {
|
function unedit (id) {
|
||||||
state.todos.all.forEach(function (todo) {
|
state.todos.all.forEach(function (todo) {
|
||||||
if (todo.id === id) todo.editing = false
|
if (todo.id === id) todo.editing = false
|
||||||
})
|
})
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (newTodo) {
|
function update (newTodo) {
|
||||||
@@ -78,7 +62,7 @@ function todoStore (state, emitter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(todo, newTodo)
|
Object.assign(todo, newTodo)
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function del (id) {
|
function del (id) {
|
||||||
@@ -111,7 +95,7 @@ function todoStore (state, emitter) {
|
|||||||
})
|
})
|
||||||
active.splice(activeIndex, 1)
|
active.splice(activeIndex, 1)
|
||||||
}
|
}
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCompleted (data) {
|
function deleteCompleted (data) {
|
||||||
@@ -121,7 +105,7 @@ function todoStore (state, emitter) {
|
|||||||
state.todos.all.splice(index, 1)
|
state.todos.all.splice(index, 1)
|
||||||
})
|
})
|
||||||
state.todos.done = []
|
state.todos.done = []
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle (id) {
|
function toggle (id) {
|
||||||
@@ -135,7 +119,7 @@ function todoStore (state, emitter) {
|
|||||||
var index = arr.indexOf(todo)
|
var index = arr.indexOf(todo)
|
||||||
arr.splice(index, 1)
|
arr.splice(index, 1)
|
||||||
target.push(todo)
|
target.push(todo)
|
||||||
emitter.emit('render')
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAll (data) {
|
function toggleAll (data) {
|
||||||
@@ -155,6 +139,11 @@ function todoStore (state, emitter) {
|
|||||||
state.todos.active = []
|
state.todos.active = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
}
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
state.todos.clock += 1
|
||||||
emitter.emit('render')
|
emitter.emit('render')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-20
@@ -2,7 +2,7 @@ var EventEmitter = require('events').EventEmitter
|
|||||||
var spok = require('spok')
|
var spok = require('spok')
|
||||||
var tape = require('tape')
|
var tape = require('tape')
|
||||||
|
|
||||||
var todoStore = require('./store')
|
var todoStore = require('./stores/todos')
|
||||||
|
|
||||||
tape('should initialize empty state', function (t) {
|
tape('should initialize empty state', function (t) {
|
||||||
var emitter = new EventEmitter()
|
var emitter = new EventEmitter()
|
||||||
@@ -19,25 +19,6 @@ tape('should initialize empty state', function (t) {
|
|||||||
t.end()
|
t.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
tape('restore previous state', function (t) {
|
|
||||||
var emitter = new EventEmitter()
|
|
||||||
var state = {
|
|
||||||
todos: {
|
|
||||||
idCounter: 100,
|
|
||||||
active: [],
|
|
||||||
done: [],
|
|
||||||
all: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
todoStore(state, emitter)
|
|
||||||
spok(t, state, {
|
|
||||||
todos: {
|
|
||||||
idCounter: 100
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.end()
|
|
||||||
})
|
|
||||||
|
|
||||||
tape('todos:create', function (t) {
|
tape('todos:create', function (t) {
|
||||||
var emitter = new EventEmitter()
|
var emitter = new EventEmitter()
|
||||||
var state = {}
|
var state = {}
|
||||||
|
|||||||
-190
@@ -1,190 +0,0 @@
|
|||||||
var html = require('bel') // cannot require choo/html because it's a nested repo
|
|
||||||
|
|
||||||
module.exports = mainView
|
|
||||||
|
|
||||||
function mainView (state, emit) {
|
|
||||||
emit('log:debug', 'Rendering main view')
|
|
||||||
return html`
|
|
||||||
<body>
|
|
||||||
<section class="todoapp">
|
|
||||||
${Header(state, emit)}
|
|
||||||
${TodoList(state, emit)}
|
|
||||||
${Footer(state, emit)}
|
|
||||||
</section>
|
|
||||||
<footer class="info">
|
|
||||||
<p>Double-click to edit a todo</p>
|
|
||||||
<p>choo by <a href="https://yoshuawuyts.com/">Yoshua Wuyts</a></p>
|
|
||||||
<p>Created by <a href="http://shuheikagawa.com">Shuhei Kagawa</a></p>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function Header (state, emit) {
|
|
||||||
return html`
|
|
||||||
<header class="header">
|
|
||||||
<h1>todos</h1>
|
|
||||||
<input class="new-todo"
|
|
||||||
value=${state.todos.input}
|
|
||||||
autofocus
|
|
||||||
placeholder="What needs to be done?"
|
|
||||||
onkeydown=${createTodo} />
|
|
||||||
</header>
|
|
||||||
`
|
|
||||||
|
|
||||||
function createTodo (e) {
|
|
||||||
var value = e.target.value
|
|
||||||
if (!value) return
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
emit('todos:input', '')
|
|
||||||
emit('todos:create', value)
|
|
||||||
} else {
|
|
||||||
emit('todos:input', value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Footer (state, emit) {
|
|
||||||
var filter = state.href.replace(/^\//, '') || ''
|
|
||||||
var activeCount = state.todos.active.length
|
|
||||||
var hasDone = state.todos.done.length
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<footer class="footer">
|
|
||||||
<span class="todo-count">
|
|
||||||
<strong>${activeCount}</strong>
|
|
||||||
item${state.todos.all === 1 ? '' : 's'} left
|
|
||||||
</span>
|
|
||||||
<ul class="filters">
|
|
||||||
${filterButton('All', '', filter, emit)}
|
|
||||||
${filterButton('Active', 'active', filter, emit)}
|
|
||||||
${filterButton('Completed', 'completed', filter, emit)}
|
|
||||||
</ul>
|
|
||||||
${hasDone ? deleteCompleted(emit) : ''}
|
|
||||||
</footer>
|
|
||||||
`
|
|
||||||
|
|
||||||
function filterButton (name, filter, currentFilter, emit) {
|
|
||||||
var filterClass = filter === currentFilter
|
|
||||||
? 'selected'
|
|
||||||
: ''
|
|
||||||
|
|
||||||
var uri = '#' + name.toLowerCase()
|
|
||||||
if (uri === '#all') uri = '/'
|
|
||||||
return html`
|
|
||||||
<li>
|
|
||||||
<a href=${uri} class=${filterClass}>
|
|
||||||
${name}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteCompleted (emit) {
|
|
||||||
return html`
|
|
||||||
<button class="clear-completed" onclick=${deleteAllCompleted}>
|
|
||||||
Clear completed
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
|
|
||||||
function deleteAllCompleted () {
|
|
||||||
emit('todos:deleteCompleted')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function TodoItem (todo, emit) {
|
|
||||||
var clx = classList({ completed: todo.done, editing: todo.editing })
|
|
||||||
return html`
|
|
||||||
<li id=${todo.id} class=${clx}>
|
|
||||||
<div class="view">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle"
|
|
||||||
checked="${todo.done}"
|
|
||||||
onchange=${toggle} />
|
|
||||||
<label ondblclick=${edit}>${todo.name}</label>
|
|
||||||
<button
|
|
||||||
class="destroy"
|
|
||||||
onclick=${destroy}
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
class="edit"
|
|
||||||
value=${todo.name}
|
|
||||||
onkeydown=${handleEditKeydown}
|
|
||||||
onblur=${update} />
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
|
|
||||||
function toggle (e) {
|
|
||||||
emit('todos:toggle', todo.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function edit (e) {
|
|
||||||
emit('todos:edit', todo.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function destroy (e) {
|
|
||||||
emit('todos:delete', todo.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function update (e) {
|
|
||||||
emit('todos:update', {
|
|
||||||
id: todo.id,
|
|
||||||
editing: false,
|
|
||||||
name: e.target.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditKeydown (e) {
|
|
||||||
if (e.keyCode === 13) update(e) // Enter
|
|
||||||
else if (e.code === 27) emit('todos:unedit') // Escape
|
|
||||||
}
|
|
||||||
|
|
||||||
function classList (classes) {
|
|
||||||
var str = ''
|
|
||||||
var keys = Object.keys(classes)
|
|
||||||
for (var i = 0, len = keys.length; i < len; i++) {
|
|
||||||
var key = keys[i]
|
|
||||||
var val = classes[key]
|
|
||||||
if (val) str += (key + ' ')
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function TodoList (state, emit) {
|
|
||||||
var filter = state.href.replace(/^\//, '') || ''
|
|
||||||
var items = filter === 'completed'
|
|
||||||
? state.todos.done
|
|
||||||
: filter === 'active'
|
|
||||||
? state.todos.active
|
|
||||||
: state.todos.all
|
|
||||||
|
|
||||||
var allDone = state.todos.done.length === state.todos.all.length
|
|
||||||
|
|
||||||
var nodes = items.map(function (todo) {
|
|
||||||
return TodoItem(todo, emit)
|
|
||||||
})
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<section class="main">
|
|
||||||
<input
|
|
||||||
class="toggle-all"
|
|
||||||
type="checkbox"
|
|
||||||
checked=${allDone}
|
|
||||||
onchange=${toggleAll}/>
|
|
||||||
<label for="toggle-all" style="display: none;">
|
|
||||||
Mark all as done
|
|
||||||
</label>
|
|
||||||
<ul class="todo-list">
|
|
||||||
${nodes}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
|
|
||||||
function toggleAll () {
|
|
||||||
emit('todos:toggleAll')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
var html = require('bel') // cannot require choo/html because it's a nested repo
|
||||||
|
|
||||||
|
var Header = require('../components/header')
|
||||||
|
var Footer = require('../components/footer')
|
||||||
|
var Todos = require('../components/todos')
|
||||||
|
var Info = require('../components/info')
|
||||||
|
|
||||||
|
module.exports = mainView
|
||||||
|
|
||||||
|
function mainView (state, emit) {
|
||||||
|
return html`
|
||||||
|
<body>
|
||||||
|
<section class="todoapp">
|
||||||
|
${state.cache(Header, 'header').render()}
|
||||||
|
${state.cache(Todos, 'todos').render()}
|
||||||
|
${state.cache(Footer, 'footer').render()}
|
||||||
|
</section>
|
||||||
|
${state.cache(Info, 'info').render()}
|
||||||
|
</body>
|
||||||
|
`
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ var nanobus = require('nanobus')
|
|||||||
var assert = require('assert')
|
var assert = require('assert')
|
||||||
var xtend = require('xtend')
|
var xtend = require('xtend')
|
||||||
|
|
||||||
|
var Cache = require('./component/cache')
|
||||||
|
|
||||||
module.exports = Choo
|
module.exports = Choo
|
||||||
|
|
||||||
var HISTORY_OBJECT = {}
|
var HISTORY_OBJECT = {}
|
||||||
@@ -39,25 +41,30 @@ function Choo (opts) {
|
|||||||
this._hrefEnabled = opts.href === undefined ? true : opts.href
|
this._hrefEnabled = opts.href === undefined ? true : opts.href
|
||||||
this._hasWindow = typeof window !== 'undefined'
|
this._hasWindow = typeof window !== 'undefined'
|
||||||
this._createLocation = nanolocation
|
this._createLocation = nanolocation
|
||||||
|
this._cache = opts.cache
|
||||||
this._loaded = false
|
this._loaded = false
|
||||||
this._stores = []
|
this._stores = []
|
||||||
this._tree = null
|
this._tree = null
|
||||||
|
|
||||||
// properties that are part of the API
|
// state
|
||||||
this.router = nanorouter()
|
var _state = {
|
||||||
this.emitter = nanobus('choo.emit')
|
events: this._events,
|
||||||
this.emit = this.emitter.emit.bind(this.emitter)
|
components: {}
|
||||||
|
}
|
||||||
var events = { events: this._events }
|
|
||||||
if (this._hasWindow) {
|
if (this._hasWindow) {
|
||||||
this.state = window.initialState
|
this.state = window.initialState
|
||||||
? xtend(window.initialState, events)
|
? xtend(window.initialState, _state)
|
||||||
: events
|
: _state
|
||||||
delete window.initialState
|
delete window.initialState
|
||||||
} else {
|
} else {
|
||||||
this.state = events
|
this.state = _state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// properties that are part of the API
|
||||||
|
this.router = nanorouter({ curry: true })
|
||||||
|
this.emitter = nanobus('choo.emit')
|
||||||
|
this.emit = this.emitter.emit.bind(this.emitter)
|
||||||
|
|
||||||
// listen for title changes; available even when calling .toString()
|
// listen for title changes; available even when calling .toString()
|
||||||
if (this._hasWindow) this.state.title = document.title
|
if (this._hasWindow) this.state.title = document.title
|
||||||
this.emitter.prependListener(this._events.DOMTITLECHANGE, function (title) {
|
this.emitter.prependListener(this._events.DOMTITLECHANGE, function (title) {
|
||||||
@@ -76,11 +83,11 @@ Choo.prototype.route = function (route, handler) {
|
|||||||
Choo.prototype.use = function (cb) {
|
Choo.prototype.use = function (cb) {
|
||||||
assert.equal(typeof cb, 'function', 'choo.use: cb should be type function')
|
assert.equal(typeof cb, 'function', 'choo.use: cb should be type function')
|
||||||
var self = this
|
var self = this
|
||||||
this._stores.push(function () {
|
this._stores.push(function (state) {
|
||||||
var msg = 'choo.use'
|
var msg = 'choo.use'
|
||||||
msg = cb.storeName ? msg + '(' + cb.storeName + ')' : msg
|
msg = cb.storeName ? msg + '(' + cb.storeName + ')' : msg
|
||||||
var endTiming = nanotiming(msg)
|
var endTiming = nanotiming(msg)
|
||||||
cb(self.state, self.emitter, self)
|
cb(state, self.emitter, self)
|
||||||
endTiming()
|
endTiming()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -128,8 +135,9 @@ Choo.prototype.start = function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._setCache(this.state)
|
||||||
this._stores.forEach(function (initStore) {
|
this._stores.forEach(function (initStore) {
|
||||||
initStore()
|
initStore(self.state)
|
||||||
})
|
})
|
||||||
|
|
||||||
this._matchRoute()
|
this._matchRoute()
|
||||||
@@ -200,9 +208,10 @@ Choo.prototype.toString = function (location, state) {
|
|||||||
assert.equal(typeof location, 'string', 'choo.toString: location should be type string')
|
assert.equal(typeof location, 'string', 'choo.toString: location should be type string')
|
||||||
assert.equal(typeof this.state, 'object', 'choo.toString: state should be type object')
|
assert.equal(typeof this.state, 'object', 'choo.toString: state should be type object')
|
||||||
|
|
||||||
// TODO: pass custom state down to each store.
|
var self = this
|
||||||
|
this._setCache(this.state)
|
||||||
this._stores.forEach(function (initStore) {
|
this._stores.forEach(function (initStore) {
|
||||||
initStore()
|
initStore(self.state)
|
||||||
})
|
})
|
||||||
|
|
||||||
this._matchRoute(location)
|
this._matchRoute(location)
|
||||||
@@ -236,3 +245,23 @@ Choo.prototype._prerender = function (state) {
|
|||||||
routeTiming()
|
routeTiming()
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Choo.prototype._setCache = function (state) {
|
||||||
|
var cache = new Cache(state, this.emitter.emit.bind(this.emitter), this._cache)
|
||||||
|
state.cache = renderComponent
|
||||||
|
|
||||||
|
function renderComponent (Component, id) {
|
||||||
|
assert.equal(typeof Component, 'function', 'choo.state.cache: Component should be type function')
|
||||||
|
var args = []
|
||||||
|
for (var i = 0, len = arguments.length; i < len; i++) {
|
||||||
|
args.push(arguments[i])
|
||||||
|
}
|
||||||
|
return cache.render.apply(cache, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the state gets stringified, make sure `state.cache` isn't
|
||||||
|
// stringified too.
|
||||||
|
renderComponent.toJSON = function () {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -9,12 +9,14 @@
|
|||||||
"html/index.js",
|
"html/index.js",
|
||||||
"html/raw.js",
|
"html/raw.js",
|
||||||
"html/index.d.ts",
|
"html/index.d.ts",
|
||||||
|
"component/cache.js",
|
||||||
|
"component/index.js",
|
||||||
"dist",
|
"dist",
|
||||||
"example"
|
"example"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "mkdir -p dist/ && browserify index -p bundle-collapser/plugin > dist/bundle.js && browserify index -p tinyify > dist/bundle.min.js && cat dist/bundle.min.js | gzip --best --stdout | wc -c | pretty-bytes",
|
"build": "mkdir -p dist/ && browserify index -p bundle-collapser/plugin > dist/bundle.js && browserify index -p tinyify > dist/bundle.min.js && cat dist/bundle.min.js | gzip --best --stdout | wc -c | pretty-bytes",
|
||||||
"deps": "dependency-check --entry ./html/index.js . && dependency-check . --extra --no-dev --entry ./html/index.js",
|
"deps": "dependency-check --entry ./html/index.js . && dependency-check . --extra --no-dev --entry ./html/index.js --entry ./component/index.js",
|
||||||
"inspect": "browserify --full-paths index -g unassertify -g uglifyify | discify --open",
|
"inspect": "browserify --full-paths index -g unassertify -g uglifyify | discify --open",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"start": "bankai start example",
|
"start": "bankai start example",
|
||||||
@@ -34,8 +36,10 @@
|
|||||||
"bel": "^5.1.3",
|
"bel": "^5.1.3",
|
||||||
"document-ready": "^2.0.1",
|
"document-ready": "^2.0.1",
|
||||||
"nanobus": "^4.2.0",
|
"nanobus": "^4.2.0",
|
||||||
|
"nanocomponent": "^6.5.0",
|
||||||
"nanohref": "^3.0.0",
|
"nanohref": "^3.0.0",
|
||||||
"nanolocation": "^1.0.0",
|
"nanolocation": "^1.0.0",
|
||||||
|
"nanolru": "^1.0.0",
|
||||||
"nanomorph": "^5.1.2",
|
"nanomorph": "^5.1.2",
|
||||||
"nanoquery": "^1.1.0",
|
"nanoquery": "^1.1.0",
|
||||||
"nanoraf": "^3.0.0",
|
"nanoraf": "^3.0.0",
|
||||||
|
|||||||
@@ -29,3 +29,19 @@ tape('should render on the server with hyperscript', function (t) {
|
|||||||
t.equal(res.toString().trim(), exp, 'result was OK')
|
t.equal(res.toString().trim(), exp, 'result was OK')
|
||||||
t.end()
|
t.end()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
tape('should expose a public API', function (t) {
|
||||||
|
var app = choo()
|
||||||
|
|
||||||
|
t.equal(typeof app.route, 'function', 'app.route prototype method exists')
|
||||||
|
t.equal(typeof app.toString, 'function', 'app.toString prototype method exists')
|
||||||
|
t.equal(typeof app.start, 'function', 'app.start prototype method exists')
|
||||||
|
t.equal(typeof app.mount, 'function', 'app.mount prototype method exists')
|
||||||
|
t.equal(typeof app.emitter, 'object', 'app.emitter prototype method exists')
|
||||||
|
|
||||||
|
t.equal(typeof app.emit, 'function', 'app.emit instance method exists')
|
||||||
|
t.equal(typeof app.router, 'object', 'app.router instance object exists')
|
||||||
|
t.equal(typeof app.state, 'object', 'app.state instance object exists')
|
||||||
|
|
||||||
|
t.end()
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user