Forms with LiftScreen and Wizard

Một phần của tài liệu Manning lift in action the simply functional web framework for scala (Trang 154 - 160)

The more code you write in your Lift applica- tions, the more you may find that you have some loose conceptual relationships between the various snippets within your application in order to create some kind of flow, or a par- ticular way of servicing user input and making application choices therein. In such a situa- tion, neither having these as separate snippets nor converting them to one large stateful snippet feels like the right solution, and this is where LiftScreen and Wizard can help.

Consider a user interaction flow like that detailed in figure 6.2.

Check and display cookie content

Figure 6.2 Page flow requiring user input on page A, which dictates the following page, B or C, both of which lead to D. An example would be choosing a delivery type at an ecommerce checkout, where different couriers require different information.

This kind of simple page flow is common in a lot of applications. What is typically very tricky is managing the relationships and state between each logical section.

Wizard provides a structured system for defining these screen relationships, and LiftScreen provides a mechanism for implementing the contents of each screen.

6.3.1 LiftScreen

When you require a page flow in your application, it isn’t uncommon for it to be hard- coded in one aspect or another, be it templating, snippets, or something else. This is obviously less than optimal; it would be far better to have well-defined stories that you could test in a single environment, devoid of a HTTP environment. Wizard provides such a construct by allowing you to knit together multiple LiftScreen instances in a unified and declarative fashion.

Let’s take a moment to manually construct a single, isolated LiftScreen to collect some user input, validate it, and then serve a response based upon the user input. The next listing shows the most basic type of LiftScreen.

import net.liftweb.http.{LiftScreen,S}

object AskAboutIceCreamBasic extends LiftScreen { val flavor =

field("What's your favorite Ice cream flavor", "") def finish(){

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

}

This small example shows the smallest LiftScreen you can make, with a single field and an action function to be executed upon the submission of the form. In this case, the finish method simply reports a notice via the message infrastructure.

NOTE Both LiftScreen and Wizard support AJAX operations right out of the box. Chapter 9 covers this AJAX support in full with all the necessary back- ground information.

All LiftScreen implementations must descend from the net.liftweb.http.Lift- Screen trait and implement a set of fields either manually—as in this example—or by registering a Mapper or Record type. You must also implement the finish(): Unit method so that the screen has a final action, even if that action does nothing.

In order to manufacture a field in the output, this listing calls the field method and assigns it to a value called flavor. The field method is essentially a factory method for the internal Field type, and it takes two mandatory parameters and one optional one:

■ The label for the field (mandatory).

■ The default value for the field (mandatory). Importantly, the type and subse- quent rendering of the field is, by default, asserted from the type of the default Listing 6.14 Basic LiftScreen implementation

133 Forms with LiftScreen and Wizard

value. String gets a text box, Boolean gets a check box, and so forth. If you use a custom type, you can override the rendering of the field by implementing a custom FormBuilderLocator and appending that to LiftRules.appendGlobal- FormBuilder.

■ A varargs of FilterOrValidate functions that are applied for validation (optional). LiftScreen ships with several basic validation helpers for rudimen- tary operations, such as non-null values, matching to a regular expression, and ensuring minimum and maximum lengths.

Currently, the example in listing 6.14 will take in a string value and report a notice upon submission, but it would be helpful to apply some validation rules and do some kind of computation based upon the field input. The following listing shows the origi- nal example with some modifications and with basic validation.

object AskAboutIceCreamTwo extends LiftScreen {

val flavor = field("What's your favorite Ice cream flavor", "", trim, valMinLen(2,"Name too short"), valMaxLen(40,"That's a long name")) val sauce = field("Like chocolate sauce?", false) def finish(){

if(sauce){

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

} }

As you can see, this version has a few modifications to add validation options to the flavor field. These simply aggregate together so you can apply whatever validation you require B. There’s also a new field called sauce that demonstrates a different type assertion around display elements, and as field has been passed a Boolean, Lift will automatically create a check box C.

You may be wondering how all of this actually gets displayed. Handily, LiftScreen takes care of all the rendering by using the following template resolution path:

1 Look for the template located at allTemplatePath in the context of that Lift- Screen object.

2 Look for the wizard-all.html template in the webapp/templates-hidden directory.

3 Use the built-in markup defined by allTemplateNodeSeq.

Given the template markup resolved by the aforementioned search path, all the fields, labels, and validations will be correctly rendered. All you need do is style it to your own tastes, and because it’s essentially just snippet markup, there’s no limit upon how you can position items or sections on the screen. It’s very much up to you.

Listing 6.15 Applying validation to the LiftScreen sample

Validation functions

B

Checkbox field

C

CUSTOM FIELD TYPES

As discussed earlier in this section, LiftScreen automatically determines what kind of user interface element it should render based upon the type of the value it’s instanti- ated with. Although this is helpful for the default use cases, there are times when you’ll want to customize that behavior, such as in the case of a password. The password is a string, so it does need a text input, but it must be a password input and not a regu- lar text input field. Another example would be rendering a text area instead of a one- line text input.

Fortunately, it’s simple enough to create an ad hoc field type in your LiftScreen. Implementing a custom field allows you to build a field for a type that isn’t supported by Lift directly, so you can present your own domain classes however you wish. That’s demonstrated in the following listing.

object ScreenWithCustomField extends LiftScreen { val password = new Field {

type ValueType = String override def name = "Password"

override implicit def manifest = buildIt[String]

override def default = ""

override def toForm: Box[NodeSeq] = SHtml.password(is, set _) }

def finish() = println("Submitted") }

This implementation of LiftScreen includes a custom field type called ValueType. The key thing about this is that the ValueType lets the Field know what type the con- tent is. Finally, the toForm method provides the customized implementation of the UI control itself. In this example, supplying SHtml.password will result in a password input being presented to the user.

This is exceedingly helpful for a one-time implementation, but it can result in duplication of code if you have a specialized type that you need to display in several screens. In this case, it’s much better to use a global form builder so that every form in which you need to represent a particular type doesn’t need a repeat imple- mentation of the display logic. The following listing shows an example of this global form building.

import net.liftweb.common.Empty

import net.liftweb.util.FormBuilderLocator import net.liftweb.http.{SHtml,LiftRules}

case class Book(reference: Long, title: String)

➥LiftRules.appendGlobalFormBuilder(FormBuilderLocator[List[Book]](

(books,setter) => SHtml.select(books.map(b =>

(b.reference.toString, b.title)), Empty, v => println(v)))) Listing 6.16 Implementing an ad hoc custom field type with LiftScreen

Listing 6.17 Implementing a custom form build

135 Forms with LiftScreen and Wizard

This listing defines a custom type: Book. In practice, this might be one of your custom domain types or something similar, but it will suffice for this example. When Lift- Screen is presented with a List[Book] type, it will automatically render it as a drop- down list with no additional configuration or input at the call site. By calling to Lift- Rules.appendGlobalFormBuilder, you only define this functionality once and it becomes available globally within your application.

Form builders are functions that LiftScreen uses to create the interface elements for any given form, and in this example the code just takes the list of books and con- structs the drop-down list by using SHtml.select. In order to use this in conjunction with a field, you just need to do this:

val book = field("Choose book", books)

Now you know how to make single-page forms with LiftScreen, but what if you wanted to create a multipage wizard, as we suggested in the introduction to this sec- tion? Well, you can take what you learned about LiftScreen and wrap that in a Wizard, which is a collection of LiftScreen definitions.

6.3.2 Wizard

Lift’s Wizard system builds on the base of LiftScreen and allows you to build com- plex, stateful workflows with only a few lines of code. In this example, we build a small form that simulates a registration system. On the first screen, users enter their name and age, and depending on whether they’re under 18 or not, they will be presented with a second screen asking them to populate their parents’ names. Of course, this isn’t applicable to users who are over 18, so in that case the parents screen is skipped and the final screen is displayed right away.

Before diving into the code, don’t forget to add the wizard dependency to your project. Unlike LiftScreen, which is generic and lives in the main WebKit JAR, Wizard carries more infrastructure, so it’s in a separate JAR called lift-wizard. Add the follow- ing definition to your SBT project class:

val wizard = "net.liftweb" %% "lift-wizard" % liftVersion

The following listing shows the example wizard.

import net.liftweb.http.S

import net.liftweb.wizard.Wizard object PetSurveyWizard extends Wizard {

object completeInfo extends WizardVar(false) val you = new Screen { val yourName = field("First Name", "",

valMinLen(2, "Name Too Short"), valMaxLen(40, "Name Too Long"))

Listing 6.18 Using Lift’s Wizard

Extend Wizard

B type

Define screens

C

val yourAge = field("Age", 1, minVal(5, "Too young"),

maxVal(125, "You should be dead")) override def nextScreen = if (yourAge.is< 18) parents

else pets }

val parents = new Screen { val parentName = field("Parent or Guardian's name", "", valMinLen(2, "Name Too Short"),

valMaxLen(40, "Name Too Long")) }

val pets = new Screen {

val pet = field("Pet's name", "", valMinLen(2, "Name Too Short"), valMaxLen(40, "Name Too Long")) }

def finish(){

S.notice("Thank you for registering your pet") completeInfo.set(true)

} }

In this listing, you can see that with the lift-wizard package on the classpath, you can import the Wizard type and create a singleton called PetSurveyWizard, which is a subtype of WizardB. In this object, several Screen instances are defined C. As the name suggests, these define the screens (and fields) that the end user will interact with. The fields themselves are defined exactly the same as for LiftScreen, so we won’t cover them again. Interestingly, you can see the nextScreen method D; overrid- ing this and providing your own implementation gives you the flexibility you need to define very complex workflows through linked screens. The Screen type itself also has other things you can override, such as the confirmScreen_? method to tell Lift that this screen is for confirmation only.

More broadly, the Wizard type has several helpful features that can assist in making sophisticated user interaction flows. For example, a Wizard can have a snapshot taken of its state at any given time, and you can restore that snapshot at a later date in the same session. At the time of writing, these snapshots were not serializable, but that’s certainly on the roadmap, so check the wiki or scaladoc for up-to-date information.

Although you can’t currently save the Wizard state directly, it’s possible to persist form data to the database, such as by calling the save method on a Mapper instance you might hold in a WizardVar.

As you can see, Wizard provides you with a helpful out-of-the-box frame for creating multipage user interactions, whatever they might be. In the next section, we look at another module of Lift that provides functionality for certain common web components.

Define link to next screen

D

Define screens

C

137 Widgets

Một phần của tài liệu Manning lift in action the simply functional web framework for scala (Trang 154 - 160)

Tải bản đầy đủ (PDF)

(426 trang)