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,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') {
|
||||
app.use(require('choo-devtools')())
|
||||
}
|
||||
app.use(require('./store'))
|
||||
app.use(require('./stores/todos'))
|
||||
|
||||
app.route('/', require('./view'))
|
||||
app.route('#active', require('./view'))
|
||||
app.route('#completed', require('./view'))
|
||||
app.route('*', require('./view'))
|
||||
app.route('/', require('./views/main'))
|
||||
app.route('#active', require('./views/main'))
|
||||
app.route('#completed', require('./views/main'))
|
||||
app.route('*', require('./views/main'))
|
||||
|
||||
module.exports = app.mount('body')
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
module.exports = todoStore
|
||||
|
||||
function todoStore (state, emitter) {
|
||||
if (!state.todos) {
|
||||
state.todos = {}
|
||||
|
||||
state.todos.active = []
|
||||
state.todos.done = []
|
||||
state.todos.all = []
|
||||
|
||||
state.todos.idCounter = 0
|
||||
state.todos = {
|
||||
clock: 0,
|
||||
idCounter: 0,
|
||||
active: [],
|
||||
done: [],
|
||||
all: []
|
||||
}
|
||||
|
||||
// Always reset when application boots
|
||||
state.todos.input = ''
|
||||
|
||||
// Register emitters after DOM is loaded to speed up DOM loading
|
||||
emitter.on('DOMContentLoaded', function () {
|
||||
// CRUD
|
||||
emitter.on('todos:create', create)
|
||||
emitter.on('todos:update', update)
|
||||
emitter.on('todos:delete', del)
|
||||
|
||||
// Special
|
||||
emitter.on('todos:input', oninput)
|
||||
|
||||
// Shorthand
|
||||
emitter.on('todos:edit', edit)
|
||||
emitter.on('todos:unedit', unedit)
|
||||
emitter.on('todos:toggle', toggle)
|
||||
@@ -32,10 +20,6 @@ function todoStore (state, emitter) {
|
||||
emitter.on('todos:deleteCompleted', deleteCompleted)
|
||||
})
|
||||
|
||||
function oninput (text) {
|
||||
state.todos.input = text
|
||||
}
|
||||
|
||||
function create (name) {
|
||||
var item = {
|
||||
id: state.todos.idCounter,
|
||||
@@ -47,21 +31,21 @@ function todoStore (state, emitter) {
|
||||
state.todos.idCounter += 1
|
||||
state.todos.active.push(item)
|
||||
state.todos.all.push(item)
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function edit (id) {
|
||||
state.todos.all.forEach(function (todo) {
|
||||
if (todo.id === id) todo.editing = true
|
||||
})
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function unedit (id) {
|
||||
state.todos.all.forEach(function (todo) {
|
||||
if (todo.id === id) todo.editing = false
|
||||
})
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function update (newTodo) {
|
||||
@@ -78,7 +62,7 @@ function todoStore (state, emitter) {
|
||||
}
|
||||
|
||||
Object.assign(todo, newTodo)
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function del (id) {
|
||||
@@ -111,7 +95,7 @@ function todoStore (state, emitter) {
|
||||
})
|
||||
active.splice(activeIndex, 1)
|
||||
}
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function deleteCompleted (data) {
|
||||
@@ -121,7 +105,7 @@ function todoStore (state, emitter) {
|
||||
state.todos.all.splice(index, 1)
|
||||
})
|
||||
state.todos.done = []
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function toggle (id) {
|
||||
@@ -135,7 +119,7 @@ function todoStore (state, emitter) {
|
||||
var index = arr.indexOf(todo)
|
||||
arr.splice(index, 1)
|
||||
target.push(todo)
|
||||
emitter.emit('render')
|
||||
render()
|
||||
}
|
||||
|
||||
function toggleAll (data) {
|
||||
@@ -155,6 +139,11 @@ function todoStore (state, emitter) {
|
||||
state.todos.active = []
|
||||
}
|
||||
|
||||
render()
|
||||
}
|
||||
|
||||
function render () {
|
||||
state.todos.clock += 1
|
||||
emitter.emit('render')
|
||||
}
|
||||
}
|
||||
+1
-20
@@ -2,7 +2,7 @@ var EventEmitter = require('events').EventEmitter
|
||||
var spok = require('spok')
|
||||
var tape = require('tape')
|
||||
|
||||
var todoStore = require('./store')
|
||||
var todoStore = require('./stores/todos')
|
||||
|
||||
tape('should initialize empty state', function (t) {
|
||||
var emitter = new EventEmitter()
|
||||
@@ -19,25 +19,6 @@ tape('should initialize empty state', function (t) {
|
||||
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) {
|
||||
var emitter = new EventEmitter()
|
||||
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>
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user