Background

Much of the web is creating input forms for users to submit, validating those input forms and if the forms pass validation, an action is performed.  If the forms don't pass validation, the user is told which fields caused the validation problems and is given an opportunity to fix the problems.  Lift provides a single-screen input/validation mechanism called LiftScreen and a multi-page input/validation mechanism (with stateful next/previous buttons) called Wizard.  This post will discuss LiftScreen and the next post will discuss Wizard.

Both Wizard and Screen share the following attributes:
  • All logic can be tested without involving HTTP
  • All logic is declarative
  • All state is managed by Lift
  • The back-button works as the user would expect it to work
  • The form elements are strongly typed
  • The rendering logic and templates is divorced from the form logic
  • Basics

    First, let's declare a very simple input screen that asks your favorite ice cream flavor:

    object AskAboutIceCream1 extends LiftScreen {
      val flavor = field(S ? "What's your favorite Ice cream flavor", "")

      def finish() {
        S.notice("I like "+flavor.is+" too!")
      }
    }

    We create an object, a Scala singleton, called AskAboutIceCream1 which extends LiftScreen.  We declare a single field called flavor.  In our view, we refer to the LiftScreen with the following code:

    <lift:AskAboutIceCream1/>

    And we get a display:

      Cancel Finish

    When we submit the form, a notice is displayed agreeing with our ice cream choice.  But, we can enter a blank ice cream name and it will still be accepted.  That's not optimal.  We need to add some validation:

    object AskAboutIceCream2 extends LiftScreen {
      val flavor = field(S ? "What's your favorite Ice cream flavor", "",
                         trim, 
                         valMinLen(2, "Name too short"),
                         valMaxLen(40, "That's a long name"))

      def finish() {
        S.notice("I like "+flavor.is+" too!")
      }
    }

    This code trims the incoming string (removes any leading and trailing spaces) and then makes sure the length is reasonable.  So, if we enter a blank value, we get:

    •  
      • Name too short
      Cancel Finish

    We can add another field, this time a Boolean which turns into a checkbox:

    object AskAboutIceCream3 extends LiftScreen {
      val flavor = field(S ? "What's your favorite Ice cream flavor", "",
                         trim, valMinLen(2,S ? "Name too short"),
                         valMaxLen(40,S ? "That's a long name"))

      val sauce = field(S ? "Like chocalate sauce?", false)

      def finish() {
        if (sauce) {
          S.notice(flavor.is+" tastes especially good with chocolate sauce!")
        }
        else S.notice("I like "+flavor.is+" too!")
      }
    }

    And our display looks like:

      Cancel Finish
    The Boolean sauce field defaults to creating a checkbox rather than an text field.

    We can also do cross-field validation:

    object AskAboutIceCream4 extends LiftScreen {
      val flavor = field(S ? "What's your favorite Ice cream flavor", "",
                         trim, valMinLen(2,S ? "Name too short"),
                         valMaxLen(40,S ? "That's a long name"))

      val sauce = field(S ? "Like chocalate sauce?", false)

      override def validations = notTooMuchChocolate _ :: super.validations

      def notTooMuchChocolate(): Errors = {
        if (sauce && flavor.toLowerCase.contains("chocolate")) "That's a lot of chocolate"
        else Nil
      }

      def finish() {
        if (sauce) {
          S.notice(flavor.is+" tastes especially good with chocolate sauce!")
        }
        else S.notice("I like "+flavor.is+" too!")
      }
    }

    So, you you change the chocolate box and enter a flavor that contains chocolate, you get an error indicating that there's just too much chocolate.

    Working with Mapper and Record instances

    Turns out that LiftScreen works just ducky with Mapper and Record:

    object PersonScreen extends LiftScreen {
      object person extends ScreenVar(Person.create)

      override def screenTop =
      <b>A single screen with some input validation</b>

      _register(() => person.is)