2016-05-12 22:42:17 +00:00
|
|
|
/*
|
2016-05-15 20:22:36 +00:00
|
|
|
todomvc spec is at: https://github.com/tastejs/todomvc/blob/master/app-spec.md
|
2016-05-15 19:01:41 +00:00
|
|
|
- BUG: transitions don't happen because the nodes are being replaced rather than edited.
|
2016-05-15 18:55:01 +00:00
|
|
|
*/
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-05-15 18:55:01 +00:00
|
|
|
assertion type todo(id, title, completed);
|
2016-05-13 02:23:58 +00:00
|
|
|
|
2016-05-15 20:16:22 +00:00
|
|
|
message type createTodo(title);
|
2016-05-15 18:55:01 +00:00
|
|
|
message type setTitle(id, title);
|
|
|
|
message type setCompleted(id, completed);
|
|
|
|
message type deleteTodo(id);
|
|
|
|
message type clearCompletedTodos();
|
|
|
|
message type setAllCompleted(completed);
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-05-15 18:55:01 +00:00
|
|
|
// Derived model state
|
|
|
|
assertion type activeTodoCount(n);
|
|
|
|
assertion type completedTodoCount(n);
|
|
|
|
assertion type totalTodoCount(n);
|
|
|
|
assertion type allCompleted();
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-05-15 18:55:01 +00:00
|
|
|
// View state
|
|
|
|
assertion type show(completed);
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
2016-05-13 00:10:16 +00:00
|
|
|
|
2016-05-15 20:16:22 +00:00
|
|
|
function todoListItemModel(initialId, initialTitle, initialCompleted) {
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-08-25 12:12:32 +00:00
|
|
|
field this.id = initialId;
|
|
|
|
field this.title = initialTitle;
|
|
|
|
field this.completed = initialCompleted;
|
2016-08-07 19:33:09 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
stop on message deleteTodo(this.id);
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
assert todo(this.id, this.title, this.completed);
|
2016-05-15 18:55:01 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message setCompleted(this.id, $v) { this.completed = v; }
|
|
|
|
on message setAllCompleted($v) { this.completed = v; }
|
2016-05-15 18:55:01 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message setTitle(this.id, $v) { this.title = v; }
|
|
|
|
|
|
|
|
on message clearCompletedTodos() {
|
|
|
|
if (this.completed) :: deleteTodo(this.id);
|
2016-05-15 18:55:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-05-15 18:55:01 +00:00
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
var ESCAPE_KEY_CODE = 27;
|
|
|
|
var ENTER_KEY_CODE = 13;
|
|
|
|
|
|
|
|
function getTemplate(id) {
|
|
|
|
return document.getElementById(id).innerHTML;
|
|
|
|
}
|
|
|
|
|
|
|
|
function todoListItemView(id) {
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-08-25 12:12:32 +00:00
|
|
|
stop on retracted todo(id, _, _);
|
2016-08-07 19:33:09 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
this.ui = new Syndicate.UI.Anchor();
|
|
|
|
field this.editing = false;
|
|
|
|
|
|
|
|
during todo(id, $title, $completed) {
|
|
|
|
during show(completed) {
|
|
|
|
assert this.ui.html('.todo-list',
|
|
|
|
Mustache.render(getTemplate(this.editing
|
|
|
|
? 'todo-list-item-edit-template'
|
|
|
|
: 'todo-list-item-view-template'),
|
|
|
|
{
|
|
|
|
id: id,
|
|
|
|
title: title,
|
|
|
|
completed_class: completed ? "completed" : "",
|
|
|
|
checked: completed ? "checked" : "",
|
|
|
|
}),
|
|
|
|
id);
|
2016-05-15 18:55:01 +00:00
|
|
|
}
|
2016-08-25 12:12:32 +00:00
|
|
|
}
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message this.ui.event('.toggle', 'change', $e) {
|
|
|
|
:: setCompleted(id, e.target.checked);
|
|
|
|
}
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message this.ui.event('.destroy', 'click', _) {
|
|
|
|
:: deleteTodo(id);
|
|
|
|
}
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message this.ui.event('label', 'dblclick', _) {
|
|
|
|
this.editing = true;
|
|
|
|
}
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message this.ui.event('input.edit', 'keyup', $e) {
|
|
|
|
if (e.keyCode === ESCAPE_KEY_CODE || e.keyCode === ENTER_KEY_CODE) {
|
2016-05-12 22:42:17 +00:00
|
|
|
this.editing = false;
|
2016-05-12 21:42:49 +00:00
|
|
|
}
|
2016-08-25 12:12:32 +00:00
|
|
|
}
|
|
|
|
on message this.ui.event('input.edit', 'blur', $e) {
|
|
|
|
this.editing = false;
|
|
|
|
}
|
|
|
|
on message this.ui.event('input.edit', 'change', $e) {
|
|
|
|
var newTitle = e.target.value.trim();
|
|
|
|
:: (newTitle ? setTitle(id, newTitle) : deleteTodo(id));
|
|
|
|
this.editing = false;
|
2016-05-12 18:34:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-15 18:55:01 +00:00
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2016-05-12 21:42:49 +00:00
|
|
|
ground dataspace G {
|
|
|
|
Syndicate.UI.spawnUIDriver();
|
2016-05-12 18:34:23 +00:00
|
|
|
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-08-25 12:12:32 +00:00
|
|
|
on message Syndicate.UI.globalEvent('.new-todo', 'change', $e) {
|
|
|
|
var newTitle = e.target.value.trim();
|
|
|
|
if (newTitle) :: createTodo(newTitle);
|
|
|
|
e.target.value = "";
|
2016-05-12 21:42:49 +00:00
|
|
|
}
|
2016-05-12 18:34:23 +00:00
|
|
|
}
|
|
|
|
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-05-15 18:55:01 +00:00
|
|
|
this.ui = new Syndicate.UI.Anchor();
|
2016-05-14 00:16:25 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
during activeTodoCount($count) {
|
|
|
|
assert this.ui.context('count').html('.todo-count strong', '' + count);
|
|
|
|
assert this.ui.context('plural').html('.todo-count span.s', 's') when (count !== 1);
|
|
|
|
}
|
2016-05-15 18:55:01 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
during totalTodoCount(0) {
|
|
|
|
assert Syndicate.UI.uiAttribute('section.main', 'class', 'hidden');
|
|
|
|
assert Syndicate.UI.uiAttribute('footer.footer', 'class', 'hidden');
|
|
|
|
}
|
2016-05-15 18:55:01 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
during completedTodoCount(0) {
|
|
|
|
assert Syndicate.UI.uiAttribute('button.clear-completed', 'class', 'hidden');
|
|
|
|
}
|
|
|
|
on message Syndicate.UI.globalEvent('button.clear-completed', 'click', _) {
|
|
|
|
:: clearCompletedTodos();
|
|
|
|
}
|
2016-05-15 18:55:01 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
during allCompleted() {
|
|
|
|
on start { :: Syndicate.UI.setProperty('.toggle-all', 'checked', true); }
|
|
|
|
on stop { :: Syndicate.UI.setProperty('.toggle-all', 'checked', false); }
|
|
|
|
}
|
|
|
|
on message Syndicate.UI.globalEvent('.toggle-all', 'change', $e) {
|
|
|
|
:: setAllCompleted(e.target.checked);
|
|
|
|
}
|
2016-05-15 18:55:01 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on asserted todo($id, _, _) {
|
|
|
|
todoListItemView(id);
|
2016-05-15 18:55:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-08-25 12:12:32 +00:00
|
|
|
field this.completedCount = 0;
|
|
|
|
field this.activeCount = 0;
|
|
|
|
on asserted todo($id, _, $c) { if (c) this.completedCount++; else this.activeCount++; }
|
|
|
|
on retracted todo($id, _, $c) { if (c) this.completedCount--; else this.activeCount--; }
|
|
|
|
assert activeTodoCount(this.activeCount);
|
|
|
|
assert completedTodoCount(this.completedCount);
|
|
|
|
assert totalTodoCount(this.activeCount + this.completedCount);
|
|
|
|
assert allCompleted() when (this.completedCount > 0 && this.activeCount === 0);
|
2016-05-14 00:16:25 +00:00
|
|
|
}
|
|
|
|
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-08-25 12:12:32 +00:00
|
|
|
during Syndicate.UI.locationHash($hash) {
|
|
|
|
assert Syndicate.UI.uiAttribute('ul.filters > li > a[href="#'+hash+'"]',
|
|
|
|
'class', 'selected');
|
|
|
|
}
|
2016-05-12 22:42:17 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
during Syndicate.UI.locationHash('/') {
|
|
|
|
assert show(true);
|
|
|
|
assert show(false);
|
|
|
|
}
|
|
|
|
during Syndicate.UI.locationHash('/active') {
|
|
|
|
assert show(false);
|
|
|
|
}
|
|
|
|
during Syndicate.UI.locationHash('/completed') {
|
|
|
|
assert show(true);
|
2016-05-12 21:42:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-16 19:38:56 +00:00
|
|
|
spawn {
|
2016-05-15 20:16:22 +00:00
|
|
|
var db;
|
|
|
|
|
|
|
|
if ('todos-syndicate' in localStorage) {
|
|
|
|
db = JSON.parse(localStorage['todos-syndicate']);
|
|
|
|
for (var i in db.todos) {
|
|
|
|
var t = db.todos[i];
|
|
|
|
todoListItemModel(t.id, t.title, t.completed);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
db = {nextId: 0, todos: {}};
|
2016-08-25 12:12:32 +00:00
|
|
|
on start {
|
|
|
|
react {
|
|
|
|
stop on asserted Syndicate.observe(createTodo(_)) {
|
|
|
|
:: createTodo('Buy milk');
|
|
|
|
:: createTodo('Buy bread');
|
|
|
|
:: createTodo('Finish PhD');
|
|
|
|
}
|
2016-05-15 20:16:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
on message createTodo($title) {
|
|
|
|
todoListItemModel(db.nextId++, title, false);
|
|
|
|
}
|
2016-05-15 20:16:22 +00:00
|
|
|
|
2016-08-25 12:12:32 +00:00
|
|
|
during todo($id, _, _) {
|
|
|
|
during todo(id, $title, $completed) {
|
|
|
|
on start {
|
|
|
|
db.todos[id] = {id: id, title: title, completed: completed};
|
2016-05-15 20:16:22 +00:00
|
|
|
localStorage['todos-syndicate'] = JSON.stringify(db);
|
|
|
|
}
|
|
|
|
}
|
2016-08-25 12:12:32 +00:00
|
|
|
on stop {
|
|
|
|
delete db.todos[id];
|
|
|
|
localStorage['todos-syndicate'] = JSON.stringify(db);
|
|
|
|
}
|
2016-05-15 20:16:22 +00:00
|
|
|
}
|
|
|
|
}
|
2016-05-12 21:42:49 +00:00
|
|
|
}
|