When architecting user interface components programmers usually engage in copious amounts of complecting. However we shouldn’t be blamed as traditional approaches do not make the boundaries clear. In the last post I alluded to the possibility that CSP provides opportunities to pull things apart and that there might be real value in such a separation of concerns.
I believe there are three main elements not called Model View Controller (nor any other arbitrary variation on that tired old theme). The trichotomic design I’d like to suggest is more fundamental:
When you look at UI components written in good object oriented style whether in Java, Objective-C, or JavaScript you nearly always find these three clear and distinct aspects complected together. In a traditional design you would have event stream processing provided by a base class or mixin. By subclassing you can provide custom interface representations. When it comes to the most complex and application specific code - event stream coordination - many well intentioned components devolve into an async mess made worse by the requisite complex state management.
Ok, but what value can we derive if we take the time to pull these elements apart as described?
Despite Cascading Style Sheets’ many flaws, with media queries designers can flexibly address many different clients in an open ended manner - define your base styles and then override only those parts relevant for a particular client.
User interface programmers don’t have the luxury of CSS for complex interactions yet are faced with the same rapidly growing heterogenous population of clients - components written in a traditional style either bloat to address fragmentation or libraries provide incompatible versions of the same conceptual components like this and that.
What would it look like to pull apart these concerns? And once we pull them apart do we actually have a more responsive system?
Rather than tackling a complex component like a non-toy autocompleter in its entirety as I suggested in the last post, instead I’d like focus on how we can productively apply the separation of concerns to a subcomponent of the autocompleter - the drop down menu.
First let’s consider interface representations for our submenu process. In order to be responsive we’d like to make no commitments at all - the only thing we want to describe is a protocol that different concrete representations must conform to. In ClojureScript this is simple to express:
(defprotocol IHighlightable
(-highlight! [list n])
(-unhighlight! [list n]))
Done.
We’re now free to use any visual representation we please and we’ll see this in a moment.
What about event stream processing?
(defn key-events [prevent-default?]
(->> (r/listen js/document :keydown prevent-default?)
(r/map key-event->keycode)
(r/filter KEYS)
(r/map key->keyword)))
Again we see the
reactive approach. We
take the stream of raw key events, map them into key codes, filter
codes that don’t correspond to the selection process controls (in this
case up arrow, down arrow, and enter) and then map those into the
messages that the highlighter process actually listens for: :next
,
:previous
, :clear
, or a number. This is our process
protocol.
It’s critical that the currency of event coordination be at higher level than concrete events sources like key presses and mouse movement - this will allow our system to be responsive. Another benefit that falls out of designing an abstract stream protocol is that we don’t need to complicate our program with superflous coordination APIs.
Just to drive the point home one more time - the highlighter process does not care how the stream events are constructed. They could just as easily come from a mouse, a finger on a touch screen, a Leap Motion device, or a Clojure vector (think testing).
Now what about event stream coordination?
(defn highlighter [in list]
(let [out (chan)]
(go (loop [highlighted ::none]
(let [e (<! in)]
(if (or (#{:next :previous :clear} e) (number? e))
(let [highlighted (handle-event e highlighted list)]
(>! out highlighted)
(recur highlighted))
(do (>! out e)
(recur highlighted))))))
out))
highlighter
takes two arguments, in
- an input channel of events,
and list
- a UI rendering target. We enter a loop with one piece of
local state - the current highlighted index. We process every event we
receive from in
. If we have an event we can handle we side effect
the rendering target accordingly. Note if we cannot handle the message
we just write it to out
, more on this later.
Lets see this in action. Place your mouse into the grey area (you may need to click) and trying pressing the up and down arrows. NOTE: mousing over the grey areas will steal your key presses, simply mouse out to restore normal behavior:
It seems we have a text based interface similar to the kind you might find in a Rogue-like. In fact our rendering surface is a JavaScript array!
(let [ui (array " Alan Kay"
" J.C.R. Licklider"
" John McCarthy")]
(create-example "ex0"
key-events
(fn []
(dom/set-text! (dom/by-id "ex0-ui") (.join ui "\n")))
(fn [events]
(highlighter events ui))))
With each key press we mutate this array, concatenate it into a
string, and render that into a pre
tag. We’ll see in a moment that
all of this code can be reused without modification to target an HTML
interface.
But before we do that we should address a deficiency - we cannot select anything in the submenu. Let’s add another interface representation protocol:
(defprotocol ISelectable
(-select! [list n])
(-unselect! [list n]))
We don’t need additional event stream processing, the stream above
produces all the information we need - it includes :select
events
and the highlighter simply passes it along since it cannot handle that
message itself. We can move straight away to event stream
coordination:
(defn selector [in list data]
(let [out (chan)]
(go (loop [highlighted ::none selected ::none]
(let [e (<! in)]
(if (= e :select)
(do
(when (number? selected)
(-unselect! list selected))
(-select! list highlighted)
(>! out [:select (nth data highlighted)])
(recur highlighted highlighted))
(do
(>! out e)
(if (or (= e ::none) (number? e))
(recur e selected)
(recur highlighted selected)))))))
out))
This looks remarkably similar to highlighter
, in fact we’re feeding
the output of highlighter
into selector
! Again place your mouse
over the grey area and press either the up arrow, down arrow, or enter
keys. You’ll now see that selections are preserved:
We can inherit functionality just by composing streams. We simply pass along the messages we don’t understand. This all sounds eerily familiar.
Because we have committed to so little we can now reap the rewards of
the design, what follows is an HTML submenu component that uses all of
the prior logic - the only difference is that the initial stream
includes hover information (which are converted into integers) from
the list items as well as mouse clicks (which are converted into
:select
s). Put your mouse in the grey area - note that hovering,
clicking, arrows keys, and enter all work.
By teasing apart the architecture of the UI components we now have a truly modular and responsive system. It’s also pleasantly small for the amount of flexibility we get in return.
In the next post we’ll see how to put the result of this post and the original post into a cohesive whole.
You can see all the code for this post here.