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.