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.