-
Notifications
You must be signed in to change notification settings - Fork 0
The sample application
The sample application should provide enough information to help you get started building your own applications. It is small enough to comprehend in a short time and large enough to demonstrate how to deal with many issues that will come up in a much larger application.
The application presents a form which takes a name and then displays a greeting message. While performing this simple function, it demonstrates several interesting things about building this kind of application in ClojureScript. Some of these things include:
- Code organization
- Using events to decouple components
- The importance of plain Clojure data
- Sending Clojure data to the server and back
- Creating highly interactive user interfaces
- Keeping application code out of the view
Although there are things named Model, View and Controller in this application it is not exactly MVC as commonly practiced. The paradigm used in this application is a mix of MVC, Functional reactive programming and Dataflow programming.
The backbone of the application is an event dispatching system. Normally, when something happens, we call a function to do something about it. For some key events in the system, it is better to simply report what has happened and not make a decision about how this event should be dealt with. That decision can be made later, without changing the code which reports the event. In a multi-threaded environment we would use a queue for this. In a single-threaded environment we are essentially using a queue which only holds a single element. The event dispatching system implements the ability to report events and to provide functions which will react to events.
There are many advantages to this kind of decoupling but the one disadvantage is that it can be a bit harder to follow the flow of execution (although doing this is probably the wrong way to think about this type of program). The diagram below shows a typical interaction with the sample application and tracks the flow of execution.
As you can see, the orange squares down the middle of the diagram show the stream of events that are generated as the application runs. Each event contains data that completely describes that event. Instead of hiding this data away in function calls, we expose it. This stream of events is a collection of Clojure data which represents what has happened as the application runs. This data can be the basis of many interesting features. See the one.sample.logging
and one.sample.history
namespaces for examples of how useful features can be added without modifying any existing code. If we were to record these events (which could be done without modifying existing code) they could be: queried to find bugs, used to replay user actions to reproduce problems or used to test specific components independent of others.
To get a better feel for the sample application, let's walk through what happens from the point where the application starts, to the rendering of the initial view.
The first thing that happens when the application starts is the firing
of an :init
event. This function is located in the one.sample.core
namespace.
(defn ^:export start
[]
(dispatch/fire :init))
The controller (one.sample.controller
) responds to :init
, :form
and :greeting
events by calling the corresponding action
multimethod.
(dispatch/react-to #{:init :form :greeting}
(fn [t d] (action (assoc d :type t))))
When an :init
event is fired, the state
atom will be reset to
contain {:state :init}
.
(defmethod action :init [_]
(reset! state {:state :init}))
The state
atom is being watched, and when it changes, the watcher
function will fire a :state-change
event.
(def state (atom nil))
(add-watch state :state-change-key
(fn [k r o n]
(dispatch/fire :state-change n)))
Views react to :state-change
events by rendering a view which
displays the current state of the application.
(dispatch/react-to #{:state-change} (fn [_ m] (render m)))
When a :state-change
event is fired, and the current state is :init
, the render
function will initialize the user interface.
(defmethod render :init [_]
(fx/initialize-views (:form snippets) (:greeting snippets))
(add-input-event-listeners "name-input")
(fx/disable-button "greet-button")
(event/listen (by-id "greet-button")
"click"
#(dispatch/fire :greeting
{:name (value (by-id "name-input"))})))
This view will add the snippets for the form and greeting page to the DOM, configure event listeners for the name input field and disable the submit button. Finally, an event listener is added to the submit button which will fire a :greeting
event which contains the name entered in the input field.
The fx
alias above refers to the one.sample.animation
namespace where all of the animations are defined.
For more details about the sample application, see Model, View and Controller.