Ur/Web Tutorial

Ur/Web Tutorial

Step 4

Previous | Next

In this step, we're going to add a "detail" view for blog entries. This will demonstrate how links and parameters work in Ur/Web, as well as mutual recursion.

To create our detail links, we're going to want a new function that takes a parameter -- the id of the entry we want to view, in this case.

urblog.ur

We're going to create detail as a mutually recursive function with list. In order to link to something in Ur/Web, it's necessary that it be visible within the scope of the caller. If we want to be able to link from a detail page back to the main list, they each depend on the other being defined. We use the and keyword to introduce mutually recursive definitions.

[...]
and detail (i:int) =
   res <- oneOrNoRows (SELECT * FROM entry WHERE entry.Id = {[i]});
   return
   (case res of
      None => <xml>
               <head><title>Entry Not Found</title></head>
               <body>
                  <h1>Entry not found</h1>
               </body>
              </xml>
    | Some r => <xml>
                  <head>
                     <title>{[r.Entry.Title]}</title>
                     <link rel="stylesheet" type="text/css" href="http://expdev.net/urtutorial/step4/style.css"/>
                  </head>
                  <body>
                  <h1>{[r.Entry.Title]}</h1>
                  <h2>By {[r.Entry.Author]} at {[r.Entry.Created]}</h2>
                  <div class={blogEntry}>
                  <p>{[r.Entry.Body]}</p>
                  </div>
                  <a link={list ()}>Back to all entries</a>
                  </body>
                </xml>)
[...]

To link to our new detail page, we add a link from the entry title within the list function:

[...]
fun list () =
    rows <- queryX (SELECT * FROM entry)
            (fn row => 
            <xml>
               <div class={blogEntry}>
                  <h1><a link={detail row.Entry.Id}>{[row.Entry.Title]}</a></h1>
                     <h2>By {[row.Entry.Author]} at {[row.Entry.Created]}</h2>
                  <p>{[row.Entry.Body]}</p>
[...]

urblog.urs

To ensure our new detail function visible to the outside world, so we add its signature to the signature definition:

val list : unit -> transaction page
val detail : int -> transaction page
val main : unit -> transaction page

Note that this is not strictly necessary, as Ur/Web automatically exports functions that previously-exported functions depend upon. In any case, it may be good practice to include these kinds of top-level pages into your signature file to take advantage of the "self-documenting" effect that these signature definitions may have upon your applications. My personal approach to learning about the operation of an unfamiliar Ur/Web application usually involves reading all the signature definitions first; this might guide you as to what is or isn't appropriate to include.

Adding Comments

So you'll notice that we have so far restricted ourselves to creating functions that generate complete pages suitable for serving directly to the user. What if we want to create a function that generates just part of a page, and then include that content on other pages? It turns out Ur/Web gives us plenty of scope within which to do this. We'll demonstrate this by creating a comment-displaying function that we can then include elsewhere.

urblog.ur

We need a table in which to store comments on entries. We define this table much like we did our original one:

table comment : {Id : int, Entry : int, Created : time, Author : string, Body : string }
	PRIMARY KEY Id,
	CONSTRAINT Entry FOREIGN KEY Entry REFERENCES entry(Id)

We also introduce a new style blogComment.

Our comment function is pretty simple. The only thing we have to do is specify a type annotation -- Ur/Web can't always figure out the type of functions on its own. In this case we specify that our function is transaction xbody. This type (xbody) is that of XML that is suitable for inclusion within a body tag. The transaction, as before, indicates that this is part of a monad, and is therefore suitable for inclusion within other transactions (as all content that will be delivered to users must be).

fun comments i : transaction xbody =
   queryX (SELECT * FROM comment WHERE comment.Entry = {[i]})
      (fn row =>
         <xml>
            <div class={blogComment}>
               <p>{[row.Comment.Body]}</p>
               <p>By {[row.Comment.Author]} at {[row.Comment.Created]}</p>
            </div>
         </xml>)

Now, we can't just call this function directly, because it is returning a transaction (which is necessary because it queries our database), so instead we must bind it within our entry detail function:

and detail (i:int) =
   res <- oneOrNoRows (SELECT * FROM entry WHERE entry.Id = {[i]});
   comm <- comments i;
   [...]
                  <p>{[r.Entry.Body]}</p>
                  </div>
                  {comm}
   [...]

urblog.urp

We need to update our project file and style sheet (adding the rewrite for the new style declaration):

allow url http://expdev.net/urtutorial/step4/style.css
rewrite style Urblog/blogEntry blogEntry
rewrite style Urblog/blogComment blogComment
database dbname=urblog
sql urblog.sql

urblog

Before we get too carried away rewriting styles one by one, there's a convenient shorthand that will save us some typing:

rewrite style Urblog/*

This could be used to replace both of the rewrite style lines above.

Running the example

You will need to recompile as usual; however, because of the new comments table, you will need to import the generated urblog.sql again, possibly dropping the existing entry table in the process. You might like to manually add an example comment to an entry.

Previous | Next