Dynamic HTML with Knockout AirConf 2014

Brian M Hunt

Brian's Blog: brianmhunt.github.io
Presentation repository: brianmhunt/Airconf-2014-Knockout

The following are the steps and respective excerpts from the Atom.io snippets/snippets.cson, used to generate the code during the presentation.

  1. Part 1: Getting Started
    1. (html)
      Step 1: Add <script> tags
      
      <!-- SCRIPTS -->
      <script src='../lib/knockout.js'></script>
      <script src='../lib/lodash.js'></script>
      <!-- plugins -->
      <script src='../lib/knockout-projections.js'></script>
      <script src='../lib/knockout.punches.js'></script>
      <!-- Some data -->
      <script src='../data/hosd.js'></script>
      <!-- Our javascript (we'll be editing) -->
      <script src="./my.js"></script>
    2. (js)
      Step 2: Add KO to my.js
      var view = {
        defaults: window.hosd
      };
      
      ko.applyBindings(view);
  2. Part 2: Show a list of items
    1. (html)
      Step 3: Add a bound list to the screen
      <h1>List of State defaults</h1>
      
      <ul data-bind='foreach: defaults'>
        <li>
          <span data-bind='text: year'></span>
        </li>
      </ul>
    2. (html)
      Step 4: Switch to {{ }} interpolation from ko.punches
      <b>{{ year }}</b> {{ state }}
    3. (js)
      Step 5: Enable punches
      ko.punches.enableAll();
  3. Part 3: Grep items
    1. (html)
      Step 6: Add grepping
      <input type='text' data-bind='textInput: grep'/>
    2. (js)
      Step 7: Use projections to filter the view
      function filter(item) {
        if (!view.grep()) {
          return true;
        }
        return (item.year + " " + item.state).toLowerCase()
          .indexOf(view.grep().toLowerCase()) >= 0;
      }
      
      var view = {
        grep: ko.observable(),
      };
      
      view.defaults = ko.observableArray(window.hosd).filter(filter);
    3. (js)
      Step 8: Use rate limit to improve performance
      grep: ko.observable().extend({rateLimit: 200}),
  4. Part 4: Reuse items with WebComponents
    1. (js)
      Step 9: Registering a knockout component for state-default
      ko.components.register("state-default", {
        viewModel: function(params) {
          this.year = params.data.year || "unknown";
          this.state = params.data.state;
          this.comment = params.data.comment;
        },
        template: "<b>{{ year }}</b> {{ state }}"
      });
    2. (html)
      Step 10: Change to a web component
      <state-default params="data: $data"></state-default>
    3. (js)
      Step 11: Change the template to an element-by-reference
      template: {element: "state-default-template"}
    4. (html)
      Step 12: Add state-default-template <template>
      <b>{{ year }}</b> {{ state }}
  5. Part 5: Using the components: binding
    1. (html)
      Step 13: Uh oh - Change state template to a <tr>
      <table>
        <thead>
          <th> Year </th> <th> State </th> <th> Comment </th>
        </thead>
        <tbody data-bind='foreach: defaults'>
          <tr data-bind='component: {
              name: "state-default-tr", params: {data: $data} }'>
          </tr>
        </tbody>
      </table>
      
      <template id='state-default-tr-template'>
        <td> {{ year }} </td> <td> {{ state }} </td> <td> {{ comment }} </td>
      </template>
    2. (js)
      Step 14: Change ko.component to <tr>/row
      ko.components.register("state-default-tr", {
        viewModel: function(params) {
          this.year = params.data.year || "unknown";
          this.state = params.data.state;
          this.comment = params.data.comment;
        },
        template: {element: "state-default-tr-template"}
      });
  6. Part 6: Sorting with filters
    1. (html)
      Step 16: Add sort-by html
      <div class='sort'>
        Sort by ({{ sort_by()|default:"unsorted" }}):<br/>
        <button data-bind='click: sort_click'>None</button>
        <button data-bind='click: sort_click'>State</button>
        <button data-bind='click: sort_click'>Year</button>
      </div>
    2. (js)
      Step 17: Add sort-by and results to the view
      var view = {
        grep: ko.observable().extend({rateLimit: 200}),
        sort_by: ko.observable(),
        sort_click: function (vm, evt) {
          view.sort_by(evt.target.innerText);
        }
      };
      
      var unsorted_defaults = ko.observableArray(window.hosd)
        .filter(filter)
        .extend({rateLimit: 200});
      var sortable_defaults = ko.observableArray(unsorted_defaults());
      unsorted_defaults.subscribe(sortable_defaults)
      
      function year_sort(a, b) {
        return a.year == b.year ? 0 : (a.year < b.year ? -1 : 1);
      }
      
      function state_sort(a, b) {
        return a.state == b.state ? 0 : (a.state < b.state ? -1 : 1);
      }
      
      view.defaults = ko.computed(function () {
        var sort_by = view.sort_by();
        var items = sortable_defaults();
        if (!sort_by || sort_by == 'None') {
          return items;
        }
        return items.sort(sort_by == 'State' ? state_sort : year_sort)
      });
    3. (js)
      Step 19: Uh-oh erratic results
      var view = {
        grep: ko.observable().extend({rateLimit: 200}),
        sort_by: ko.observable(),
        defaults: ko.observableArray(),
        sort_click: function (vm, evt) {
          view.sort_by(evt.target.innerText);
        },
      };
      
      var unsorted_defaults = ko.observableArray(window.hosd).filter(filter);
      
      function year_sort(a, b) {
        return a.year == b.year ? 0 : (a.year < b.year ? -1 : 1);
      }
      
      function state_sort(a, b) {
        return a.state == b.state ? 0 : (a.state < b.state ? -1 : 1);
      }
      
      function sort_defaults_fn() {
        var sort_by = view.sort_by();
        var items = unsorted_defaults().concat();
        if (!sort_by || sort_by == 'None') {
          view.defaults(items);
        }
        view.defaults(items.sort(sort_by == 'State' ? state_sort : year_sort))
      }
      var sort_defaults = _.debounce(sort_defaults_fn, 200);
      
      unsorted_defaults.subscribe(sort_defaults);
      view.sort_by.subscribe(sort_defaults);
      sort_defaults();
    4. (js)
      Step 20: Knowing when not to .filter
      var view = {
        grep: ko.observable().extend({rateLimit: 200}),
        sort_by: ko.observable(),
        sort_click: function (vm, evt) {
          view.sort_by(evt.target.innerText);
        },
      };
      
      function year_sort(a, b) {
        x = _.parseInt(a.year);
        y = _.parseInt(b.year);
        return x == y ? 0 : (x < y ? -1 : 1);
      }
      
      function state_sort(a, b) {
        return a.state == b.state ? 0 : (a.state < b.state ? -1 : 1);
      }
      
      function compute_defaults() {
        var items = _(window.hosd)
          .filter(filter)
          .value();
      
        if (view.sort_by() == 'State') {
          items.sort(state_sort);
        } else if (view.sort_by()) {
          items.sort(year_sort);
        }
      
        return items;
      }
      
      view.defaults = ko.computed(compute_defaults);

This page is also using Knockout with web components. Check out todo.js and index.html at github.com/brianmhunt/brianmhunt.github.io/tree/master/airconf-2014