May 12, 2016

A cljs Tree Widget with Transitions

Did you know that the <input type=file> won't let you select directories? It turns out that this is a security restriction implemented by browsers. Which makes sense in most cases ... but it sure is annoying when you need to let users choose a directory!

This week, I was working on a side project and and I wanted a nice user interface for allowing users to choose which directories might contain photos.

So, I needed a directory tree widget. I was thinking something along the lines of what you see in left pane of Windows Explorer.

Also, for a little extra challenge and finesse, I thought it'd be cool if the widget had a fade and slide in/out effect when you expand/collapse the tree.

So, this week's clojurescript-widget-of-the-week ("clowow"?!) is a directory tree browser, written with clojurescript, reagent, bootstrap and css3 transitions:

HTML and CSS

The structure of the html is pretty simple. It's basically a bunch of nested unordered lists. Each node in the tree is a <li>. When a node has children, the li contains an entire <ul> structure.

The buttons use twitter bootstrap css and glyphicons.

Data Structures

After reading about om next, falcor and relay, I was inspired to use "references" inside maps and I'm pretty happy with how this turned out.

The idea is that there are 2 data structures to represent the state.

The first data structure can be whatever you need it to be. In this case, the first data structure represents a filesystem of directories.

The second data structure is used internally by the tree to render itself. You can think of it as a cache of the first data structure. This data structure represents tree nodes and it's really all that the tree widget needs in order to render. It contains references back to the original data structure, but, besides that, the tree widget doesn't need to know or care about the first data structure.

This way, the tree component can be used to display not only directories, but any content you need.

Click here to see it in action. As you click to expand folders, you can watch the global state and how the two data structures change.

The map under [:filesystem] could really be any map you want.

The map located under [:local :tree :root] is what drives the reagent component.

Scroll further down on the page to see the source code with more explanation.

Transitions!

So far, this widget is just a "normal" reagent component backed by an immutable data structures. (I think this pattern is so freaking awesome, it's tough to call it "normal"!)

But the really fun part of this widget for me was digging into CSS Transitions.

How do Transitions work?

No matter which library you use, at the end of the day, any transition you see on a web page is a result of changing values inside the css styles over time.

For example, changing the opacity of an element from 100% to 0% will cause it to appear to fade. Changing the height from full height to 0 will cause an element to appear to slide up and out of view.

How does jQuery do it?

Most people are probably familiar with jQuery's transition methods like jQuery.hide() and jQuery.show(). These are really convenient.

If you take a look at jQuery source code, behind the scenes, these methods are simply changing the value of the css opacity over time. There's no magic there.

In a mutable world, the strategy is usually to make a call to change the css style, and once it's complete, call a callback.

I've found that this is very convenient for simple cases, but the trouble starts soon after you try to build anything complicated. Having to coordinate multiple transitions involves coordinating multiple callbacks and it becomes tedious quickly.

For example, if I had written this widget with jQuery, I probably would have passed a callback to change the state of the node from expanded to collapsed into the jQuery.hide method. This works ok, but then what if I also needed to make an ajax call before expanding the folder? I would have quickly ended up having several callbacks chained together. It might have looked something like this where the first method makes remote ajax call, then the fadeout is started in a callback, and yet another callback to finally set the state to collapsed:

node.getRemoteData($('node').hide(setCollapsed(node)));

And that's not all! I would probably also want to set a loading flag somewhere while the ajax request is happening. That would involve at least another callback. Callbacks can get convoluted quickly!

Immutable Transitions

In a react/clojurescript environment, it takes a little more thought up front, but I think the immutable strategy is much nicer than the jQuery strategy I just described.

In the immutable/react world, we want to set the css opacity style to 0 right after a component is mounted, and then immediately start incrementing opacity to 100%.

As soon as it reaches 100%, the element is ready to go.

If we need to make a remote call, that can happen before mounting the new component. It's also easy to add a loading flag to global state and have react components react accordingly.

CSS3 Transitions

Now, I must admit that writing the code to start changing css styles right after components are mounted can be tricky. It is nice to have the option to manage this all on your own when you want (or need) absolute control over animations. In fact, here's a great article that describes a way to code this yourself.

But I think a good compromise between convenience and control is to use CSS3 Transitions.

CSS3 Transitions are baked into the browser so they are really fast. They're easy to declare inside css and there's a React addon called CSSTransitionGroup that already does most of the hard work of starting transitions right after components have mounted.

There's a great reagent recipe here that shows how to use CSSTransitionGroup with clojurescript and reagent.

Remember to exclude all react dependencies

As mentioned in the recipe, when you want to use the CSSTransitionGroup addon, remember to exclude the react library and add a dependency on the react-with-addons library like so:

[reagent "0.5.0" :exclusions [cljsjs/react]]
[cljsjs/react-with-addons "0.13.3-0"]

If you still see errors like this:

ERROR: JSC_CONSTANT_REASSIGNED_VALUE_ERROR. constant React assigned a
value more than once.

Make sure that you don't have any other dependencies that might be bringing react in as a transitive dependency. In my case, I had to also exclude react from devcards:

[devcards "0.2.1" :exclusions [cljsjs/react]]

Make sure the CSSTransitionGroup component is mounted

The CSSTransitionGroup component doesn't work unless it's mounted to the dom.

I pulled some of my hair out trying to figure out why the transitions weren't working. Then I realized that I had put the css-transition-group component inside part of the node component that wasn't always mounted. For example, I made the mistake of doing this:

(when expanded
  [css-transition-group {:transition-name "node"
                         :transition-enter-timeout (* transition-in 1000)
                         :transition-leave-timeout (* transition-out 1000)}
   ...
])

This won't work, because the css-transition-group will only mount after expanded is set to true. This causes both the list item and the css-transition-group component to be mounted at the same time and, by then, is it's too late for the transition to actually do it's thing.

Changing to this fixes this issue by making sure the css-transition-group is mounted before child list items are created:

[css-transition-group {:transition-name "node"
                         :transition-enter-timeout (* transition-in 1000)
                         :transition-leave-timeout (* transition-out 1000)}
 (when expanded
   ...
   )]

Summary

It was fun to finally have time to dig into css transitions and understand how they work with react. I see a lot of potential for using animations and transitions to make UI's easier to use. And the great part is that adding transitions to existing react/cljs code doesn't add a whole bunch of unnecessary complexity ... but seeing as everything else with clojure and clojurescript is so nice, I don't know why I'm surprised!

Tags: clojure reagent react clojurescript