Controllers in Keechma react to route changes and implement any code that has side effects.
Anything that produces a side effect is implemented in a controller. Controllers are the only place in the application where you have access to the application's state atom.
For each URL you can have multiple controllers running at once. Each controller managing a substate of the application. This way you can split the application logic in small pieces, with clearly defined responsibilites.
Controllers have their behavior defined with multimethods which can be overridden with specific implementations. They have a number of implemented functions but right now we're only interested in the params function.
The params function receives the route params and returns a subset of the params needed for the controller to run or nil. The Controller Manager relies on the return value of the params function to decide if the controller should be started, stopped or left alone.
Whenever the URL changes, the Controller Manager will do the following:
params function of all registered controllersnil and the current value is nil it won't do a thingnil and the current value is not nil it will start the controllernil and the current value is nil it will stop the controllernil and the current value is not nil but those values are same, it won't do a thingnil and the current value is not nil but those values are different it will restart the controllerLet's say you have to implement a note taking application (similar to Evernote). You have two URLs:
The routes could look like this:
(def routes [":page"
":page/:note-id"])
/starred the params received by the Controller Manager would be {:page "starred"}/starred/1 the params received by the Controller Manager would be {:page "starred" :note-id 1}Since we need to show the list of notes on both URLs, the NoteList controller should just care about the :page param:
(defrecord NoteListController [])
(defmethod controller/params NoteListController [_ route-params]
(get-in route-params [:data :page]))
The NoteList controller's params function ensures that it will be running on each URL that has the :page param.
The NoteDetails controller should run only when the :note-id param is present:
(defrecord NoteDetailsController [])
(defmethod controller/params NoteDetailsController [_ route-params]
(get-in route-params [:data :note-id]))
When the user is on the /starred page the NoteDetails controller will not be running. It will only run on the /starred/1 URL.
When using the controllers to manage the application state mutations you can ensure the following:
Controllers in Keechma are future proof. If the UI layout changed and the note details page doesn't show the list of notes anymore, the only thing that you would need to update is the NoteList controller's params function; everything else would remain the same.
If React allows you to reason about your app like you're re-rendering everything every time something changes, Keechma's controllers allow you to reason about your app like you're reloading everything every time the URL changes.
Besides data loading, controllers have another task: they react to user commands.
Whenever the user performs an action - clicks on a button or submits a form - that action is sent to the Controller Manager. Based on the :topic, this action will be routed to the appropriate controller.
Each controller can implement the handler function which receives the in-chan as an argument. User commands will be placed on that channel and the controller reacts accordingly.
(defrecord UserController [])
(defmethod controller/handler UserController [_ app-db in-chan _])
;; Commands will be placed on the `in-chan` which is passed into the handler function
UI components don't define the :topic at the sending time, it is globally set for each UI component.
(defn renderer [ctx]
[:button {:on-click #(ui/send-command ctx :reload-user)} "Reload User"])
;; Define a (Reagent) component that sends the command
(defn button-component (ui/constructor {:renderer renderer
:topic :user})
;; Set up the component
When you define the application config map (which will be used to start and stop the application), you register each controller under the key. This key will be used as a :topic on which the controller will listen to commands.
(def app-config {:controllers {:user UserController}
;; UserController will listen on the `:user` topic
:components {:main button-component}})
Controllers can only receive commands if they are currently running. Otherwise, the command will be dropped.
Here are the API docs for the Controller Manager and for the Controllers.