Skip to content

Commit

Permalink
choo components (#639)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
yoshuawuyts authored Mar 30, 2018
1 parent 357d508 commit bddcfbe
Show file tree
Hide file tree
Showing 17 changed files with 411 additions and 259 deletions.
41 changes: 41 additions & 0 deletions component/cache.js
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions component/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('nanocomponent')
15 changes: 15 additions & 0 deletions example/components/footer/clear-button.js
Original file line number Diff line number Diff line change
@@ -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')
}
}
18 changes: 18 additions & 0 deletions example/components/footer/filter-button.js
Original file line number Diff line number Diff line change
@@ -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>`
}
50 changes: 50 additions & 0 deletions example/components/footer/index.js
Original file line number Diff line number Diff line change
@@ -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>`
}
}
32 changes: 32 additions & 0 deletions example/components/header.js
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
16 changes: 16 additions & 0 deletions example/components/info.js
Original file line number Diff line number Diff line change
@@ -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>`
}
}
65 changes: 65 additions & 0 deletions example/components/todos/index.js
Original file line number Diff line number Diff line change
@@ -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')
}
}
64 changes: 64 additions & 0 deletions example/components/todos/todo.js
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 5 additions & 5 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Loading

0 comments on commit bddcfbe

Please sign in to comment.