A note to any presumptive reader: This is an ongoing sketch done during a quest to understand what state machines can do for UI construction. Code samples are only intended as art, i.e. hopefully illustrative, possibly interesting, only accidentally executable.
A web page is a state
Once upon a time, all web pages were static. Then the situation was simple: The browser was in the same state as long as the same web page was shown.
Clicking link triggers a transition
When the user clicked a link, the browser performed a transition to another state. These states can be represented by the URL. But what we as users see on the screen is the result of the browser loading and rendering a html page that corresponds to the URL. What we observe is a very simple state machine. We now understands the browser and the web page using sate machine concepts.
So what is a HTML tag?
Lets now zoom in our view and consider a single HTML tag. What is it and where do it belong? We can observe it:
- When the html file is loaded and the document is parsed, a given HTML tag will be realized as a DOM element and typically rendered to the screen.
- When the user has clicked a non-local link and the current page unloads, the DOM element is deleted.
A HTML tag represents a boundary action
So because the exists in the html file, a node is created upon ENTRY of the web page as a state, and then deleted when the state EXITS. This means that a HTML tag stands for a combination of and Entry action and an Exit action. For convenience, lets call that a boundary action.
Boundary actions are natural and helpful
While the term "Boundary Action" is not typically part of State machine terminology, it has two advantages: It is convenient and it can offer something any sane entry action would kill for: The opportunity to undo what the entry action has done. For example, a friendly implementation will keep the environment in which the entry action was executed alive until the corresponding exit action executes. Let's say the an entry action starts a timer that will trigger a transition after 10 seconds. Then a user events causes the state to transition after only 3 seconds. Since the timer is implemented as a boundary action, it can delete the timer as part of it's exit action.
So let's implement a HTML tag as a boundary action
Assuming we have a state machine that controls the internals of a web page, and that each state accepts boundary actions, how can we implement a HTML tag? Let's give it a try:
Upon entry of the state, the tag should create a DOM element. Upon exit, the element should be removed. In order to keep hold on the environment that the boundary action needs, we use double closures in the spirit of Reified Continuation Passing Style, see
[ The Art of the State ] Why it is impossible to write an Identity Function in JavaScript, and how to do it anyway
rekreanto ・ Sep 28 '20
First iteration:
const tag =
( tagname ) => // defined by user
// ::::: [ definition time above ] ::::: [ mount time below ] :::::
( parent ) => // given by the implementation
// ::::: [ mount time above ] ::::: [ runtime below ] :::::
( ) => { // called upon entry
// ENTRY ACTIONS
const e = document.createElement( tagname );
parent.appendChild( e );
return () => { // called upon exit
//EXIT ACTIONS
parent.removeChild( e );
}
}
Oh, it seems we have forgotten something? Non-atomic HTML tags can have content, i.e. other HTML tags. It is a really bad idea to forget about one's kids!
Second iteration:
const tag =
( tagname ) => // defined by user
( ...boundaryActions ) => // also defined by user
// ::::: [ definition time above ] ::::: [ mount time below ] :::::
( parent ) => // given by the implementation
// ::::: [ mount time above ] ::::: [ runtime below ] :::::
( ) => { // called upon entry
// ENTRY ACTIONS
const e = document.createElement( tagname );
parent.appendChild( e );
const entryActions = boundaryActions.map( bAct => bAct( parent ) );
const exitActions = entryActions.map( enAct => enAct( ) );
return () => { // called upon exit
//EXIT ACTIONS
exitActions.forEach( exAct => exAct( ) ); // cleanup children
parent.removeChild( e ); // cleanup self
}
}
// Sample HTML
// N.B. strings and objects are not supported
// as boundary actions in the code above
tag('div')
( tag('h1')( "The Art of The State, part I" )
, tag('p' )( "...hopefully illustrative, possibly interesting, certainly useless" )
, tag('h2')( "Are we there yet?" )
, tag('p' )
( "Short answer: No."
, tag('input')({ type:'text', placeholder: 'WHY NOT?' })
, tag('ol')
( tag('li')( "ENTRY ACTIONS" )
, tag('li')( "EXIT ACTIONS" )
)
)
)
;
Are we there yet?
Short answer: No.
So, assuming we have a state machine that controls our web page, can we add HTML content in a safe and sane way?
Well, I think we nailed the essentials: To uncreate in the exit action all that we created in the entry action.
But, alas we cannot create any visible DOM nodes, yet. One way to do that would be to look for any string among the boundaryActions inputs, and promote it to boundary action, and make a deletable text node from it. Also, we have no way of adding attributes to the element. That could be done in a similar fashion, looking for an object to promote to boundary action, thus adding/deleting its props to our DOM node. (This way of interpreting data types as something else is used to great advantage in both jQuery and Hiccup.)
Also we should add a variant of out tag implementation that, in the spirit of jQuery don't create a DOM node, but looks it up and allows modifications.
Third, the notion of context is probably incomplete, since, when we map events to transitions, we have no where to send the transition. The DOM creation actions had parent
, maybe the event handlers need something similar.
How might those boundary actions be useful?
This is utterly useless in the case described above, where every web page is static. But consider the typical case in 2020, when almost every web page has dynamic elements, then we have different landscape: We can define a web-page according to a plan. A plan where states are explicit and all DOM creation and manipulation can occur absolutely only in one place, namely on the borders of the states. And every action that performs a mutation will also undo that mutation at the exactly appropriate point in time: when it's state exits.
Summary
- We found out that under the assumption that the browser is a state machine, and that to visit a page is to be in a state, a HTML tag represents a Boundary Action.
- Also, under the further assumption that aspects of a web page can change while it is visited, and that these changes are reified as states, the place where DOM is specified, can no more be restricted to the html file. But in stead of spreading out DOM manipulation to anywhere in any script, we concluded that DOM must be described relative to the current state, and thus can only occur in the form of boundary actions attached to the states.
- We found that implementing the boundary actions using double closures, we could provide an encapsulated environment where the boundary action could hold on to references needed for clean-up work during exit.
Open questions:
- What do we need on order to make it possible to define event handlers as boundary actions? (i.e. how to support some way to make transitions able to influence the state of the machine)
- What primitives do we need for the state machine that is just assumed in this post?
- Which primitives can be boundary actions, and which need to be something else?
- So instead of a state machine, where states are combines using branching and choice, we are talking about a UI machine, where User Interfaces combines using branching and choice?
- Then we should have a primitive that creates a User Interface, taking as parameters boundary actions and sub-UI's?
- How to support parameterised states? Can update functions be base case transitions, and can we have "duck stating"*
- duck stating -- a term formed after "duck typing", where a type is not what something is but wht it can be used as. Thus, under suck stating, state can be used wither as qualitative state or quantitative state or both.
- Can duck stating allow update functions to be used as base case transitions?
- Can duck stating be a key to naturally fit in parameterized states (param-or)?
Top comments (0)