Skip to content
Brenton Ashworth edited this page Jan 11, 2012 · 5 revisions

Model

The file src/app/cljs/one/sample/model.cljs contains the one.sample.model namespace that implements the model for the sample application.

The idea behind models in this application is that they represent the current state of the application. When a model is updated, views may need re-rendering to display new information to the user.

To avoid coupling the view directly to the models, we fire events when a model's state changes.

ClojureScript provides direct support for this in atoms and watchers. In the one.sample.model namespace there are two atoms: state and greeting-form. The state atom represents the current state of the entire application. It contains a key, :state, which can have a value of :init, :form or greeting.

When the state atom is updated, a :state-change event is fired by its watcher. Functions in the one.sample.view namespace will react by rendering either the form or greeting view. The :init state will cause everything to be re-rendered.

We can experiment with this from the REPL. Start a ClojureScript REPL and open the sample application in the browser. Once you have an active REPL, enter the one.sample.model namespace. You may also want to log all events to the console.

(in-ns 'one.sample.model)
(one.logging/start-display (one.logging/console-output))

If we update the state atom, setting the state to :greeting and the name to "James", the greeting view will be displayed. Changing the state back to :init will re-display the form.

(swap! state assoc :state :greeting :name "James")
(swap! state assoc :state :form)

If you are following along, you may have noticed that the field label is moving down the page. That is because we are not following a valid sequence of events. In real usage, the label would always be above the field when the form is displayed. Moving the label down will put it into the correct position. We can fix this by setting the state to :init which will start the application from the building.

(swap! state assoc :state :init)

The greeting-form atom represents the state of the form that accepts the user's name. Both the state of the entire form and the state of the fields are represented. One possible state of the form is represented by the map below.

{:status :editing
 :fields {"name-input" {:status :error
                        :value "a"
                        :error "Name is too short!"}}}

This state means that the form is currently being edited and the name-input field has an error. There is a specific view of this form that corresponds to this state.

A ClojureScript watcher will fire a :form-change event when this atom is changed. The data associated with the event will contain both the old and new state of the atom. The reactor function for each field can use the old and new state to calculate the state transition which has just occurred and perform the appropriate animation.

In the browser, click in the text field, type the letter "a" and then click outside of the field. We can now inspect the value of the greeting-form atom.

@greeting-form

Notice that the value that is printed is the same as the map shown above but with a longer error message. What do you think would happen if we directly update the greeting-form atom?

(swap! greeting-form assoc :status :finished)

When we do this, the "Done!" button is enabled as if the form has been completed. The atom was updated and a :form-change event was fired. The view is trying to render the inconsistent state of the form. We can fix this by changing :status back to :editing.

(swap! greeting-form assoc :status :editing)

If the state becomes inconsistent, the view will reflect that. Apart from defining atoms and adding watches, the rest of the code in the one.sample.model namespace is concerned with making sure the state is always consistent.

When the field gains focus, the event [:editing-field "name-input"] is fired. When the field loses focus, the event-id [:field-finished "name-input"] is fired. When the field is changed, the event-id [:field-changed "input-field"] is fired. The model reacts to each of these events. The reaction for the first event is created in the code shown below.

(dispatch/react-to (fn [e] (= (first e) :editing-field))
                   (fn [[_ id] _] (set-editing id)))

If the first element of the event-id for a fired event is :editing-field, call a reactor function which will update both the status of the field and the form to :editing.

When the first element of the event-id is :field-changed, the field will be validated before the greeting-form state is changed. If the field is not valid, that will be represented in the state and then rendered in the view.

Notice that the view doesn't need to know anything about where and when validation occurs. It only needs to know how to render each state.