【问题标题】:Nested objects without generics support: workarounds没有泛型支持的嵌套对象:解决方法
【发布时间】:2012-08-07 20:52:12
【问题描述】:

由于 Play 模板层缺乏泛型支持,遇到了 catch 22 的情况。

我有几个购物车屏幕都需要一个用户和一个付款+可选的自定义字段。

case class Conference(
  user: User,
  payment: Payment
  ... custom fields here
)

因此,我没有按照购物车模型复制所有用户和支付表单字段,而是按上述方式进行合并并实现嵌套表单。

现在,问题出现在模板层,到目前为止还没有泛型支持。

父/容器表单像这样拉入嵌套的子表单:

@(_form: Form[Conference])

@user.nested( UserForm.form.fill(_form.get.user) )
@payment.nested( PaymentForm.form.fill(_form.get.payment) )

然后子用户表单看起来像:

@(_form: Form[User])

@inputText(_form("user.firstName"), '_label-> "First Name*", 'class-> "required")
@inputText(_form("user.lastName"), '_label-> "Last Name*", 'class-> "required")
...

和用户模型:

case class User(firstName: String, lastName: String ...)

当用户模型中没有用户属性时,如何访问“user.firstName”、“user.lastName”等? Play Form 应用方法是:

def apply(key: String): Field = Field(
    this,
    key,
    constraints.get(key).getOrElse(Nil),
    formats.get(key),
    errors.collect { case e if e.key == key => e },
    data.get(key))

基本上它会寻找属性data.user.firstName,这显然是行不通的。

我考虑过向用户模型添加用户属性:

case class User(firstName: String, lastName: String ...) {
  val user: User
}

但不确定这是否会起作用和/或对案例类伴随对象应用/取消应用造成严重破坏。

无论如何,鉴于缺乏泛型,有什么可行的解决方案?

如果支持泛型,我们可以传入一个上限,一切都会好起来的:

trait CartOrder {
  user: User,
  payment: Payment
}
case class Conference(...) extends CartOrder

然后嵌套的用户表单被传递一个包含用户属性的实例,我们很好

@[T <: CartOrder](_form: Form[T])
@inputText(_form("user.firstName"), '_label-> "First Name*", 'class-> "required")
...

【问题讨论】:

  • 我不确定我是否得到您的问题回复:泛型。那你不能把它参数化为 Form[CartOrder] 吗?顺便说一句,关于用户问题。您可以做的一件事是将您的用户映射与用户表单分开。然后,您可以在会议表单中重用映射,然后在用户表单中使用 userMapping.withPrefix("user") 以使访问密钥保持一致。
  • 对,我实际上一直在努力这样做;但是它不像指定 Form[CartOrder] 那样简单,会报错:“class play.api.data.Form 在类型 T 中是不变的”。所以编译器接受 Conference 派生自 CartOrder,但 Play 定义了 Form[T] 而不是 Form[+T]
  • 哦,好的。我不知道它是不变的。我可以向您建议的另一件事是,拥有一个 CartOrder 类型的表单/映射,然后对所有可选字段使用可选映射,然后在主映射函数中返回适当的具体类型。虽然不是特别优雅
  • 我稍后会发布我的解决方案,基本上你必须传入超级类型的会议、订阅等购物车模型,我称之为 CartModel 或 CartOrder,等等。这个超类型本身必须包含用户和支付的表单映射。这还不错,但没有直接 T <: supertype>

标签: forms scala generics playframework


【解决方案1】:

如果类型安全不是问题(Forms 并不是所有类型都安全的),您可以使用 Form[_] 作为嵌套模板的参数类型。


如果您确实想要类型安全,一种选择是为 Form 创建一个协变的包装类,并使用它来代替 Form。一种实现是:

package views.html

import play.api.data._
import play.api.libs.json.JsValue

object FormView {
    implicit def formToFormView[A, T >: A](form: Form[A]): FormView[T] = new FormView[T] {
        type F = A
        def realForm = form
    }
}

trait FormView[+T] {
    type F <: T

    def realForm: Form[F]

    def apply(key: String): Field = realForm(key)

    def constraints : Map[String, Seq[(String, Seq[Any])]] = realForm.constraints

    def data: Map[String, String] = realForm.data

    def error(key: String): Option[FormError] = realForm.error(key)

    def errors(key: String): Seq[FormError] = realForm.errors(key)

    def errors: Seq[FormError] = realForm.errors

    def errorsAsJson: JsValue = realForm.errorsAsJson

    def get: T = realForm.get

    def formats: Map[String, (String, Seq[Any])] = realForm.formats

    def globalError: Option[FormError] = realForm.globalError

    def globalErrors: Seq[FormError] = realForm.globalErrors

    def hasErrors: Boolean = realForm.hasErrors

    def hasGlobalErrors: Boolean = realForm.hasGlobalErrors

    override def hashCode: Int = realForm.hashCode

    def mapping: Mapping[F] = realForm.mapping

    def value: Option[T] = realForm.value
}

现在不是你的模板了

@(_form: Form[CartOrder])

由于不变性而不起作用,您可以使用

@(_form: FormView[CartOrder])

您可以简单地传入任何Form[T],其中TCartOrder 的子类型,例如

@user.nested(_form)

implicits 将处理从 Form 到 FormView 的转换

可以在以下位置找到完整示例:https://github.com/thatsmydoing/formtest

【讨论】:

  • +1 用于广告素材封装方法。很好奇,Play 的 Form 实现在什么方面不是类型安全的? Form[_] 显然把类型安全抛到了窗外,所以不要走那条路。
  • 无论你给form.apply()什么键,它都会给你一个Field,即使这个键不存在。它对模板很有用,因为您可以只使用 Form[_] 并且它仍然可以工作。此外,如果您有 Forms 返回相同的类型,但有不同的键,它们不完全是“相同”的类型。
【解决方案2】:

好的,dilly-o 就这样跟随(如果你有更好的方法,请加入):

play.api.data.Form[T] 是不变的,所以没有骰子将会议的超类型(即 CartOrder)传递给用户表单。换句话说,这会爆炸:

// user.scala.html
@(_form: Form[CartOrder])

基本上你必须传入一个本身是 Form 可映射的实例。

为了解决模板层 fun house 的问题,我实施了以下 hack:

case class CartModel(user: User, payment: Payment)

编辑
变得更好,在下面的 CartForm 映射器中添加了 bind 帮助器,这使得视图中的语法更加简洁

object CartForm {
  import play.api.data.Forms._  
  val form = 
    Form( mapping(
      'user -> UserForm.mapper,
      'payment -> PaymentForm.mapper )(CartModel.apply)(CartModel.unapply) )

  // hey, generics!
  def bind[T <: Form[_ <: CartContract]](t: T) =
    t.value map{x=> form.fill( CartModel(x.user, x.payment) )} getOrElse form
}

然后在会议父表单中,拉入用户表单字段,如下所示:

@user.nested( CartForm.bind(_form) )

然后用户表单会收到:

@(_form: Form[CartModel])

一般来说,嵌套表单消除了很多样板,因此总体而言,进步。不依赖中间表单映射器会很棒,但这是我现在能想到的最好的......

【讨论】:

    【解决方案3】:

    猜你喜欢

    @(_form: Form[_ <: CartOrder])
    

    而不是建议

    @[T <: CartOrder](_form: Form[T])
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-07-09
      • 2017-10-19
      • 1970-01-01
      • 2015-03-22
      • 1970-01-01
      • 2013-12-28
      • 2010-10-18
      • 1970-01-01
      相关资源
      最近更新 更多