TodoMVC with Silk and MiniMVP

This page walks through the TodoMVC variant written with Silk and MiniMVP. It shows the same model as TodoMVC with Silk, on the same page. The two variants differ in how the page follows the model. There the view renders every part again after every change. Here the view is a tree of presenters, one per part. Each presenter shows its part again only when its data changed. This page shows only what differs. It builds on Keeping the page in step with MiniMVP too.

The page

silk-minimvp.html is silk.html with its own title, its own bundle, and its own class to start, SilkMiniMVPTodoApp. Its main section, its footer and its clear-completed button carry the hidden attribute, as on the Silk page. A presenter hides its frame from the moment it is made, in its own way. So attach lifts the hidden of the three once the tree is refreshed. From then on the parts are the presenters’ alone.

The view

The view is the package TodoMVC-Silk-MiniMVP. It imports 'mini-mvp/MiniMVP' beside the model and Silk, and the project depends on @ambers/mini-mvp 0.3.0. It keeps the todos under todos-amber-silk-minimvp. SilkMiniMVPTodoApp is the root presenter, on the .todoapp section, and its children are two sections:

childPresenters
	^ { self mainPresenter. self footerPresenter }
mainPresenter
	^ mainPresenter ifNil: [
		mainPresenter := SectionPresenter
			shows: [ todoList isEmpty ifFalse: [ true ] ]
			in: (self frame at: '.main')
			children: { self toggleAllPresenter. self listPresenter } ]

The footer is made the same way, with the count, the filters and the clear-completed button as its children. SectionPresenter is a parent whose children are given to it. MiniMVP has no such class, so the variant has one: childPresenters answers what shows:in:children: was given. A section’s block answers nil while the list is empty, since ifFalse: alone answers nil when its receiver is true. So the sections come and go with the todos, and their children are refreshed only while they show.

attach binds the input, the mark-all checkbox and the clear-completed button, as SilkTodoApp >> attachTo: does. It refreshes the tree, and then lifts the hidden of the three parts. Each binding changes the model and sends refresh to the root. routeTo: sets the filter and refreshes.

The parts

Each part has a presenter with a block over the TodoList. The count and the clear-completed button are leaves:

countPresenter
	^ SilkLeafPresenter
		shows: [ todoList activeCount ]
		in: (self frame at: '.todo-count')
		do: [ :count | { Silk STRONG: count. ' ', todoList activeCountLabel } ]
clearCompletedPresenter
	^ SilkLeafPresenter
		shows: [ todoList hasCompleted ifTrue: [ 'Clear completed' ] ]
		in: (self frame at: '.clear-completed')

The count’s payload is the strong count and the words the model chooses. The button’s data is its text, or nil, so the button shows while something is completed. The mark-all checkbox and the filter links have no contents to replace. Each gets a small presenter of its own, a subclass of MiniPresenter that implements present:. CheckboxPresenter shows todoList allAreCompleted:

present: aBoolean
	self frame propAt: 'checked' put: aBoolean

TodoFiltersPresenter shows todoList filter, and marks its link:

present: aTodoFilter
	(self frame allAt: 'a') do: [ :each |
		each
			domClass: 'selected'
			be: (TodoFilter fromRoute: (each attrAt: 'href')) == aTodoFilter ]

The three filters are the same objects every time, so = notices a change of filter.

The list

TodoListPresenter is a parent whose children are one TodoItemPresenter per todo. The todos come and go, so the list presenter keeps its children in step before every refresh:

refresh
	self keepItemsInStep.
	super refresh
keepItemsInStep
	| todos |
	todos := todoList todos.
	self removeItemsNotIn: todos.
	self addItemsFor: (todos reject: [ :each | self hasItemFor: each ])
itemPresenterOn: aTodo
	^ (TodoItemPresenter
		shows: [ (todoList filter shows: aTodo) ifTrue: [ { aTodo title. aTodo isCompleted } ] ]
		in: self frame LI)
		setTodo: aTodo todoApp: todoApp;
		yourself

self frame LI puts a new list item at the end of the list. That item is the frame of the new presenter. removeItemsNotIn: sends remove to the item of a todo that is gone. The item cuts its list item out of the page. An item’s data is its todo’s title and state, as an array, so = notices a retitle and a toggle. The data is nil while the filter hides the todo, and nil hides the frame. So a filtered-out item stays in the list, hidden, and the items keep the order of the todos. MiniMVP has no presenter for a collection whose members come and go. TodoListPresenter is what one takes: refresh and five private methods.

One todo

TodoItemPresenter builds the item of the template when its data changed:

presentTitle: aString completed: aBoolean
	| view |
	self frame
		domClass: 'completed' be: aBoolean;
		resetContents.
	view := self frame DIV: 'class' -> 'view'.
	(view INPUT: { 'class' -> 'toggle'. 'type' -> 'checkbox'. 'checked' -> (aBoolean ifTrue: [ 'checked' ]) })
		on: #change bind: [ todoApp toggle: todo ].
	(view LABEL: aString) on: #dblclick bind: [ self startEditing ].
	(view BUTTON: 'class' -> 'destroy') on: #click bind: [ todoApp remove: todo ].
	edit := self frame INPUT: 'class' -> 'edit'.
	edit on: #blur bind: [ self isEditing ifTrue: [ self finishEditing ] ].
	edit on: #keydown bind: [ :event |
		event key = 'Enter' ifTrue: [ self finishEditing ].
		event key = 'Escape' ifTrue: [ self stopEditing ] ]

present: takes the array apart and sends this. The frame is the list item, so the class completed goes on it. resetContents empties it before the item is built again. The bindings do what those of SilkTodoItem do, and the app refreshes the tree after each change. A toggle changes one todo, so one item is built again, and the other items keep their elements.

Editing

The edit input is bound once, when the item is built, and stays bound. startEditing adds the class editing, puts the title into the input and focuses it. finishEditing takes the class off and sends todoApp retitle: todo to: with the input’s value. stopEditing takes the class off alone, for Escape. isEditing asks the list item’s classList, through asDomNode. A blur saves only while the item is editing, so the blur that follows Enter saves nothing a second time. When the title changed, the refresh builds the item again. When it did not, nothing happens, and the class is gone already.

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 mainPresenter, footerPresenter two sections with the same block
Focus the input on load silk-minimvp.html autofocus, from the template
Enter adds, trimmed, empty ignored bindNewTodo, TodoList >> add: one binding; the model trims
Mark all as complete bindToggleAll, toggleAllPresenter one binding, one CheckboxPresenter
Mark all cleared after Clear completed toggleAllPresenter, TodoList >> allAreCompleted nothing; the checkbox follows the model once the section shows again
Toggle, class completed TodoItemPresenter >> presentTitle:completed:, SilkMiniMVPTodoApp >> toggle: one binding, one domClass:be:, one association
Destroy button on hover TodoItemPresenter >> presentTitle:completed:, TodoListPresenter >> removeItemsNotIn: one binding; the stylesheet shows the button
Double-click edits TodoItemPresenter >> presentTitle:completed:, startEditing one binding, one method
Enter or blur saves, Escape cancels, empty destroys finishEditing, stopEditing, SilkMiniMVPTodoApp >> retitle:to:, TodoList >> retitle:to: two bindings; the model removes the todo
The counter, item or items countPresenter, TodoList >> activeCountLabel one leaf; the model words it
Clear completed, hidden when none bindClearCompleted, clearCompletedPresenter one binding, one leaf
Persistence under todos-amber-silk-minimvp start, TodoStore one key; the model saves
Routing, class selected, kept on reload followRoute, routeTo:, TodoFiltersPresenter one listener, one fromRoute: per link

A presenter with its data block replaces each render method of the Silk variant. What that variant renders in one render, this one refreshes in one refresh. Only the parts whose data changed are touched.

The tests of the view

SilkMiniMVPTodoAppTest, in TodoMVC-Silk-MiniMVP-Tests, answers attach:to: with the app made on the element and attached. The 25 tests of TodoAppTest read what the page shows. A hidden item does not count, and the state of a hidden part is not read. So they pass for a view that hides a filtered-out item. They pass too when it leaves the mark-all checkbox alone while the main section is hidden. Two tests of its own pin what the presenters promise. A toggle leaves the elements of the other items as they are. A destroyed todo’s item leaves the page.

Running it

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