Skip to content

Tutorial 0.4 Linking

macourtney edited this page Sep 13, 2010 · 1 revision

This tutorial will introduce the linking features of Conjure.

Making the index linkable.

We can now read a message from the database and display it as long as it’s the first message in the database. What if you have more than one message in the database and want to display what ever the user chooses. We could do it by passing the id of the message to display to the index action, then show the appropriate message.

We’ve already passed parameters from the controller to the view. How do we pass parameters from the user’s browser to an action? We can pass parameters using the web standards for passing parameters via get and post requests. Conjure will take care of much of the work for us.

Let’s say you want to display the message with id 2 (likely your “Hello World!” message id). The url for passing the parameter would look like:

http://localhost:8080/home/index?id=2

Now we need to pull the parameter from the url and get the appropriate message. We can do this by updating our home index action. Conjure nicely adds all of the parameters under the key :params in the request-map passed to all controller actions. To get the id parameter from the request-map in the index action, we do:

(:id (:params request-map))

We can then call message/get-record passing the id in a map to get the corresponding message:

(message/get-record (:id (:params request-map)))

Since we can’t be sure the user will always pass an id to the index action, we should use our find-first function if no id exists. Our final index action looks like:

(defn index [request-map]
  (let [id (:id (:params request-map))
        message (if id (message/get-record id) (message/find-first))]
    (render-view (home-request-map request-map) (:text message))))

I’ve added a let to break out each step and make the code more readable.

Now, if you use the url above, you should see your “Hello World!” message again. To prove the id works, click on the messages tab and add another message, “Goodbye World!” The id for “Goodbye World!” will likely be 3. Using 3 as the id (replace 3 with your actual id) enter the following url to show “Goodbye World!”:

http://localhost:8080/home/index?id=3

Since accessing rows in the database by id is so common, Conjure allows you to shorten your url by simply adding the id as the last dir in the url path. Showing “Hello World!” again using a short url looks like:

http://localhost:8080/home/index/2

Now that we can pass the id of the message to show, let’s make some links to avoid having to type out those long urls all the time.

Link-to

We want to create a link tag to link to the first message which looks like:

<a href="http://localhost:8080/home/index/2">Message 2</a>

We could manually create a link using clj-html which would look like:

[:a { :href "http://localhost:8080/home/index/2" } "Message 2"]

Or we could use Conjure’s built in link function called link-to:

(link-to "Message 2" request-map { :params { :id 2 } })

We don’t save a lot of typing, but it’s much easier to read than the clj-html way. Let’s add it to our index.clj view which will look like:

(defview [message]
  (html/html 
    [:div { :class "article" }
      [:h1 "Welcome to Conjure!"]
      [:p message]
      (link-to "Message 2" request-map { :params { :id 2 } })]))

You can refresh your browser to see the new link. Click on it and you will see the “Hello World!” message again. You can change the id to 3 to show the “Goodbye World!” message.

You may be wondering where the request-map came from. It’s not a parameter for the view, and it’s not part of conjure.view.base. When you called the view from the controller, you pass a request map to it. In the index action it’s the part that looks like:

(render-view (home-request-map request-map) (:text message))

(home-request-map request-map) is the request-map (plus some special home related stuff). Defview in index.clj, passes that request map to your view code. You don’t have to declare it or anything, it magically appears.

Link-to-if

The link is great, but useless if we are already looking at the “Hello World!” message. Let’s disable it if the message is already the one showing.

We can disable a link based on a condition using the link-to-if function. Link-to-if works just like link-to, but adds a condition parameter. If condition evaluates to true, then the link is enabled. If condition is false, the link is disabled.

We know the “Hello World!” message is displayed if (:id (:params request-map)) is “2”. Let’s create a function called message-displayed? which takes the request-map and id of a message, and returns true if the message with the given id is displayed. The function looks like:

(defn
  message-displayed? [request-map id]
  (= (:id (:params request-map)) (str id)))

Add the function into your view before the defview call. Then update your link-to to the new link-to-if call:

(link-to-if (not (message-displayed? request-map 2)) "Message 2" request-map { :params { :id 2 } })

Since, we want to disable the link if the message is not displayed we add the call to not around message-displayed?.

Your complete index.clj file should look like:

(ns views.home.index
  (:use conjure.view.base)
  (:require [clj-html.core :as html]))

(defn
  message-displayed? [request-map id]
  (= (:id (:params request-map)) (str id)))

(defview [message]
  (html/html 
    [:div { :class "article" }
      [:h1 "Welcome to Conjure!"]
      [:p message]
      (link-to-if (not (message-displayed? request-map 2)) "Message 2" request-map { :params { :id 2 } })]))

If you refresh your browser and point it to the “Hello World!” message, the “Message 2” link will be disabled. Point your browser to the “Goodbye World!” message, and the “Message 2” link will be enabled.

Link-to-if with text function

Instead of passing text to either link-to functions, we can pass a function which takes a request-map as a parameter and returns a string to use as the text of the link. The request-map is not the request-map given to the view, but one created by combining the request-map and the parameters given to the link-to function.

For example, if the view’s request-map is something like { :controller “home”, :action “index”, :params { :id “3” } } and the parameters passed to the link-to function is { :params { :id 2 } }, then the request-map passed to the text function looks like { :controller “home”, :action “index”, :params { :id 2 } }.

In our link-to-if function call, we could create a text function which pulls the message id from the request-map and adds “Message”. Our text function would look like:

(defn
  message-text [request-map]
  (str "Message " (:id (:params request-map))))

We can then replace “Message 2” with our text function in our call to link-to-if which will then look like:

(link-to-if (not (message-displayed? request-map 2)) message-text request-map { :params { :id 2 } })

Note, we’re not calling message-text, but passing the function itself.

Putting it all together

Our link-to-if function call is getting kinda long, let’s turn it into a function which takes the id of the message to link to. Our message link function looks like:

(defn
  message-link [request-map id]
  (link-to-if (not (message-displayed? request-map id)) message-text request-map { :params { :id id } }))

We can replace the link-to-if call inside our defview with:

(message-link request-map 2)

If you refresh your server, everything looks exactly the same. Why did we do this? Well, if we now pass all of the messages from the database to our view, we can link to all of them.

First let’s update our home index action in home_controller.clj. The new index action looks like:

(defn index [request-map]
  (let [id (:id (:params request-map))
        message (if id (message/find-record { :id id }) (message/find-first))]
    (render-view (home-request-map request-map) (:text message) (message/find-records ["true"]))))

Note the added (message/find-records [“true”]) to get all of the records in the database.

Next we need to add a parameter to our view to catch all of the messages. Our new view definition looks like:

(defview [message messages]

Finally, we need to loop through each message in messages, and show a link. We can do this by mapping messages to our message link then concatenating the resulting string list together so clj-html can handle it. The new call to message-link looks like:

(apply str (map #(message-link request-map (:id %)) messages))

Unfortunately, this results in all of the message links getting crammed together. We can easily fix this by adding non breaking spaces between each link. The easiest way to add the spaces is to add them to the message-link function. The new message link function looks like:

(defn
  message-link [request-map id]
  (str 
    (link-to-if (not (message-displayed? request-map id)) message-text request-map { :params { :id id } })
    "&nbsp;"))

Putting it all together, our final index.clj looks like:

(ns views.home.index
  (:use conjure.view.base)
  (:require [clj-html.core :as html]))

(defn
  message-displayed? [request-map id]
  (= (:id (:params request-map)) (str id)))

(defn
  message-text [request-map]
  (str "Message " (:id (:params request-map))))

(defn
  message-link [request-map id]
  (str 
    (link-to-if (not (message-displayed? request-map id)) message-text request-map { :params { :id id } })
    "&nbsp;"))

(defview [message messages]
  (html/html 
    [:div { :class "article" }
      [:h1 "Welcome to Conjure!"]
      [:p message]
      (apply str (map #(message-link request-map (:id %)) messages))]))

Now we can click to show any message in the system. When a message is displayed, it’s link is disabled. Try it out for yourself.

PreviousTutorial IndexNext

Clone this wiki locally