TodoMVC - A Declarative Implementation in Eve
Corey Montella - 19 Aug 2016
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,
completed, 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,
editing, toggles 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 the interface for TodoMVC here. All styling is handled in a separate CSS file. The app consists of three parts:
- Header - Contains the
@toggle-allbutton as well as
@new-todo, which is an input box for entering new todos.
- Body - Contains
@todo-list, the list of todos. The work here is handled in the second block.
- 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.
match [@app filter] 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 = count[given: [#todo completed: false]] then c else 0 bind [#link rel: "stylesheet" href: "/examples/todomvc.css"] // Links to an external stylesheet [#div class: "todoapp" children: [#header children: [#h1 text: "todos"] [#input @new-todo, class: "new-todo", autofocus: true, placeholder: "What needs to be done?"] [#input @toggle-all, class: "toggle-all", type: "checkbox", checked: all-checked]] [#div class: "main" children: [#ul @todo-list, class: "todo-list"]] [#footer children: [#span @todo-count, class: "todo-count", children: [#strong text: todo-count] [#span text: " items left"]] [#ul @filters, class: "filters", children: [#li children: [#a href: "#/all" class: [selected: is(filter = "all")] text: "all"]] [#li children: [#a href: "#/active" class: [selected: is(filter = "active")] text: "active"]] [#li children: [#a href: "#/completed" class: [selected: is(filter = "completed")] text: "completed"]]] [#span @clear-completed, class: [clear-completed: true, hidden: none-checked], text: "Clear completed"]]]
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
match [@app filter] parent = [@todo-list] (todo, body, completed, editing) = if filter = "completed" then ([#todo, body, completed: true, editing], body, true, editing) else if filter = "active" then ([#todo, body, completed: false, editing], body, false, editing) else if filter = "all" then ([#todo, body, completed, editing], body, completed, editing) bind parent.children += [#li, class: [todo: true, completed, editing], todo, children: [#input #todo-checkbox, todo, class: [toggle: true, hidden: editing], type: "checkbox", checked: completed] [#label #todo-item, class: [hidden: editing], todo, text: body] [#input #todo-editor, class: [edit: true, hidden: toggle[value: editing]], todo, value: body, autofocus: true] [#button #remove-todo, class: [destroy: true, hidden: editing], todo]
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
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.
match element = [@new-todo value] kd = [#keydown element, key: "enter"] commit [#todo 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.
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.
#todo-checkbox- toggles the completed status of the checkbox.
@toggle-all- marks all todos as complete or incomplete, depending on the initial value. If all todos are marked incomplete, clicking
@toggle-allwill mark them complete. If only some are marked complete, then clicking
@toggle-allwill mark the rest complete. If all todos are marked as complete, then clicking
@toggle-allwill mark them all as incomplete.
#todo-editor- blurring the
@todo-editorwill cancel the edit
#todo-editor- this has the same effect as blurring
#todo-editor- commits the new text in
#todo-editor, replacing the original body
match (todo, body, editing, completed) = if [#click element: [#todo-checkbox todo]] then (todo, todo.body, todo.editing, toggle[value: todo.completed]) else if [#click element: [@toggle-all checked]] then ([#todo body], body, todo.editing, toggle[value: checked]) else if [#double-click element: [#todo-item todo]] then (todo, todo.body, true, todo.completed) else if [#blur element: [#todo-editor todo value]] then (todo, value, false, todo.completed) else if [#keydown element: [#todo-editor todo] key: "escape"] then (todo, todo.body, false, todo.completed) else if [#keydown element: [#todo-editor todo value] key: "enter"] then (todo, value, false, todo.completed) commit todo <- [body, completed, editing]
Deleting a todo from the list is accomplished by removing the
#todo tag from the todo’s record. Recall from the “Drawing the Todo List” section that we select todos by including
#todo in the record. Therefore, if we remove that tag from the todo, then that todo will no longer appear in the list, although it will still persist in the database.
match todo = if [#click element: [#remove-todo todo]] then todo else if [#click element: [@clear-completed]] then [#todo completed: true] commit todo -= #todo
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.
match value = if [#url hash-segment: [index: 1, value]] then value else "all" bind [@app filter: value]