Building the page with Silk
Silk sees an element as a stream of its children. You put things into it with <<. They appear where its cursor stands, which is the end of the element unless you move it. Silk is built on Domite, a thin layer over the DOM. The Hello Amber project from the install page imports 'silk/Silk' already. Every example here runs as it is in a workspace of that project.
The examples put their work 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 work in
'#app' asSilk answers a Silk on the first element that the CSS selector finds, with the cursor at its end. Silk new stands at the end of the body. Inside an element, aSilk at: 'p' finds the first match, and Silk allAt: 'button' answers a collection with every match in the page.
Putting things in
<< puts one thing at the cursor:
'#app' asSilk << 'Hello'
A message in capital letters makes an element of that name: P, DIV, H2, LI. With a colon, it also puts its argument into the new element. Either way the element goes in at the cursor, and the message answers a Silk on it. A cascade sends several to the same Silk:
'#app' asSilk
H2: 'Shopping';
P: { 'class' -> 'hint'. 'Type a thing and press Enter.' };
HR
This makes <h2>Shopping</h2><p class="hint">Type a thing and press Enter.</p><hr>. What goes in depends on what it is:
| You put | Silk puts in |
|---|---|
| a string | its text |
| a number or another object | its printString |
an association, 'class' -> 'hint' |
an attribute of the element |
| an array or another collection | each of its items, in turn |
| a block | what the block puts; it gets the Silk as its argument |
| another Silk | its element |
So there are three ways to nest. Keep the answer of a capital message and fill it:
| list |
list := '#app' asSilk UL.
list LI: 'Milk'; LI: 'Bread'
Or make the inner elements first. Sent to the class, Silk LI: 'Milk' makes an element that is not in the page yet:
'#app' asSilk UL: (#('Milk' 'Bread') collect: [ :each | Silk LI: each ])
Or let a block fill the new element:
'#app' asSilk DIV: [ :div | #('Milk' 'Bread') do: [ :each | div SPAN: each ] ]
All three give the same list. The cursor can move, too: reset puts it before the first child, so '#app' asSilk reset; P: 'First of all' puts the paragraph at the top.
Attributes and properties
attrAt:put: sets an attribute of the element, and attrAt: reads it, or answers nil. propAt: and propAt:put: read and set a property of the DOM element. The value of an input is one: input propAt: 'value'. For anything else, element answers the DOM element itself, and the JavaScript page says how to talk to it:
| hint |
hint := '#app' asSilk at: '.hint'.
hint attrAt: 'title' put: 'A hint'.
hint element classList toggle: 'done'
Events and input
on:bind: runs a block when the element gets an event. The block takes the DOM event as its argument, if it wants it. This adds an input and a button, and each click adds what was typed to a list:
| app input list |
app := '#app' asSilk.
input := app INPUT: { 'placeholder' -> 'What to buy?' }.
(app BUTTON: 'Add') on: #click bind: [
list LI: (input propAt: 'value').
input propAt: 'value' put: '' ].
list := app UL
The block runs at the click, when list is set already. A key is in the event:
input on: #keydown bind: [ :event |
event key = 'Enter' ifTrue: [ list LI: (input propAt: 'value') ] ]
The sample binds its buttons the same way, in HelloAmber >> augmentPage: '#silk-tag' asSilk on: #click bind: [ self doSilkTAG ]. off:unbind: takes a block off again.
Replacing and removing
resetContents empties the element and leaves the cursor at its end. A cascade empties and fills:
'#app' asSilk resetContents; P: 'Fresh start'
To remove an element itself, cut it out between two cursors. setToBefore moves a cursor up to the parent, just before its element, and setToAfter just after it. cutUpTo: cuts what lies between two cursors:
aSilk copy setToBefore cutUpTo: aSilk copy setToAfter
Each message answers the moved cursor, so the line needs no parentheses. The cut answers a Silk on what it took out. Put that answer into another Silk, and the element moves there, with its event bindings.
An object that puts itself on the page
Silk puts an object by sending it renderOnSilk: with itself. Any object that answers renderOnSilk: can therefore be put with <<. Blocks and associations are such objects, and a class of yours can be one too. This shopping list keeps its items and shows them:
Object subclass: #ShoppingList
slots: {#items. #list}
package: 'HelloAmber'
initialize
super initialize.
items := OrderedCollection new
renderOnSilk: aSilk
| input |
input := aSilk INPUT: { 'placeholder' -> 'What to buy?' }.
input on: #keydown bind: [ :event |
event key = 'Enter' ifTrue: [
self add: (input propAt: 'value').
input propAt: 'value' put: '' ] ].
list := aSilk UL.
self renderItems
renderItems
list resetContents.
items do: [ :each |
(list LI: each) on: #click bind: [ 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 list on the page:
'#app' asSilk << ShoppingList new
Type Milk and press Enter, then Bread: the list shows both. Click Milk, and it goes. After every change, renderItems empties the list and puts every item again. It is the simplest way to keep the page in step with the objects.
One rule comes with renderOnSilk:. Silk takes an answer of nil to mean that the object does not render itself, and then puts its printString. A method without ^ answers self, which is what you want.
When the markup should come from the page rather than from code, snippets put it there.