Skip to content

Increment the Counter

Brenton Ashworth edited this page Jun 7, 2013 · 16 revisions

Increment the Counter

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

  • Emitters
  • the Default Emitter
  • the application model (maybe)
  • Application Model deltas
  • transform-enable and transform-disable

Starting the dev tools

Before we make any changes to the code, start up the development application:

lein repl
(use 'dev)
(run)

and navigate to the Data Renderer.

http://localhost:3000/tutorial-client-data-ui.html

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

[io.pedestal.app :as app]

We are now ready to begin.

Observing change

In the last section we touched briefly on the idea that we need some way to observe changes to the data model. We did not have to explicitly set this up but were able to rely on the defaults.

In Pedestal, Emitters are used to report change. We can always write our own emitter but Pedestal comes with a powerful one build in. The Default Emitter is added to the dataflow definition for us when we do not add one ourselves.

A dataflow description can include an :emit key which configures a sequence of emitters. The default emitter which was added to our dataflow 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 if we had omitted the :emit entry.

Emitters are configured with a vector, the same as transform functions. 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. Let's make a slight modification to the definition above and add a prefix.

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

If we save this file and refresh the page we can see that :my-counter now exists under :tutorial. We can see the emitted deltas by looking at the console.

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

These deltas represent the change to our data model which was caused by sending the initial message. Remember that we set this up in the tutorial-client.start namespace. A renderer can consume this data and draw changes in the user interface. We can think about these deltas as describing changes to a tree, building up nodes and setting values. The Data Renderer receives these changes and draws them for us.

Each delta has the same general format:

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

The op is one of:

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

For more information about the application model and application model deltas see the Application overview document.

Attaching transforms to nodes

What we would like to do is let the renderer know that some user action related to the :my-counter node can send a message which will cause the counter to increment. We know the message that we need to send.

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

We use a :transform-enable delta to describe this.

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

Here the op is :transform-enable. The path is [:tutorial :my-counter]. The arguments to :transform-enable are the transform name 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 [:tutorial :my-counter] :inc [{msg/topic [:my-counter]}]]

Each message which does not have a msg/type will be assigned a type that is the same as the transform name.

We need to arrange for our emitter to send this :transform-enable delta. There are many ways that this can be done. Since we only need to send this once, when the application is initialized, we will create an initialization emitter to do the job.

First we create an emitter function which will send this delta.

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

Emitter functions receive one argument, which we are ignoring here, and return a sequence of deltas.

We would like for this emitter function to be called only during initialization. To do this we will use the map form for configuring an emitter and place this emitter function under the :init key.

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

Emitters placed under the :init key are called only when Focus changes. Focus will be covered later in this tutorial.

With these few changes, the Data Renderer will now show a button which we can click to cause the counter to increment.

If you look in the console as you click this button you will see the deltas being rendered with each click.

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

What is the purpose of transform-enable and -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 we get from 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. We could later decide to add a new message or add a field to a message and the rendering code would not have to change.

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

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

Next steps

We plan to make this application collaborative by allowing any number of people to connect to our service and everyone will be able to see everyone else's counter. This means that we will need to be able to send our counter value off to the server and receive counter values from others. In the next section, we will update the data model so that we have a place to store these counters. We will also add a simulator which will allow us to act as if we were sending and receiving counters even before we have created the service to handle this.

The tag for this step is step3.

Home

Clone this wiki locally