Skip to content
Yaroslaff Fedin edited this page Jun 20, 2011 · 7 revisions

Interactions

Why need to invent

The reason why DOM frameworks became so popular in recent years is simple: it enables everybody write dynamic scenarios for their websites and chain them together. Javascript's functional nature works really good when we need to split execution down to steps and execute them in order:

  • When I click at submit button...
  • Form gets asynchronously submitted
  • Replace #content element with response
  • Fade it in
  • After that, fade out the form

The usual approach is something along these (mootools, pseudocode) lines:

var form = document.getElement('form');
button = form.getElement('button[type=submit]');
button.addEvent('click', function() {
  form.send(function(response) {
    var fragment = document.createFragment(response);
    var content = document.id('content');
    document.id(fragment);
    content.parentNode.replaceChild(fragment, content).fade('in').chain(function() {
      form.fade('out');
    })
  })
})

DOM frameworks have functional but somewhat sparse tools. It's not the worst possible code, but it is a:

  • glue code - A spaghetti of different domains connected together to make something useful. Event listener, submission flow, data retrieval, an update and animation in only a few lines of code.
  • nested callback hell - The only way to set flow is to nest callbacks. When there are more layers added on top, like error handling and branching, it gets even worse. Some people are trying to solve the problem by introducing pseudo-continuations, promises and other functional abstractions and compositions, but at the heart of all it still stays ugly and slow.

Chains

Another approach is to look at the scenario in a more abstract way. We could try to represent scenario as a chain of commands to apply an action on a target.

  • When I click at the button...
  • Form gets asynchronously submitted (target: form, action: submit)
  • Update #content element with response (target: #content, action: replace)
  • Fade it in
    (target: #content, action: display, state: true)
  • After that, fade out the form
    (target: form, action: display, state: false)

The chain executes as it reads - from top to bottom. When it reaches asynchronous step (like submitting a form), it stops execution until it gets the successful response. The response is passed to the next step, and the chain goes on.

Every time a chain starts or continues after the break, it does so with arguments. An action that requests data will continue the chain after request is done and pass the response to the next action. And that action should make use of that response, because following actions will not be able to access it anymore.

Actions

LSD has around 20 built in actions that can be executed on widgets and elements.

Synchronous actions

Some actions can be executed right away and they will not need to wait until something is finished. Those are synchronous actions.

Reversible

The actions that can be undone are called reversible. They are often are executed in indeterminate state. That means, that the action is specified, but wether it should be done or undone, is upon action.

  • Toggle - toggles the checkbox state. LSD makes use of HTML5 commands abstractions, so most checkbox may not look like them. For example, selecting an item in a list can be done with check too. Toggle will either check if target is unchecked, or uncheck otherwise.

  • Counter (alias Increment) - parses any HTML element and if it contains a phrase like "10 comments", the action will increment it (and pluralize the word). It's pretty smart and will still work even if the number or the noun is wrapped into some html tag. The opposite of that is Decrement action, that reduces the found number by one.

  • State(name) - changes the state of the node. Requires state name argument. If a node is an element, the action will toggle the class with the name of the state. If it's a widget, it will try to set the state with that name.

  • Focus - focuses the node. Provides Blur action alias that does opposite.

Irreversible

For some actions what's done is done. There's no way back, and there's no need to.

  • Update - updates a node with the new content. Often used as callback to actions that request data. Update itself empties the node and puts the new content in. But there's the whole family of self-content update actions:

    • Replace - replaces a node with the given content. It doesn't matter if content has more nodes than one.
    • Append - put the given content at the bottom of the node
    • Prepend - put the given content at top of the node
    • Before - put content before the node
    • After - put content after the node
  • Clone - clones a tree and insert it below original. When an input with index in name (like person[organizations][1]) is cloned, the containing form will increment the index until it finds an untaken one. This action allows all kinda of clonable field sets possible.

  • History - grabs a node's href or action attribute and makes a history checkpoint as if the link was visited. Back button will return to the original url (or a previous checkpoint)

  • Set - Set a node's value to a object given in arguments. If the node is an input, the value attribute will be set. If the node is marked with microdata and the argument is object, the microdata item will be set and all matched itemprops will be updated. If it's a simple node, the innerHTML will be set.

Asynchronous actions

An action that triggers some process and waiting for its completion is called asynchronous. It breaks the execution chain and proceeds only when the waiting is over. Asynchronous action may not be reversed, but it may be cancelled while in progress.

  • Submit - triggers submission for element. Submission is applicable to a:

    • submittable node like form or dialog.
    • link node or a widget that has either href or src attribute
    • clickable node. Then submission .click()'s the widget.
    • resourceful widget with HTML5 microdata itemscope & itemtype attributes. Submission of a resourceful widget without itemid attribute result in a POST to the url in itemtype. Submission of widget with itemid will result in a PUT to itemtype/itemid. In other words, it a resource with itemid attribute is considered saved.
  • Dialog - clones a target node with all its children and displays it as dialog. Proceeds only when dialog is successfully submitted.

  • Edit - turns a node into an editable form. It converts all microdata-marked elements with editable attribute and turn them into a form fields. The form will submit the resource and update it and hide itself upon getting successful response.

Possibly asynchronous actions

Some actions are not always asynchronous, but sometimes they are.

  • Delete - removes a tree of nodes from the DOM. If the deleted node is a resourceful widget (itemtype, itemscope and itemid are set), it sends a DELETE request to itemtype, stops chain execution and continues only if request comes backs successful. For nodes that are not resourceful widgets, it simply disposes the target synchronously and continues.

  • Display - hides or shows the node. Usually it simply toggles display state of the node. If the node is a widget, action calls hide or show methods on it. Widget may decide to use animation to hide or show and return animation object back to action. In that case, the action will be considered asynchronous and execution chain will be broken until animation is complete.

Create action chain

Chain in options

Defining a chain in options is pretty straightforward. chain option accepts an object of links with name as keys and functions and values.

Let's see how a form that updates #content element may be implemented:

LSD.Widget.Form = new Class({
  options: {
    pseudos: ['fieldset', 'form'],
    chain: {
      submission: function() {
        return {target: this, action: 'submit'}
      },
      update: function() {
        return {target: document.id('content'), action: 'update'}
      }
    }
  }
});
var form = new LSD.Widget({tag: 'form'});
form.callChain(); //sends form, updates #content

A form submits itself and breaks the chain. When it gets the response, it continues the chain and updates the element with the content from the previous step (passed to update action behind the scenes). To trigger the execution of the chain, one you call the .callChain() method, but you rarely will need it.

Commands

Most of the interactive elements on the page execute their actions in response to a user interaction. Checking the checkbox on can check the dependent checkboxes, or clicking a button can open a dialog.

There is a good abstraction to this in HTML5 commands spec. It says, basically, that each widget on the page may have one command. Each widget has its way trigger the command (click the button, change text value), but a single widget has a single outcome.

A typical command has name, type and action. There are three types of the commands:

  • Command - is a irreversible action. Sending a form is an example of that.
  • Checkbox - reversible action that can be done and undone by repeating interaction.
  • Radio - a "choose one" kind of a action. Checking item in a group, unchecks others.

Every interactive widget on the page falls into one of the three groups. For example <select> is a radiogroup of <option> commands, and <select multiple> is a set of checkbox commands defined by <option> elements.

Often widgets don't have a specified command and they figure it out from the widget configuration. For example, a widget with a href or action attribute is considered as a widget that sends requests by LSD. And this recognition also defines the action of a command.

So given that we use form widget with action attribute that turns it asynchronous, we make form define a command that will have submit as its action. command pseudo class makes widget generate command.

LSD.Widget.Form = new Class({
  options: {
    pseudos: ['fieldset', 'form', 'command'],
    chain: {
      update: function() {
        return {target: document.id('content'), action: 'update'}
      }
    }
  }
});
var form = new LSD.Widget({tag: 'form'});
form.callChain(); //sends form, updates #content

The submission link was removed, but the form is still sent when .callChain() is called.

And there's still an update action that has a hardcoded target element which makes it hard to reuse in different situations. But there's a solution to that.

Chain in target expressions

In a perfect world there is a small number of distinct entities and infinite possibilities to composite them. But the burden of quick thinking composition is often legacy code and inflexible ad-hoc solutions that take more time to change, than to rewrite from scratch.

And here you should demand more - a way to tie pieces together in a way that does not leave any code behind, and target selectors are the solution.

Previous examples used an #content element to be updated with response that comes from the widget, like this:

<form action="/people" transport="xhr">
  <button type="submit">Submit</button>
</form>
<div id="content"></div>

In a regular html a layout like the following would send a form into a new tab:

<form action="/people" target="_blank">
</form>

LSD reuses target attribute to access other nodes on the page and even call actions on them. In the following example, a form generates the same action chain (submit & update) as before, but it doesnt have any of those defined in the class itself.

<form action="/people" transport="xhr" target="$$ #content">
  <button type="submit">Submit</button>
</form>
<div id="content"></div>

LSD.Widget.Form = new Class({
  options: {
    pseudos: ['fieldset', 'form', 'command']
  }
});

var form = new LSD.Widget(document.getElement('form'));
form.callChain();

As you can guess, target attribute also defines the action with the target retrieved by selector in attribute. The default action for asynchronous widgets is update, but it can be overridden in the selector itself:

<form action="/people" transport="xhr" target="$$ #content :append()">
</form>

Let's take a closer look at selector.

$$ #content :append()
^  ^         ^- and append content to it
|  '- element with id=content
'- Find in <body>

It reads as "append content in to #content element". The two-dollar is a special combinator and it means "document.body". There are 4 combinators like that:

  • && (default) - Find in root widget. Finds widget in LSDOM.
  • & - Find in this widget.
  • $$ - Find an element in document.body
  • $ - Find elements in this element.

The part at the end is a name of the action. Parenthesizes is optional, but it's there for a better readability. The action should be separated from selector with space.

Multiple selectors may be separated with comma in one expression. Each selector and action is converted into chain link where they are executed in order.

When the action is not set in expression, the default target action is used for the widget (and for asynchronous widgets it's update).

A few examples of other target values:

:target => "grid item :delete()"
// Delete all items in the grid

:target => "& :toggle()"
// Make widget toggle its checkedness (applicable to checkboxes)

:target => "$ + a :submit(), $ :hide()"
// Submit the next element to this node (which is a link), and hide the node after it's done

When the only thing that glues widgets together is a selector in template, and the actual widget classes are clean, it is easy to maintain and change. Changes to selectors can be done along with changes to markup.

Clone this wiki locally