TodoMVC with jQuery

This page walks through the TodoMVC variant written with jQuery. 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. This page shows only what differs from TodoMVC with the DOM alone. jQuery was Amber’s idiom before 2015, and the classic IDE is written on it. The variant shows what the wrapper is, as history and as the thinnest wrapper there is.

Taking jQuery in

jQuery comes through @ambers/contrib-jquery 0.8.2, which depends on jquery from npm. The project gets jquery 4.0.0. The package imports 'amber/jquery/Wrappers-JQuery'. The test runner under Node.js has a stub for jquery, which amber init writes into every Gruntfile.js.

Wrappers-JQuery is 59 lines. It gives asJQuery to a string, to an object, to a block and to a proxy. Each answers jQuery(...) of the receiver. JQuery current is jQuery itself. When the package loads, its class opts jQuery out of Smalltalk, so that JQuery current ajax: reaches a property of the function. Advanced JavaScript interop says what that does. Everything else is jQuery’s own API, through the proxy. find: is find, on:do: is on, and prop:value: is prop with two arguments.

The page

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

The view

The view is the package TodoMVC-JQuery, with two classes, JQueryTodoApp and JQueryTodoItem. It keeps the todos under todos-amber-jquery. JQueryTodoApp has three slots. todoList is the TodoList. app is a jQuery on the .todoapp section, and listElement one on the ul. start attaches it to '.todoapp' asJQuery, and attachTo: keeps the two, binds and renders, as SilkTodoApp >> attachTo: does.

Binding

A jQuery finds inside itself with find:, binds with on:do:, and reads and writes a value with val:

bindNewTodo
	| input |
	input := app find: '.new-todo'.
	input on: 'keydown' do: [ :event |
		event key = 'Enter' ifTrue: [
			todoList add: input val.
			input val: ''.
			self render ] ]

The event is jQuery’s own event object, which carries key. The checkbox above the list is bound with change, and its state is checkbox prop: 'checked'. The button is bound with click. The window is a jQuery too, window asJQuery, and followRoute binds hashchange on it.

A jQuery handler that answers false stops the event, and a block answers its last expression. The handlers here end in self render or in a send to the app, and none answers false.

Rendering

render renders the same five things as the Silk variant, in the same order. The counter is emptied and filled, and '<strong>' asJQuery makes an element from markup:

renderCount
	(app find: '.todo-count')
		empty;
		append: ('<strong>' asJQuery text: todoList activeCount);
		append: ' ', todoList activeCountLabel

A string that starts with < makes elements, and any other string is a selector. The list is emptied the same way, and each todo is a JQueryTodoItem that renderIn: puts into it. The links of the footer are walked with jQuery’s each:, which gives the index and the element:

renderFilters
	(app find: '.filters a') each: [ :index :each |
		each asJQuery
			toggleClass: 'selected'
			value: (TodoFilter fromRoute: (each getAttribute: 'href')) == todoList filter ]

toggleClass:value: adds the class when its second argument is true, and removes it when false. A block given to each: that answers false stops the loop. This one answers the jQuery.

The three other things are properties, set with prop:value:. renderVisibility sets hidden, not jQuery’s show and hide. Those set the inline style and leave the hidden attribute of the page alone, so the parts would stay hidden.

One todo

JQueryTodoItem has four slots: todo and todoApp, as SilkTodoItem has, and listItem and edit, the jQueries it keeps for editing. The markup of the item is a string, as jQuery code has it:

markup
	^ '<li><div class="view"><input class="toggle" type="checkbox"><label></label><button class="destroy"></button></div><input class="edit"></li>'

renderIn: makes the item from it, sets its state, binds it and puts it into the list:

renderIn: aJQuery
	listItem := self markup asJQuery.
	listItem toggleClass: 'completed' value: todo isCompleted.
	(listItem find: '.toggle')
		prop: 'checked' value: todo isCompleted;
		on: 'change' do: [ todoApp toggle: todo ].
	(listItem find: 'label')
		text: todo title;
		on: 'dblclick' do: [ self startEditing ].
	(listItem find: '.destroy') on: 'click' do: [ todoApp remove: todo ].
	edit := listItem find: '.edit'.
	aJQuery append: listItem

text: puts the title as text, so a title with < in it stays text. The bindings do what those of SilkTodoItem do.

Editing

startEditing gives the item the class editing with addClass:, and puts the title into the input with val:. Then it binds the edit, and focuses the input with trigger: 'focus'. jQuery has a word for taking a handler off, off:with:, so the item binds as SilkTodoItem does:

bindEdit
	| save |
	save := [ todoApp retitle: todo to: edit val ].
	edit on: 'blur' do: save.
	edit on: 'keydown' do: [ :event |
		event key = 'Enter' ifTrue: [
			edit off: 'blur' with: save.
			save value ].
		event key = 'Escape' ifTrue: [
			edit off: 'blur' with: save.
			todoApp render ] ]

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 prop:value:
Focus the input on load jquery.html autofocus, from the template
Enter adds, trimmed, empty ignored bindNewTodo, TodoList >> add: one on:do:; the model trims
Mark all as complete bindToggleAll, renderToggleAll one on:do:, one prop:value:
Mark all cleared after Clear completed renderToggleAll, TodoList >> allAreCompleted nothing; the model answers false for no todos
Toggle, class completed JQueryTodoItem >> renderIn:, JQueryTodoApp >> toggle: one on:do:, one toggleClass:value:
Destroy button on hover JQueryTodoItem >> renderIn:, JQueryTodoApp >> remove: one on:do:; the stylesheet shows the button
Double-click edits JQueryTodoItem >> renderIn:, startEditing one on:do:, one method
Enter or blur saves, Escape cancels, empty destroys JQueryTodoItem >> bindEdit, JQueryTodoApp >> retitle:to:, TodoList >> retitle:to: two on:do:; the model removes the todo
The counter, item or items renderCount, TodoList >> activeCountLabel one cascade; the model words it
Clear completed, hidden when none bindClearCompleted, renderVisibility one on:do:, one prop:value:
Persistence under todos-amber-jquery start, TodoStore one key; the model saves
Routing, class selected, kept on reload followRoute, routeTo:, renderFilters one on:do: on the window, one fromRoute: per link

jQuery has a word for everything the specification asks, and each word is a message through the proxy. Nothing reaches past it, and the window is a jQuery like an element. Two things cost. The markup of an item is a string in the code. A part of it is found by a selector after the fact. And every event, class and property is a string too, so a misspelt one is found when it does nothing. Against the DOM alone, the item takes 12 lines and a line of markup instead of 32.

The tests of the view

JQueryTodoAppTest, in TodoMVC-JQuery-Tests, answers attach:to: with a JQueryTodoApp attached to anElement asJQuery. The 25 tests of TodoAppTest are the same as for the other variants.

Running it

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