Building the page with Web

Web renders the way Seaside does. A block gets a canvas, called html by habit, and sends it one message per tag: html li: 'Milk' makes a list item. Each tag message answers a brush on the new element. Attributes and events are messages to the brush. The Hello Amber project from the install page imports 'amber/web/Web' already. Every example here runs as it is in a workspace of that project.

The examples render into an empty element. Add it to index.html, after the sample’s list, and reload the page:

<div id="app"></div>

An element to render into

'#app' asBrush answers a brush on the first element that the CSS selector finds. with: renders into it, after what is there:

'#app' asBrush with: [ :html | html h2: 'Shopping' ]

with: takes a string, a block, another brush or a widget. A block gets a canvas that renders into the brush’s element.

Tags, attributes and nesting

The canvas has a message for every HTML tag: html p, html div, html ul, html li, html input, html button, html h1 to html h6, and the rest. html tag: 'main' makes any other. A tag message with a colon puts its argument in. Without one, it answers the brush, and a cascade sends that brush the attributes and the contents:

'#app' asBrush with: [ :html |
	html h2: 'Shopping'.
	html p class: 'hint'; with: 'Type a thing and press Enter.'.
	html ul with: [
		html li: 'Milk'.
		html li: 'Bread' ] ]

The block given to ul with: renders inside the list, so html li: there makes the items. This gives <h2>Shopping</h2><p class="hint">Type a thing and press Enter.</p><ul><li>Milk</li><li>Bread</li></ul>.

The brush has a message for the common attributes: id:, class:, href:, src:, type:, value:, placeholder:, title: and more. at:put: sets any other: html div at: 'data-id' put: '7'. A loop is plain Smalltalk:

'#app' asBrush with: [ :html |
	html ul with: [
		#('Milk' 'Bread') do: [ :each | html li: each ] ] ]

Events and input

A brush has a message for each common event: onClick:, onDblClick:, onKeyDown:, onChange:, onSubmit:, onBlur: and more. onEvent:do: binds any other. The block takes the DOM event as its argument, if it wants it.

The brush sets attributes, but reads nothing back from the element. For the value of an input, ask the DOM element: input asDomNode value. This adds an input and a button, and each click adds what was typed to a list:

| input list |
'#app' asBrush with: [ :html |
	input := html input.
	input placeholder: 'What to buy?'.
	html button with: 'Add'; onClick: [
		list with: [ :h | h li: input asDomNode value ].
		input asDomNode value: '' ].
	list := html ul ]

A brush keeps its canvas, so list with: adds to the list at any later time. A key is in the event:

input onKeyDown: [ :event |
	event key = 'Enter' ifTrue: [ list with: [ :h | h li: input asDomNode value ] ] ]

The sample binds a button already on the page the same way, in HelloAmber >> augmentPage: '#amber-with' asBrush onClick: [ self doAmberWith ].

Replacing and removing

contents: empties the element and renders anew. empty only empties it:

'#app' asBrush contents: [ :html | html p: 'Fresh start' ]

To remove an element itself, ask the DOM: aBrush asDomNode remove.

A widget

A widget is an object that renders itself on a canvas. Subclass Widget and implement renderOn:. This shopping list keeps its items and shows them:

Widget subclass: #ShoppingListWidget
	slots: {#items. #list}
	package: 'HelloAmber'
initialize
	super initialize.
	items := OrderedCollection new
renderOn: html
	| input |
	input := html input.
	input placeholder: 'What to buy?'.
	input onKeyDown: [ :event |
		event key = 'Enter' ifTrue: [
			self add: input asDomNode value.
			input asDomNode value: '' ] ].
	list := html ul.
	self renderItems
renderItems
	list contents: [ :html |
		items do: [ :each |
			html li with: each; onClick: [ self remove: each ] ] ]
add: aString
	items add: aString.
	self renderItems
remove: aString
	items remove: aString.
	self renderItems

Create the class and the five methods in Helios, as the Helios page shows. Then put the widget on the page:

ShoppingListWidget new appendToBrush: '#app' asBrush

'#app' asBrush with: ShoppingListWidget new does the same. Type Milk and press Enter, then Bread: the list shows both. Click Milk, and it goes. After every change, renderItems renders the whole list again. It is the simplest way to keep the page in step with the objects.

With jQuery

The sample also imports contrib-jquery’s 'amber/jquery/Web-JQuery'. With it, a brush answers asJQuery, for code that wants jQuery’s own API: (html p: 'Hi') asJQuery fadeOut. Web does not need jQuery, and a project that has no use for it drops the import.

When the markup should come from the page rather than from code, snippets put it there.