Skip to content
brentonashworth edited this page Jan 24, 2012 · 6 revisions

View

The file src/app/cljs/one/sample/view.cljs contains the one.sample.view namespace which is responsible for rendering views in the sample application.

A "view" is anything that changes the user interface. This could mean manipulating the DOM, changing a CSS class on an element or running an animation.

View rendering actions occur in a reactive way based on changing states over time in the model. Views are only changed when the application state changes. When this happens, Clojure provides both the old and new value. This allows us to easily render different views based on both the current and previous state.

All views react to either a :state-change or :form-change event. :state-change events are fired when the state of the application changes. :form-change events are fired when the state of the form changes.

There are three functions in one.sample.view which are responsible for rendering views: render, render-form-field and render-button. Each of these is implemented as a multimethod that dispatches based on some function of the current state.

The easiest of the three functions to understand is render because it dispatches on the value associated with the :state key in the application state. To follow along, start a ClojureScript REPL and enter the one.sample.view namespace.

(in-ns 'one.sample.view)

Rendering form and greeting screens

When the application starts, :state is set to :init. A reaction is defined that "reacts" to a :state-change event by calling the render function, like so:

(dispatch/react-to #{:state-change} (fn [_ m] (render m)))

;=> {:max-count nil, :event-pred #{:state-change}, :reactor #<function ...>}

From the REPL, we can see what happens when the render function is called with this state.

(render {:state :init})

This view will remove everything from the DOM and then re-add the elements required by the application.

Rendering the form

The form for this application is an example of how dynamic a user interface in a JavaScript application can be. The idea behind this form is that it is validated while it is being edited and that the form cannot be submitted until it contains valid data.

The function add-input-event-listeners, adds listeners to the input field that will fire events to indicate that the field is being edited or that the field's value has changed. The model is listening for these events and will validate the input and update the form state accordingly.

The view reacts to :form-change events which are fired when the form's state changes. When a :form-change is received, the data associated with the event contains both the old and new state of the form. This data is transformed before calling the render-form-field and render-button functions.

The render-form-field function receives a map with :transition and :id keys and will run the correct animation depending on the state transition which has just occurred.

(render-form-field {:transition [:editing :empty] :id "name-input"})
;=> true

The transition above means that the field is no longer being edited and is empty..

The render-button function receives a vector of two elements which represents the state transition of the entire form.

(render-button [:editing :finished])

The Vector [:editing :finished] means that the form has transitioned from being edited to being finished and can be submitted.

Go ahead and try calling the render function with other valid states.

(render {:state :greeting :name "James"})
(render {:state :form})
(render {:state :init})

Where does all the HTML for the view come from?

The first definition in the one.sample.view namespace looks like this:

(def snippets (snippets/snippets))

We can inspect the contents of snippets from the REPL.

snippets
;=> {:form "<div id=....>" :greeting "<div id=...>"}

It contains a map of keywords to strings of HTML. Were do these strings come from?

snippets contains the value returned from calling (snippets/snippets). This is a call to the snippets macro in the one.sample.snippets namespace.

(defn- snippet [file id]
  (render (html/select (html/html-resource file) id)))

(defmacro snippets
  "Expands to a map of HTML snippets which are extracted from the
  design templates."
  []
  {:form (snippet "form.html" [:div#form])
   :greeting (snippet "greeting.html" [:div#greeting])})

The macro uses Enlive to extract the required HTML from the HTML templates in the templates directory and return them in a map. Enlive does not work in ClojureScript but does work from Clojure. Macros in ClojureScript are just regular Clojure macros.

Each time the ClojureScript application is compiled, the templates defined in the snippets function will be pulled into the ClojureScript application. This scenario allows the separation of the development activities from the design activities.

Because of the way reloading is implemented, if you edit a template and then refresh the page, the changes to the template will be visible immediately.

Clone this wiki locally