Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

bindMapper

tromberg edited this page · 22 revisions
Clone this wiki locally

Goals and Background

For data-oriented display and input forms, right now there are two choices when you use the Mapper framework: CRUDify and custom snippets.

CRUDify provides a lot out of the box and can then be tweaked in a few places. But I’ve found it to be best for Prototyping and a few administrative functions where a precise design is not important and workflows are simple. When trying to use it more intensively I found myself having to rely heavily on the CRUDify source code for documentation. A typical white-box framework situation.

The other alternative are custom snippets. They allow every conceivable HTML to be produced, and have no dependence on the Mapper library themselves. I used this approach a lot, but ended up with a lot of boilerplate code; e.g. if you add a field to a Mapper class, you have to also add it in the edit and view template, and in the bind statements in each Snippet.

The code really threatened to become repetitive and complicated when I needed to integrate further aspects:

  • Dealing with empty field values (e.g. hide the field altogether with its caption)
  • Displaying field errors/warnings

Finally, with the classic bind, a snippet only lends itself to reuse in different templates in a limited way. All the bind parameters are created before making the call to bind, which means that lookups etc. have to be performed even if the corresponding tag is not bound in the current template.

Update: There is now also the mapper.view package. It allows more flexibility than CRUDify, and the customization can often be achieved by using certain tags inside the snippet tag. It is more on the “comfortable”/CRUDify side, but does not let you do everything that could be achieved with a custom Snippet. MBindHelper has no such limitations, but may be a bit less comfortable.

Binding Deluxe – binding a single Mapper instance with MBindHelper.bindMapper

With the MBindHelper trait and object provided here, you can now write more concise templates and Snippets that have a lot of standard built-in features and only do the work that is actually required in the current context. (M stands for Mapper.)

The code was developed and tested on Lift-1.1M8, Scala 2.7.6. (Updated now to Lift-2.0RC1 and Scala 2.7.7)

Showing fields

Let’s say you want to display a given user profile (your User class extends Lift’s MegaProtoUser). For example, you might want to do it table-style, with a field in each row, captions in the first column, values in the second column. Using MBindHelper, you would use a template like this:

<lift:Users.myDisplayUserSnippet>
  <template:show>
    <tr><td><field:caption />:</td><td><field:value /></td></tr>
  </template:show>
  <table>
    <tbody>
      <show:firstName />
      <show:lastName />
      <show:email />
      ....
    </tbody>
  </table>
</lift:Users.myDisplayUserSnippet>

The <template:show> tag introduces a per-field template. This template is then applied every time you use a <show:someFieldName> tag. It remains active until you change it to something else. (This may not be the optimal solution when working with very complex and nested templates, so it might work a bit differently in the future.) The default show template will simply display the value (whatever the field returns from its asHtml method). You can switch back to the default template by using an empty <template:show /> tag.

The Snippet code for this case couldn’t be simpler:

import be.romberg.liftweb.util.MBindHelper._

def myDisplayUserSnippet(content:NodeSeq):NodeSeq = {
  val userToDisplay: User = ... // fetch the user to display
  bindMapper(content, userToDisplay, NoSpecialBinding)
}

In this case, for every <show:…> tag in your template there has to be a corresponding field in the User class.

However, usually things are not so simple. For example, let’s say instead of first and last name on a separate line, we want to display the niceName. In this case we could simply change our template to:

<lift:Users.myDisplayUserSnippet>
  <template:show>
    <tr><td><field:caption />:</td><td><field:value /></td></tr>
  </template:show>
  <table>
    <tbody>
      <show:niceName/>
      ....
    </tbody>
  </table>
</lift:Users.myDisplayUserSnippet>

and modify our Snippet as follows:


def myDisplayUserSnippet(content:NodeSeq):NodeSeq = {
  val userToDisplay: User = ...
  bindMapper(content, userToDisplay, {
    case "niceName" => showValue("Name", user.niceName)
  })
}

The third parameter to the bindMapper method works actually almost exactly like the classic bind parameters in BindHelpers.bind. Only that, with bind, one uses a map (“niceName” → someNodeSeq) and here we use a PartialFunction[String, NodeSeq]. showValue is a convenience function that applies the currently active per-field ‘show’ template to the caption and field value you supply to it. So our niceName will end up in a table row just like a regular field. But in many cases, you will not want to use the current per-field template at all. If you need access to the current node, it is available via the currentNode method.

The special bindings can also be used to override the default behaviour for existing fields. bindMapper will check the special bindings first, and only looks for the field if the partial function doesn’t match.

bindMapper will hide empty fields by default. But what is an empty field? The Mapper API doesn’t really have that concept. For now we use a combined approach that deals with many cases. First, MBindHelper defines an isFieldEmpty method that decides for various field types whether a field is empty. This is one of the cases where you might want to override the MBindHelper trait and produce your own MBindHelper singleton. The second technique is that it checks whether the text content of the produced NodeSeq (for a field that’s whatever asHtml delivers) is empty.

You can override how empty fields are displayed by defining a showEmpty field template, e.g.:

<template:showEmpty>
  <tr><td><field:caption/>:</td><td>EMPTY</td></tr>
</template:showEmpty>

Finally, if you only need a different show template for one field, you can simply define it inside the show tag, e.g.

<show:niceName>
  <p><field:caption />:<br/><field:value /></p>
</show:niceName>

Generating input forms

Generating input forms works almost the same. Instead of <show:…> tags, you simply use <input:…> tags. By default, bindMapper will look up the field in the Mapper instance you provided, and use the toForm method to generate a field. However, the per-field template is a bit more complex. Here’s the default per-field template:


<p><label><field:caption /></label>{"\u2003"}<span class="fieldhint"><field:hint /></span><br /><field:input />{"\u2002"}<field:message errorClass="errorBox" warningClass="warningBox" noticeClass="noticeBox" /></p>

(The unicode characters are equivalent to m- and n-spaces; my current IDE doesn’t like literal Entity References.) The template binds 4 parameters. ‘caption’ is already well-known. ‘input’ binds to the input field as generated by the toForm method. ‘message’ will generate a <lift:Msg..> tag with the uniqueFieldId of the field (cf. Appendix B of the Lift Book). The ‘hint’, my own extension, is an additional field hint that is only needed in input forms, such as “Enter up to 5 authors”. Putting that into the displayName would look bizarre when the field is simply viewed.

With the standard MappedField classes as provided by Lift, the hint remains empty. Your field will need to extend the HintedField trait provided here and override the fieldHint method.

Again, the specialBindings are checked first. If you want to use the current per-field input template for your special binding, you can use the makeInputField method.

List binding

A very common scenario is to have a whole result set to display.That’s where MBindHelper provides a second method, bindMappers (notice the ‘s’ at the end). For example, to display a list of users, your template could be:


<lift:Users.myListUsersSnippet>
  <template:show>
    <td><field:value /></td>
  </template:show>
  <template:showEmpty><td></td></template:showEmpty>
  <table>
    <thead>
      <th>#</th>
      <caption:firstName />
      <caption:lastName />
      <caption:email />
    </thead>
    <tbody>
      <list:each evenClass="listEvenRow">
        <tr><td><show:listIndex /></td><show:firstName /><show:lastName /><show:email /></tr>
      </list:each>
    </tbody>
  </table>
  <p>There are <list:count /> users in total.</p>
  <list:ifEmpty>There currently aren't any users</list:ifEmpty>
</lift:Users.myListUsersSnippet>

The template has an inner section contained in a <list:each> tag that will be applied to every Mapper instance in your result set. Everything around it is used for the header and footer of your list, and you can refer to the displayNames of your fields by using a <caption:fieldName> tag, and to the total number of items with the <list:count> tag. The default template for captions is to enclose the caption in a <th> tag, and you can override this with a <template:caption> tag. If your result set is empty, then only the content inside a <list:ifEmpty> tag is produced.

The inner section is processed exactly like the single-Mapper templates described in the previous section. (We actually call bindMapper n times.) The only additional field you get is listIndex, which will always produce the current row number. (The show template is not applied here, that’s why we need to enclose listIndex in a td tag.)

To provide easy row coloring, the <list:each> tag has two attributes, oddClass and evenClass that can contain class names to be applied to each outermost tag of a row (here: tr), alternating row by row.

The partial function that you can use for your special Bindings is a bit more complicated than with simple bindMapper. Instead of matching a simple String (tag name), you match a tuple of three. The first element contains the current Mapper instance. The second contains the current row number. The third is the tag name, just like before. The special binding will also be checked for each caption tag in the outer section. In that case, the first element of the tuple will be ‘Empty’, and the row index will be 0.

bindMappers will accept a Seq[YourMapperType]. The usual findAll(…) methods return Lists, so that’s fine. I’ve noticed that there is a possible problem with Arrays – flatMap will actually go through the elements twice, therefore creating unnecessary calls to your special bindings, and leading to wrong row numbers. Hopefully this issue will be fixed with the new collections framework.

Work left to do / Open issue to discuss

  • Proper maven package
  • Enable nesting of per-field template definitions
  • mix-in XML attributes from <show:…> and <input:…> tags
  • option to display number of field errors on top or bottom of input form
Something went wrong with that request. Please try again.