In this step of the tutorial we are going to introduce one of the rather unique features of Ur/Web. Metaprogramming over row types allows us to write programs that are able to perform limited computation over the structure of rows. In practical terms, this means that we can write code that operates independently of the particular table or form inputs, and which can therefore be reused in many different situations. We're going to demonstrate this by taking a pre-existing component (the Crud -- create, read, update, delete -- example, slightly modified) from the Ur/Web site and apply it to our application to enable the creation and editing of blog entries.
How does this differ from components parameterised on table structure, you might ask? Well, that's a reasonable way of thinking about some of what this metaprogramming enables, but in Ur/Web it goes further. We can use familiar programming constructs (e.g. map and fold) to compute new types from old ones. We can invoke appropriate display functions based upon the types of the fields in a row. This gives much of the same flexibility that dynamic web languages can provide; however, Ur/Web does it in a way that is statically checked at compile time.
The Crud component is reasonably simple. We'll skip over most explanation of how it actually works, because you can divert back to that later. The component consists of two files -- a signature and an implementation.
Now we need to include our new Crud module:
allow url http://expdev.net/urtutorial/step6/style.css rewrite style Urblog/blogEntry blogEntry rewrite style Urblog/blogComment blogComment rewrite style Urblog/commentForm commentForm database dbname=urblog sql urblog.sql crud urblog
Including our Crud module is pretty simple, if you're accustomed to the use of functors in SML. Briefly, a functor takes a module (structure) as input and returns another structure. In this instance we are obligated to provide a structure with a table, a title field and a record mapping columns to functions that know how to display them on the page:
open Crud.Make(struct val tab = entry val title = "Blog Admin" val cols = {Title = Crud.string "Blog Title", Created = Crud.time "Created", Author = Crud.string "Author", Body = Crud.string "Entry Body"} end)
We used open in the last example to import everything from the structure returned by Crud.Make into the current namespace. As a result, we need to make our brand new admin function visible to the outside world:
val list : unit -> transaction page val detail : int -> transaction page val main : unit -> transaction page val admin : unit -> transaction page
If you go through the process of compiling this example as usual and importing the generated SQL file, you should be able to view your brand new admin interface at this URL:
http://localhost:8080/Urblog/admin
So, the Crud interface works, but it's not very pretty. Particularly, the way in which body text is displayed and edited is somewhat inconvenient. The default 'string' column transformations are inappropriate. We'll go ahead and define a custom transformation for the Body field:
val cols = {Title = Crud.string "Blog Title", Created = Crud.time "Created", Author = Crud.string "Author", Body = {Nam = "Entry Body", Show = fn b => <xml>{[String.length b]} characters</xml>, Widget = fn [nm :: Name] => <xml><textarea{nm}/></xml>, WidgetPopulated = fn [nm :: Name] b => <xml><textarea{nm}>{[b]}</textarea></xml>, Parse = readError, Inject = _} }
Now we are using a text area instead of a single-line text field to create and update our entry text. Obviously you could add styling or custom formatting for the other fields too.
One function provided by Ur/Web to draw attention to is readError. This is a helpful function that will throw a runtime error if the submitted data is incorrectly formatted with respect to the expected type for that field.
We've made use of the String.length function, which means we need to tell the compiler where to find the built-in String module. We do this by adding a line to the urp file:
[...] $/string crud urblog
This syntax will be familiar to users of various Standard ML build tools. The $/ preceeding the filename of the module basically says "look in the standard places for this", where the "standard places" are defined more carefully in the Ur/Web manual. In a properly-configured installation this should do the right thing without any intervention on your part.