TodoMVC with Web

This page walks through the TodoMVC variant written with Web. It shows the same model as TodoMVC with Silk, on the same page. Its view has the same shape: one class that binds and renders, and one that shows a todo. The two differ in the library. This page shows only what differs. It builds on Building the page with Web too.

The page

web.html is silk.html with its own title, its own bundle, and its own class to start, WebTodoApp. Its parts carry the hidden attribute as there.

The view

The view is the package TodoMVC-Web, with two classes, WebTodoApp and WebTodoItem. It imports 'amber/web/Web' beside the model, and 'amber/core/Platform-DOM' for one conversion. The project depends on @ambers/contrib-web 0.11.2. It keeps the todos under todos-amber-web. WebTodoApp has three slots. todoList is the TodoList, app a brush on the .todoapp section, and listBrush a brush on the ul. start attaches it to '.todoapp' asBrush, and attachTo: keeps the two brushes, binds and renders, as SilkTodoApp >> attachTo: does.

Finding the parts

A brush has no word for an element inside it. '.main' asBrush would find the first .main of the whole page. A test puts a second copy of the markup on it. So the view asks the DOM, and wraps what it finds:

brushAt: aSelector
	^ TagBrush fromElement: (app asDomNode querySelector: aSelector)
elementsAt: aSelector
	^ PlatformDom toArray: (app asDomNode querySelectorAll: aSelector)

TagBrush fromElement: makes a brush on an element already on the page, as asBrush does for a selector. PlatformDom toArray: turns the NodeList into an Array, so that do: works on it. Every lookup of the view goes through these two.

Binding

A brush sets attributes and binds events, and reads nothing back. The value of the input is read from the DOM element:

bindNewTodo
	| input |
	input := self brushAt: '.new-todo'.
	input onKeyDown: [ :event |
		event key = 'Enter' ifTrue: [
			todoList add: input asDomNode value.
			input asDomNode value: ''.
			self render ] ]

onKeyDown: is Web’s. value and value: go to the element, through asDomNode. The checkbox above the list is bound with onChange:, and its state is checkbox asDomNode checked. The button is bound with onClick:. Web has no word for an event of the window, so followRoute asks the window itself:

followRoute
	window addEventListener: 'hashchange' do: [ self routeTo: window location hash ].
	self routeTo: window location hash

The first keyword names the function, and the second is free to choose, as the JavaScript page says.

Rendering

render renders the same five things as the Silk variant, in the same order. The list takes a collection of widgets:

renderList
	listBrush contents: (todoList shownTodos collect: [ :each | WebTodoItem on: each in: self ])

contents: empties the brush and appends what it is given. A collection appends each of its items, and a widget renders itself. The counter is a block over the canvas:

renderCount
	(self brushAt: '.todo-count') contents: [ :html |
		html strong: todoList activeCount.
		html with: ' ', todoList activeCountLabel ]

The three other things are properties of elements that stay on the page. A brush has hidden, which sets the attribute, and removeAt:, which takes it off. The view sets the property instead, as the Silk variant does, through asDomNode:

renderVisibility
	(self brushAt: '.main') asDomNode hidden: todoList isEmpty.
	(self brushAt: '.footer') asDomNode hidden: todoList isEmpty.
	(self brushAt: '.clear-completed') asDomNode hidden: todoList hasCompleted not

renderToggleAll sets checked the same way. A brush has class:, which replaces every class of the element. The links of the footer keep their classes, so renderFilters goes to classList:

renderFilters
	(self elementsAt: '.filters a') do: [ :each |
		each classList
			toggle: 'selected'
			force: (TodoFilter fromRoute: (each getAttribute: 'href')) == todoList filter ]

toggle:force: adds the class when its second argument is true, and removes it when false, as domClass:be: does in Silk.

One todo

WebTodoItem is a Widget. It has four slots: todo and todoApp, as SilkTodoItem has, and listItem and edit, the brushes it keeps for editing. renderOn: builds the item of the template:

renderOn: html
	listItem := html li.
	todo isCompleted ifTrue: [ listItem class: 'completed' ].
	listItem with: [
		html div class: 'view'; with: [
			| toggle |
			toggle := html input class: 'toggle'; type: 'checkbox'; yourself.
			todo isCompleted ifTrue: [ toggle at: 'checked' put: 'checked' ].
			toggle onChange: [ todoApp toggle: todo ].
			html label with: todo title; onDblClick: [ self startEditing ].
			html button class: 'destroy'; onClick: [ todoApp remove: todo ] ].
		edit := html input class: 'edit'; yourself.
		edit onBlur: [ self isEditing ifTrue: [ self finishEditing ] ].
		edit onKeyDown: [ :event |
			event key = 'Enter' ifTrue: [ self finishEditing ].
			event key = 'Escape' ifTrue: [ self stopEditing ] ] ]

The method follows the markup of the template from the top, as the Silk one does. html li makes the item, and with: renders inside it. The elements are new, so class: and the checked attribute are enough for them. An attribute takes a string. So the class and the checkbox are each an ifTrue:, where Silk takes an association with nil. The bindings do what those of SilkTodoItem do. The edit input is bound here, once per render, since the item is built again after every change.

Editing

startEditing gives the item the class editing, puts the title into the input and focuses it. A brush has no word for one class among others, so the three go to classList:

startEditing
	listItem asDomNode classList add: 'editing'.
	edit asDomNode value: todo title.
	edit asDomNode focus
finishEditing
	self stopEditing.
	todoApp retitle: todo to: edit asDomNode value

stopEditing removes the class, for Escape. A blur saves only while the item is editing, and isEditing asks the item’s classList. So the blur that follows Enter saves nothing a second time. The Silk variant takes the blur handler off the input instead, with off:unbind:. Web binds with addEventListener and has no word for taking a handler off. So the item keeps the guard, as the MiniMVP variant does.

What each point costs

The table of TodoMVC with Silk, for this variant:

The specification says Where it is What it takes
Hide main and footer with no todos renderVisibility two hidden:, through asDomNode
Focus the input on load web.html autofocus, from the template
Enter adds, trimmed, empty ignored bindNewTodo, TodoList >> add: one onKeyDown:; the model trims
Mark all as complete bindToggleAll, renderToggleAll one onChange:, one checked: through asDomNode
Mark all cleared after Clear completed renderToggleAll, TodoList >> allAreCompleted nothing; the model answers false for no todos
Toggle, class completed WebTodoItem >> renderOn:, WebTodoApp >> toggle: one onChange:, one class:, one attribute
Destroy button on hover WebTodoItem >> renderOn:, WebTodoApp >> remove: one onClick:; the stylesheet shows the button
Double-click edits WebTodoItem >> renderOn:, startEditing one onDblClick:, one method
Enter or blur saves, Escape cancels, empty destroys WebTodoItem >> renderOn:, finishEditing, stopEditing, TodoList >> retitle:to: two bindings, one guard; the model removes the todo
The counter, item or items renderCount, TodoList >> activeCountLabel one contents:; the model words it
Clear completed, hidden when none bindClearCompleted, renderVisibility one onClick:, one hidden: through asDomNode
Persistence under todos-amber-web start, TodoStore one key; the model saves
Routing, class selected, kept on reload followRoute, routeTo:, renderFilters one listener on the window, one fromRoute: per link

Web has a word for building. There are the tag messages, class:, at:put:, with:, contents:, the on...: bindings, and a widget that renders itself. It has none for what the view does to elements that are already there. Finding one inside another, reading or setting a property, and one class among others reach past it to the DOM. So do taking a handler off and an event of the window. The Silk variant reaches past Silk three times. This one does so in its two lookups and in every editing method. Three of its five rendering methods and two of its three bindings do too.

The tests of the view

WebTodoAppTest, in TodoMVC-Web-Tests, answers attach:to: with a WebTodoApp attached to TagBrush fromElement: anElement. The 25 tests of TodoAppTest are the same as for the Silk variants.

Running it

Open http://localhost:4000/web.html, and try it as TodoMVC with Silk says. The todos are in localStorage under todos-amber-web. The test runner of Helios runs the 25 tests of WebTodoAppTest on the page.