Skip to content
Brenton Ashworth edited this page Jun 18, 2013 · 19 revisions

Rendering

In this section we work on rendering our application. Rendering is the process of drawing something on the screen which represents our application and with which users can interact. In the previous section we created two HTML templates which we will make use of here.

It is important to note that rendering is more general that binding data to HTML. As we will see in a later section, we sometimes want to render with something other than HTML.

As we work through this section, the following concepts will be introduced.

  • Recording
  • Playing recordings
  • Writing rendering code

Making a recording

In Pedestal, rendering means mapping rendering deltas to functions which make whatever change is required for each delta. Pedestal provides a way to record a sequence of deltas for later playback. We can use this feature to generate deltas which represent a typical use of our system.

The only way we have to interact with our system at the moment is the Data UI. To make a recording, go to the DataUI

http://localhost:3000/tutorial-client-data-ui.html?renderer=auto

and then type the key combination

Alt-Shift-R

You will see an indicator that you are currently recording in the lower right-hand corner of the screen.

As you now interact with the application, each rendering deltas is being recorded. Press the :inc button a few times. After you recorded a sufficient amount of activity, type the key combination:

Alt-Shift-R

to stop recording.

This will open a dialog which will ask you to name the recording. It asks for both a keyword and a name. For the keyword enter :tutorial and for the name enter Tutorial Recording.

Clicking Continue with save the recording and clicking Cancel will delete it.

The recording is Clojure data and will stored in the file

tools/recording/tutorial-client/tutorial.clj

You could write a file like this from scratch or edit this file to create alternate interactions. Being able to generate and work with this kind of data allows us to capture hard-to-simulate interactions and work on rendering them without having to drive the application.

Playing recordings

To play a recording, open the render page by clicking on Render in the Tools menu or by opening the page below.

http://localhost:3000/_tools/render

On this page you will see a list of each recording that you have created. Each recording will have three links. The link that is the recording's name will play through all the deltas at once. This is useful when you want to see how a specific point in time will be rendered.

The second link is named Break it will play through the deltas in chunks. For recorded data, each chunk is the output of a single transaction. This allows us to see the deltas in the same way that the browser sees them. If you look at the data you will see the :break keyword within the sequence of deltas. This defines where the breaks will occur. You may add or remove breaks if it is helpful.

The last like is named Step. It allows you to step through each delta one at a time. This is the most helpful mode when working on a new renderer.

Click on the Step link and then open the JavaScript console so that you can see the log messages which are being printed.

With focus on the main window, use the right and left arrow keys to navigate forward and backward through the deltas. As you do this you will see each delta printed to the console. These deltas are also being fed through our renderer but we don't see anything because it is not set up to process them.

Rendering

How does our application know where to find the renderer? If you look in the namespace tutorial-client.start and specifically at the main function, you will see that we create render configuration and pass it to the create-app function which passed it to the renderer function.

A rendering function is a function which receives deltas and the input-queue and performs the rendering side-effects. The render function produced by calling push-render/renderer produces this function for us and uses the render configuration which we provide, which is defined in tutorial-client.rendering. It is here where we will write our custom rendering code.

Before we start, require the namespace below

[io.pedestal.app.render.push.handlers :as h]

As we start playing through our recording, the first couple deltas that we see will be

[:node-create [] :map]
[:node-create [:tutorial] :map]

The first delta will only be seen once. It represents the creation of the root node of our application model tree.

The second delta is where we create the root node of this :tutorial application. To render this delta we will add the template to the page, all subsequent rendering will fill in the values of this template. Unfortunately this means that the first the rendering function we write will also be the most complex.

A render configuration is a vector of tuples which map rendering deltas to functions. Each vector has the op then the path and the function to call. In the example below we will call our own custom function to render the new template node and we will use a default function to destroy the node when it is removed.

(defn render-config []
  [[:node-create [:tutorial] render-template]
   [:node-destroy [:tutorial] h/default-destroy]])

When using the push renderer, every rendering function receives three arguments: the renderer, the delta and the input-queue. The renderer helps us to map paths to the DOM. The delta contains all of the information which we need to make the change and the input-queue allows us to send messages back to the application.

(defn render-template [renderer [_ path] input-queue]
  (let [parent (render/get-parent-id renderer path)
        id (render/new-id! renderer path)
        html (templates/add-template renderer path (:tutorial-client-page templates))]
    (dom/append! (dom/by-id parent) (html {:id id}))))

In the example above, we destructure the delta to get the information we need. This is a common pattern. We will usually always grab the path from the delta.

The body of this function is as complicated as a rendering function can get. This function does the following things:

  1. get the parent id for the current path from the renderer
  2. generate a new id for this path
  3. add the dynamic template to the renderer at this path
  4. add the template to the DOM under the parent id, providing the default values

For complex render functions, this is also a common pattern: get the parent id, create a new id, add some child content under the parent.

The functions render/get-parent-id and render/new-id! are simple to understand. See io.pedestal.app.render.push for more information.

The templates/add-template takes all the hard work out of dealing with dynamic templates. This function associates the template with the given path and returns the function which generates the initial HTML. Calling the returned function with a map of data will return HTML which can be added to the DOM.

The template is retried from the templates map which is created as the top of this namespace.

(def templates (html-templates/tutorial-client-templates))

After adding this code, if we refresh the browser and then step through the deltas, we should see the template get added to the DOM when the delta below is received.

[:node-create [:tutorial] :map]

If we go backward with left arrow key, we will see this template get removed from the DOM.

Rendering transforms

A :transform-enable delta provides a sequence of messages to send when some event occurs. We will arrange for these messages to be sent when a button is clicked. Because these messages don't have any parameters, and we are wiring up a simple click event, we can use library functions from the io.pedestal.app.render.push.handlers namespace to wire up these events.

(defn render-config []
  [...

   [:transform-enable [:tutorial :my-counter] (h/add-send-on-click "inc-button")]
   [:transform-disable [:tutorial :my-counter] (h/remove-send-on-click "inc-button")]])

The add-send-on-click handler will arrange for the messages included in this :transform-enable to be sent when the element with id inc-button is clicked. The remove-send-on-click handler will remove this event listener when a :transform-enable is received.

This is one example of how small, focused handlers can lead to reusable code.

Changing value in a template

By design, we have arranged for most of the values that will be plugged into our template to have a path which ends with the template field name. This means that we can write one function to handle all of these cases.

(defn render-value [renderer [_ path _ new-value] input-queue]
  (let [key (last path)]
    (templates/update-t renderer [:tutorial] {key (str new-value)})))

This function uses the templates/update-t function to update a value in a template. update-t has three arguments: the renderer, the path that the template is associated with and the map of value to update in the template. In this case the key is the last part of the path.

All value changes for the paths [:tutorial :*] and [:pedestal :debug :*] are sent to this function to update the values in the template.

(defn render-config []
  [...

   [:value [:tutorial :*] render-value]
   [:value [:pedestal :debug :*] render-value]])

Rendering lists

Remember that we have two templates, one for the whole page and one for element in the list of other counters. If we were to write a function to add the :other-counter template, we would realize that it is almost identical to the render-template function above. Instead of doing this, let's make a more general function which can be used in both cases.

Here is the new version of render-template.

(defn render-template [template-name initial-value-fn]
  (fn [renderer [_ path :as delta] input-queue]
    (let [parent (render/get-parent-id renderer path)
          id (render/new-id! renderer path)
          html (templates/add-template renderer path (template-name templates))]
      (dom/append! (dom/by-id parent) (html (assoc (initial-value-fn delta) :id id))))))

This is a function which returns a function. It takes the template name and a function which, when passed the delta, will return a map of initial values for the template. It returns a rendering function.

When we receive the deltas for the other counters, they will look like this:

[:node-create [:tutorial :other-counters] :map]
[:node-create [:tutorial :other-counters "abc"] :map]
[:value [:tutorial :other-counters "abc"] nil 42]

When we receive the delta

[:node-create [:tutorial :other-counters] :map]

We will want to create the container for the other counters. When we receive the delta

[:node-create [:tutorial :other-counters "abc"] :map]

we will add the template for this counter. When we receive the delta

[:value [:tutorial :other-counters "abc"] nil 42]

We will set the value the template.

Because we have re-written the render-template function, we can handle creating new templates with this configuration:

[:node-create [:tutorial :other-counters :*]
  (render-template :other-counter
                   (fn [[_ path]] {:counter-id (last path)}))]

That function assumes that there is a parent id under which these templates can be added as children. The template that we created has a div with id other-counters where we would like these elements to go.

The changes below will associate this id with the parent path of all counter nodes

(defn render-other-counters-element [renderer [_ path] _]
  (render/new-id! renderer path "other-counters"))

(defn render-config []
  [...

   [:node-create [:tutorial :other-counters] render-other-counters-element]
   ...
   ])

Finally we add the function to update the other counter values.

(defn render-other-counter-value [renderer [_ path _ new-value] input-queue]
  (let [key (last path)]
    (templates/update-t renderer path {:count (str new-value)})))

(defn render-config []
  [...

   [:value [:tutorial :other-counters :*] render-other-counter-value]])

The final version of the render configuration is shown below.

(defn render-config []
  [[:node-create [:tutorial] (render-template :tutorial-client-page
                                              (constantly {:my-counter "0"}))]
   [:node-destroy [:tutorial] h/default-destroy]
   [:transform-enable [:tutorial :my-counter] (h/add-send-on-click "inc-button")]
   [:transform-disable [:tutorial :my-counter] (h/remove-send-on-click "inc-button")]
   [:value [:tutorial :*] render-value]
   [:value [:pedestal :debug :*] render-value]

   [:node-create [:tutorial :other-counters] render-other-counters-element]
   [:node-create [:tutorial :other-counters :*]
    (render-template :other-counter
                     (fn [[_ path]] {:counter-id (last path)}))]
   [:value [:tutorial :other-counters :*] render-other-counter-value]])

With these changes in place, we should now be able to step forward and backward in the rendering aspect and see each of these functions performing its specific task.

Development and production aspects

With a working custom renderer, we can now go click on Development or Production in the tools menu and see a working version of the application.

Clean up

You may notice that the counter starts a 1 before we have clicked the button. This is because early in the process we added some code which sends a messages when the application starts. This is no longer required.

In the namespace tutorial-client.start, remove the following line from the create-app function.

(p/put-message (:input app) {msg/type :inc msg/topic [:my-counter]})

Next steps

In this step we have got the point where we can use the Data UI to see our app work with a simulated back-end and we can run with the real renderer in the Development and Production aspects. It would be nice if we could see our simulated app work with the our custom renderer so that we tell that is was really working without having to fire up and service.

In the next section we will see how we can customize the development tools to create new aspects that allow us to see part of our application in a new and interesting way.

The tag for this step is step7.

Home | Aspects

Clone this wiki locally