第3章、Lift中的表单处理

本章将介绍如何使用Lift:提交表单处理表单数据,并使用表单元素。表单提交的最终结果可以是在数据库中更新的记录,所以您可能还会发现第7 第8章分别讨论关系数据库和MongoDB时有用。

在表单处理将数据传递到服务器的情况下,第5章还有与表单处理相关的配方。

您将在本章中找到许多示例,作为https://github.com/LiftCookbook/cookbook_forms的源代码

普通旧表格处理

问题

您希望以常规,老式,非Ajax的方式处理表单数据。

提取表单值S.param,处理值,并产生一些输出。

例如,我们可以显示表单,处理输入值,并将消息作为通知发回。该模板是一个常规的HTML表单,添加了一个代码段:

<form data-lift="Plain" action="/plain" method="post">
  <input type="text" name="name" placeholder="What's your name?">
  <input type="submit" value="Go">
</form>

在片段中,我们可以挑选出的字段的值nameS.param("name")

package code.snippet

import net.liftweb.common.Full
import net.liftweb.http.S
import net.liftweb.util.PassThru

object Plain {

  def render = S.param("name") match {
    case Full(name) =>
      S.notice("Hello "+name)
      S.redirectTo("/plain")
    case _ =>
      PassThru
  }

}

第一次通过这个代码片段,没有参数,所以我们只是把表单传回给浏览器,这PassThru是做什么的。然后,您可以在该name字段中输入一个值并提交表单。这将导致提升处理模板再次,但这次,name输入的值结果将是您的浏览器重定向到一个页面,并显示一条消息。

讨论

手动从请求中提取参数并没有充分利用Lift,但有时您需要执行此操作,并且S.param可以处理请求参数。

结果S.param是a Box[String],在前面的例子中,我们对这个值进行模式匹配。有多个参数,您可能已经看到S.param以这种方式使用:

def render = {
  for {
    name <- S.param("name")
    pet <- S.param("petName")
  } {
    S.notice("Hello %s and %s".format(name,pet))
    S.redirectTo("/plain")
  }

 PassThru
}

如果两个namepetName提供,对身体for进行评估。

S.params(name)
List[String] 使用给定的名称为所有请求参数 生成一个
S.post_? 和 S.get_?
告诉你请求是GET还是POST
S.getRequestHeader(name)
给出了 Box[String] 用于在具有给定名称的请求的标头
S.request
访问 Box[Req] ,它可以访问有关请求的进一步HTTP特定信息

作为使用的一个例子S.request,我们可以List[String]为参数名称中所有请求参数的值产生一个name

val names = for {
  req <- S.request.toList
  paramName <- req.paramNames
  if paramName.toLowerCase contains "name"
  value <- S.param(paramName)
} yield value

请注意,通过打开,S.request我们可以通过paramNames函数访问所有参数名称Req

屏幕或向导为表单处理提供了替代方案,但有时您只想从请求中提取值,如本配方所示。

也可以看看

简单提升涵盖了各种处理方式。

Ajax表单处理

问题

您希望通过Ajax在服务器上处理一个表单,而无需重新加载整个页面。

将表单标记为Ajax表单,data-lift="form.ajax"并在提交表单时提供在服务器上运行的功能。

这是一个表单的示例,将收集我们的名称并通过Ajax将其提交到服务器:

<form data-lift="form.ajax">
  <div data-lift="EchoForm">
    <input type="text" name="name" placeholder="What's your name?">
    <input type="submit">
  </div>
</form>

<div id="result">你的名字将在这里回应</div>

以下片段将通过Ajax回显名称:

package code.snippet

import net.liftweb.util.Helpers._
import net.liftweb.http.SHtml.{text,ajaxSubmit}
import net.liftweb.http.js.JsCmd
import net.liftweb.http.js.JsCmds.SetHtml
import xml.Text

object EchoForm extends {

  def render = {

    var name = ""

    def process() : JsCmd = SetHtml("result", Text(name))

    "@name" #> text(name, s => name = s) &
    "type=submit" #> ajaxSubmit("Click Me", process)
  }
}

render方法将name输入字段绑定到将分配用户输入到变量的任何函数name请注意,您通常会看到s => name = s以较短的形式写入name = _

按下按钮后,process调用函数,返回值为nameHTML的HTML元素result

讨论

data-lift="form.ajax"该配方一部分确保Lift将Ajax处理机制添加到表单中。这意味着<form>输出中元素将结束如下:

<form id="F2203365740CJME2G" action="javascript://"
  onsubmit="liftAjax.lift_ajaxHandler(
    jQuery('#'+&quot;F2203365740CJME2G&quot;).serialize(),
    null, null, &quot;javascript&quot;);return false;">
  ...
</form>

换句话说,当表单被要求提交时,Lift将通过Ajax序列化表单。这意味着你根本不一定需要提交按钮。在这个示例中,单个文本字段,如果省略提交按钮,可以按Return键触发序列化。这将触发s => name = s在我们的常规data-lift="EchoForm"代码段中绑定函数换句话说,name即使没有提交按钮也会分配该值

添加一个提交按钮可以让我们在所有字段的所有功能执行完成后执行操作。

请注意,Lift的方法是将表单序列化到服务器,执行与字段关联的功能,执行submit函数(如果有的话),然后将JavaScript结果返回给客户端。默认的序列化过程是serialization在表单上使用jQuery的方法。这个序列化字段除了提交按钮和文件上传。

提交造型

SHtml.ajaxSubmit函数生成<input type="submit">页面元素。您可能更喜欢使用风格的按钮进行提交。例如,使用Twitter Bootstrap,带有图标的按钮将需要以下标记

<button id="submit" class="btn btn-primary btn-large">
  <i class="icon-white icon-ok"></i> 提交
</button>

在窗<button>体内按一下即可触发提交。但是,如果您绑定了该按钮SHtml.ajaxSubmit,则内容,因此样式将丢失。

要解决这个问题,您可以为一个隐藏的字段分配一个函数。当与其他任何字段一样提交表单时,将调用此函数。我们的片段中唯一改变的部分是CSS选择器绑定

import net.liftweb.http.SHtml.hidden

"@name" #> text(name, s => name = s) &
"button *+" #> hidden(process)

*+替换规则是指一个值追加到按钮的子节点。这将包括一个这样的形式的隐藏字段

<input type="hidden" name="F11202029628285OIEC2" value="true">

提交表单时,隐藏字段被提交,并且像任何字段一样,Lift将调用与其相关的功能:process在这种情况下。

效果是类似的ajaxSubmit,但不完全相同。在这种情况下,我们会在后面添加一个隐藏字段<button>,但您可以将其放置在您找到方便的表单上。然而,有一个并发症:什么时候process被叫?是否name已经被分配或之后?这取决于字段的呈​​现顺序。也就是说,在HTML模板中,将按钮放在文本字段之前(因此在本示例中移动隐藏字段的位置),在process设置名称之前调用函数。

有几种方法。或者,确保您以这种方式使用的隐藏字段在您的表单中显示较晚,或者确保该函数被称为迟到与a formGroup

import net.liftweb.http.SHtml.hidden
import net.liftweb.http.S

"@name" #> text(name, s => name = s) &
"button *+" #> S.formGroup(1000) { hidden(process) }

formGroup加法操作函数标识符以确保稍后进行排序,导致该函数process的调用晚于默认组(0)中的字段。

Lift2.6和3.0可能包含ajaxOnSubmit,这将提供ajaxSubmit隐藏方法的可靠性和灵活性。如果要在Lift 2.5中尝试,Antonio Salazar Cardozo创建了一个可以包含在您的项目中帮手

也可以看看

功能顺序在Lift Cool Tips wiki页面中讨论

有关表单序列化过程的更多详细信息,请查看jQuery 文档

“Ajax文件上传”描述了Ajax文件上传。

Ajax JSON表单处理

问题

您想通过Ajax处理表单,以JSON格式发送数据。

使用Lift的jlift.js JavaScript库和JsonHandler类。

例如,我们可以创建一个“座右铭服务器”,接受机构名称和机构的座右铭,并对这些价值观进行一些行动。我们只是回到客户的名字和座右铭。

考虑这个HTML,它不是一种形式,而是包含jlift.js

<html>
<head>
  <title>JSON表单</title>
</head>
<body data-lift-content-id="main">

<div id="main" data-lift="surround?with=default;at=content">

  <h1>Json表单示例</h1>

  <!-- Required for JSON forms processing -->
  <script src="/classpath/jlift.js" data-lift="tail"></script>

  <div data-lift="JsonForm" >

    <script id="jsonScript" data-lift="tail"></script>

    <div id="jsonForm">

      <label for="name">
        机构
        <input id="name" type="text" name="name" value="Royal Society" />
      </label>

      <label for="motto">
        座右铭
        <input id="motto" type="text" name="motto" value="Nullius in verba" />
      </label>

      <input type="submit" value="Send" />

    </div>

    <div id="result">
      结果将显示在此处。
    </div>

  </div>

</div>
</body>
</html>

这个HTML向用户呈现包含在一个<div>被叫中的两个字段,一个名字和一个座右铭jsonForm还有一些结果的占位符,您会注意到jsonScript一些JavaScript代码的占位符。jsonForm会被操纵,以确保它通过Ajax发送,jsonScript将与Lift的代码来执行序列所取代。这在代码段代码中发生:

package code.snippet

import scala.xml.{Text, NodeSeq}

import net.liftweb.util.Helpers._
import net.liftweb.util.JsonCmd
import net.liftweb.http.SHtml.jsonForm
import net.liftweb.http.JsonHandler
import net.liftweb.http.js.JsCmd
import net.liftweb.http.js.JsCmds.{SetHtml, Script}

object JsonForm {

  def render =
    "#jsonForm" #> ((ns:NodeSeq) => jsonForm(MottoServer, ns)) &
    "#jsonScript" #> Script(MottoServer.jsCmd)

  object MottoServer extends JsonHandler {

    def apply(in: Any): JsCmd = in match {
      case JsonCmd("processForm", target, params: Map[StringString], all) =>
        val name = params.getOrElse("name", "No Name")
        val motto = params.getOrElse("motto", "No Motto")
        SetHtml("result",
          Text("The motto of %s is %s".format(name,motto)) )
    }
  }
}

像许多片段一样,此Scala代码包含render绑定到页面上的元素方法。具体来说,jsonForm正在被替换SHtml.jsonForm,这将采取NodeSeq(这是输入字段),并将其转换为将JSON值提交的形式。提交的内容将是我们的MottoServer代码。

jsonScript元素绑定到JavaScript,将执行值到服务器的传输和编码。

如果您点击“发送”按钮并观察网络流量,您将看到以下内容发送到服务器:

{
  "command": "processForm",
  "params": {"name":"Royal Society","motto":"Nullius in verba"}
}

这是在与之匹配的模式中的all参数的值Lift已经照顾了管道来实现这一点。JsonCmdMottoServer.apply

示例中的模式匹配的结果是选出两个字段值,并发回JavaScript以更新results <div>:“皇家学会的座右铭是Nullius in verba”。

讨论

JsonHandler类和SHtml.jsonForm方法一起进行了大量的工作对我们来说。jsonForm方法是安排表单字段编码为JSON,并通过Ajax发送到我们MottoServerJsonCmd其实这是JsonCmd一个默认的命令名"processForm"

我们的MottoServer课程正在寻找(匹配)JsonCmd,提取表单域的值,并将它们作为在页面上JsCmd更新的客户端回显<div>

MottoServer.jsCmd部分正在生成将表单字段传递到服务器所需的JavaScript。正如我们将在后面看到的,这提供了一个通用功能,我们可以使用它来向服务器发送不同的JSON值和命令。

还要注意,从网络流量来看,发送的表单字段将以页面上给出的名称进行序列化。没有发送该映射的“F ...”值到服务器上的函数调用。这样做的结果是,动态添加到页面的任何字段也将被序列化到服务器,在那里它们可以被拾取MottoServer

脚本jlift.js正在提供管道来做这个事情。

在进行之前,请说服自己,在服务器端(MottoServer.jsCmd)上生成JavaScript,当客户端提交表单时执行该脚本,以将结果传递到服务器。

附加命令

在前面的例子中,我们JsonCmd使用命令名匹配"processForm"您可能想知道可以提供其他命令,还是该target的含义

为了演示如何实现其他命令,我们可以添加两个附加按钮。这些按钮只会将座右铭转换为大写或小写。服务器端render方法更改如下:

def render =
  "#jsonForm" #> ((ns:NodeSeq) => jsonForm(MottoServer, ns)) &
  "#jsonScript" #> Script(
    MottoServer.jsCmd &
    Function("changeCase", List("direction"),
      MottoServer.call("processCase", JsVar("direction"),
        JsRaw("$('#motto').val()"))
    )
  )

JsonForm保持不变。我们仍然包括MottoServer.jsCmd,我们仍然想包装字段,并像以前一样提交。我们添加的是一个额外的JavaScript函数changeCase,它调用一个参数,direction并且身体调用MottoServer各种参数。当它在页面上呈现时,它将显示为这样:

function changeCase(direction) {
  F299202CYGIL({'command': "processCase", 'target': direction,
    'params': $('#motto').val() });
}

F299202CYGIL功能(或类似名称)由Lift产生作为其一部分MottoServer.jsCmd,并且它是负责将数据传送到服务器。在这种情况下,它提供的数据是一个由不同的命令(processCase)组成的JSON结构,它是JavaScript direction评估的目标,也是#motto表单字段值的jQuery表达式的结果的参数

什么时候changeCase调用函数?这取决于我们,调用该函数的一个非常简单的方法是通过添加到HTML中:

<button onclick="javascript:changeCase('upper')">大写字母小提琴</button>
<button onclick="javascript:changeCase('lower')">小写字母</button>

当按下这些按钮之一时,结果将是以指令发送到服务器的JSON值,processCase并相应地direction进行params设置。剩下的就是修改我们在服务器上MottoServer拿起这个JsonCmd

object MottoServer extends JsonHandler {

  def apply(in: Any): JsCmd = in match {

    case JsonCmd("processForm", target, params: Map[StringString], all) =>
      val name = params.getOrElse("name", "No Name")
      val motto = params.getOrElse("motto", "No Motto")
      SetHtml("result",
        Text("The motto of %s is %s".format(name,motto)) )

    case JsonCmd("processCase", direction, motto: String, all) =>
      val update =
        if (direction == "upper") motto.toUpperCase
        else motto.toLowerCase
      SetValById("motto", update)

  }
}

第一个JsonCmd没有改变。第二个匹配发送的参数,并导致使用大写或小写版本的座右铭更新表单域。

也可以看看

Lift演示网站包含更多的例子JsonHandler

如果要通过REST处理JSON,请查看无状态JSON示例

提升操作,第9.1.4节讨论了“使用JSON表单与AJAX”,与“探索提升 ”第10.4节一样

使用日期选择器进行输入

问题

您希望提供一个日期选择器,方便用户为您的表单提供日期。

使用标准提升SHtml.text输入字段并附加JavaScript日期选择器。在这个例子中,我们将使用jQuery UI日期选择器。

我们的表单将包括一个被称为birthday日期选择器的输入字段,以及jQuery UI JavaScript和CSS:

<!DOCTYPE html>
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
  <title>jQuery日期选择器</title>
</head>
<body data-lift-content-id="main">
<div id="main" data-lift="surround?with=default;at=content">

  <h3>你的生日是什么时候?</h3>

  <link data-lift="head" type="text/css" rel="stylesheet"
    href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/css/smoothness
          /jquery-ui-1.10.2.custom.min.css">
  </link>

  <script data-lift="tail"
    src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js">
  </script>

  <div data-lift="JqDatePicker?form=post">
    <input type="text" id="birthday">
    <input type="submit" value="Submit">
  </div>

</div>
</body>
</html>

这通常会产生一个常规的文本输入字段,但是我们可以通过添加JavaScript来将日期选择器附加到输入字段来进行更改。您可以在模板中执行此操作,但在此示例中,我们将增强文本字段作为代码段代码的一部分:

package code.snippet

import java.util.Date
import java.text.SimpleDateFormat

import net.liftweb.util.Helpers._
import net.liftweb.http.{S, SHtml}
import net.liftweb.http.js.JsCmds.Run
import net.liftweb.common.Loggable

class JqDatePicker extends Loggable {

  val dateFormat = new SimpleDateFormat("yyyy-MM-dd")

  val default = (dateFormat format now)

  def logDate(s: String) : Unit = {
    val date : Date = tryo(dateFormat parse s) getOrElse now
    logger.info("Birthday: "+date)
  }

  def render = {
    S.appendJs(enhance)
    "#birthday" #> SHtml.text(default, logDate)
  }

  val enhance =
    Run("$('#birthday').datepicker({dateFormat: 'yy-mm-dd'});")
}

请注意,render我们将常规SHtml.text字段绑定到具有ID的元素birthday,但也将JavaScript附加到页面。JavaScript选择birthday输入字段并将配置的日期选择器附加到它。

当提交字段时,将logDate使用文本字段的值调用方法。我们将文本解析成一个java.util.Date对象。tryoLift助手将捕获任何ParseException和返回Box[Date]如果劣枣供应,这是我们打开,或默认为当前日期。

运行此代码并提交表单将生成一条日志消息,如:

INFO code.snippet.DatePicker  - 生日:Sun Apr 21 00:00:00 BST 2013

讨论

该配方中概述的方法可以与其他日期选择器库一起使用。关键是要配置日期选择器,以提交给服务器的值的形式提交日期。这是日期的“有线格式”,并且不一定与用户在浏览器中看到的格式相同,具体取决于所使用的浏览器或JavaScript库。

HTML5日期选择器

HTML5规范包括用于各种日期输入类型的支持:datetimedatetime-localdatemonthtime,和week例如

<input type="date" name="birthday" value="2013-04-21">

这种类型的输入字段将以yyyy-mm-dd格式提交日期,我们的代码段将能够处理。

随着更多的浏览器实现这些类型,它将成为可能依赖于它们。但是,您可以默认使用HTML5浏览器 - 本地日期选择器,并根据需要返回到JavaScript库。差异如图3-1所示


与Chrome中的浏览器原生日期选择器相比,附有jQuery UI日期选择器的输入字段
图3-1。与Chrome中的浏览器原生日期选择器相比,附有jQuery UI日期选择器的输入字段

要检测浏览器是否支持type="date"输入,我们可以使用Modernizr库。这是我们的模板中的一个附加脚本

<script data-lift="tail"
  src="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min.js">
</script>

我们将在我们的代码片段中使用它。实际上,我们需要对代码片段进行两个更改:

  1. type="date"属性添加到输入字段。
  2. 修改JavaScript只将jQuery UI日期选择器附加到不支持type="date"输入的浏览器中

在代码中,

def render = {
  S.appendJs(enhance)
  "#birthday" #> SHtml.text(default, logDate, ("type"->"date"))
}

val enhance = Run(
  """
    |if (!Modernizr.inputtypes.date) {
    | $('input[type=date]').datepicker({dateFormat: 'yy-mm-dd'});
    |}
  """.stripMargin)

"type" -> "date"参数上SHtml.text是设置属性type的值date上得到的<input>场。

当此代码段运行并且页面被渲染时,jQuery UI日期选择器将被附加到type="date"只有浏览器不支持该类型的输入字段

也可以看看

深入HTML5描述如何检测浏览器功能。

jQuery UI API文档列出了日期选择器的各种配置选项。

HTML5 date输入类型以RFC 3339格式提交日期

自动完成建议

问题

您想要提供一个自动完成的小部件,以便在用户输入文本字段时向用户提供建议。

使用JavaScript自动完成小部件,例如,通过AutoCompleteLift窗口小部件模块中的类来进行jQuery UI自动完成

首先将Lift窗口小部件模块添加到build.sbt中

libraryDependencies += "net.liftmodules" %% "widgets_2.5" % "1.3"

要启用自动完成小部件,请在Boot.scala中进行初始化

import net.liftmodules.widgets.autocomplete.AutoComplete
AutoComplete.init()

我们可以创建一个常规的表单代码段:

<form data-lift="ProgrammingLanguages?form=post">
  <input id="autocomplete">
  <input type="submit">
</form>

的连接AutoComplete类与的ID的元素autocomplete

package code.snippet

import net.liftweb.util.Helpers._
import net.liftweb.common.Loggable

import net.liftmodules.widgets.autocomplete.AutoComplete

class ProgrammingLanguages extends Loggable {

  val languages = List(
    "C", "C++", "Clojure", "CoffeeScript",
    "Java", "JavaScript",
    "POP-11", "Prolog", "Python", "Processing",
    "Scala", "Scheme", "Smalltalk", "SuperCollider"
  )

  def render = {

    val default = ""

    def suggest(value: String, limit: Int) =
      languages.filter(_.toLowerCase.startsWith(value))

    def submit(value: String) : Unit =
      logger.info("Value submitted: "+value)

    "#autocomplete" #> AutoComplete(default, suggest, submit)
  }

}

这个代码段的最后一行显示了AutoComplete该类的绑定,它需要:

  • 要显示的默认值
  • 一个功能将从输入的文本值产生建议 - 结果是一个Seq[String]建议
  • 提交表单时调用的函数

运行此代码呈现如图3-2所示


编程语言片段的呈现
图3-2。编程语言片段的呈现

提交表单时,该submit函数将被传递给选定的值。submit函数只是记录此值:

 INFO code.snippet.ProgrammingLanguages  - 提交的值:Scala

讨论

自动完成小部件使用jQuery自动完成。NodeSeq通过检查AutoComplete.apply方法可以看出这一点

        
        
<span>
<head>
<link type= "text/css" rel= "stylesheet"
href= "/classpath/autocomplete/jquery.autocomplete.css" >
</link>
<script type= "text/javascript"
src= "/classpath/autocomplete/jquery.autocomplete.js" >
</script>
<script type= "text/javascript" >
// <![CDATA[
jQuery ( document ). ready ( function (){
var data = "/ajax_request?F846528841915S2RBI0=foo" ;
jQuery ( "#F846528841916S3QZ0V" ).
autocomplete ( data , { minChars : 0 , matchContains : true }).
result ( function ( event , dt , formatted ) {
jQuery ( "#F846528841917CF4ZGL" ). val ( formatted );
}
);
});;
// ]]>
</script>
</head>
<input type= "text" value= "" id= "F846528841916S3QZ0V" ></input>
<input name= "F846528841917CF4ZGL" type= "hidden" value= ""
id= "F846528841917CF4ZGL" ></input>
</span>

这个标记块是从AutoComplete(default, suggest, submit)呼叫生成的。这里发生的情况是,与“Lift”窗口小部件模块捆绑在一起的jQuery UI自动完成JavaScript和CSS正在页面中。“添加到页面的头部”中,Lift将<head>将该标记一部分合并<head>最终的HTML页面中。

当页面加载时,jQuery UI autocomplete功能被绑定到输入字段,并配置了一个URL,它将把用户的输入传递给我们的suggest函数。所有suggest需要做的是返回一个Seq[String]jQuery自动填充代码的值给用户显示。

jQuery 1.9

jQuery 1.9删除了$.browser自动填充小部件需要方法。解决这个问题的方法是在你的模板中包含jQuery迁移包:

         
         
<script data-lift= "head"
src= "http://code.jquery.com/jquery-migrate-1.2.1.js" >
</script>
提交新值

AutoComplete小部件的一个特点是,如果您输入一个新值(一个不建议),然后按提交,则不会将其发送到服务器。也就是说,您需要点击其中一个建议来选择它。如果这不是你想要的行为,你可以调整它。

在该render方法中,我们可以通过在页面中添加JavaScript来修改行为:

         
         
import net.liftweb.http.S
import net.liftweb.http.js.JsCmds.Run
S . appendJs ( Run (
"""
|$('#autocomplete input[type=text]').bind('blur',function() {
| $(this).next().val($(this).val());
|});
""" . stripMargin ))

有了这一点,当输入字段失去焦点时 - 例如,当按下提交按钮时,输入字段的值被存储为要发送到服务器的值。

替代自动完成JavaScript

看看小部件模块构建自动完成功能的方式可能会让您深入了解如何将其他JavaScript自动完成库并入Lift应用程序。这个想法是包括JavaScript库,将其连接到页面上的元素,然后安排要调用的服务器来生成建议。当然,如果您只有几个项目供用户选择,您可以只在网页上包含这些项目,而不是往返于服务器。

作为服务器生成的建议的例子,我们可以看一下事先键入的内容包含在Twitter的引导组件。

要结合Typeahead,模板需要更改以包含库,并以Typeahead所期望的方式标记输入字段

我们已经添加了一个占位符,js该占位符将会返回给服务器的JavaScript ID 我们稍等一会。

Typeahead的工作方式是将它附加到我们的输入字段,并在需要提出建议时告诉它调用JavaScript函数。该JavaScript函数将被调用askServer,并给出两个参数:用户输入的输入到目前为止(query)和JavaScript函数调用时可用的建议(callback)。Lift片段需要使用该query值,然后调用JavaScript callback函数,并提供任何建议。

实现这一点的代码片段如下:

         
         
package code.snippet
import net.liftweb.util.Helpers._
import net.liftweb.common. { Full , Empty , Loggable }
import net.liftweb.http._
import net.liftweb.http.js.JsCmds._
import net.liftweb.http.js.JsCmds.Run
import net.liftweb.http.js.JE.JsVar
import net.liftweb.json.JsonAST._
import net.liftweb.json.DefaultFormats
class ProgrammingLanguagesTypeAhead extends Loggable {
val languages = List (
"C" , "C++" , "Clojure" , "CoffeeScript" ,
"Java" , "JavaScript" ,
"POP-11" , "Prolog" , "Python" , "Processing" ,
"Scala" , "Scheme" , "Smalltalk" , "SuperCollider"
)
def render = {
implicit val formats = DefaultFormats
def suggest ( value : JValue ) : JValue = {
logger . info ( "Making suggestion for: " + value )
val matches = for {
q <- value . extractOpt [ String ]. map ( _ . toLowerCase ). toList
lang <- languages . filter ( _ . toLowerCase startsWith q )
} yield JString ( lang )
JArray ( matches )
}
val callbackContext = new JsonContext ( Full ( "callback" ), Empty )
val runSuggestion =
SHtml . jsonCall ( JsVar ( "query" ), callbackContext , suggest _ )
S . appendJs ( Run (
"""
|$('#autocomplete').typeahead({
| source: askServer
|});
""" . stripMargin ))
"#js *" #> Function ( "askServer" , "query" :: "callback" :: Nil ,
Run ( runSuggestion . toJsCmd )) &
"#autocomplete" #> SHtml . text ( "" , s => logger . info ( "Submitted: " + s ))
}
}

从代码片段的底部开始,我们将常规Lift SHtml.text输入绑定到自动填充字段。当表单提交时,这将收到所选值。我们还将JavaScript占位符绑定到一个叫做JavaScript函数的定义askServer这是Typeahead在需要建议时会调用的功能。

我们定义的JavaScript函数有两个参数:the querycallbackaskServer导致它运行的东西被称为runSuggestion,这是一个jsonCall到服务器的身体,具有的价值query

这些建议是由suggest函数进行的,该函数期望能够String在JSON值中找到一个它使用此值来查找列表中的匹配项languages这些被返回作为JArrayJString,这将被视为JSON回数据在客户端上。

客户端对数据做了什么?它使用建议调用该callback功能,从而导致显示更新。我们指定它的callbackvia JsonContext,这是一个类,它允许我们指定一个自定义函数来调用成功的请求到服务器。

通过查看HTML页面中生成的JavaScript可以帮助您了解这一点askServer

         
         
<script id= "js" >
function askServer ( query , callback ) {
liftAjax . lift_ajaxHandler ( 'F268944843717UZB5J0=' +
encodeURIComponent ( JSON . stringify ( query )), callback , null , "json" )
}
</script>

当用户键入文本字段时,Typeahead将askServer使用提供的输入进行调用Lift的Ajax支持安排该值,query序列化到我们suggest在服务器上的功能,并将结果传递给callback此时,Typeahead再次接管并显示建议。

键入Scala文本字段并按提交将在服务器上产生如下所示的顺序:

         
         
INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Sc)
INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Sca)
INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Sca)
INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Scal)
INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Scala)
INFO csProgrammingLanguagesTypeAhead - 已提交:Scala

也可以看看

“从按钮触发服务器端代码”介绍jsonCall

关于新值的小部件模块的行为在模块的GitHub页面的故障单中进行了说明增强模块是参与Lift的一个途径,第11章介绍了其他的贡献方式。

jQuery UI自动完成文档介绍如何配置窗口小部件。Lift小部件模块附带的版本是1.0.2版。

Typeahead小部件是Twitter Bootstrap的一部分。

提供无线电按钮选择

问题

您希望用户使用单选按钮选择一个选项。

使用SHtml.radioElem呈现选项的单选按钮。

为了说明这一点,让我们创建一个表单来允许用户指出他的性别:

        
        
object BirthGender extends Enumeration {
type BirthGender = Value
val Male = Value ( "Male" )
val Female = Value ( "Female" )
val NotSpecified = Value ( "Rather Not Say" )
}

我们使用枚举,但它可以是任何类。所述toString类的将被用作显示给用户的标签。

为了呈现这些选项并处理选项的选择,我们在代码片段中使用这个枚举:

        
        
package code.snippet
import net.liftweb.common._
import net.liftweb.util.Helpers._
import net.liftweb.http.SHtml
import net.liftweb.http.SHtml.ChoiceHolder
object Radio extends Loggable {
import BirthGender._
val options : Seq [ BirthGender ] = BirthGender . values . toSeq
val default : Box [ BirthGender ] = Empty
val radio : ChoiceHolder [ BirthGender ] =
SHtml . radioElem ( options , default ) { selected =>
logger . info ( "Choice: " + selected )
}
def render = ".options" #> radio . toForm
}

而不是在render方法中的一个表达式中生成单选按钮,我们已经拉出了中间值来显示它们的类型。radio.toForm调用正在生成单选按钮,我们将它们绑定到以下.option模板中的CSS选择器

        
        
<div data-lift= "Radio?form=post" >
<span class= "options" >
<input type= "radio" > 选项1 </input>
<input type= "radio" > 选项2 </input>
</span>
<input type= "submit" value= "Submit" >
</div>

class="options"范围将被替换为代码中的单选按钮,并且在提交表单时,提供的函数SHtml.radioElem将被调用,导致所选值被记录。例如,如果没有选择单选按钮:

INFO code.snippet.Radio  - 选择:空

或者如果选择了第三个按钮:

INFO code.snippet.Radio  - 选择:完全(而不是说)

讨论

Lift的很多SHtml方法返回一个NodeSeq,可以直接绑定到我们的HTML中。然而,radioElem不同之处在于它给了我们一个ChoiceHolder[T],并NodeSeq从中产生一个,我们正在呼唤toForm这对于如何定制单选按钮有所影响,我们稍后会看到。

radioElem方法需要三个参数:


         
         
SHtml . radioElem ( options , default ) { selected =>
logger . info ( "Choice: " + selected )
}

第一个是一组选项来显示,作为一个Seq[T]第二个是预先选择的价值Box[T]在这个例子中,我们没有预先选择的值,它被表示为Empty最后,当提交表单时,有类型的函数运行Box[T] => Any

请注意,即使用户没有选择任何值,您的函数将被调用,它将被传递给该值Empty

要了解一些更多的事情,请查看由radioElem以下产生的默认HTML 

        
        
<span>
<input value= "F317293945993CDMQZ" type= "radio" name= "F317293946030HYAFP" >
<input name= "F317293946030HYAFP" type= "hidden" value= "F317293946022HCGEG" >
男性 <br>
</span>
<span>
<input value= "F31729394600RIE253" type= "radio" name= "F317293946030HYAFP" >
女性 <br>
</span>
<span>
<input value= "F317293946011OMEMM" type= "radio" name= "F317293946030HYAFP" >
而不是说 <br>
</span>

请注意:

  • 所有输入字段都具有相同的随机生成名称。
  • 输入字段具有随机生成的值。
  • 有一个隐藏的字段添加到第一个项目。

这可能是一个惊喜,如果你只是期待这样的事情:

        
        
<input type= "radio" name= "gender" value= "Male" > 男性 <br>
<input type= "radio" name= "gender" value= "Female" > 女性 <br>
<input type= "radio" name= "gender" value= "NotSpecified" > 而不是说 <br>

通过使用随机值,Lift通过防范一系列基于表单的攻击来帮助我们,例如提交我们不期望的值,或者在不希望设置的字段上设置值。

每个随机单选按钮值在服务器上与BirthGender我们的options序列中值相关联提交表单时,Lift选择所选值(如果有),查找相应的BirthGender值,并调用我们的功能。

隐藏的字段确保即使没有选择单选按钮也将调用该函数。这是因为浏览器至少会提交隐藏字段,这足以触发服务器端功能。

自定义HTML

默认的HTML包装a中的每个单选按钮<span>,并将它们与a分隔开<br>让我们改变一下,使其与Twitter Bootstrap框架一起工作,并将每个选项放在<label>一个类中。

要自定义HTML,您需要了解这ChoiceHolder是一系列项目的容器。每个项目都是ChoiceItem

final case class ChoiceItem[T](key: T, xhtml: NodeSeq)

key我们的例子是一个BirthGender实例,并且xhtml是单选按钮输入字段(加上第一个选项的隐藏字段)。有了这个知识,我们可以写一个帮手来生成NodeSeq我们想要的风格:

         
         
import scala.xml.NodeSeq
import net.liftweb.http.SHtml.ChoiceItem
object LabelStyle {
def htmlize [ T ]( item : ChoiceItem [ T ]) : NodeSeq =
< label class = "radio" >{ item . xhtml } { item . key . toString }</ label >
def toForm [ T ]( holder : ChoiceHolder [ T ]) : NodeSeq =
holder . items . flatMap ( htmlize )
}

htmlize方法<label>使用我们想要的类生成一个元素,它包含radio(item.xhtml)和label(item.key.toString的文本toForm正在申请的htmlize功能,所有的选择。

我们可以在我们的代码段中应用:

def render = ".options" #> LabelStyle.toForm(radio)

结果如下:

         
         
<label class= "radio" >
<input value= "F234668654428LWW305" type= "radio" name= "F234668654432WS5LWK" >
<input name= "F234668654432WS5LWK" type= "hidden" value= "F234668654431KYJB3S" >
</label>
<label class= "radio" >
<input value= "F234668654429MB5RF3" type= "radio" name= "F234668654432WS5LWK" >
</label>
<label class= "radio" >
<input value= "F234668654430YULGC1" type= "radio" name= "F234668654432WS5LWK" >
而不是说
</label>

toForm方法可以将选项包装在某些其他HTML中,例如<ul>但在这种情况下,它不是:它只适用htmlize于每个ChoiceItem因此,我们可以LabelStyle在我们的Lift应用程序中进行默认设置:

ChoiceHolder.htmlize = c => LabelStyle.htmlize(c)

这样做是因为toFormChoiceHolder推迟到ChoiceHolder.htmlize,并且ChoiceHolder.htmlize是可以分配给一个变量。

字符串值

如果要直接使用String选项的值,可以使用SHtml.radio虽然它也产生一个ChoiceHolder,它不同于radioElem它使用String与标签和值相同的。只有当用户选择了一个值时,才会调用与每个选项相关联的功能。

SHtml.radio我们的示例的一个版本将如下所示:


          
          
SHtml . radio (
"Male" :: "Female" :: "Rather Not Say" :: Nil ,
Empty ,
selected => logger . info ( "Choice: " + selected )
)

这是一个类似的结构radioElem:有一个选项列表,一个默认的,一个调用的函数,它产生一个ChoiceHolder[String]提交表单时,我们的函数将传递String所选项值。如果没有选择单选按钮,则不调用该功能。

有条件地禁用复选框

问题

您要根据条件检查disabled属性添加到a SHtml.checkbox

创建CSS选择器转换以添加禁用的属性,并将其应用于复选框转换。

例如,假设你有一个简单的复选框:

        
        
class Likes {
var likeTurtles = false
def render =
"#like" #> SHtml . checkbox ( likeTurtles , likeTurtles = _ )
}

和相应的模板:


         
         
<!DOCTYPE html>
<head>
<meta content= "text/html; charset=UTF-8" http-equiv= "content-type" />
<title> 禁用复选框 </title>
</head>
<body data-lift-content-id= "main" >
<div id= "main" data-lift= "surround?with=default;at=content" >
<div> 选择你喜欢的东西: </div>
<form data-lift= "Likes" >
<label for= "like" > 你喜欢乌龟吗 </label>
<input id= "like" type= "checkbox" >
</form>
</div>
</body>
</html>

此外,假设您要在大约50%的时间内禁用它。我们能做到这一点,通过调整NodeSeq从产生SHtml.checkbox

        
        
package code.snippet
import net.liftweb.util.Helpers._
import net.liftweb.util.PassThru
import net.liftweb.http.SHtml
class Likes {
var likesTurtles = false
def disable =
if ( math . random > 0.5d ) "* [disabled]" #> "disabled"
else PassThru
def render =
"#like" #> disable ( SHtml . checkbox ( likesTurtles , likesTurtles = _ ) )
}

当复选框呈现时,大约一半的时间将被禁用。

讨论

disable方法返回一个NodeSeq => NodeSeq函数,意思是当我们应用它时,我们需要给它一个 NodeSeq,这正是SHtml.checkbox提供的。

[disabled]CSS选择器一部分是选择禁用的属性,并将其替换为右侧的值,#>在该示例中为“禁用”。

这个组合意味着禁用属性将被设置在复选框的一半时间,而一半的时间NodeSeq 将PassThru不会改变复选框NodeSeq

也可以看看

“返回代码段标记不变”描述了该PassThru功能。

使用带多个选项的选择框

问题

您希望在选择框中显示多个选项,并允许用户选择多个值。

使用SHtml.multiSelect(options, default, selection)以下是用户最多可以选择三个选项的示例:

        
        
package code.snippet
import net.liftweb.util.Helpers._
import net.liftweb.http.SHtml
import net.liftweb.common.Loggable
class MultiSelect extends Loggable {
case class Item ( id : String , name : String )
val inventory =
Item ( "a" , "Coffee" ) ::
Item ( "b" , "Milk" ) ::
Item ( "c" , "Sugar" ) :: Nil
val options : List [( String String )] =
inventory . map ( i => ( i . id -> i . name ))
val default = inventory . head . id :: Nil
def render = {
def selection ( ids : List [ String ]) : Unit = {
logger . info ( "Selected: " + ids )
}
"#opts *" #>
SHtml . multiSelect ( options , default , selection )
}
}

在这个例子中,用户正在呈现一个三个列表,第一个被选中,如图3-3所示提交表单时,将selection调用函数,并显示所选选项值的列表。

从多个选项中进行选择
图3-3。从多个选项中进行选择

使用该代码段的模板可以是:

<div data-lift="MultiSelect?form=post">
  <p>我能得到什么?</p>
  <div id="opts">选项到这里</div>
   <input type="submit" value="Place Order">
</div>

这将呈现如下:

<form action="/" method="post"><div>
  <p>我能得到什么?</p>
  <div id="opts">
   <select name="F25749422319ALP1BW" multiple="true">
     <option value="a" selected="selected">咖啡</option>
     <option value="b">牛奶</option>
     <option value="c"></option>
   </select>
  </div>
  <input value="Place Order" type="submit">
</form>

讨论

回想一下,HTML选择由一组选项组成,每个选项都有一个值和一个名称。为了反映这一点,前面的例子是我们 inventory的对象,并把它变成一个被称为的字符串对的列表options

给定的函数SHtml.multiSelect将接收选项的值(ID),而不是名称。也就是说,如果您运行代码,并选择“咖啡”和“牛奶”,该功能将会看到List("a", "b")

选择无选项

请注意,如果未选择任何选项,则不会调用处理函数。这在问题1139中有描述

解决这个问题的一种方法是添加一个隐藏的功能来重置列表。例如,我们可以将以前的代码修改为状态片段,并记住我们选择的值

package code.snippet

import net.liftweb.util.Helpers._
import net.liftweb.http.{StatefulSnippet, SHtml}
import net.liftweb.common.Loggable

class MultiSelectStateful extends StatefulSnippet with Loggable {

  def dispatch = {
    case _ => render
  }

  case class Item(id: String, name: String)

  val inventory =
    Item("a", "Coffee") ::
    Item("b", "Milk") ::
    Item("c", "Sugar") :: Nil
  val options : List[(StringString)] =
    inventory.map(i => (i.id -> i.name))

  var current = inventory.head.id :: Nil

  def render = {

    def logSelected() =
      logger.info("Values selected: "+current)

    "#opts *" #> (
      SHtml.hidden( () => current = Nil) ++
      SHtml.multiSelect(options, current, current = _)
    ) &
    "type=submit" #> SHtml.onSubmitUnit(logSelected)

  }

}

模板未更改,代码段已修改为引入current值和隐藏函数以重置该值。我们已经绑定提交按钮,以便在提交表单时简单地记录所选值。

每次提交表单时current,ID列表将设置为您在浏览器中选择的任何内容。但是请注意,我们已经开始使用一个隐藏的功能重置current到空列表。这意味着如果接收函数multiSelect从未被调用,因为没有选择任何内容,所以存储的值current将反映出来Nil

这可能是有用的,这取决于您在应用程序中需要哪些行为。

类型安全选项

如果您不想String使用某个选项值,可以使用multiSelectObj在这个变体中,选项列表仍然提供一个文本名称,但是这个值就是一个类。同样,默认值列表将是一个类实例的列表。

对代码的唯一更改是为List[(Item,String)]选项生成一个,并使用Item默认值:

val options : List[(ItemString)] =
  inventory.map(i => (i -> i.name))

val default = inventory.head :: Nil

从这些数据生成多重选择的调用是类似的,但是注意我们的selection函数现在收到一个列表Item

def render = {

  def selection(items: List[Item]) : Unit = {
    logger.info("Selected: "+items)
  }

  "#opts *" #>
    SHtml.multiSelectObj(options, default, selection)
  }
枚举

您可以使用multiSelectObj枚举

package code.snippet

import net.liftweb.util.Helpers._
import net.liftweb.http.SHtml
import net.liftweb.common.Loggable

class MultiSelectEnum extends Loggable {

  object Item extends Enumeration {
    type Item = Value
    val Coffee, Milk, Sugar = Value
  }

  import Item._

  val options : List[(ItemString)] =
    Item.values.toList.map(i => (i -> i.toString))

  val default = Item.Coffee :: Nil

  def render = {

    def selection(items: List[Item]) : Unit = {
      logger.info("Selected: "+items)
    }

    "#opts *" #>
      SHtml.multiSelectObj(options, default, selection)
  }

}

枚举版本的工作方式与类型安全版本相同。

也可以看看

“Ajax Form Processing”中的“提交样式”讨论讨论了使用隐藏字段作为函数调用。

“选择选项更改时的呼叫服务器”介绍了如何在浏览器中更改选择时触发服务器端操作。

探索Lift第6章 “ Lift中的形式”讨论了多重选择和其他类型的形式元素。

上传文件

问题

您想要一个代码片段,允许用户将文件上传到Lift应用程序。

FileParamHolder在您的代码段中使用a ,并在提交表单时从中提取文件信息。

从表单开始,标有multipart=true

<html>
<head>
  <title>文件上传</title>
  <script id="jquery" src="/classpath/jquery.js" type="text/javascript">
  </script>
  <script id="json" src="/classpath/json.js" type="text/javascript"></script>
</head>
<body>
<form data-lift="FileUploadSnippet?form=post;multipart=true">
   <label for="file">
     选择一个文件:<input id="file"></input>
   </label>
   <input type="submit" value="Submit"></input>
</form>
</body>
</html>

我们将文件输入SHtml.fileUpload和提交按钮绑定到一个功能来处理上传的文件:

package code.snippet

import net.liftweb.util.Helpers._
import net.liftweb.http.SHtml._
import net.liftweb.http.FileParamHolder
import net.liftweb.common.{Loggable, Full, Empty, Box}


class FileUploadSnippet extends Loggable {

  def render = {

    var upload : Box[FileParamHolder] = Empty

    def processForm() = upload match {
      case Full(FileParamHolder(_, mimeType, fileName, file)) =>
        logger.info("%s of type %s is %d bytes long" format (
         fileName, mimeType, file.length) )

      case _ => logger.warn("No file?")
    }

    "#file" #> fileUpload(f => upload = Full(f)) &
      "type=submit" #> onSubmitUnit(processForm)
  }
}

fileUpload绑定确保将文件分配给该upload变量。这允许我们在提交表单时访问方法Array[Byte]中的文件processForm

讨论

HTTP包括一种multipart/form-data用于支持二进制数据上传的编码类型?form=post;multipart=true模板中的参数标记与此编码形式,并且生成的HTML将看起来像这样

<form enctype="multipart/form-data" method="post" action="/fileupload">

当浏览器提交表单时,Lift会检测到multipart/form-data编码并从请求中提取任何文件。这些都可以作为uploadedFiles一个上Req对象,例如:

val files : List[FileParamHolder] = S.request.map(_.uploadedFiles) openOr Nil

然而,当我们处理一个带有单个上载字段的表单时SHtml.fileUpload,将输入绑定到我们的upload变量上更容易使用f => upload = Full(f)当选择文件并通过此字段上传文件时,Lift安排功能被调用。如果文件为零长度,则不调用该函数。

Lift的默认行为是将文件读入内存并将其呈现为FileParamHolder在这个食谱中,我们是在字段上的模式匹配,FileParamHolder并简单地打印出我们对文件的了解。我们忽略第一个参数,它将是Lift为该字段生成的名称,但是捕获mime类型,原始文件名和文件中的原始数据。

您可能不想将此方法用于非常大的文件。其实,LiftRules提供了一些您可以控制的大小限制:

LiftRules.maxMimeFileSize
上传的任何单个文件的最大大小(默认为7 MB)
LiftRules.maxMimeSize
多部分上传的最大大小(默认为8 MB)

为什么要两个设置?因为表单提交时,表单上可能会有多个字段。例如,在配方中,提交按钮的值作为其中一个部分发送,并且文件作为另一个发送。因此,您可能希望限制文件大小,但允许提交一些字段值或多个文件。

如果您达到大小限制,将从底层文件上传库中抛出异常。您可以捕获异常,如“Catch Any Exception”中所述

LiftRules.exceptionHandler.prepend {
  case (_, _, x : FileUploadIOException) =>
    ResponseWithReason(BadResponse(), "Unable to process file. Too large?")
}

请注意,容器(Jetty,Tomcat)或任何Web服务器(Apache,Nginx)也可能对文件上传大小有限制。

在某些情况下将文件上传到内存中可能会很好,但您可能希望将较大的项目上传到磁盘,然后将其作为流加载到“Lift”中。Lift通过以下设置支持此功能:

LiftRules.handleMimeFile = OnDiskFileParamHolder.apply

handleMimeFile变量期望被赋予一个函数,它接收一个字段名称,mime类型,文件名,InputStream并返回a FileParamHolder这个默认的实现是InMemFileParamHolder,但是改为OnDiskFileParamHolder意味着Lift会将文件首先写入磁盘。您当然可以实现自己的处理程序,除了使用OnDiskFileParamHolderInMemFileParamHolder

随着OnDiskFileParamHolder文件将被写入一个临时位置(System.getProperty("java.io.tmpdir")),但是当你完成这个文件后,你可以把它删除。例如,我们的代码段可能会更改为:

def processForm() = upload match {

  case Full(content : OnDiskFileParamHolder) =>
    logger.info("File: "+content.localFile.getAbsolutePath)
    val in: InputStream = content.fileStream
    // ...do something with the stream here...
    val wasDeleted_? = content.localFile.delete()

  case _ => logger.warn("No file?")
}

注意OnDiskFileParamHolder实现FileParamHolder,所以将匹配FileParamHolder配方中使用的原始模式。但是,如果您访问该file字段OnDiskFileParamHolder,则会将该文件导入内存,从而将其存储在磁盘上将其处理为流。

如果要监视服务器端上传的进度,可以。有一个挂钩LiftRules,被称为上传正在运行:

def progressPrinter(bytesRead: Long, contentLength: Long, fieldIndex: Int) {
  println("Read %d of %d for %d" format (bytesRead, contentLength, fieldIndex))
}

LiftRules.progressListener = progressPrinter

这是整个多部分上传的进度,而不仅仅是上传的文件。特别地,contentLength可能不知道(在这种情况下,它将是-1),但是如果知道的话,它是完整的多部分上传的大小。在这个食谱的例子中,这将包括文件的大小,还包括提交按钮的值。这也解释了fieldIndex哪个是正在处理哪个部分的计数器。在这个例子中,这两个部分的值为0和1。

也可以看看

HTTP文件上传机制在RFC 1867 “基于表单的文件上传”中有所描述

“在REST服务中接受二进制数据”讨论了在REST服务的上下文中的文件上传。

有关通过与JavaScript库集成的Ajax文件上传示例,请参见“Ajax文件上传”,提供进度指示器和拖放支持。

带布局的干燥表单

问题

您想要使用DRY声明式表单,就像LiftScreen您希望能够完全控制表单的呈现方式,而不是字段的线性布局。“LiftScreen”

使用CssBoundLiftScreen字段绑定用于命名展示位置,并可选择覆盖特定单个字段元素的布局。

以下是一个示例代码段:

package code.snippet

import scala.xml.NodeSeq
import net.liftweb.http._
import net.liftweb.http.FieldBinding.Self

object AccountInfoEditor extends CssBoundLiftScreen {
  val formName = "accountedit"

  override def allTemplate = savedDefaultXml
  protected def defaultAllTemplate = super.allTemplate

  // Pull the definition of the "normal" field from a template, if it exists
  override def defaultFieldNodeSeq: NodeSeq =
    Templates("accounts" :: "account_edit_field" :: Nil).openOr(
    <div>
      <label class="label field"></label>
      <span class="value fieldValue"></span>
      <span class="help"></span>
      <div class="errors">
        <div class="error"></div>
      </div>
    </div>)

  override def finish() {
    println("Account Edited for: "+firstName)
    S.notice("Done.")
  }

  // An example source of an account:
  case class Account(firstName: String, lastName: String, address: String)
  def accountToEdit = new Account("Ada", "Lovelace", "Ockham Park, Surrey")

  // Fields:
  val firstName = field("First Name", accountToEdit.firstName,
    trim, valMinLen(1, "First name is required"),
    FieldBinding("firstName"))

  val lastName = field("Last Name", accountToEdit.lastName,
    trim, valMinLen(1, "Last name is required"),
    FieldBinding("lastName"))

  val address = textarea("Address", accountToEdit.address,
    trim, valMinLen(1, "Address is required"), valMaxLen(255, "Address too long"),
    FieldBinding("address", Self))

}

该片段将呈现(假设)用户帐户记录,允许用户编辑三个字段。这些字段具有验证,并且将被绑定到模板中。

相应的模板可能称为webapp / accountinfo.html

<!DOCTYPE html>
<head>
  <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
  <title>自定义CSS绑定屏幕</title>
</head>
<body data-lift-content-id="main">
<div id="main" data-lift="surround?with=default;at=content">

  <div data-lift="AccountInfoEditor">

    <div>
      <!--
      Drop regular Lift Screen elements where you want them.
      Here we're putting the Next (or Finish) button at the top.
      -->
      <button class="next"></button>
    </div>

    <div class="fields">

      <h2>帐户详细资料</h2>

      <!--
        The template applied to each field is taken from
        accounts/account_edit_field.html
      -->
      <div id="accountedit_firstName_field" class="large"></div>
      <div id="accountedit_lastName_field" class="large"></div>

      <!--
      The code binds this field with Self, meaning the
      field template is inline in this template and will
      be different:
      -->
      <div id="accountedit_address_field">
        <div class="large">你喜欢送礼物的地址:</div>
        <div class="errors">
          <div class="error"></div>
        </div>
        <span class="value fieldValue" style="width:10em; height:5em"></span>
      </div>

    </div>

  </div>

</div>

</body>
</html>

运行代码段将导致CssBoundLiftScreendefaultFieldNodeSeq布局应用于每个字段,但我们在模板中定制的“地址”字段除外。

按“完成”按钮可触发验证,显示错误(如果有),或通过该finish方法完成编辑

讨论

CssBoundLiftScreen使用相同的模型(默认情况下)LiftScreen,但使用CSS类来标识默认模板中的元素(例如, wizard-all.html)。这种强大的机制消除了代码和HTML中的重复; 然而,LiftScreen牺牲灵活性,因为没有办法制作高度定制的表格。

CssBoundLiftScreen通过允许您控制表单呈现的每个元素,可以删除此限制。您可以将所有表单字段布局控制到单个字段的布局。您可以在代码段使用网站中嵌入自定义模板:

<div data-lift="AccountInfoEditor">
   <!-- template for Screen goes here -->
</div>

并提供一些额外的绑定提示。该形式仍然非常小,而完全由设计师控制。

解决方案部分中的示例演示了最重要的方面。特别地,您应该注意,您仍然必须为模板提供源,并且您可以为模板的子部分指定替代方法。在该示例中,我们允许HTML设计器直接访问此屏幕的字段模板,方法是将布局放在自定义模板文件accounts / account_edit_field.html中

其余的很容易。您必须formName为Scala 提供声明 CssBoundLiftScreen,并FieldBinding为每个字段提供一个参数。结合内部函数(可以覆盖),将生成可以在模板中使用的唯一(但是已知的)名称。默认模式为:“formName_fieldName_field”。所以,如果你将表单命名为“myform”,并将一个字段绑定到“address”,那么你的HTML模板应该包含一个ID为“myform_address_field”的div。这就是使用您的字段模板的正常绑定所需要的。

要调整特定字段的字段布局,可以Self在Scala字段绑定中指定这表明该特定字段的模板应来自字段的div。

如果你想获得更多的爱好者,还有其他字段绑定,例如 Dynamic(() => NodeSeq),使用提供的函数在每次渲染时为该字段生成一个模板。

也可以看看

更多的例子可以在:

LiftScreen描述在Lift WikiSimply Lift

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值