Working with the DOM

Posted on September 27, 2017

Previously we looked at Dynamics, which combine the fundamental Event and Behavior types to allow us to do some things more efficiently. Now we’ll put them to use and actually create some DOM elements.

We’ll be building up pieces of a todo-list application along the way. It is probably becoming a cliche by now, but it is familiar to a lot of people and will give me something concrete to use while demonstrating some of the cooler things that reflex provides as the series progresses.

The DomBuilder monad

When we want to create some DOM elements we use the reflex-dom library.

The main thing that it provides is an FRP aware builder monad for the DOM. The DomBuilder typeclass indicates that we are building up a DOM tree.

We can lay out elements using el:

and can add text nodes with text:

or dynText:

As an aside: the PostBuild typeclass gives us access to an Event which fires when the element is added to the DOM tree, and is handy for setting up initial values and so on.

With the above pieces in hand, we can put together a simple div with some text in it using:

or:

That’s all well and good, but it’s very static.

Buttons

The simplest thing we can add to make things more interactive is a button.

We can add a button to our DOM tree with button:

We can use this to create a very boring todo item:

This is following some common reflex advice about components: start with Dynamics as inputs and Events as outputs. We’ll come back to this later, and will see when to break those rules, but it’s a very useful place to start.

We use Dynamics as much as we can to update the DOM. The Behavior aspect of the Dynamic keeps track of some state, and the Event aspect of the Dynamic fires when that state updates. For the smaller changes to the DOM, we can attach a handler for that Event at a particular DOM node to do what needs doing when the Event fires. If you set things up correctly, this can lead to the same effect as working with a virtual DOM but without needing to do the diffing or patching. The fact that we can pass these Dynamics around as first class values is the cherry on top.

It also introduces MonadWidget, which is a constraint synonym for a long list of typeclasses that are often used when created components that will be translated to parts of a DOM tree, including the DomBuilder t m constraint.

If we want to see something happen when that Event is fired, we can use it to modify the text we are displaying.

This gives us a marginally less boring todo item:

We’re using the RecursiveDo language extension and the mdo keyword in the above code, which was discussed in the previous post. We use RecursiveDo with reflex code when we have cycles in our FRP network.

We use it more often with reflex-dom. This is because we are working in a builder monad, and so the order in which widgets appear in the code is the order in which the widgets are laid out on the page. Sometimes a widget will need access to an Event or Dynamic produced by a widget that is laid out further down on the page, and RecursiveDo lets us make the forward references that enable that.

It can be a little mind-bending the first time you come across, but it doesn’t take long until it starts to feel natural.

Various functions for creating elements

There are variants of the el function which allow for more customization.

Adding classes

We can add a class to an element:

or we can add class to an element that will change over time:

This lets us make our todo item a little prettier by adding a class to the item itself, and by using color to indicate when an item has been removed:

Adding attributes

There are also functions which allow us to specify arbitrary attributes:

We could use this to hide the text when an item is removed:

The code above contains a use of the helper =:, which is the Map.singleton function in operator form. It pops up in reflex code, so it’s good to know about.

Handling new events

All of the above functions for producing DOM elements have variants that expose the underlying element.

They all have a prime at the end of their names and return a pair. For instance:

We can use these along with domEvent:

to create new reflex Events from various DOM events. It looks hideous, but it is fairly easy to use.

If we wanted a clickable link we could do something like:

To extend our previous example, we could clear the “removed” state of our item when the text is double clicked:

We can use this to add support for any of the usual DOM events to our widgets.

Checkboxes

There are more complicated inputs than buttons.

The simplest step up is a checkbox. This gives us a little bit of insight into the design of components in reflex-dom.

We have a data type that contains the information we need to create the component:

which typically has a Default instance:

and we have a data type that contains the information we might want from the component:

Both of these data types have lenses available. We’re only making basic usage of them for the time being, but they can be very useful.

Our use of lenses will be limited to setting up our configuration data types:

and to accessing fields of the resulting data type:

The two data types are linked together with a function that lays the checkbox out in the DOM tree - in this case it also has an argument for the initial state of the checkbox:

We’re now ready to add a checkbox to our todo item:

although we’d rather have some visual indicator that the checkbox is working correctly:

We’ll also use checkboxes for some other todo-list related functionality.

We can make a component we can use for clearing completed items, and have it only be visible when at least one todo item is completed:

We can also make a component that has a checkbox which causes all todo items to be marked as complete or incomplete, depending on the state of the checkbox. We’ll also make this binding bidirectional - if all of the items are marked as complete, the checkbox will become checked, and that ceases to be the case then the checkbox will become unchecked.

Text inputs

We’re going to skip ahead, from the simple checkbox to the much more complex text input.

There are other inputs in reflex-dom, but once you can handle the checkbox and the text input you should be ready to use the other inputs without too much help.

The text input has a larger configuration data type:

the usual Default instance:

and a much larger data type for the information we might want from the component:

The initial value is part of the configuration data type now, so we can add these to the page with a slightly simpler function:

We’re going to use this to make an input we can use to add items to our todo list. We want to fire an Event with the Text of the item that we want to add when there is text in the input and the user presses the enter key. We also want to clear the input when that happens.

Let’s put this together one piece at a time.

We’ll start by putting a text input on the page:

and then we’ll set some placeholder text for it:

We can get hold of the current value of the text input:

and by jumping through some type conversion hoops we can create an Event that fires when the user presses enter:

We can use that to sample the value of the text input at the time that the user pressed enter:

and we can filter out the times at which that happened while the text input was empty:

That gives us the Event that we wanted to return:

and thanks to the wonders of RecursiveDo, we can use that Event to clear the text input:

It’s the biggest piece of functionality that we have put together so far, but it does what is says on the tin:

Playing along at home

If you want to test out your understanding of how you can use reflex-dom to work with the DOM, there are exercises here. These exercises build up incrementally as the series progresses, so it would probably best to start the exercises beginning at the start of the series.

Next up

There is much more to say about working with the DOM, but this should have given you an understanding of the basics. The next few posts will cover some additional tools and techniques while using and expanding on the functionality presented here.

In the next post we’ll look at some tools reflex provides for making modifications to the graph in response to Events.

We’re preparing educational materials about the reflex library, and using it to see what exciting things we can do with FRP.

> Dave Laing

Dave is a programmer working at the Queensland Functional Programming Lab.