>

TodoMVC

<section id="todoapp"></section>
<div class="template">
  <section class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input class="new-todo" placeholder="What needs to be done?" autofocus 
             keydown.enter+="$.onEnter" keyup+="$.onChange" value@="$.newTodo">
    </header>
    <section class="main" x-style="display@: $.todos.length ? '' : 'none';">
      <input type="checkbox" class="toggle-all" id="toggle-all" checked@="$.allDone" change+="$.onToggle">
      <label for="toggle-all">Mark all as complete</label>
      <ul class="todo-list" x-ref="list">
        <li x-type="TodoAdapter" x-for="todo of $.todos | filter($.status)" x-key="todo.id"
            label#="todo.label" completed#="todo.completed" destroy+="$.removeTodo(todo)"></li>
      </ul>
    </section>
    <footer class="footer" x-style="display@: $.todos.length | display;">
      <span class="todo-count">
        <strong x-ref="strong">@{ $.remainingCount }</strong> @{ $.remainingCount | pluralize('item') } left
      </span>
      <ul class="filters">
        <li>
          <a x-class="selected@: $.status === 'all'" href="#/">All</a>
        </li>
        <li>
          <a x-class="selected@: $.status === 'active'" href="#/active">Active</a>
        </li>
        <li>
          <a x-class="selected@: $.status === 'completed'" href="#/completed">Completed</a>
        </li>
      </ul>
      <button class="clear-completed" click+="$.clearCompleted()"
              x-style="display@: $.todos.length > $.remainingCount | display;" >Clear completed</button>
    </footer>
  </section>
</div>
<ul class="template">
  <li class="todo-adapter" x-class="completed@: $.completed; editing@: $.editing;">
    <div class="view">
      <input type="checkbox" class="toggle" checked@="$.completed" change+="$.onToggle">
      <label dblclick+="$.startEditing()">@{ $.label }</label>
      <button class="destroy" click+="$.destroy()"></button>
    </div>
    <input class="edit" x-ref="editor" value@="$.text" change+="$.onChange"
           keyup.escape+="$.cancelEditing()" keyup.enter+="$.doneEditing()" blur+="$.doneEditing()">
  </li>
</ul>
.template {
  display: none;
}
var Skin = Exact.Skin;
var Store = Exact.Store;
var Component = Exact.Component;
var Collection = Exact.Collection;
var id = 0;
var STATUS = {
  ALL: 'all', ACTIVE: 'active', COMPLETED: 'completed'
};
function filter(todos, status) {
  return todos.filter(function(todo) {
    return status === STATUS.ALL || (status === STATUS.COMPLETED ? todo.completed : !todo.completed);
  });
}
var ENJ = {
  read: function() {
    var item = localStorage.getItem('todos');
    if (item) {
      return JSON.parse(item);
    }
  },
  save: function(todos) {
    localStorage.setItem('todos', JSON.stringify(todos));
  }
};
ENJ.TodoAdapter = Exact.defineClass({
  extend: Component,
  statics: {
    template: Skin.query('.template .todo-adapter'),
    defaults: function() {
      return {
        completed: false,
        editing: false,
        label: '',
        text: ''
      }
    }
  },
  register: function() {
    Exact.help(this).bind('onToggle', 'onChange');
  },
  onToggle: function(event) {
    this.set('completed', event.target.checked);
  },
  onChange: function(event) {
    this.set('text', event.target.value.trim());
  },
  startEditing: function() {
    this.set('text', this.label);
    this.set('editing', true);
    this.editor.focus();
  },
  doneEditing: function() {
    if (this.editing) {
      this.submit(this.text);
    }
    this.cancelEditing();
  },
  cancelEditing: function() {
    this.set('editing', false);
  },
  submit: function(text) {
    if (text) {
      this.set('label', text);
    } else {
      this.destroy();
    }
  },
  destroy: function() {
    this.send('destroy');
  }
});
ENJ.TodoApp = Exact.defineClass({
  extend: Component,
  statics: {
    defaults: function() {
      return {
        todos: Collection.from([]),
        status: STATUS.ALL,
        newTodo: '',
        remainingCount: 0
      }
    },
    descriptors: {
      allDone: {
        depends: ['remainingCount'],
        get: function() {
          return this.remainingCount === 0;
        },
        set: function(value) {
          this.todos.forEach(function(todo) {
            todo.set('completed', value);
          });
        }
      }
    },
    resources: {
      filter: filter,
      display: function(visible) {
        return visible ? '' : 'none';
      },
      pluralize: function(num, str) {
        return str + (num !== 1 ? 's' : '');
      },
      TodoAdapter: ENJ.TodoAdapter
    },
    template: Skin.query('.template .todoapp')
  },
  register: function() {
    Exact.help(this).bind('onEnter', 'onChange', 'onToggle');
  },
  ready: function() {
    var records = ENJ.read();
    if (records) {
      for (var i = 0, n = records.length; i < n; ++i) {
        this.addTodo(Store.create(records[i]));
      }
    }
  },
  refresh: function() {
    var remainingCount = this.remainingCount;
    this.set('remainingCount', this.todos.length - filter(this.todos, STATUS.COMPLETED).length);
    if (this.status !== STATUS.ALL && this.remainingCount !== remainingCount ) {
      this.send('changed.todos');
    }
    ENJ.save(this.todos.slice(0));
  },
  addTodo: function(todo) {
    todo.on('changed.completed', this.invalidate);
    todo.id = ++id;
    this.todos.insert(todo);
    this.invalidate();
  },
  removeTodo: function(todo) {
    this.todos.remove(todo);
    this.invalidate();
    todo.off();
  },
  clearCompleted: function() {
    for (var i = this.todos.length - 1; i >= 0; --i) {
      if (this.todos[i].completed) {
        this.todos.splice(i, 1);
      }
    }
  },
  onToggle: function(event) {
    this.set('allDone', event.target.checked);
  },
  onChange: function(event) {
    this.set('newTodo', event.target.value.trim());
  },
  onEnter: function(event) {
    if (this.newTodo) {
      this.addTodo(Store.create({
        label: this.newTodo,
        completed: false
      }));
    }
    this.set('newTodo', '');
  }
});
var app = Component.create(ENJ.TodoApp);
function onHashChange () {
  var hash = window.location.hash;
  var status = STATUS[hash.replace(/#\/?/, '').toUpperCase()];
  app.set('status', status || STATUS.ALL);
}
onHashChange();
window.onhashchange = onHashChange;
app.attach(Skin.query('#todoapp'));