In this blogpost we'll implement a simple - "hello world" level - app with Keechma that will introduce you to the Keechma router. Instead of printing the static "Hello World!" string, we'll write the app that can greet you by name.
App functionality can be defined like this:
- User can enter their name in the input box
- As the user is entering their name it's stored in the URL (like
?name=user-name
) - Application reads the value from the URL and displays the message ("Hello user-name")
Routes in Keechma are used as the minimal representation of the application state, and route data in Keechma is reactive - whenever the URL changes the router converts the URL into a Clojure map and stores it in the app DB.
Since we haven't defined any route patterns, the router will serialize the route params into the URL query params - that's why the URL will look like ?name=user-name
.
Component
Here's the complete component code:
(defn hello-world-routing-render [ctx]
(let [current-name (or (get-in @(ui/current-route ctx) [:data :name]) "")]
[:div
[:label {:for "name"} "Enter your name"]
[:div
[:input
{:id "name"
:on-change (fn [e] (ui/redirect ctx {:name (.-value (.-target e))}))
:value current-name}]]
(when (seq current-name)
[:h1 (str "Hello " current-name)])]))
First thing that you may notice is that the renderer function accepts an argument called ctx
. This argument is passed to each component (unless it's a pure component, but we'll get to that later) and it's purpose is to connect the component with the rest of the app.
Here we can see one of the core Keechma principles in action - no globals. Instead of depending on a (shared) global variable to communicate with the rest of the app, Keechma provides each component with their own view of the world. When the application is started, each component gets the ctx
argument partially applied. Whenever your component does something that's affecting the rest of the app, it does so with the help of the ctx
argument.
There are many different things that a component can do with it's context, but for now we'll focus on two functions ui/current-route
and ui/redirect
.
Reading the current route data
On the 2nd line you can see the code that looks like this: (get-in @(ui/current-route ctx) [:data :name])
. Let's decompose the code and go through it part by part:
@(ui/current-route ctx)
does a few things:
- It gets the current route subscription from the component context
- It dereferences the current route subscription and reads it's value
Subscription performance and caching
If you are an experienced Reagent user, you might notice that we're using the "Form-1" component here which should create a new subscription on each component re-render. Fortunately Keechma is caching it's subscriptions so this doesn't happen - every time the component is re-rendered it gets the same
current-route
subscription.
After we have read the current route value we extract the name from it's :data
map. Whenever you read the route you will get a map that looks like this:
{:route "pattern-that-was-used-to-match-the-url"
:data {:key "value"}}
Most of the time, you'll only want to read the values in the :data
attribute.
If our URL looked like this: ?name=Mihael
the route map returned by the ui/current-route
function would look like this:
{:data {:name "Mihael"}}
In this case the :route
attribute is missing, since we haven't defined any route patterns (yet!).
Storing the name in the route
Next part that interests us is the input
field. Here's where the action happens:
[:input
{:id "name"
:on-change (fn [e] (ui/redirect ctx {:name (.-value (.-target e))}))
:value current-name}]]
This input field is a standard controlled React component. This means that both the value
and the on-change
props are defined and they control the current value of the input field.
Input's :value
is set to the current route's :name
, which will be an empty string when we start the app (remember the 2nd line of our component). Whenever the input
value is changed, the on-change
handler is called. Let's figure out what's going in there:
(fn [e] (ui/redirect ctx {:name (.-value (.-target e))}))
- First we read the value of the event target (the input field in this case)
- Then we call the
ui/redirect
function and pass it the params that we want to represent in the route
This means that on each input change the URL will change to reflect that change. URL will be converted to a Clojure map and stored in the app DB. Since we're dereferencing the ui/current-route
subscription in the component, Reagent will re-render the component with a new route value.
Adding pretty routes
Right now the app is using query params to serialize the route params, but let's say that we want the route to look like this: name/Mihael
. We want to have the same functionality, but we want to change the route so the URLs look nicer.
It turns out it's trivial to do so. Remember, in Keechma route params are just data, and how they are serialized into the URL is of no concern to the rest of the app.
(def app-definition
{:components {:main hello-world-routing-component}
:html-element (.getElementById js/document "app")})
(def pretty-route-app-definition
{:components {:main hello-world-routing-component}
:html-element (.getElementById js/document "app")
:routes ["name/:name"]})
If you compare these two app definitions, you'll notice that the only difference is in the :routes
attribute. Keechma uses patterns defined in this attribute to serialize and deserialize the route params. The rest of the app stays the same!
Route defaults
If we want to go a step further we can add a default value to the route param. Let's change the route definition so it looks like this:
[["" {:name "Student"}]
["name/:name" {:name "Student"}]]
Now our application is greeting people even if they don't enter their name. In this case we're defining multiple route patterns:
["" {:name "Student"}]
- This pattern will match an empty route (for instance when you load the app for the first time), and it will set the:name
param to "Student"["name/:name" {:name "Student"}]
- This pattern will match any route that starts withname/
even if the:name
param is not set - in that case it will set the:name
to "Student"
Keechma principles
In this post we wrote a pretty small app, but nonetheless it helped us to uncover two important Keechma principles:
- In Keechma routes are just data, your components never care how the URL looks, they only care about the data that the URL contains
- Routes are reactive - they get stored in the app DB, and you can treat them like any other part of your state
We're working hard on the v1 release, and if you want to keep track of Keechma news and releases, subscribe to our newsletter: