-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.