Skip to content

Step 03: user input and state

Valentin Waeselynck edited this page Sep 5, 2015 · 7 revisions

Browse code - Diff - Live demo

To reset your workspace for this step, run git checkout step-3, then refresh your page.


In this step, we'll add basic search capability to our list of phones. This will be the occasion to see how state is managed in Reagent.

Filtering phones by text search

Let's define a simple function that matches phone to a text search from its data:

(defn matches-search? "Determines if a phone item matches a text query."
  [search data]
  (let [qp (-> search (or "") str/lower-case re-pattern)]
    (->> (vals data)
         (filter string?) (map str/lower-case)
         (some #(re-find qp %))
         )))

This function simply looks up all the text attributes of a phone, and returns true if there is one that contains the search text.

Now, let's improve our <phones-list> component so that it accounts for the text search:

(defn <phones-list> "An unordered list of phones"
  [phones-list search]
  [:div.container-fluid
   [:ul
    (for [phone (->> phones-list (filter #(matches-search? search %)))]
      ^{:key (:name phone)} [<phone-item> phone]
      )]])

This works, but this does not tell us where we get the search value from, nor how the user may change it. Let's see about that.

Holding state in Reagent atoms

In order to implement our search feature, we need to add state to our application. Since we're in ClojureScript, it's natural for us to manage our state with an atom. But instead of user a Clojure atom, we're going to use Reagent's own flavour of atom (aka ratom):

(defonce state
  (rg/atom {:phones hardcoded-phones-data
            :search ""
            }))

The 'magic' of Reagent atoms is that components that dereference a Reagent atom know that they must re-render whenever the atom is updated.

(Note: You may have noticed that we've used defonce instead of def in this case. This is necessary so that Figwheel preserves our state when we save changes to our code; this is called making our code reloadable, and is beyond the scope of this tutorial).

We'll make a new component that dereferences our state atom, and uses its value to render the list of phones. That's the component that we will mount to the dom.

(defn <top-cpnt> []
  (let [{:keys [phones search]} @state]
    [:div.container-fluid
     [<phones-list> phones search]
     ]]))
    
;; ...

(defn mount-root "Creates the application view and injects ('mounts') it into the root element." 
  []
  (rg/render 
    [<top-cpnt>]
    (.getElementById js/document "app")))

So now our phones list uses the :search property, but there isn't yet a way for the user to modify it. However, you can. All you have to do is open your REPL, place yourself in the reagent-phonecat.core namespace, and mess around with the state:

(in-ns 'reagent-phonecat.core)
(swap! state assoc :search "fast")

For a more dramatic effect, you may want to put your REPL next to your browser on your screen.

Next move is to implement our search input component. First, let's define a function to update the state with a new search string:

(defn update-search [state new-search]
  (assoc state :search new-search))

This is a pure function: it takes a state value and returns a new value. It does not know about our state atom and does not mutate it, which makes it more testable.

Now, the search input component itself:

(defn <search-cpnt> [search]
  [:span 
   "Search: "
   [:input {:type "text" 
            :value search
            :on-change (fn [e] (swap! state update-search (-> e .-target .-value)))}]])

All that's left to do is to add our search-cpnt to our top-cpnt

(defn <top-cpnt> []
  (let [{:keys [phones search]} @state]
    [:div.container-fluid
     [:div.row
      [:div.col-md-2 [<search-cpnt> search]]
      [:div.col-md-8 [<phones-list> phones search]]
      ]]))

Summary

We've learned several things in this step :

  • To manage state, Reagent provides its own flavor of Clojure's atom, called ratom. The 'magic of Reagent' is that components that deref ratoms automatically re-render when the value stored in the ratom changes.
  • You attach event handlers to DOM elements by adding :on-click, :on-change properties to the attributes maps.
  • As we saw in the <phones-list> component, having the whole expressive power of ClojureScript in rendering functions lets you declare sophisticated UIs very effectively.

In the next step, we'll go even further by learning about Reagent Cursors, a generalisation of ratoms which provide a restricted view into a ratom.