Silk is just too flexible
Oscillating indecisiveness personified as an essay
Introduction
Silk¹ is the young, in-development library for Amber Smalltalk for creating and manipulation of the DOM elements of a host web page (Amber runs in the browser).
There already is a library for that — it would be strange if there wasn’t one already in a system designed to run inside a browser; that library is named Web. Inspired heavily from Seaside, it does its job well, but has its own weak spots:
- it depends on jQuery,
- it is hard to go from existing DOM content to object representation,
- it looks a bit bulky.
So, Silk was created to try a different perspective. First point was to not depend on any external library, but only on handful of selected DOM API, most prominently on insertBefore. Second point was draw inspiration from different places — in particular, from AppJet² which did create HTML by functional composition and itself drew from MochiKit. And third, trying to make things that were hard in Web less so.
Streams
One of the things that was bothering me in Web was the duo of HTMLCanvas
/ TagBrush
, so having two different types of entities for “the thing you write on” (a place on the page) and “a thing you write” (a DOM element), though most of the time you are writing inside an element anyway. It brought nuisances and confusion here and there, during the everyday use.
That’s why I decided to put the whole framework around the insertBefore
DOM API. This API inserts a DOM node inside another DOM node just before its specified child DOM node (or to the end, if you specify to insert before null). So a Silk object represents the parameters of an insertBefore call — the DOM node to insert into and the place where to do the insert (as a reference node to insert before). In other words, Silk object represents an insertion point or a cursor in a web page.
This metaphor led automatically to the notion of another one — to look at the whole web page as a stream of nodes, each of them being stream of nodes itself all the way down to the text nodes.
As Amber has streams already implemented, those were reused, and each Silk object is itself a basic stream, it can use its API: peek
, next
, atEnd
, resetContents
and more; most importantly, <<
. This API puts an object on the stream. So, this inserts a text at the insertion point:
aSilkObject << 'Hello, world'.
Drawing from AppJet / MochiKit, you can put different objects in the stream, and they put different things into the DOM element. You can put an association, and attribute gets set; you can put different Silk object, and the DOM node it represents gets inserted:
parent << ('id'->'foo') << 'Beware, child follows: ' << child
But, <<
stream API has already a useful trait: if you put a Collection in a stream, its elements get put one by one. So the above can as well be:
parent << {'id'->'foo'. 'Beware, child follows: '. child}
Things begin to get nice and scary at the same time.
Tags
DOM elements, commonly called just tags (eg, things like <p></p>
, <script></script>
, <img />
), are most often created for immediate insertion into the parent node, not to float in midair. So, the most convenient API is aimed at creating as well as inserting a DOM element directly at the insertion point. To insert a div element, you do:
aSilkObject DIV
This creates div element, inserts it in aSilkObject, and returns the Silk object representing the insertion point inside the newly created empty div element. In case you want to create a detached div element, you can do it by sending a message directly to Silk class:
Silk DIV
So, the create-and-insert example can be written also as:
| newDiv |
newDiv := Silk DIV.
aSilkObject << newDiv.
^ newDiv
or, if you don’t care if return value is aSilkObject, not the new div:
aSilkObject << Silk DIV
But, you often do not want to insert empty elements, but elements with content. Like in:
aSilkObject P << 'Hello, world!'
This happens so often that there is a convenient form of element-creation message with an argument. This argument is put into newly created element as its content, so the above is equivalent to:
aSilkObject P: 'Hello, world!'
Thanks to the “flatten collections” ability of the stream <<
API, it is possible to put more pieces of content into newly created element using one argument:
aSilkObject P: {'Hello, world!'. Silk BR. 'こんにちは、世界!'}
DocumentFragment
And to complicate things even more, you can create Silk object representing insertion point in a newly created DocumentFragment instance:
Silk newStream
For those who don’t know, a DocumentFragment is an object that is like an element, but does not have tag name nor any attributes, and is not part of a page. What makes it special is that whenever you insert it into any DOM node, its child nodes are inserted instead (and a document fragment itself gets emptied). This can be useful for many things (IIRC jQuery uses it for bulk moving).
Flexibility
The question is now, how do we do in Silk the equivalent of this jQuery call (question credits @RichardEng) (this kind of code using raw HTML is just too common for my liking):
$('#client-main').append(
'<form>
<table>
<tr><td>Username:</td><td><input name="name"></td></tr>
<tr><td>Password:</td><td><input name="password" type="password"></td></tr>
<tr><td><input type="submit" value="Okay"></td></tr>
</table>
</form>'
);
For the first part, Silk has asSilk
API that uses DOM querySelector
API, so the first line is ok, but now, how to create this structure? There’s just too many possibilities. And I myself don’t know, which is more “right” and which is not, which gets adopted, and more importantly, won’t user base be confused by so many ways to do the same thing?³
As direct use of <<
is pretty low-level, I am not including scenarios with direct use of it, instead compose them from convenience TAG
and TAG:
messages with combination of dynamic array and cascading. Few possibilities follow:
"dynamic arrays everywhere"
'#client-main' asSilk
FORM: {
Silk TABLE: {
Silk TR: {
Silk TD: 'Username:'.
Silk TD: {Silk INPUT: {'name'->'name'}}}.
Silk TR: {
Silk TD: 'Password:'.
Silk TD: {Silk INPUT: {'name'->'password'. 'type'->'password'}}}.
Silk TR: {
Silk TD: {Silk INPUT: {'type'->'submit'. 'value'->'Okay'}}}}}
"dynamic arrays only for more"
'#client-main' asSilk
FORM: (
Silk TABLE: {
Silk TR: {
Silk TD: 'Username:'.
Silk TD: (Silk INPUT: 'name'->'name')}.
Silk TR: {
Silk TD: 'Password:'.
Silk TD: (Silk INPUT: {'name'->'password'. 'type'->'password'})}.
Silk TR: (
Silk TD: (Silk INPUT: {'type'->'submit'. 'value'->'Okay'}))})
"chaining and cascades where possible,
dynamic arrays only for more"
'#client-main' asSilk
FORM
TABLE
TR: {
Silk TD: 'Username:'.
Silk TD: (Silk INPUT: 'name'->'name')};
TR: {
Silk TD: 'Password:'.
Silk TD: (Silk INPUT: {'name'->'password'. 'type'->'password'})};
TR: (
Silk TD: (Silk INPUT: {'type'->'submit'. 'value'->'Okay'}))
"chaining and cascades where possible,
prefer cascading even more using `Silk newStream`
(though this means you cannot set attributes),
dynamic arrays only for more"
'#client-main' asSilk
FORM
TABLE
TR: (Silk newStream
TD: 'Username:';
TD: (Silk INPUT: 'name'->'name');
yourself);
TR: (Silk newStream
TD: 'Password:';
TD: (Silk INPUT: {'name'->'password'. 'type'->'password'});
yourself);
TR: (
Silk TD: (Silk INPUT: {'type'->'submit'. 'value'->'Okay'}))
"primarily using in: API with blocks,
dynamic arrays for more leafs, parentheses for one"
'#client-main' asSilk
FORM
TABLE in: [ :table |
table TR in: [ :row |
row TD: 'Username:'.
row TD INPUT: 'name'->'name' ].
table TR in: [ :row |
row TD: 'Password:'.
row TD INPUT: {'name'->'password'. 'type'->'password'} ].
table TR TD INPUT: {'type'->'submit'. 'value'->'Okay'} ]
Footer
If you asked why the messages for element creation are all uppercase — because DNU mechanism is used to intercept them, so you can create any tag that is alphanumeric only.
If you asked why weren’t blocks involved directly as objects that you can put in stream, there is an idea to use them differently (if you want to use them immediately, in: API allows you to do it), specifically, to allow them to specify dynamic content, as in:
aSilk H1: {
'id'->'time'.
'The time is '.
[ :h1 | h1 << Date now asString ].
'.' }
and adding the refresh API, so that whenever you would do
'h1#time' asSilk refresh
the actual time would appear in a header. The virtue of blocks executable many times allows for this more powerful usage.
Originally appeared in Smalltalk Reinassance. In case of differences, this version is authoritative.
1 Named after a resource that spiders (and one of the characters, Webber, whose name was the first candidate) produce in the game "Don’t Starve".
2 This is where I personally started (not counting a few amateur uses on some pages) my life with JavaScript. So I naturally see JavaScript as a server-side language (then, node.js came). Also, I always dreamt of creating similar online experience of “have app in minutes” for Amber itself.
3 This is in fact the main concern of the whole article — though it was needed to introduce Silk to get the picture. Are you thinking this “n ways to do the same thing” can scare away the users, or are my fears just too paranoid?