MVC

Problem

I want to create a rich web app and need a bit more than a view layer.

MVC comes to mind with the following qualities:

Solution

Choo provides a simple framework to accomplish this, with a few caveats:

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const choo = require('choo')
const html = require('choo/html')
const get = require('lodash.get')
const set = require('lodash.set')
class Model {
constructor(storage){
this.storage = storage
}
async getPosts(userId){
const posts = get(this.storage, `posts`)
return Object.entries(posts).map(([id, post]) => {
post.id = id
post.likes = Object.keys(post.likes || {})
return post
})
}
async setLike(userId, postId){
set(this.storage, `posts.${postId}.likes.${userId}`, true)
}
}
function store(model, state, emitter){
state.userId = '1'
emitter.on('*', console.log)
emitter.on('posts:load', () => {
model.getPosts(state.userId).then(posts => {
state.posts = posts
emitter.emit('render')
})
})
emitter.on('posts:like', postId => {
model.setLike(state.userId, postId).then(() => {
emitter.emit('posts:load')
})
})
}
function postsView(state, emit){
function onLike(postId){
return () => emit('posts:like', postId)
}
const posts = state.posts.map(post => {
return html`
<li>
${post.text}
<span onclick=
${onLike(post.id)}>👍 (${post.likes.length})</span>
</li>
`

})
return html`
<body>
<ul>
${posts}
</ul>
</body>
`

}
var app = choo()
const storage = {
posts: {
1: {
text: 'a',
likes: {
2: true
}
},
2: {
text: 'b'
},
3: {
text: 'c'
}
}
}
const model = new Model(storage)
app.use(store.bind(store, model))
app.route('/', postsView)
app.mount('body')
app.emitter.emit('posts:load')

Alternative

A Choo-inspired, but more literal interpretation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const html = require('bel')
const qs = require('sheet-router/qs')
const EventEmitter = require('events')
const wayfarer = require('wayfarer')
const yo = require('yo-yo')
function createApp(state, win, router, renderer, queryParser, createEmitter){
var render;
return {
use: function(route, controller, view){
router.on(route, params => {
state.route = route
state.params = params
state.query = queryParser(win.location.href)
const emitter = createEmitter()
const emit = emitter.emit.bind(emitter)
emitter.on('render', () => {
render(view(state, emit))
})
emitter.on('navigate', href => {
win.history.pushState({}, state.title, href)
router(win.location.pathname)
})
controller(state, emitter)
})
},
mount: function(el){
render = renderer.update.bind(renderer, el)
router(win.location.pathname)
}
}
}
const model = {
todos: ['a', 'b', 'c']
}
function view(state, emit){
const filter = html`
<input placeholder="Filter" value="
${state.query.filter}" onkeyup=${onKeyUp}>
`

function onKeyUp(e){
emit('filter', e.target.value)
}
var items = state.todos.map(todo => html`
<li>
${todo}</li>
`
)
return html`
<body>
${filter}
<ul>
${items}
</ul>
</body>
`

}
function controller(model, state, emitter){
emitter.on('filter', filter => {
emitter.emit('navigate', `/?filter=${filter}`)
})
const pattern = new RegExp(state.query.filter)
state.todos = model.todos.filter(pattern.test.bind(pattern))
emitter.emit('render')
}

const app = createApp({}, window, wayfarer('/404'), yo, qs,
function(){ return new EventEmitter() })
app.use('/', controller.bind(controller, model), view)
app.mount(document.body)

Feedback

Thoughts? Suggestions? Hit me up @erikeldridge

License

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 4.0 International License, and code samples are licensed under the MIT license.