Skip to content

Instantly share code, notes, and snippets.

@MrNice
Last active August 6, 2019 10:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MrNice/b64f6b5b98a64433d0b4 to your computer and use it in GitHub Desktop.
Save MrNice/b64f6b5b98a64433d0b4 to your computer and use it in GitHub Desktop.
Mimic Polymer with Reagent

I enjoy playing with the myriad of frameworks and toolkits which are available for modern web application development. One of the more interesting and practical projects in recent years has been Polymer, a Polyfill runtime to give browser the ability to use web components, today. After Polymer hit 1.0 at Google I/O, I decided that it was worth taking another look.

What I found surprised me - using Polymer is actually quite similar to ClojureScript programming. The main difference is that Polymer draws too much inspiration from imperative programming languages, even though it is essentially a declarative layer atop JavaScript and HTML.

Below, I compare the two in as much detail as I can.

A Polymer Component

Here's one of the advanced examples from the current Polymer site.

<dom-module id="friend-list">
  <link rel="import" type="css" href="friend-list.css">
  <template>
    <firebase-collection data="{{data}}"
                      location="https://users1.firebaseio.com/users">
    </firebase-collection>
    <template is="dom-repeat" items="{{data}}">
      <contact-card starred="{{item.starred}}">
        <img src="{{item.img}}" alt="{{item.name}}">
        <span>{{item.name}}</span>
      </contact-card>
    </template>
  </template>
</dom-module>

Not included: JS code for registering and calling this dom-module.

This is a Polymer component to describe a hypothetical friend list. First, this component will pull a collection of users from firebase, and then use the special Polymer is="dom-repeat" attribute to create a contact-card for every user in that collection. This template ignores edge case concerns, such as "What if there aren't any users" and "What if there are 10,000 users?"

Let's look at what this component is actually doing. It is able to connect to a database, retrieve data, and use that data to create the friend list. The friend-list is actually composed of a collection of contact-cards, which are described in another dom-module. Being able to compose components together is incredibly powerful because it enables us to follow the DRY principle in our web development.

The Polymer runtime will translate this into HTML, CSS, and JavaScript at runtime. Anyone who has mastered HTML should be able to understand what this component does - even if they don't understand how it is done. However, creating more complicated dom-components (such as the firebase-collection) will definitely require some JavaScript programming knowledge.

Polymer is clearly a great system for defining entire web applications as a tree of composable components - especially since it both leverages and mimics the oh-so-familiar HTML. Let's see what the ClojureScript version looks like.

Same thing in ClojureScript (using reagent)

;; Assume firebase-collection returns a reagent/atom (ratom)
(def users (firebase-collection "https://users1.firebaseio.com/users"))

(defn friend-item [{:keys [starred img name]}]
  [contact-card {:starred starred}
    [:img {:src img :alt name}]
    [:span name]])

(defn friend-list [users]
  ;; Could also be [:div.friend-list
  [:div {:class "friend-list"}
    (map friend-item @users)])
    
(reagent/render [friend-list users] (.getElementById js/document "friend-list"))

With Reagent, we structure our HTML as Clojure data structures. Instead of dom-modules, we simply create functions which return ClojureScript data structures (technically known as edn, which I like to think of as a more abstract and flexible JSON). We use nested Clojure Vectors (similar to Arrays) to represent the HTML tree, and we use Clojure Maps (kv-pairs, similar to JS objects / ruby and python hashes) to define html attributes.

Notice that we also have full access to ClojureScript within the data structure, as friend-list creates a friend item for every user in users using Clojure's core map function. It's really nice to be able to do repetition with the language itself, rather than with a something like dom-repeat. It's annoying to create special edge cases (only templates can dom-repeat, and only within Polymer) with typos that cannot be caught by a compiler or a runtime.

As always with Lisp, we can compose functions, so that in this example, friend-item is logically separated from friend-list, just as contact-item is separated from friend-item. Finally, we use reagent/render to attach the friend-list component to the DOM.

The one bit of magic within comes from the behavior of the reagent/atom. Whenever firebase-collection is altered, any component which uses it will be automagically rerendered and shoved into React. Again, this component does nothing to manage the size of friend list, and therefore will at least be noticably slow for large collections.

I really like how the flow of data and usage is very clear in the Reagent version. We create a ratom which represents the value of the collection. Then we define the transform between an element in the collection and its markup. Then, we define the final structure of the component, and express which elements should be rendered, and how. In this case, every element of the collection is rendered (as map is a 1:1), but we could easily filter the collection, or even utilize transducers to create more complicated data pipelines, all within the component itself. Finally, we attach the component to the DOM, through React.

Analysis

Both Polymer and Reagent make it easy to create applications out of composable components. However, Reagent is significantly more succinct and expressive, while retaining all of the power of ClojureScript. There is no context swap between "I'm writing a component" and "I'm writing code." Polymer is really just a component domain specific language, with a way to connect the components to the JavaScript runtime. If you want to create unique functionality, you will need to drop into a general purpose language (JS) anyway, so having a solid divide between the two does not make much sense.

Also, designers are smart people too. If they can grok Polymer, and your codebase is sufficiently well structured, then they will be able to understand enough Clojure to be able to update these templates. In fact, they will probably enjoy the minimalism of edn over HTML, as

[:p "text"]

beats

<p>text</p>.

But not as much as

[:div.col-xs-6.col-sm-3 "Some text"]

beats

<div class="col-xs-6 col-sm-3">Some text</div>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment