Skip to content

Increment the Counter

Gabriel Horner edited this page Jul 10, 2013 · 16 revisions

To follow along with this section, start with tag v2.0.2.

In this section, you will add the ability to increment the counter by generating an event in the user interface. This will not require many changes to the code but will introduce the following concepts:

  • Emitters
  • The default emitter
  • Application model deltas
  • transform-enable and transform-disable

Starting the dev tools

Once again, before making changes to the code, start up the development tools

cd tutorial-client # If you're not already in this directory
lein repl
(use 'dev)
(start)

and navigate to the Data UI (http://localhost:3000/tutorial-client-data-ui.html).

All of the changes that you will make will be made to the tutorial-client.behavior namespace. Before you begin, require the io.pedestal.app namespace.

[io.pedestal.app :as app]

Observing change

The Making a Counter section of the tutorial touched briefly on the idea that there must be some way to report and observe changes to the data model. In that section you did not have to explicitly set this up because the defaults were good enough.

In pedestal-app, emitters are used to report change. You could always write your own emitter, but pedestal-app comes with a powerful one built in. The default emitter is added to the dataflow definition for you when no emitters are configured.

A dataflow description can include an :emit key which configures a sequence of emitters. The default emitter can be manually configured like this:

(def example-app
  {:version 2
   :transform [[:inc [:my-counter] inc-transform]]
   :emit [[#{[:*]} (app/default-emitter [])]]})

This dataflow has the exact same behavior as one which omits the :emit key.

Emitters are configured with a vector, in a similar manner to transform functions. This means that order is important. Each emitter vector has two elements: a set of inputs and an emitter function.

[#{[:*]} (app/default-emitter [])]

The input set for the vector above is #{[:*]} and the emitter function is the default emitter. The vector passed to default-emitter is a prefix which will be added to all emitted deltas. To cause all emitted deltas to have paths which start with [:main] use (app/default-emitter [:main]).

(def example-app
  {:version 2
   :transform [[:inc [:my-counter] inc-transform]]
   :emit [[#{[:*]} (app/default-emitter [:main])]]})

Refresh the browser after making this change and you will see that :my-counter now exists under :main. The emitted deltas can be seen in the JavaScript console.

[:node-create [] :map]
[:node-create [:main] :map]
[:node-create [:main :my-counter] :map]
[:value [:main :my-counter] nil 1]

These deltas represent the change to the data model caused by sending the initial message. Remember that this initial message is sent from the tutorial-client.start namespace. A renderer can consume this data and draw changes in the user interface. These deltas can be thought of as describing changes to a tree, building up nodes and setting values. The Data UI receives these changes and draws them for you as a nested tree.

Each delta has the same general format:

[:op [:path :to :node] args]

where op is one of:

:node-create
:node-destroy
:value
:attr
:transform-enable
:transform-disable

As you go through this tutorial, you will see how each of these operations is used. The most difficult operations to understand are transform-enable and transform-disable. These will covered next.

Attaching transforms to nodes

A renderer is code which receives rendering deltas and affects appropriate changes to the UI. The renderer is also responsible for wiring up event capture and converting events to messages which are added to the dataflow's input queue. As an application's state changes, the messages that it should send will change.

In this application, the renderer will need to know that some user action related to the :my-counter node can send a message which will cause the counter to increment. The message that will be sent is shown below.

{msg/type :inc msg/topic [:my-counter]}

A :transform-enable delta is used to describe this.

[:transform-enable [:main :my-counter]
 :increment-counter [{msg/type :inc msg/topic [:my-counter]}]]

Here the op is :transform-enable, the path is [:main :my-counter], and the arguments are the transform name, :increment-counter, and a vector of messages. When this transform is triggered, all of these messages will be sent.

There is a shorter way to write this.

[:transform-enable [:main :my-counter] :inc [{msg/topic [:my-counter]}]]

Each message in the vector that does not have a msg/type will be assigned a type that is the same as the transform name. In this case, that will be :inc.

The default emitter can automatically translate the data model into deltas, but it does not know about transforms. Transforms are introduced by emitters. They are part of the application model but not the data model. There are two ways to send transform deltas: post processing or writing a custom emitter. For this, a custom emitter makes the most sense.

(defn init-main [_]
  [[:transform-enable [:main :my-counter] :inc [{msg/topic [:my-counter]}]]])

The function above is an emitter function. It takes a map of inputs as its only argument and returns a vector of deltas. For this emitter, the inputs are ignored.

This transform-enable delta should only be sent one time, when the application starts. To configure this emitter function to behave in this way, add {:init init-main} to the emit vector in the dataflow definition.

(def example-app
  {:version 2
   :transform [[:inc [:my-counter] inc-transform]]
   :emit [{:init init-main}
          [#{[:*]} (app/default-emitter [:main])]]})

Each entry in the vector of emitters can be described as a vector or a map.

[#{[:*]} (app/default-emitter [:main])]

is the same as (and expands to)

{:in #{[:*]} :fn (app/default-emitter [:main])}

An :init emitter can also be added to any of these maps.

{:in #{[:*]} :fn (app/default-emitter [:main]) :init init-main}

Emitter functions placed under the :init key are called only when Focus changes. Focus will be covered later in this tutorial. If a map includes both a :fn and :init key, then, when focus changes, only the :init function will be called. In the example above, these emitters have been placed on separate lines so that they will both be called during the initialization process.

With these few changes, the Data UI will now show a button which can be clicked to cause the counter to increment.

The JavaScript console will show the generated deltas each time the button is clicked.

[:value [:main :my-counter] 1 2]

This change shows one of the key benefits of the pedestal-app workflow: you can make changes to behavior and then interact with behavior without having to think about rendering.

What is the purpose of transform-enable and transform-disable?

You may be wondering why :transform-enable and :transform-disable are necessary. They do not actually change the dataflow in any way. Any message can be sent to the dataflow at any time. There are several benefits that come with using these deltas, including but not limited to:

  • a message abstraction
  • improved support for testing
  • automatic rendering

The most important of these benefits is the fact that it provides an abstraction for messages. A :transform-enable delta assigns a name to a sequence of messages. If these messages do not have any parameters then the rendering code does not need to know anything about the messages. It just needs to know how to arrange to send them when something happens. If the messages do have parameters then it only needs to know about the parameters. One could later decide to add a new message or add a field to a message and the rendering code would not have to change.

Tests can be written that discover which actions may be performed by responding to :transform-enable deltas.

Finally, having a standard way to describe messages allows for renderers to be created which can automatically render user interfaces during development, generate administrator user interfaces and leverage lots of standard library functions for wiring up events.

Next steps

The plan is to make this application collaborative by allowing any number of people to connect to a service, and each user will be able to see everyone else's counter. This requires that the local counter be sent off to a back-end service and that the application be able to receive other counter values from this service.

You could jump right in and start making the service, introducing a development dependency between the client and the service. When building a complex application, it can be a drag on development to always have to run the whole system in order the work on the client.

Instead of making the service, in the next couple of sections, you will create something which can simulate the service locally. This will allow for rapid changes to the application, independent of service implementation.

The tag for this section is v2.0.3.

Home | Simulating Service Push

Clone this wiki locally