Cherrytree for Knockout

cherrytree-for-knockout is a way to define your routing in your Knockout-based applications. Inspired by components, it allows you to associate templates and view models in a heirarchial fashion, similar to libraries like angular-ui-router.

Interactive Example


var demoSite = {
  // we mark this abstract as we don't want this page to be
  // routable directly, only it's children
  abstract: true,
  path: '/',
  // we declare any data that needs to be resolved
  // or injected into the view model constructor
  // in the resolve block here. Promises are used as most often this
  // is where you need to fetch something from the server
  // before rendering or loading the view.
  resolve: {
    forums: function(transition, resolutions) {
      // query the server for all the forums.
      // return axios.get(....).then(r => r.data)
      return Promise.resolve([{
        id: 7,
        name: 'cherrytree-for-knockout demo forum'
      }])
    }
  },
  // in the real world you would import your templates through
  // your favorite module system
  // like `require('text!./forum-templates.html')`
  // only the root routeView binding needs to be passed the router.
  // Nested routeViews do not need any options.
  template: `
<section class="demo-site">
  <h1 data-bind="text: header"></h1>
  <div data-bind="routeView: true"></div>
</section>`,
  // view models are called with new, but you can just return an object as seen below.
  viewModel: function() {
    this.header = 'A forum board example'
  }
},

forumList = {
  path: '',
  // the routeHref binding is an easy way to generate a link to another route.
  template: `
<ul class="forums" data-bind="foreach: forums">
  <li><a data-bind="routeHref: { name: 'threads.list', params: { forumId: id } },
    text: name"></a></li>
</ul>`,
  // you can just sreturn the params if there is nothing else to add.
  viewModel: function(params) {
   return params
  }
},

forum = {
  // paths are relative extentions of where they are used, for better composability
  path: ':forumId',
  abstract: true,
  template: '\
<section class="forum">\
  <h2 data-bind="text: title"></h2>\
  <div data-bind="routeView: true"></div>\
</section>',
  resolve: {
    forum: (t, r) => Promise.resolve(r.forums.find(f => f.id == t.params.forumId)),
    threads: function(transition, resolutions) {
      // query the server for the threads in this forum, e.g.
      // return axios.get(....).then(r => r.data)
      return Promise.resolve([{
        id: 1,
        name: 'Welcome to the forums!'
      }, {
        id: 2,
        name: 'A newbie question...'
      }])
    }
  },
  viewModel: function(params, route) {
    this.title = 'Viewing forum ' + params.forum.name
  }
},

threadsList = {
  // when specifying params to routeHref, you only need to specify additional params
  template: `
<ul class="threads" data-bind="foreach: threads">
  <li><a data-bind="routeHref: { name: \'thread\', params: { threadId: id } },
   text: name"></a></li>
</ul>`,
  viewModel: function(params) {
    return params
  }
},

thread = {
  resolve: {
    thread: (tr, r) => Promise.resolve(
      r.threads.find(t => t.id == tr.params.threadId)),
    posts: function(transition, resolutions) {
      // Again this would be queried from the server
      // return axios.get(....).then(r => r.data)
      return Promise.resolve([{
        author: 'Bob',
        text: 'Sample post'
      }, {
        author: 'Jill',
        text: 'Sample reply'
      }, {
        author: 'Bob',
        text: 'Thank you!'
      }])
    }
  },
  path: 'threads/:threadId',
  // if you use routeHref with just a string, it will use that
  // as the route name and reuse the current params
  template: `
    <section class="thread">
      <nav><a data-bind="routeHref: 'threads.list'">Back to
      <span data-bind="text: forum.name"></span></a></nav>
      <h4 data-bind="text: thread.title"></h4>
      <ul data-bind="foreach: posts">
        <span class="author" data-bind="text: author"></span>
        <li data-bind="text: text"></li>
      </ul>
    </section>`,
  viewModel: function(params, route) {
    Object.assign(this, params)
  }
},

router = cherrytree({ pushState: false })

router.map(function(route) {
  route('demo-site', demoSite, function() {
    route('forums.list', forumList)
    route('forum', forum, function() {
      route('threads.list', threadsList)
      route('thread', thread)
    })
  })
})

router.use(ko.bindingHandlers.routeView.middleware)
router.listen()

ko.applyBindings({ router }, document.querySelector('main'))