Permalink
Fetching contributors…
Cannot retrieve contributors at this time
207 lines (151 sloc) 9.41 KB
# TodoMVC
[TodoMVC][1] is a specification for a todo list web application. Its original purpose was to help developers evaluate the plethora of Javascript frameworks implementing the Model-View-Controller (MVC) design pattern. Today, it has evolved into a benchmark for programming languages and frameworks targeting the web, so TodoMVC apps don’t necessarily reflect the MVC design pattern.
TodoMVC is a great example to demonstrate Eve's capabilities, as our semantics naturally enable a concise implementation; without optimizing for line count, the program you're reading only contains 63 lines of Eve code.
## Todos
Each todo is tagged `#todo` and has a `body`, which is the text of the todo entered by the user. Additionally, each todo has two flags. The first flag is the `completed` flag affects how the todo is displayed, and allows the user to filter todos based on completed status; we can look at "completed" todos, "active" todos, or "all" todos. The second flag is the `editing` flag, used to toggle an editing mode on the todo. This is used later to allow the user to update the body text of the todo.
These todos exist in the context of an `#app`, which we use to hold global state information. We use it to place a filter on the todos. The filter can be one of "completed", "active", or "all".
### The Application View
We draw Todo MVC here. All styling is handled in a separate CSS file. The app consists of three parts:
1. __Header__ - Contains the `#toggle-all` button as well as `#new-todo`, which is an input box for entering new todos.
2. __Body__ - Contains `#todo-list`, the list of todos. The work here is handled in the second block.
3. __Footer__ - Contains the count of todos, as well as control buttons for filtering, and clearing completed todos.
In this block, we do a little work to determine todo-count, all-checked, and none-checked. Other than that, this block simply lays out the major control elements of TodoMVC. A key aspect of this block is the `bind` keyword. This denotes the beginning of the action phase of the block, and tells Eve that to update records as data changes. This is the key component that enables Eve to react to user interaction and update the display.
~~~
search
all-checked = if not([#todo completed: "false"]) then "true"
else "false"
none-checked = if [#todo completed: true] then "false"
else "true"
todo-count = if c = gather/count[for: [#todo completed: "false"]] then c
else 0
bind
// Links to an external stylesheet
[#ui/link rel: "stylesheet" href: "/assets/css/examples/todomvc.css"]
[#ui/div #app-wrapper class: "todoapp" | children:
[#html/element tagname: "header" | children:
[#ui/h1 text: "todos"]
[#ui/input #new-todo, class: "new-todo", autofocus: "true",
placeholder: "What needs to be done?"]
[#ui/input #toggle-all class: "toggle-all", type: "checkbox",
checked: all-checked]]
[#ui/div class: "main" | children:
[#ui/ul #todo-list]]
[#html/element tagname: "footer" children:
[#ui/span #todo-count, class: "todo-count" | children:
[#html/element tagname: "strong" text: todo-count]
[#ui/span text: " items left"]]
[#ui/ul #filter-list, class: "filters" | children:
[#filter-link filter: "all"]
[#filter-link filter: "active"]
[#filter-link filter: "completed"]]
[#ui/span #clear-completed text: "Clear completed", class: [clear-completed: "true", hidden: none-checked]]]]
~~~
Now we'll fill in the details for our filter link component.
~~~
search
me = [#filter-link filter]
selected = if [#app filter] then "true" else "false"
bind
me <- [#ui/li children: [#ui/a text: filter | href: "#/{{filter}}" class: [selected]]]
~~~
### Drawing the Todo List
Now we look at how the todos are actually displayed in the application. We attach it to `#todo-list` using its `children` attribute. Each todo display element consists of:
- a **list item**, with a checkbox for toggling the completed status of the todo
- a **label** displaying the text of the todo
- an **input textbox** for editing the text of the todo
- a **button** for deleting the todo
~~~
search
[#app filter]
parent = [#todo-list]
(todo, body, completed, editing) =
if filter = "completed"
found = [#todo completed: "true"]
then (found, found.body, "true", found.editing)
else if filter = "active"
found = [#todo completed: "false"]
then (found, found.body, "false", found.editing)
else if filter = "all"
found = [#todo]
then (found, found.body, found.completed, found.editing)
bind
parent.children +=
[#ui/li class: [todo: "true", completed, editing], todo sort: todo.number | children:
[#ui/input #todo-checkbox todo, type: "checkbox", checked: completed, class: "toggle" class: [hidden: editing]]
[#html/element tagname: "label" #todo-item, class: [hidden: editing], todo, text: body]
[#ui/input #todo-editor todo, value: body, autofocus: "true", class: "edit" class: [hidden: logic/toggle[value: editing]]]
[#ui/button #remove-todo todo, class: "flat destroy" class: [hidden: editing]]]
~~~
Thanks to Eve's set semantics, we don't need any loops here; for every unique `#todo` in the database, Eve will do the work of adding another `#li` as a child of `#todo-list`.
## Responding to User Events
### Creating a New Todo
A user can interact with TodoMVC in several ways. First and foremost, the user can create new todos. When the `@new-todo` input box is focused and the user presses enter, the value of the input is captured and a new todo is created.
~~~
search
element = [#new-todo value]
kd = [#html/event/key-down element, key: "enter"]
num = if other-todo = [#todo number]
1 = gather/sort[for: (number, other-todo) direction: "down"]
then number + 1
else 1
commit
[#todo number: num body: value, editing: "false", completed: "false", kd]
element.value := ""
~~~
Of note here is the record `[#todo body: value, editing: "false", completed: "false", kd]`. The inclusion of the `kd` attribute might seem strange, but its purpose is to guarantee the uniqueness of the todo. Let’s say we want to add two todos with the same body. If `kd` were not an attribute, then the two todos would be exactly the same and Eve’s set semantics would collapse them into a single todo. Therefore, we need some way to distinguish todos with identical bodies. Adding `kd` allows for this.
### Editing Todos
Here we handle all the ways we edit a todo. Editing includes changing the body as well as toggling the status of between complete and active.
- click `#todo-checkbox` - toggles the completed status of the checkbox.
- click `#toggle-all` - marks all todos as complete or incomplete, depending on the initial value. If all todos are marked incomplete, clicking `#toggle-all` will mark them complete. If only some are marked complete, then clicking `#toggle-all` will mark the rest complete. If all todos are marked as complete, then clicking `#toggle-all` will mark them all as incomplete.
- escape `#todo-editor` - cancels the edit
- enter `#todo-editor` - commits the new text in `#todo-editor`, replacing the original body
- blur `#todo-editor` - has the same effect as enter
~~~
search
(found, body, editing, completed) =
if [#html/event/click element: [#todo-checkbox todo]]
then (todo, todo.body, todo.editing, logic/toggle[value: todo.completed])
else if [#html/event/click element: [#toggle-all checked]]
todo = [#todo]
then (todo, todo.body, todo.editing, logic/toggle[value: checked])
else if [#html/event/double-click element: [#todo-item todo]]
then (todo, todo.body, "true", todo.completed)
else if [#html/event/blur element: [#todo-editor todo value]]
todo = [editing: "true"]
then (todo, value, "false", todo.completed)
else if [#html/event/key-down element: [#todo-editor todo] key: "escape"]
then (todo, todo.body, "false", todo.completed)
else if [#html/event/key-down element: [#todo-editor todo value] key: "enter"]
then (todo, value, "false", todo.completed)
commit
found.body := body
found.completed := completed
found.editing := editing
~~~
### Deleting Todos
We remove a todo from the list by setting the todo's record to special `none` value. Doing so completely erases that todo from the database.
~~~
search
deleted-todo = if [#html/event/click element: [#remove-todo todo]]
then todo
else if [#html/event/click element: [#clear-completed]]
then [#todo completed: "true"]
commit
deleted-todo := none
~~~
### Filtering Todos (Routing)
The TodoMVC specification requires filtering via the URL. This is actually how the filter buttons work; if you look at their href attributes, they modify the URL with certain tags:
- all - displays all todos
- active - displays active todos only
- completed - displays completed todos only
We can extract this value using `#url`, which has a hash-segment attribute that automatically parses the URL for us, returning the `value` (expected to be any one of the above). Any other value will fail to show any todos, but the application will not break.
~~~
search
url = [#html/url]
filter = if url.hash-segment = [index: 2, value]
then value
else "all"
bind
[#app | filter]
~~~
[1]: http://todomvc.com/