ClojureScript was already an incredible platform for experimenting with different approaches for writing browser based applications. However, things have changed dramaticly for the better. The new core.async library introduces Go-like channels and blocks to ClojureScript. With this new library we can write blocking code and control the flow of state in a program with a great deal of precision.
This is not a tutorial on how to program in ClojureScript. It is an exploration of different programming patterns that are made possible by core.async.
If you are new to Clojure this cheetsheet may help.
First we’ll create a function that captures click events and directs them into a channel.
This function turns a CSS selector into a channel of click
chan function creates a channel. When an
element with the provided selector get’s clicked we use the async
put! function to put a message value into the
channel. After wiring it up we return the newly created channel.
You can put any value you want into a channel. We are using a vector as an expedient format for a message and it’s attached data.
Let’s use this function to create some click channels:
new-todo-click and the
cancel-new-form-click are channels which will produce
message values when a user clicks on their respective elements.
go block creates a context in which we can make
blocking calls. In this case, we are going to use the
function which blocks execution from continuing until a value is
available on the provided channel.
The loop renders the current state and then blocks and waits for a
click on the a.new-todo element. When the element is clicked the
(<! new-todo-click) call unblocks returning a value
(which we ignore). We then render a new state where a todo form modal
is displayed by the template renderer. The loop then blocks again
waiting for the a.cancel-new-todo element to get clicked.
The code is written sequentially and captures the behavior of the program explicitly without callbacks.
Here is a running example below. Give it a try by opening and closing the form as many times as you want.
There is a serious bug in this code. To see the bug, click the add task button to open the modal form and then continue clicking it 5 more times. Now cancel the form and you will notice that you have to cancel it 6 times before the form goes away.
Take a moment to reflect on this and the code that caused it.
While this is a disconcerting bug it demonstrates further how channels behave. Channels stack up the signals in a queue like buffer. Note that once we have received a value from the new-todo-click channel another one doesn’t get pulled out of the channel until there is a value is received from the cancel-new-form-click.
To me it is amazing that you can express this level of control over execution order and program state in such a straight forward manner. It is basically an implicit state machine.
a modal window is a child window that requires users to interact with it before they can return to operating the parent application
The event producing element’s below the dom screen are still operable. If the dom screen doesn’t size properly because of a CSS conflict or the modal code didn’t keep pace with the current crop of mobile browsers then users are going to be able to operate on those event bound elements. That’s not what we are intending.
If modal means ‘I will respond to no other events except the ones that are explicitely defined in the modal itself’, well then … shouldn’t we code it that way?
Let’s create a couple of channel operations to help:
merge-chans function merges the values from all the
channel args passed to it into a single channel. It does this by using
alts! function which blocks on a group of channels
waiting for a value to become available on one of them. (If there is a
tie it picks one randomly.)
filter-chan creates a channel that only passes on values
that meet the condition of a predicate. All other values are discarded.
With these tools we can easily filter out all the message values that we don’t want to respond to. Essentially eating any unwanted user actions.
Now things should work as we would would expect.
To test this, click on the add task button as many times as you want and then click the cancel button. All those extra add task clicks are ignored.
Now that we have accurate modal semantics let’s finally add a task.
Alright let’s create a channel that handles form submissions:
That should do it. This is very similar to the
click-chan function except that it responds to form
Here we add a new
task-form-submit channel and merge it
input-chan. We also add it to the filter so that
when the modal is open we only get submit and cancel messages. We
then switch on the message name and operate on the state depending on
which message we get.
Again try the example.
Now we are adding the feature to complete individual todos. This gives us a good opportunity to break out the modal functionality into a separate function and refactor a little.
So we refactored things a bit. We moved the creation of the input
channels to its own function and now we have two functions that
represent two contexts for user interaction. The
add-task-modal functions both take the current
state and a channel of input messages.
This is an extremely interesting discovery. The
input-chan is passed from context to context. Each
context defines the meaning and availablility of a user action.
Imagine a more complex set of user interactions where you dive from
one context down into another and then coming back up through
them. This gives you precise control over the users experience and
with no need to manually record a trail of where your user has been.
In these examples you will notice that the program state is neither global or mutable. There is no set of central objects that we access and change from various callbacks. In each example as actions are taken a new version of state is created from the current state and then that state is passed on to the next part of the program that needs to operate on it. State is completely contained and local to it’s particular process.
You might notice that the cancel action merely returns the state that
was passed into the
add-task-modal function. Reseting
merely means returning to the state we were in before any changes were
With core.async you can take what would have previously been very complex and turn it into something easily managed. Parallax? No problem and no library neccessary. Drawing progam? So much simpler. Story telling animations become absolutely straight forward. Did someone say Tetris?
If you are new to Clojure/ClojureScript keep in mind that core.async is simply icing on a pretty sweet cake.
I have tried to peek your interest in ClojureScript, core.async and new ways of thinking about developing where you have much more certainty about the state of your program at any given moment.
Special thanks to reviewers: