-
Notifications
You must be signed in to change notification settings - Fork 0
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-changed "name-input"]
is fired. The model reacts to both 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 form.
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.