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

Controller

The file src/app/cljs/one/sample/controller.cljs contains the one.sample.controller namespace which implements the controller for the sample application.

Models and Views are easy to understand but what do we mean here by Controller? Events generated by the application may result in changes to the internal application state or to remote services. Whenever coordination needs to be done to update multiple models or make requests to external services, those changes are handled by the controller.

In the sample application, the state of the application can change between :init, :form and :greeting. The :init and :form state only require a simple change to the state atom. The :greeting state requires a network round-trip to send the name to the server and find out if that name has already been entered.

Controllers are all about coordination of data flowing through the system. Models and Views are end-points where something definitive happens: Models are changed, Views are rendered. Controllers are always passing data along to something else.

Talking to a Clojure server

The sample application sends data to a backend service written in Clojure. This is done in a function named remote.

We can try this function from the ClojureScript REPL.

(in-ns 'one.sample.controller)
(remote :add-name "James" #(js/alert (pr-str %)))

remote accepts a keyword which represents the dispatch value for the function on the server, data (a name) and a callback function. In the example above the callback will show an alert which displays the printed value returned from the server.

The remote function is implemented on top of request from one.browser.remote.

(defn remote [f data on-success]
  (request f (str (host) "/remote")
           :method "POST"
           :on-success #(on-success (reader/read-string (:body %)))
           :on-error #(swap! state assoc :error "Error communicating with server.")
           :content (str "data=" (pr-str {:fn f :args data}))))

The server side is implemented by defining a route with Compojure.

(defroutes remote-routes
  (POST "/remote" {{data "data"} :params}
        (pr-str
         (remote
          (binding [*read-eval* false]
            (read-string data))))))

Please note that remote in the second example has nothing to do with the remote function in the first example. One is a function on the client and the other a function on the server. They do not need to have the same name.

The main thing to notice here is that we are using pr-str and read-string on both the client and server to serialize and deserialize Clojure data for transport over the network.

In the browser, the string that constitutes the content of the POST request is created in the following way:

(str "data=" (pr-str {:fn f :args data}))

An example of some actual content is shown below.

(str "data=" (pr-str {:fn :add-name :args {:name "James"}}))
;=> data={:fn :add-name, :args {:name "James"}}

On the server the value of data is read with read-string and passed to the remote function which will dispatch to the correct method based on the value associated with the :fn key. The return value from the call to remote is then printed with pr-str.

If the connection is successful, the following function is called in the browser.

#(on-success (reader/read-string (:body %)))

This calls the user-provided on-success function, passing it the result of reading the body of the response with read-string.

From this simple example, I hope it is obvious that being able to both print and read Clojure data from Clojure and ClojureScript greatly simplifies client-server applications. Data is central in Clojure. Its rich data literals allow us to represent complex data structures including Clojure source code. Being able to send this data over a network without having to encode it in another format eliminates much of the hard work associated with client-server programming.

Clone this wiki locally