todomvc: Implement many more features; redesign to fix bug
This commit is contained in:
parent
9e673c1588
commit
299be35d8f
|
@ -24,7 +24,7 @@
|
||||||
<label for="toggle-all">Mark all as complete</label>
|
<label for="toggle-all">Mark all as complete</label>
|
||||||
<ul class="todo-list">
|
<ul class="todo-list">
|
||||||
<template id="todo-list-item-view-template">
|
<template id="todo-list-item-view-template">
|
||||||
<li data-id="{{id}}" class="{{hidden_class}} {{completed_class}}">
|
<li data-id="{{id}}" class="{{completed_class}}">
|
||||||
<div class="view">
|
<div class="view">
|
||||||
<input class="toggle" type="checkbox" {{checked}}>
|
<input class="toggle" type="checkbox" {{checked}}>
|
||||||
<label>{{title}}</label>
|
<label>{{title}}</label>
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template id="todo-list-item-edit-template">
|
<template id="todo-list-item-edit-template">
|
||||||
<li data-id="{{id}}" class="editing">
|
<li data-id="{{id}}" class="editing">
|
||||||
<input value="{{title}}" class="edit">
|
<input value="{{title}}" class="edit -syndicate-focus">
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
assertion type todo(id, title, completed);
|
|
||||||
assertion type show(completed);
|
|
||||||
assertion type activeTodoCount(n);
|
|
||||||
assertion type completedTodoCount(n);
|
|
||||||
assertion type totalTodoCount(n);
|
|
||||||
|
|
||||||
message type deleteTodo(id);
|
|
||||||
message type clearCompletedTodos();
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
To Do (ho ho ho)
|
To Do (ho ho ho)
|
||||||
spec is at: https://github.com/tastejs/todomvc/blob/master/app-spec.md
|
spec is at: https://github.com/tastejs/todomvc/blob/master/app-spec.md
|
||||||
|
@ -16,17 +7,59 @@ message type clearCompletedTodos();
|
||||||
- pattern the HTML more explicitly on the given template, keep changes to a minimum
|
- pattern the HTML more explicitly on the given template, keep changes to a minimum
|
||||||
- code style https://github.com/tastejs/todomvc/blob/master/contributing.md#code-style
|
- code style https://github.com/tastejs/todomvc/blob/master/contributing.md#code-style
|
||||||
|
|
||||||
- mark all as complete/incomplete; make sure it is only ever checked when all the todos are checked
|
|
||||||
- persist to localStorage; use correct keys and name.
|
- persist to localStorage; use correct keys and name.
|
||||||
|
|
||||||
- routing: spec requires that filtering be done "on a model level";
|
|
||||||
we, by using "hidden" class, are kind of partly doing it on a view
|
|
||||||
level. We could either continue to do this, or switch to a proper
|
|
||||||
model level approach, but then we'd lose stability of ordering!
|
|
||||||
|
|
||||||
- BUG: doesn't hide an item if in "Active" state and you click on the checkbox
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
assertion type todoExists(id);
|
||||||
|
assertion type todo(id, title, completed);
|
||||||
|
|
||||||
|
message type setTitle(id, title);
|
||||||
|
message type setCompleted(id, completed);
|
||||||
|
message type deleteTodo(id);
|
||||||
|
message type clearCompletedTodos();
|
||||||
|
message type setAllCompleted(completed);
|
||||||
|
|
||||||
|
// Derived model state
|
||||||
|
assertion type activeTodoCount(n);
|
||||||
|
assertion type completedTodoCount(n);
|
||||||
|
assertion type totalTodoCount(n);
|
||||||
|
assertion type allCompleted();
|
||||||
|
|
||||||
|
// View state
|
||||||
|
assertion type show(completed);
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
var nextId = 0;
|
||||||
|
function todoListItemModel(title) {
|
||||||
|
title = title.trim();
|
||||||
|
if (!title) return;
|
||||||
|
|
||||||
|
actor {
|
||||||
|
this.id = nextId++;
|
||||||
|
this.title = title;
|
||||||
|
this.completed = false;
|
||||||
|
|
||||||
|
react {
|
||||||
|
assert todoExists(this.id);
|
||||||
|
assert todo(this.id, this.title, this.completed);
|
||||||
|
|
||||||
|
on message setCompleted(this.id, $v) { this.completed = v; }
|
||||||
|
on message setAllCompleted($v) { this.completed = v; }
|
||||||
|
|
||||||
|
on message setTitle(this.id, $v) { this.title = v; }
|
||||||
|
|
||||||
|
on message clearCompletedTodos() {
|
||||||
|
if (this.completed) :: deleteTodo(this.id);
|
||||||
|
}
|
||||||
|
} until {
|
||||||
|
case message deleteTodo(this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
var ESCAPE_KEY_CODE = 27;
|
var ESCAPE_KEY_CODE = 27;
|
||||||
var ENTER_KEY_CODE = 13;
|
var ENTER_KEY_CODE = 13;
|
||||||
|
|
||||||
|
@ -34,61 +67,37 @@ function getTemplate(id) {
|
||||||
return document.getElementById(id).innerHTML;
|
return document.getElementById(id).innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextId = 0;
|
function todoListItemView(id) {
|
||||||
function addTodo(title) {
|
|
||||||
title = title.trim();
|
|
||||||
if (!title) return;
|
|
||||||
|
|
||||||
actor {
|
actor {
|
||||||
this.id = nextId++;
|
|
||||||
this.ui = new Syndicate.UI.Anchor();
|
this.ui = new Syndicate.UI.Anchor();
|
||||||
this.title = title;
|
|
||||||
this.completed = false;
|
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
this.visible = false;
|
|
||||||
|
|
||||||
react {
|
react {
|
||||||
assert todo(this.id, this.title, this.completed);
|
during todo(id, $title, $completed) {
|
||||||
|
during show(completed) {
|
||||||
during show(this.completed) {
|
|
||||||
do { this.visible = true; }
|
|
||||||
finally { this.visible = false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
assert this.ui.html('.todo-list',
|
assert this.ui.html('.todo-list',
|
||||||
Mustache.render(getTemplate(this.editing
|
Mustache.render(getTemplate(this.editing
|
||||||
? 'todo-list-item-edit-template'
|
? 'todo-list-item-edit-template'
|
||||||
: 'todo-list-item-view-template'),
|
: 'todo-list-item-view-template'),
|
||||||
{
|
{
|
||||||
completed_class: this.completed ? "completed" : "",
|
id: id,
|
||||||
hidden_class: this.visible ? "" : "hidden",
|
title: title,
|
||||||
id: this.id,
|
completed_class: completed ? "completed" : "",
|
||||||
checked: this.completed ? "checked" : "",
|
checked: completed ? "checked" : "",
|
||||||
title: this.title
|
|
||||||
}),
|
}),
|
||||||
this.id);
|
id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
on message this.ui.event('.toggle', 'change', $e) {
|
on message this.ui.event('.toggle', 'change', $e) {
|
||||||
this.completed = e.target.checked;
|
:: setCompleted(id, e.target.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
on message this.ui.event('.destroy', 'click', _) {
|
on message this.ui.event('.destroy', 'click', _) {
|
||||||
:: deleteTodo(this.id);
|
:: deleteTodo(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
on message this.ui.event('label', 'dblclick', _) {
|
on message this.ui.event('label', 'dblclick', _) {
|
||||||
var self = this;
|
|
||||||
this.editing = true;
|
this.editing = true;
|
||||||
focusMe(); // TODO this is gross
|
|
||||||
function focusMe() {
|
|
||||||
setTimeout(function () {
|
|
||||||
var q = 'li[data-id="'+self.id+'"] input.edit';
|
|
||||||
var n = document.querySelector(q);
|
|
||||||
if (!n) { return focusMe(); }
|
|
||||||
n.focus();
|
|
||||||
n.setSelectionRange(n.value.length, n.value.length);
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
on message this.ui.event('input.edit', 'keyup', $e) {
|
on message this.ui.event('input.edit', 'keyup', $e) {
|
||||||
|
@ -100,32 +109,65 @@ function addTodo(title) {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
}
|
}
|
||||||
on message this.ui.event('input.edit', 'change', $e) {
|
on message this.ui.event('input.edit', 'change', $e) {
|
||||||
this.title = e.target.value.trim();
|
var newTitle = e.target.value.trim();
|
||||||
|
:: (newTitle ? setTitle(id, newTitle) : deleteTodo(id));
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
if (!this.title) :: deleteTodo(this.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
on message clearCompletedTodos() {
|
|
||||||
if (this.completed) :: deleteTodo(this.id);
|
|
||||||
}
|
}
|
||||||
} until {
|
} until {
|
||||||
case message deleteTodo(this.id);
|
case retracted todoExists(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
ground dataspace G {
|
ground dataspace G {
|
||||||
Syndicate.UI.spawnUIDriver();
|
Syndicate.UI.spawnUIDriver();
|
||||||
|
|
||||||
actor {
|
actor {
|
||||||
react {
|
react {
|
||||||
on message Syndicate.UI.globalEvent('.new-todo', 'change', $e) {
|
on message Syndicate.UI.globalEvent('.new-todo', 'change', $e) {
|
||||||
addTodo(e.target.value);
|
todoListItemModel(e.target.value);
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor {
|
||||||
|
this.ui = new Syndicate.UI.Anchor();
|
||||||
|
|
||||||
|
react {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
during totalTodoCount(0) {
|
||||||
|
assert Syndicate.UI.uiAttribute('section.main', 'class', 'hidden');
|
||||||
|
assert Syndicate.UI.uiAttribute('footer.footer', 'class', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
during completedTodoCount(0) {
|
||||||
|
assert Syndicate.UI.uiAttribute('button.clear-completed', 'class', 'hidden');
|
||||||
|
}
|
||||||
|
on message Syndicate.UI.globalEvent('button.clear-completed', 'click', _) {
|
||||||
|
:: clearCompletedTodos();
|
||||||
|
}
|
||||||
|
|
||||||
|
during allCompleted() {
|
||||||
|
do { :: Syndicate.UI.setProperty('.toggle-all', 'checked', true); }
|
||||||
|
finally { :: Syndicate.UI.setProperty('.toggle-all', 'checked', false); }
|
||||||
|
}
|
||||||
|
on message Syndicate.UI.globalEvent('.toggle-all', 'change', $e) {
|
||||||
|
:: setAllCompleted(e.target.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
on asserted todoExists($id) {
|
||||||
|
todoListItemView(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actor {
|
actor {
|
||||||
var completedCount = 0;
|
var completedCount = 0;
|
||||||
var activeCount = 0;
|
var activeCount = 0;
|
||||||
|
@ -135,26 +177,7 @@ ground dataspace G {
|
||||||
assert activeTodoCount(activeCount);
|
assert activeTodoCount(activeCount);
|
||||||
assert completedTodoCount(completedCount);
|
assert completedTodoCount(completedCount);
|
||||||
assert totalTodoCount(activeCount + completedCount);
|
assert totalTodoCount(activeCount + completedCount);
|
||||||
}
|
assert allCompleted() when (completedCount > 0 && activeCount === 0);
|
||||||
}
|
|
||||||
|
|
||||||
actor {
|
|
||||||
var ui = new Syndicate.UI.Anchor();
|
|
||||||
react {
|
|
||||||
during activeTodoCount($count) {
|
|
||||||
assert ui.context('count').html('.todo-count strong', '' + count);
|
|
||||||
assert ui.context('plural').html('.todo-count span.s', 's') when (count !== 1);
|
|
||||||
}
|
|
||||||
during completedTodoCount(0) {
|
|
||||||
assert Syndicate.UI.uiAttribute('button.clear-completed', 'class', 'hidden');
|
|
||||||
}
|
|
||||||
during totalTodoCount(0) {
|
|
||||||
assert Syndicate.UI.uiAttribute('section.main', 'class', 'hidden');
|
|
||||||
assert Syndicate.UI.uiAttribute('footer.footer', 'class', 'hidden');
|
|
||||||
}
|
|
||||||
on message Syndicate.UI.globalEvent('button.clear-completed', 'click', _) {
|
|
||||||
:: clearCompletedTodos();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,9 +201,9 @@ ground dataspace G {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addTodo('Buy milk');
|
todoListItemModel('Buy milk');
|
||||||
addTodo('Buy bread');
|
todoListItemModel('Buy bread');
|
||||||
addTodo('Finish PhD');
|
todoListItemModel('Finish PhD');
|
||||||
}
|
}
|
||||||
|
|
||||||
// G.dataspace.setOnStateChange(function (mux, patch) {
|
// G.dataspace.setOnStateChange(function (mux, patch) {
|
||||||
|
|
Loading…
Reference in New Issue