Keeping the page in step with MiniMVP

MiniMVP keeps a frame of the page in step with a block. A presenter holds the frame and the block. On refresh it evaluates the block, and when the answer changed, it shows the answer in the frame. When the answer is nil, it hides the frame. Presenters form a tree, so one refresh on the root walks the whole page. This page builds on Building the page with Silk and Building the page with Web. Its examples run in a workspace of the Hello Amber project, once MiniMVP is in it. They need @ambers/mini-mvp 0.3.0, or newer.

Taking MiniMVP in

MiniMVP is not in the sample project. Install it, as the libraries page shows for any Amber library:

npm install @ambers/mini-mvp --save
grunt devel

Then add 'mini-mvp/MiniMVP' to the imports of your package in Helios, save, commit, and reload. MiniMVP imports neither Silk nor Web. Your package imports the one its frames are made of, and the sample imports both:

Package
	named: 'HelloAmber'
	imports: {'amber/jquery/Web-JQuery'. 'amber/web/Web'. 'mini-mvp/MiniMVP'. 'silk/Silk'}
	transport: (AmdPackageTransport namespace: 'amber-helloamber')

In Gruntfile.js, add 'mini-mvp/MiniMVP' to the libraries of the amberc section. Then grunt compiles a presenter of yours from the command line. grunt deploy carries MiniMVP along, since the optimizer follows the imports.

A presenter on a frame

A frame is an element already on the page. The examples use an input and two frames, all inside one element. Add them to index.html, after the sample’s list, and reload the page. If the #app element of the Silk page is there already, put the three inside it:

<div id="app">
	<input id="new-item" placeholder="What to buy?">
	<p id="status"></p>
	<ul id="items"></ul>
</div>

SilkLeafPresenter shows: aBlock in: aSilk do: aFactoryBlock makes a presenter on a frame. The first block answers the data. The last block makes what goes into the frame from it:

| count presenter |
count := 2.
presenter := SilkLeafPresenter
	shows: [ count ]
	in: '#status' asSilk
	do: [ :number | 'To buy: ', number printString ].
presenter refresh.
count := nil.
presenter refresh.
count := 3.
presenter refresh

The presenter hides its frame when it is made. The first refresh evaluates the block, gets 2, shows the frame and puts To buy: 2 into it. The second gets nil and hides the frame. The third gets 3, shows the frame again and puts To buy: 3. The page ends up saying To buy: 3.

A refresh compares the answer with the previous one by =. While they are equal, nothing happens. So the block answers a value, not an object that changes in place. A collection that grows is the same object, and = says so. asArray makes a new array each time, and two arrays with the same items are equal.

The payload

What the factory answers is the payload. The frame takes it as Silk takes anything with <<. So it can be a string, a tag made with Silk LI:, a block over the Silk, or a collection of those. The presenter empties the frame first. Without do:, the data itself is the payload, so SilkLeafPresenter shows: [ count ] in: '#status' asSilk puts the number as it is. A payload of nil empties the frame and leaves it shown.

A tree of presenters

A parent presenter groups presenters, so that one refresh reaches them all. Subclass MiniParentPresenter and answer the children from childPresenters. The scene is a shopping list. The model holds the items and knows nothing of the page:

Object subclass: #Shopping
	slots: {#items}
	package: 'HelloAmber'
initialize
	super initialize.
	items := OrderedCollection new
items
	^ items
add: aString
	items add: aString
remove: aString
	items remove: aString

The presenter mirrors the markup: one parent on #app, a leaf on #status and a leaf on #items:

MiniParentPresenter subclass: #ShoppingPresenter
	slots: {#shopping. #statusPresenter. #itemsPresenter}
	package: 'HelloAmber'
shopping: aShopping
	shopping := aShopping
childPresenters
	^ { self statusPresenter. self itemsPresenter }
statusPresenter
	^ statusPresenter ifNil: [
		statusPresenter := SilkLeafPresenter
			shows: [ shopping items isEmpty ifFalse: [ shopping items size ] ]
			in: (self frame at: '#status')
			do: [ :count | 'To buy: ', count printString ] ]
itemsPresenter
	^ itemsPresenter ifNil: [
		itemsPresenter := SilkLeafPresenter
			shows: [ shopping items asArray ]
			in: (self frame at: '#items')
			do: [ :items | items collect: [ :each | self itemFor: each ] ] ]
itemFor: aString
	| item |
	item := Silk LI: aString.
	item on: #click bind: [
		shopping remove: aString.
		self refresh ].
	^ item

The parent’s frame is the Silk on #app, and at: finds the frames of the leaves inside it. childPresenters is asked at every refresh, so each leaf is made once and kept. The status block answers nil while the list is empty: ifFalse: alone answers nil when its receiver is true. The items block answers a fresh array, so a change of the list is noticed. Its factory makes one li per item. A click on the item removes it from the model and refreshes the tree. The handler runs at the click, long after the factory has returned.

Create the two classes and their methods in Helios, as the Helios page shows. Then put the tree on the page and bind the input:

| shopping presenter input |
shopping := Shopping new.
presenter := ShoppingPresenter in: '#app' asSilk.
presenter shopping: shopping.
presenter refresh.
input := '#new-item' asSilk.
input on: #keydown bind: [ :event |
	event key = 'Enter' ifTrue: [
		shopping add: (input propAt: 'value').
		input propAt: 'value' put: ''.
		presenter refresh ] ]

in: makes a parent with no data of its own. Its frame is shown from the first refresh on. Type Milk and press Enter. The status line appears and says To buy: 1, and Milk is in the list. Add Bread: To buy: 2. Click Milk: the list has Bread alone, and the status says To buy: 1. Click Bread: the list is empty, and the status line goes, since its block answered nil. Every refresh walked the tree, and each leaf compared its own data.

A section that comes and goes

A parent made with shows: aBlock in: aFrame, like a leaf, is a section. It is shown, and its children refreshed, only while its block answers something other than nil. While the block answers nil, the frame is hidden and the children are left alone.

With Web

AmberWebLeafPresenter is the leaf whose frame is a brush, from asBrush. Its payload is what contents: accepts: a string, a widget, or a block over the canvas:

| count presenter |
count := 2.
presenter := AmberWebLeafPresenter
	shows: [ count ]
	in: '#status' asBrush
	do: [ :number | [ :html | html strong: number printString; with: ' to buy' ] ].
presenter refresh

This puts <strong>2</strong> to buy into the status line. A parent has no payload, so the parents are the same for both. One tree can hold leaves of both kinds.

When presenting fails

A presenter takes an answer as shown only once presenting it succeeded. When the factory or the frame raises an error, the next refresh tries the same answer again. A parent refreshes every child even when one of them fails, and then raises the first error. So one broken leaf leaves the rest of the page in step. The error still reaches the caller of refresh.

A factory must not refresh its own presenter, nor a parent above it. The answer counts as shown only after the factory returned. So the inner refresh presents it again, and so on until the stack overflows. The refresh belongs to the code that changed the model. In the example it is the handler of a key or a click. Elsewhere it is the method that took a value from the server.

new is refused on every presenter. shows:in:, shows:in:do: and in: are the ways to make one.