diff --git a/core-play28/build.sbt b/core-play28/build.sbt
index cfda0c9..fd8fb89 100644
--- a/core-play28/build.sbt
+++ b/core-play28/build.sbt
@@ -1,12 +1,12 @@
-import scalariform.formatter.preferences._
+//import scalariform.formatter.preferences._
name := """play-bootstrap-core"""
-version := "1.6.1-P28"
+version := "1.7.0-P28"
-scalaVersion := "2.13.2"
+scalaVersion := "2.13.17"
-crossScalaVersions := Seq("2.13.2", "2.12.11")
+crossScalaVersions := Seq("2.13.17", "3.3.6")
resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
@@ -17,13 +17,12 @@ libraryDependencies := libraryDependencies.value.filterNot(m => m.name == "twirl
playCore % "provided"
)
-scalariformPreferences := scalariformPreferences.value
- .setPreference(AlignSingleLineCaseStatements, true)
- .setPreference(DoubleIndentConstructorArguments, true)
- .setPreference(DanglingCloseParenthesis, Preserve)
+//scalariformPreferences := scalariformPreferences.value
+// .setPreference(AlignSingleLineCaseStatements, true)
+// .setPreference(DoubleIndentConstructorArguments, true)
+// .setPreference(DanglingCloseParenthesis, Preserve)
-
-PlayKeys.playOmnidoc := false
+//PlayKeys.playOmnidoc := false
//*******************************
// Maven settings
@@ -61,6 +60,6 @@ pomIncludeRepository := { _ => false }
credentials += Credentials(Path.userHome / ".sbt" / "sonatype.credentials")
publishConfiguration := publishConfiguration.value.withOverwrite(isSnapshot.value)
-com.typesafe.sbt.pgp.PgpKeys.publishSignedConfiguration := com.typesafe.sbt.pgp.PgpKeys.publishSignedConfiguration.value.withOverwrite(isSnapshot.value)
+PgpKeys.publishSignedConfiguration := PgpKeys.publishSignedConfiguration.value.withOverwrite(isSnapshot.value)
publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(isSnapshot.value)
-com.typesafe.sbt.pgp.PgpKeys.publishLocalSignedConfiguration := com.typesafe.sbt.pgp.PgpKeys.publishLocalSignedConfiguration.value.withOverwrite(isSnapshot.value)
+PgpKeys.publishLocalSignedConfiguration := PgpKeys.publishLocalSignedConfiguration.value.withOverwrite(isSnapshot.value)
diff --git a/core-play28/project/build.properties b/core-play28/project/build.properties
index 797e7cc..01a16ed 100644
--- a/core-play28/project/build.properties
+++ b/core-play28/project/build.properties
@@ -1 +1 @@
-sbt.version=1.3.10
+sbt.version=1.11.7
diff --git a/core-play28/project/plugins.sbt b/core-play28/project/plugins.sbt
index a93a76e..494ddf3 100644
--- a/core-play28/project/plugins.sbt
+++ b/core-play28/project/plugins.sbt
@@ -1,10 +1,10 @@
// The Play plugin
-addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.1")
+addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.7")
// web plugins
-addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2")
+//addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.2")
-addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.4")
+addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2")
-addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
+addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
diff --git a/core-play30/app/views/bs/Args.scala b/core-play30/app/views/bs/Args.scala
new file mode 100644
index 0000000..8378dce
--- /dev/null
+++ b/core-play30/app/views/bs/Args.scala
@@ -0,0 +1,86 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.bs
+
+object Args {
+
+ import play.api.i18n.MessagesProvider
+ import play.api.templates.PlayMagic.translate
+
+ /** Adds some default arguments to the parameter 'args'
+ */
+ def withDefault(args: Seq[(Symbol, Any)], default: (Symbol, Any)*) = default ++: args
+
+ /** Adds a string value to a selected arg. For example, to add "form-control" to 'class, even if there is already
+ * other extra class
+ */
+ def withAddingStringValue(args: Seq[(Symbol, Any)], arg: Symbol, value: String): Seq[(Symbol, Any)] = {
+ val (withArg, withoutArg) = args.partition(_._1 == arg)
+ (arg, withArg.headOption.map(v => s"$value ${v._2.toString}").getOrElse(value)) +: withoutArg
+ }
+ def withAddingStringValue(args: Seq[(Symbol, Any)], arg: Symbol, maybeValue: Option[String]): Seq[(Symbol, Any)] =
+ maybeValue.map(value => withAddingStringValue(args, arg, value)).getOrElse(args)
+
+ /** Removes those arguments which its value is None. It lets you omit those arguments that satisfy some criteria. It
+ * also lets you add some default arguments to the parameter 'args'.
+ */
+ def withoutNones(args: Seq[(Symbol, Any)], default: (Symbol, Any)*) = (default ++: args).filter(_._2 != None)
+
+ /** Returns only the inner arguments (those that are exclusive for an input and not for the field constructor).
+ * Removes every argument with a prefixed underscore (_) and those whose value is false. It also lets you add some
+ * default arguments to the parameter 'args'.
+ */
+ def inner(args: Seq[(Symbol, Any)], default: (Symbol, Any)*) =
+ (default ++: args).filter(arg => !arg._1.name.startsWith("_") && arg._2 != false)
+
+ /** Gets the value for the selected key
+ */
+ def get(args: Seq[(Symbol, Any)], key: Symbol) = args.find(_._1 == key).map(_._2)
+
+ /** Removes those arguments with these keys
+ */
+ def remove(args: Seq[(Symbol, Any)], keys: Symbol*) = args.filter(arg => !keys.contains(arg._1))
+
+ /** Returns true only if exists a pair with that key and its value is true.
+ */
+ def isTrue(args: Seq[(Symbol, Any)], key: Symbol) = args.exists(_ == (key, true))
+
+ /** Localizes an argument
+ */
+ def msg(arg: (Symbol, Any))(implicit msgsProv: MessagesProvider): (Symbol, Any) =
+ (arg._1, translate(arg._2)(msgsProv))
+
+ /** Localizes a value
+ */
+ def msg(a: Any)(implicit msgsProv: MessagesProvider): Any = translate(a)(msgsProv)
+}
+
+object ArgsMap {
+
+ /** Adds a string value to a selected arg. For example, to add "form-control" to 'class, even if there is already
+ * other extra class
+ */
+ def withAddingStringValue(argsMap: Map[Symbol, Any], arg: Symbol, value: String): Map[Symbol, Any] = {
+ val newValue = argsMap.get(arg).map(v => s"$value ${v.toString}").getOrElse(value)
+ argsMap + (arg -> newValue)
+ }
+ def withAddingStringValue(argsMap: Map[Symbol, Any], arg: Symbol, maybeValue: Option[String]): Map[Symbol, Any] =
+ maybeValue.map(value => withAddingStringValue(argsMap, arg, value)).getOrElse(argsMap)
+
+ /** Returns true only if the map contains an argument with that key and its value is true.
+ */
+ def isTrue(argsMap: Map[Symbol, Any], key: Symbol) = argsMap.get(key).map(_ == true).getOrElse(false)
+
+ /** Returns true only if the map contains an argument with that key and its value is any value but false.
+ */
+ def isNotFalse(argsMap: Map[Symbol, Any], key: Symbol) = argsMap.get(key).map(_ != false).getOrElse(false)
+}
diff --git a/core-play30/app/views/bs/package.scala b/core-play30/app/views/bs/package.scala
new file mode 100644
index 0000000..884953f
--- /dev/null
+++ b/core-play30/app/views/bs/package.scala
@@ -0,0 +1,239 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html
+
+package object bs {
+
+ import play.api.data.{Field, FormError}
+ import play.twirl.api.Html
+ import play.api.i18n.MessagesProvider
+ import play.api.templates.PlayMagic.translate
+ import bs.ArgsMap.isTrue
+
+ /** Class with relevant variables for a field to pass it to the helper and field constructor
+ * - args: list of available arguments for the helper and field constructor
+ */
+ class BSFieldInfo(field: Field, args: Seq[(Symbol, Any)], val msgsProv: MessagesProvider) {
+
+ /* A map with the args to work easily with them */
+ val argsMap: Map[Symbol, Any] = Args.withoutNones(args).toMap
+
+ /* Id of the input */
+ val id: String = argsMap.get(Symbol("id")).map(_.toString).getOrElse(field.id)
+
+ /* Id of the form-group */
+ val idFormField: String = argsMap.get(Symbol("_id")).map(_.toString).getOrElse(id + "_field")
+
+ /* The optional label */
+ val labelOpt: Option[Any] = argsMap.get(Symbol("_label")).orElse(argsMap.get(Symbol("_hiddenLabel")))
+
+ /* Indicates if the label must be hidden */
+ val hideLabel: Boolean = isTrue(argsMap, Symbol("_hideLabel")) || argsMap.contains(Symbol("_hiddenLabel"))
+
+ /* Name of the input */
+ def name: String = field.name
+
+ /* Value of the input */
+ val value: Option[String] = field.value.orElse(argsMap.get(Symbol("value")).map(_.toString))
+
+ /* List with every error and its corresponding ARIA id. Ex: ("foo_error_0" -> "foo error") */
+ val errors: Seq[(String, Any)] = BSFieldInfo.errors(Some(field), argsMap, msgsProv).zipWithIndex.map {
+ case (error, i) => (id + "_error_" + i, error)
+ }
+
+ /* Indicates if there is any error */
+ val hasErrors: Boolean = errors.nonEmpty || ArgsMap.isNotFalse(argsMap, Symbol("_error"))
+
+ /* The optional validation state ("success", "warning" or "error") */
+ lazy val status: Option[String] = BSFieldInfo.status(hasErrors, argsMap)
+
+ }
+
+ /** Companion object for class BSFieldInfo
+ */
+ object BSFieldInfo {
+
+ def apply(field: Field, args: Seq[(Symbol, Any)], msgsProv: MessagesProvider): BSFieldInfo = {
+ new BSFieldInfo(field, args, msgsProv)
+ }
+
+ /* List with every error */
+ def errors(maybeField: Option[Field], argsMap: Map[Symbol, Any], msgsProv: MessagesProvider): Seq[Any] = {
+ argsMap
+ .get(Symbol("_error"))
+ .filter(!_.isInstanceOf[Boolean])
+ .map {
+ case Some(FormError(_, message, args)) =>
+ Seq(msgsProv.messages(message, args.map(a => translate(a)(msgsProv)): _*))
+ case FormError(_, message, args) =>
+ Seq(msgsProv.messages(message, args.map(a => translate(a)(msgsProv)): _*))
+ case message => Seq(translate(message)(msgsProv))
+ }
+ .getOrElse {
+ maybeField
+ .filter(_ => !argsMap.get(Symbol("_showErrors")).contains(false))
+ .map { field =>
+ field.errors.map { e => msgsProv.messages(e.message, e.args.map(a => translate(a)(msgsProv)): _*) }
+ }
+ .getOrElse(Nil)
+ }
+ }
+
+ /* List with every "feedback info" except "errors" */
+ def feedbackInfosButErrors(argsMap: Map[Symbol, Any], msgsProv: MessagesProvider): Seq[Any] = {
+ argsMap
+ .get(Symbol("_warning"))
+ .filter(!_.isInstanceOf[Boolean])
+ .map(m => Seq(translate(m)(msgsProv)))
+ .getOrElse(
+ argsMap
+ .get(Symbol("_success"))
+ .filter(!_.isInstanceOf[Boolean])
+ .map(m => Seq(translate(m)(msgsProv)))
+ .getOrElse(Nil)
+ )
+ }
+
+ /* List with every "help info", i.e. a help text or constraints */
+ def helpInfos(maybeField: Option[Field], argsMap: Map[Symbol, Any], msgsProv: MessagesProvider): Seq[Any] = {
+ argsMap.get(Symbol("_help")).map(m => Seq(translate(m)(msgsProv))).getOrElse {
+ maybeField
+ .filter(_ => argsMap.get(Symbol("_showConstraints")).contains(true))
+ .map { field =>
+ field.constraints.map(c =>
+ msgsProv.messages(c._1, c._2.map(a => translate(a)(msgsProv)): _*)
+ ) ++ field.format.map(f => msgsProv.messages(f._1, f._2.map(a => translate(a)(msgsProv)): _*))
+ }
+ .getOrElse(Nil)
+ }
+ }
+
+ /* The optional validation state ("success", "warning" or "error") */
+ def status(hasErrors: Boolean, argsMap: Map[Symbol, Any]): Option[String] = {
+ if (hasErrors)
+ Some("error")
+ else if (ArgsMap.isNotFalse(argsMap, Symbol("_warning")))
+ Some("warning")
+ else if (ArgsMap.isNotFalse(argsMap, Symbol("_success")))
+ Some("success")
+ else
+ None
+ }
+
+ /* Generates automatically the input attributes for the constraints of a field */
+ def constraintsArgs(field: Field, msgsProv: MessagesProvider): Seq[(Symbol, Any)] = field.constraints.flatMap {
+ case ("constraint.required", params) => Some(Symbol("required") -> true)
+ case ("constraint.min", params: Seq[Any]) => Some(Symbol("min") -> msgsProv.messages(params.head.toString))
+ case ("constraint.max", params: Seq[Any]) => Some(Symbol("max") -> msgsProv.messages(params.head.toString))
+ case ("constraint.minLength", params: Seq[Any]) =>
+ Some(Symbol("minlength") -> msgsProv.messages(params.head.toString))
+ case ("constraint.maxLength", params: Seq[Any]) =>
+ Some(Symbol("maxlength") -> msgsProv.messages(params.head.toString))
+ case ("constraint.pattern", params: Seq[Any]) =>
+ params.head match {
+ case str: String => Some(Symbol("pattern") -> msgsProv.messages(str))
+ case func: Function0[_] =>
+ Some(
+ Symbol("pattern") -> msgsProv.messages(func.asInstanceOf[() => scala.util.matching.Regex]().toString)
+ )
+ case _ => None
+ }
+ case _ => None
+ }
+ }
+
+ /** Class with relevant variables for the global information of a multifield
+ * - fields: list of Fields
+ * - globalArguments: list of available arguments for the global helper
+ * - fieldsArguments: list of available arguments for every specific field
+ */
+ class BSMultifieldInfo(
+ fields: Seq[Field],
+ globalArguments: Seq[(Symbol, Any)],
+ fieldsArguments: Seq[(Symbol, Any)],
+ val msgsProv: MessagesProvider
+ ) {
+
+ /* A map with the args to work easily with them. The '_help is removed because the helper freeFormFieldormField will add it */
+ val argsMap: Map[Symbol, Any] = Args.withoutNones(fieldsArguments ++ globalArguments).toMap
+
+ /* List with every error */
+ val errors: Seq[Any] = {
+ val globalErrors = BSFieldInfo.errors(None, argsMap, msgsProv)
+ if (globalErrors.nonEmpty)
+ globalErrors
+ else
+ fields.flatMap { field =>
+ BSFieldInfo.errors(Some(field), argsMap, msgsProv)
+ }
+ }
+
+ /* Indicates if there is any error */
+ val hasErrors: Boolean = errors.nonEmpty || ArgsMap.isNotFalse(argsMap, Symbol("_error"))
+
+ /* The optional validation state ("success", "warning" or "error") */
+ lazy val status: Option[String] = BSFieldInfo.status(hasErrors, argsMap)
+
+ lazy val globalArgs: Seq[(Symbol, Any)] = globalArguments
+
+ lazy val fieldsArgs: Seq[(Symbol, Any)] = fieldsArguments
+ }
+
+ /** Companion object for class BSMultifieldInfo
+ */
+ object BSMultifieldInfo {
+ def apply(
+ fields: Seq[Field],
+ globalArguments: Seq[(Symbol, Any)],
+ fieldsArguments: Seq[(Symbol, Any)],
+ msgsProv: MessagesProvider
+ ): BSMultifieldInfo = {
+ new BSMultifieldInfo(fields, globalArguments, fieldsArguments, msgsProv)
+ }
+ }
+
+ /** Custom BSFieldConstructor for the library. Every BSFieldConstructor must extend this functionality.
+ */
+ trait BSFieldConstructor[F <: BSFieldInfo] {
+ /* Renders the corresponding template of the field constructor */
+ def apply(fieldInfo: F, inputHtml: Html)(implicit msgsProv: MessagesProvider): Html
+ /* Renders the corresponding template of a fake field constructor (i.e. with the same structure but without the field) */
+ def apply(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider): Html
+ }
+
+ /** Renders an input field with its corresponding wrapper using the BSFieldConstructor.
+ * - fieldInfo: a BSFieldInfo with all the information about the field.
+ * - inputDef: function that returns a Html from the BSFieldInfo.
+ */
+ def inputFormField[F <: BSFieldInfo](fieldInfo: F)(inputDef: F => Html)(implicit fc: BSFieldConstructor[F]): Html =
+ fc(fieldInfo, inputDef(fieldInfo))(fieldInfo.msgsProv)
+
+ /** Renders a fake field constructor using the BSFieldConstructor.
+ * - args: list of available arguments for the helper and the form-group
+ * - contentDef: function that returns a Html from a map of arguments
+ */
+ def freeFormField[F <: BSFieldInfo](
+ args: Seq[(Symbol, Any)]
+ )(contentDef: Map[Symbol, Any] => Html)(implicit fc: BSFieldConstructor[F], msgsProv: MessagesProvider): Html = {
+ val argsWithoutNones = Args.withoutNones(args)
+ fc(contentDef(Args.inner(argsWithoutNones).toMap), argsWithoutNones.toMap)(msgsProv)
+ }
+
+ /** Renders a multi-field constructor using the BSFieldConstructor.
+ * - fieldInfo: a BSMultifieldInfo with all the information about the fields.
+ * - contentDef: function that returns a Html from the BSMultifieldInfo
+ */
+ def multifieldFormField[F <: BSFieldInfo, M <: BSMultifieldInfo](multifieldInfo: M)(contentDef: M => Html)(implicit
+ fc: BSFieldConstructor[F]
+ ): Html =
+ freeFormField(multifieldInfo.globalArgs)(_ => contentDef(multifieldInfo))(fc, multifieldInfo.msgsProv)
+}
diff --git a/core-play30/build.sbt b/core-play30/build.sbt
new file mode 100644
index 0000000..4cca725
--- /dev/null
+++ b/core-play30/build.sbt
@@ -0,0 +1,60 @@
+//import scalariform.formatter.preferences._
+
+name := """play-bootstrap-core"""
+
+version := "1.7.0-P30"
+
+scalaVersion := "2.13.17"
+
+crossScalaVersions := Seq("2.13.17", "3.3.6")
+
+resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
+
+
+lazy val root = (project in file(".")).enablePlugins(PlayScala).disablePlugins(PlayFilters, PlayLogback, PlayPekkoHttpServer)
+
+libraryDependencies := libraryDependencies.value.filterNot(m => m.name == "twirl-api" || m.name == "play-server") ++ Seq(
+ playCore % "provided"
+)
+
+scalafmtOnCompile := true
+
+//*******************************
+// Maven settings
+//*******************************
+
+sonatypeProfileName := "com.adrianhurt"
+
+publishMavenStyle := true
+
+organization := "com.adrianhurt"
+
+description := "This is a collection of input helpers and field constructors for Play Framework to render Bootstrap HTML code."
+
+import xerial.sbt.Sonatype.*
+sonatypeProjectHosting := Some(GitHubHosting("playframework", "play-bootstrap", "contact@playframework.com"))
+
+homepage := Some(url("http://playframework.github.io/play-bootstrap"))
+
+licenses := Seq("Apache License" -> url("https://github.com/playframework/play-bootstrap/blob/master/LICENSE"))
+
+startYear := Some(2014)
+
+publishTo := {
+ val nexus = "https://oss.sonatype.org/"
+ if (isSnapshot.value)
+ Some("snapshots" at nexus + "content/repositories/snapshots")
+ else
+ Some("releases" at nexus + "service/local/staging/deploy/maven2")
+}
+
+Test / publishArtifact := false
+
+pomIncludeRepository := { _ => false }
+
+credentials += Credentials(Path.userHome / ".sbt" / "sonatype.credentials")
+
+publishConfiguration := publishConfiguration.value.withOverwrite(isSnapshot.value)
+PgpKeys.publishSignedConfiguration := PgpKeys.publishSignedConfiguration.value.withOverwrite(isSnapshot.value)
+publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(isSnapshot.value)
+PgpKeys.publishLocalSignedConfiguration := PgpKeys.publishLocalSignedConfiguration.value.withOverwrite(isSnapshot.value)
diff --git a/core-play30/project/build.properties b/core-play30/project/build.properties
new file mode 100644
index 0000000..01a16ed
--- /dev/null
+++ b/core-play30/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.11.7
diff --git a/core-play30/project/plugins.sbt b/core-play30/project/plugins.sbt
new file mode 100644
index 0000000..744b8b5
--- /dev/null
+++ b/core-play30/project/plugins.sbt
@@ -0,0 +1,10 @@
+// The Play plugin
+addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.9")
+
+// web plugins
+
+addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2")
+
+addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
+
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5")
diff --git a/play30-bootstrap4/module/app/views/b4/bsFieldConstructorCommon.scala.html b/play30-bootstrap4/module/app/views/b4/bsFieldConstructorCommon.scala.html
new file mode 100644
index 0000000..db321ea
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/bsFieldConstructorCommon.scala.html
@@ -0,0 +1,12 @@
+@(fieldInfo: b4.B4FieldInfo, inputHtml: Html, extraClasses: Option[String] = None)(wrap: Html => Html)(implicit fc: b4.B4FieldConstructor)
+
+ }
+ }(fc, msgsProv)
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/clear/package.scala b/play30-bootstrap4/module/app/views/b4/clear/package.scala
new file mode 100644
index 0000000..ca1f9ed
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/clear/package.scala
@@ -0,0 +1,67 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+package object clear {
+
+ import play.twirl.api.Html
+ import play.api.i18n.MessagesProvider
+ import play.api.mvc.{Call, RequestHeader}
+ import views.html.helper._
+ import views.html.bs.Args.{inner, isTrue}
+
+ /** Declares the class for the Clear FieldConstructor.
+ */
+ class ClearFieldConstructor(val isCustom: Boolean = false, val withFeedbackTooltip: Boolean = false)
+ extends B4FieldConstructor {
+ /* Define the class of the corresponding form */
+ val formClass = "form-clear"
+ /* Renders the corresponding template of the field constructor */
+ def apply(fieldInfo: B4FieldInfo, inputHtml: Html)(implicit msgsProv: MessagesProvider) = inputHtml
+ /* Renders the corresponding template of the form group */
+ def apply(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider) = contentHtml
+ }
+
+ /** Creates a new ClearFieldConstructor to use for specific forms or scopes (don't use it as a default one). If a
+ * default B4FieldConstructor and a specific ClearFieldConstructor are within the same scope, the more specific will
+ * be chosen.
+ */
+ def fieldConstructorSpecific(isCustom: Boolean = false, withFeedbackTooltip: Boolean = false): ClearFieldConstructor =
+ new ClearFieldConstructor(isCustom, withFeedbackTooltip)
+
+ /** Returns it as a B4FieldConstructor to use it as default within a template
+ */
+ def fieldConstructor(isCustom: Boolean = false, withFeedbackTooltip: Boolean = false): B4FieldConstructor =
+ fieldConstructorSpecific(isCustom, withFeedbackTooltip)
+
+ /** **********************************************************************************************************************************
+ * SHORTCUT HELPERS
+ * *********************************************************************************************************************************
+ */
+ def form(action: Call, args: (Symbol, Any)*)(body: ClearFieldConstructor => Html) = {
+ val cfc = fieldConstructorSpecific(
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.form(action, inner(args): _*)(body(cfc))(cfc)
+ }
+ def formCSRF(action: Call, args: (Symbol, Any)*)(
+ body: ClearFieldConstructor => Html
+ )(implicit request: RequestHeader) = {
+ val cfc = fieldConstructorSpecific(
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.formCSRF(action, inner(args): _*)(body(cfc))(cfc, request)
+ }
+
+}
diff --git a/play30-bootstrap4/module/app/views/b4/file.scala.html b/play30-bootstrap4/module/app/views/b4/file.scala.html
new file mode 100644
index 0000000..fc99065
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/file.scala.html
@@ -0,0 +1,11 @@
+@(field: Field, args: (Symbol,Any)*)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@inputFormGroup(field, withLabelFor = true, args) { fieldInfo =>
+ @if(fieldInfo.isCustom) {
+
+
+
+
+ } else {
+
+ }
+}(fc, msgsProv)
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/form.scala.html b/play30-bootstrap4/module/app/views/b4/form.scala.html
new file mode 100644
index 0000000..feb72eb
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/form.scala.html
@@ -0,0 +1,4 @@
+@(action: Call, args: (Symbol, Any)*)(body: => Html)(implicit fc: b4.B4FieldConstructor)
+
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/formCSRF.scala.html b/play30-bootstrap4/module/app/views/b4/formCSRF.scala.html
new file mode 100644
index 0000000..80f8b59
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/formCSRF.scala.html
@@ -0,0 +1,5 @@
+@(action: Call, args: (Symbol, Any)*)(body: => Html)(implicit fc: b4.B4FieldConstructor, request: RequestHeader)
+@form(action, args:_*) {
+ @helper.CSRF.formField
+ @body
+}(fc)
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/hiddenInput.scala.html b/play30-bootstrap4/module/app/views/b4/hiddenInput.scala.html
new file mode 100644
index 0000000..5ae08f3
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/hiddenInput.scala.html
@@ -0,0 +1,2 @@
+@(name: Any, value: Any, args: (Symbol, Any)*)
+
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/hiddens.scala.html b/play30-bootstrap4/module/app/views/b4/hiddens.scala.html
new file mode 100644
index 0000000..6c7d022
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/hiddens.scala.html
@@ -0,0 +1,4 @@
+@(namesAndValues: (Any, Any)*)
+@namesAndValues.map { case (name, value) =>
+
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/horizontal/bsFieldConstructor.scala.html b/play30-bootstrap4/module/app/views/b4/horizontal/bsFieldConstructor.scala.html
new file mode 100644
index 0000000..eae4f1f
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/horizontal/bsFieldConstructor.scala.html
@@ -0,0 +1,13 @@
+@(fieldInfo: b4.B4FieldInfo, inputHtml: Html, colLabel: String, colOffset: String, colInput: String)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@b4.bsFieldConstructorCommon(fieldInfo, inputHtml, extraClasses = Some("row")) { content =>
+ @fieldInfo.labelOpt.map { label =>
+
+
+ @content
+
+ }.getOrElse {
+
+ @content
+
+ }
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/horizontal/bsFormGroup.scala.html b/play30-bootstrap4/module/app/views/b4/horizontal/bsFormGroup.scala.html
new file mode 100644
index 0000000..3eeff37
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/horizontal/bsFormGroup.scala.html
@@ -0,0 +1,13 @@
+@(contentHtml: Html, argsMap: Map[Symbol, Any], colLabel: String, colOffset: String, colInput: String)(implicit msgsProv: MessagesProvider)
+@b4.bsFormGroupCommon(contentHtml, bs.ArgsMap.withAddingStringValue(argsMap, Symbol("_class"), "row")) { content =>
+ @argsMap.get(Symbol("_label")).map { label =>
+
+
+ @content
+
+ }.getOrElse {
+
+ @content
+
+ }
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/horizontal/package.scala b/play30-bootstrap4/module/app/views/b4/horizontal/package.scala
new file mode 100644
index 0000000..3a8f9a9
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/horizontal/package.scala
@@ -0,0 +1,92 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+package object horizontal {
+
+ import play.twirl.api.Html
+ import play.api.mvc.{Call, RequestHeader}
+ import play.api.i18n.MessagesProvider
+ import views.html.helper._
+ import views.html.bs.Args.{inner, isTrue}
+
+ /** Declares the class for the Horizontal FieldConstructor. It needs the column widths for the corresponding
+ * Bootstrap3 form-group
+ */
+ case class HorizontalFieldConstructor(
+ colLabel: String,
+ colInput: String,
+ val isCustom: Boolean = false,
+ val withFeedbackTooltip: Boolean = false
+ ) extends B4FieldConstructor {
+ /* The equivalent offset if label is not present (ex: colLabel = "col-md-2" => colOffset = "offset-md-2") */
+ val colOffset: String = colLabel.replace("col", "offset")
+ /* Define the class of the corresponding form */
+ val formClass = "form-horizontal"
+ /* Renders the corresponding template of the field constructor */
+ def apply(fieldInfo: B4FieldInfo, inputHtml: Html)(implicit msgsProv: MessagesProvider) =
+ bsFieldConstructor(fieldInfo, inputHtml, colLabel, colOffset, colInput)(this, msgsProv)
+ /* Renders the corresponding template of the form group */
+ def apply(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider) =
+ bsFormGroup(contentHtml, argsMap, colLabel, colOffset, colInput)(msgsProv)
+ }
+
+ /** Returns a new HorizontalFieldConstructor to use for specific forms or scopes (don't use it as a default one). If a
+ * default B4FieldConstructor and a specific HorizontalFieldConstructor are within the same scope, the more specific
+ * will be chosen.
+ */
+ def fieldConstructorSpecific(
+ colLabel: String,
+ colInput: String,
+ isCustom: Boolean = false,
+ withFeedbackTooltip: Boolean = false
+ ): HorizontalFieldConstructor =
+ new HorizontalFieldConstructor(colLabel, colInput, isCustom, withFeedbackTooltip)
+
+ /** Returns it as a B4FieldConstructor to use it as default within a template
+ */
+ def fieldConstructor(
+ colLabel: String,
+ colInput: String,
+ isCustom: Boolean = false,
+ withFeedbackTooltip: Boolean = false
+ ): B4FieldConstructor =
+ fieldConstructorSpecific(colLabel, colInput, isCustom, withFeedbackTooltip)
+
+ /** **********************************************************************************************************************************
+ * SHORTCUT HELPERS
+ * *********************************************************************************************************************************
+ */
+ def form(action: Call, colLabel: String, colInput: String, args: (Symbol, Any)*)(
+ body: HorizontalFieldConstructor => Html
+ ) = {
+ val hfc = fieldConstructorSpecific(
+ colLabel,
+ colInput,
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.form(action, inner(args): _*)(body(hfc))(hfc)
+ }
+ def formCSRF(action: Call, colLabel: String, colInput: String, args: (Symbol, Any)*)(
+ body: HorizontalFieldConstructor => Html
+ )(implicit request: RequestHeader) = {
+ val hfc = fieldConstructorSpecific(
+ colLabel,
+ colInput,
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.formCSRF(action, inner(args): _*)(body(hfc))(hfc, request)
+ }
+
+}
diff --git a/play30-bootstrap4/module/app/views/b4/inline/bsFieldConstructor.scala.html b/play30-bootstrap4/module/app/views/b4/inline/bsFieldConstructor.scala.html
new file mode 100644
index 0000000..1808ed9
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/inline/bsFieldConstructor.scala.html
@@ -0,0 +1,7 @@
+@(fieldInfo: b4.B4FieldInfo, inputHtml: Html)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@b4.bsFieldConstructorCommon(fieldInfo, inputHtml) { content =>
+ @fieldInfo.labelOpt.map { label =>
+
+ }
+ @content
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/inline/bsFormGroup.scala.html b/play30-bootstrap4/module/app/views/b4/inline/bsFormGroup.scala.html
new file mode 100644
index 0000000..2fcae8c
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/inline/bsFormGroup.scala.html
@@ -0,0 +1,7 @@
+@(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider)
+@b4.bsFormGroupCommon(contentHtml, argsMap) { content =>
+ @argsMap.get(Symbol("_label")).map { label =>
+
+ }
+ @content
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/inline/package.scala b/play30-bootstrap4/module/app/views/b4/inline/package.scala
new file mode 100644
index 0000000..bc28e56
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/inline/package.scala
@@ -0,0 +1,72 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+package object inline {
+
+ import play.twirl.api.Html
+ import play.api.mvc.{Call, RequestHeader}
+ import play.api.i18n.MessagesProvider
+ import views.html.helper._
+ import views.html.bs.Args.{inner, isTrue}
+
+ /** Declares the class for the Inline FieldConstructor.
+ */
+ class InlineFieldConstructor(val isCustom: Boolean = false, val withFeedbackTooltip: Boolean = false)
+ extends B4FieldConstructor {
+ /* Define the class of the corresponding form */
+ val formClass = "form-inline"
+ /* Renders the corresponding template of the field constructor */
+ def apply(fieldInfo: B4FieldInfo, inputHtml: Html)(implicit msgsProv: MessagesProvider) =
+ bsFieldConstructor(fieldInfo, inputHtml)(this, msgsProv)
+ /* Renders the corresponding template of the form group */
+ def apply(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider) =
+ bsFormGroup(contentHtml, argsMap)(msgsProv)
+ }
+
+ /** Creates a new InlineFieldConstructor to use for specific forms or scopes (don't use it as a default one). If a
+ * default B4FieldConstructor and a specific InlineFieldConstructor are within the same scope, the more specific will
+ * be chosen.
+ */
+ def fieldConstructorSpecific(
+ isCustom: Boolean = false,
+ withFeedbackTooltip: Boolean = false
+ ): InlineFieldConstructor =
+ new InlineFieldConstructor(isCustom, withFeedbackTooltip)
+
+ /** Returns it as a B4FieldConstructor to use it as default within a template
+ */
+ def fieldConstructor(isCustom: Boolean = false, withFeedbackTooltip: Boolean = false): B4FieldConstructor =
+ fieldConstructorSpecific(isCustom, withFeedbackTooltip)
+
+ /** **********************************************************************************************************************************
+ * SHORTCUT HELPERS
+ * *********************************************************************************************************************************
+ */
+ def form(action: Call, args: (Symbol, Any)*)(body: InlineFieldConstructor => Html) = {
+ val ifc = fieldConstructorSpecific(
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.form(action, inner(args): _*)(body(ifc))(ifc)
+ }
+ def formCSRF(action: Call, args: (Symbol, Any)*)(
+ body: InlineFieldConstructor => Html
+ )(implicit request: RequestHeader) = {
+ val ifc = fieldConstructorSpecific(
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.formCSRF(action, inner(args): _*)(body(ifc))(ifc, request)
+ }
+
+}
diff --git a/play30-bootstrap4/module/app/views/b4/inputWrapped.scala.html b/play30-bootstrap4/module/app/views/b4/inputWrapped.scala.html
new file mode 100644
index 0000000..5c7af4d
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/inputWrapped.scala.html
@@ -0,0 +1,6 @@
+@(inputType: String, field: Field, args: (Symbol,Any)*)(inputGroup: Html => Html)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@inputFormGroup(field, withLabelFor = true, bs.Args.withAddingStringValue(args, Symbol("class"), "form-control")) { fieldInfo =>
+ @inputGroup {
+
+ }
+}(fc, msgsProv)
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/multifield.scala.html b/play30-bootstrap4/module/app/views/b4/multifield.scala.html
new file mode 100644
index 0000000..8972468
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/multifield.scala.html
@@ -0,0 +1,16 @@
+@(fields: Field*)(globalArgs: Seq[(Symbol,Any)], fieldsArgs: Seq[(Symbol,Any)])(inputsHtml: b4.clear.ClearFieldConstructor => Html)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@multifieldFormGroup(fields, globalArgs, fieldsArgs) { multifieldInfo =>
+ @inputsHtml(b4.clear.fieldConstructorSpecific(isCustom = fc.isCustom))
+ @* renders a hidden form-control only to force visibility of the next feedback *@
+ @multifieldInfo.status.collect {
+ case "danger" => {}
+ case "success" => {}
+ case "warning" => {}
+ }
+ @multifieldInfo.feedbackInfos.map { text =>
+
@text
+ }
+ @multifieldInfo.helpInfos.map { text =>
+ @text
+ }
+}(fc, msgsProv)
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/package.scala b/play30-bootstrap4/module/app/views/b4/package.scala
new file mode 100644
index 0000000..5c7dee7
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/package.scala
@@ -0,0 +1,284 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html
+
+package object b4 {
+
+ import play.api.data.{Field, FormError}
+ import play.twirl.api.Html
+ import play.api.i18n.MessagesProvider
+ import bs._
+ import bs.ArgsMap.isTrue
+ import play.api.mvc.Call
+
+ /** Class with relevant variables for a field to pass it to the helper and field constructor
+ * - withLabelFor: indicates if the label's "for" attribute should be shown
+ * - args: list of available arguments for the helper and field constructor
+ */
+ case class B4FieldInfo(
+ field: Field,
+ withLabelFor: Boolean,
+ args: Seq[(Symbol, Any)],
+ override val msgsProv: MessagesProvider
+ ) extends BSFieldInfo(field, args, msgsProv) {
+
+ /* List with every "feedback info" and its corresponding ARIA id. Ex: ("foo_info_0" -> "foo constraint") */
+ val feedbackInfos: Seq[(String, Any)] =
+ if (errors.size > 0)
+ errors
+ else
+ BSFieldInfo.feedbackInfosButErrors(argsMap, msgsProv).zipWithIndex.map { case (info, i) =>
+ (id + "_feedback_" + i, info)
+ }
+
+ /* List with every "help info" (i.e. a help text or constraints) and its corresponding ARIA id. Ex: ("foo_info_0" -> "foo constraint") */
+ val helpInfos: Seq[(String, Any)] = BSFieldInfo.helpInfos(Some(field), argsMap, msgsProv).zipWithIndex.map {
+ case (info, i) => (id + "_info_" + i, info)
+ }
+
+ /* Indicates if it's a custom element */
+ def isCustom(implicit fc: b4.B4FieldConstructor): Boolean = fc.isCustom || isTrue(argsMap, Symbol("_custom"))
+
+ /* The optional validation state ("success", "warning" or "danger") */
+ override lazy val status: Option[String] = B4FieldInfo.status(hasErrors, argsMap)
+
+ /* The corresponding optional validation feedback for B4 ("valid-feedback", "warning-feedback" or "invalid-feedback") */
+ def statusB4Feedback(implicit fc: b4.B4FieldConstructor): Option[String] =
+ B4FieldInfo.statusB4Feedback(status, fc.withFeedbackTooltip)
+
+ /* List of every ARIA id */
+ val ariaIds: Seq[String] = feedbackInfos.map(_._1) ++ helpInfos.map(_._1)
+
+ /*
+ * Map with the inner args, i.e. those args for the helper itself removing those ones reserved for the field constructor.
+ * It adds the ARIA attributes and removes the underscored reserved for the field constructor and the `id and `value ones that are
+ * managed independently.
+ */
+ lazy val innerArgsMap: Map[Symbol, Any] = (
+ (if (ariaIds.size > 0) Seq(Symbol("aria-describedby") -> ariaIds.mkString(" ")) else Nil) ++
+ (if (hasErrors) Seq(Symbol("aria-invalid") -> "true") else Nil) ++
+ BSFieldInfo.constraintsArgs(field, msgsProv) ++
+ Args.inner(
+ Args
+ .remove(
+ status
+ .map(s =>
+ Args.withAddingStringValue(
+ args,
+ Symbol("class"),
+ if (s == "danger") "is-invalid"
+ else if (s == "success") "is-valid"
+ else if (s == "warning") "is-warning"
+ else ""
+ )
+ )
+ .getOrElse(args),
+ Symbol("id"),
+ Symbol("value")
+ )
+ .map {
+ case arg if arg._1 == Symbol("placeholder") => Args.msg(arg)(msgsProv.messages)
+ case other => other
+ }
+ )
+ ).toMap
+ }
+
+ /** Companion object for class B4FieldInfo
+ */
+ object B4FieldInfo {
+ /* The optional validation state ("success", "warning" or "danger") */
+ def status(hasErrors: Boolean, argsMap: Map[Symbol, Any]): Option[String] = {
+ if (hasErrors)
+ Some("danger")
+ else if (ArgsMap.isNotFalse(argsMap, Symbol("_warning")))
+ Some("warning")
+ else if (ArgsMap.isNotFalse(argsMap, Symbol("_success")))
+ Some("success")
+ else
+ None
+ }
+ /* The corresponding feedback class for helpers */
+ def statusB4Feedback(status: Option[String], withFeedbackTooltip: Boolean): Option[String] = status
+ .map {
+ case "success" => "valid"
+ case "warning" => "warning"
+ case _ => "invalid"
+ }
+ .map(_ + (if (withFeedbackTooltip) "-tooltip" else "-feedback"))
+ }
+
+ /** Class with relevant variables for the global information of a multifield
+ * - fields: list of Fields
+ * - args: list of available arguments for the helper and the form-group
+ */
+ case class B4MultifieldInfo(
+ fields: Seq[Field],
+ globalArguments: Seq[(Symbol, Any)],
+ fieldsArguments: Seq[(Symbol, Any)],
+ override val msgsProv: MessagesProvider
+ ) extends BSMultifieldInfo(fields, globalArguments, fieldsArguments, msgsProv) {
+
+ /* List with every "feedback info" */
+ val feedbackInfos: Seq[Any] = {
+ if (errors.size > 0)
+ errors
+ else
+ BSFieldInfo.feedbackInfosButErrors(argsMap, msgsProv)
+ }
+
+ /* List with every "help info" (i.e. a help text or constraints) */
+ val helpInfos: Seq[Any] = {
+ val globalHelpInfos = BSFieldInfo.helpInfos(None, argsMap, msgsProv)
+ if (globalHelpInfos.size > 0)
+ globalHelpInfos
+ else
+ fields.flatMap { field =>
+ BSFieldInfo.helpInfos(Some(field), argsMap, msgsProv)
+ }
+ }
+
+ /* The optional validation state ("success", "warning" or "danger") */
+ override lazy val status: Option[String] = B4FieldInfo.status(hasErrors, argsMap)
+
+ /* The corresponding optional validation feedback for B4 ("valid-feedback", "warning-feedback" or "invalid-feedback") */
+ def statusB4Feedback(implicit fc: b4.B4FieldConstructor): Option[String] =
+ B4FieldInfo.statusB4Feedback(status, fc.withFeedbackTooltip)
+
+ override lazy val globalArgs = {
+ val withoutHelp = Args.remove(globalArguments, Symbol("_help"))
+ val withStatus =
+ status.map(s => Args.withDefault(withoutHelp, Symbol("_class") -> s"has-$s")).getOrElse(withoutHelp)
+ withStatus
+ }
+ }
+
+ /** Custom FieldConstructor for the library. Every FieldConstructor must extend this functionality.
+ */
+ trait B4FieldConstructor extends BSFieldConstructor[B4FieldInfo] {
+ /* Define the class of the corresponding form (ex: "form-horizontal", "form-inline", ...) */
+ val formClass: String
+ val isCustom: Boolean
+ val withFeedbackTooltip: Boolean
+ }
+
+ /** Renders an input form-group using the B4FieldConstructor.
+ * - withLabelFor: indicates if the label's "for" attribute should be shown
+ * - args: list of available arguments for the helper and field constructor
+ * - inputDef: function that returns a Html from a B4FieldInfo that contains all the information about the field
+ */
+ def inputFormGroup(field: Field, withLabelFor: Boolean, args: Seq[(Symbol, Any)])(
+ inputDef: B4FieldInfo => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputFormField(B4FieldInfo(field, withLabelFor, Args.withoutNones(args), msgsProv))(inputDef)(fc)
+
+ /** Renders a form-group using the B4FieldConstructor.
+ * - args: list of available arguments for the helper and the form-group
+ * - contentDef: function that returns a Html from a map of arguments
+ */
+ def freeFormGroup(args: Seq[(Symbol, Any)])(
+ contentDef: Map[Symbol, Any] => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ freeFormField(args)(contentDef)(fc, msgsProv)
+
+ def multifieldFormGroup(fields: Seq[Field], globalArgs: Seq[(Symbol, Any)], fieldsArgs: Seq[(Symbol, Any)])(
+ contentDef: B4MultifieldInfo => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ multifieldFormField(B4MultifieldInfo(fields, globalArgs, fieldsArgs, msgsProv))(contentDef)(fc)
+
+ /** **********************************************************************************************************************************
+ * SHORTCUT HELPERS
+ * *********************************************************************************************************************************
+ */
+ def inputType(inputType: String, field: Field, args: (Symbol, Any)*)(implicit
+ fc: B4FieldConstructor,
+ msgsProv: MessagesProvider
+ ) = inputWrapped(inputType, field, args: _*)(html => html)(fc, msgsProv)
+
+ def text(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("text", field, args: _*)(fc, msgsProv)
+ def password(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("password", field.copy(value = Some("")), args: _*)(fc, msgsProv)
+ def color(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("color", field, args: _*)(fc, msgsProv)
+ def date(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("date", field, args: _*)(fc, msgsProv)
+ def datetime(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("datetime", field, args: _*)(fc, msgsProv)
+ def datetimeLocal(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("datetime-local", field, args: _*)(fc, msgsProv)
+ def email(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("email", field, args: _*)(fc, msgsProv)
+ def month(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("month", field, args: _*)(fc, msgsProv)
+ def number(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("number", field, args: _*)(fc, msgsProv)
+ def range(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("range", field, args: _*)(fc, msgsProv)
+ def search(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("search", field, args: _*)(fc, msgsProv)
+ def tel(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("tel", field, args: _*)(fc, msgsProv)
+ def time(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("time", field, args: _*)(fc, msgsProv)
+ def url(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("url", field, args: _*)(fc, msgsProv)
+ def week(field: Field, args: (Symbol, Any)*)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ inputType("week", field, args: _*)(fc, msgsProv)
+
+ def hidden(name: String, value: Any, args: (Symbol, Any)*) = hiddenInput(name, value, args: _*)
+ def hidden(field: Field, args: (Symbol, Any)*) = hiddenInput(
+ name = field.name,
+ value = field.value.orElse(bs.Args.get(args, Symbol("value"))),
+ (bs.Args.inner(bs.Args.remove(args, Symbol("value")))): _*
+ )
+
+ def radio(field: Field, args: (Symbol, Any)*)(
+ content: Tuple3[Boolean, Boolean, B4FieldInfo] => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ radioWithContent(field, args: _*)(content)(fc, msgsProv)
+ def radio(field: Field, options: Seq[(String, Any)], args: (Symbol, Any)*)(implicit
+ fc: B4FieldConstructor,
+ msgsProv: MessagesProvider
+ ) = radioWithOptions(field, options, args: _*)(fc, msgsProv)
+
+ def select(field: Field, args: (Symbol, Any)*)(
+ content: Set[String] => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ selectWithContent(field, args: _*)(content)(fc, msgsProv)
+ def select(field: Field, options: Seq[(String, String)], args: (Symbol, Any)*)(implicit
+ fc: B4FieldConstructor,
+ msgsProv: MessagesProvider
+ ) = selectWithOptions(field, options, args: _*)(fc, msgsProv)
+
+ def submit(args: (Symbol, Any)*)(text: => Html)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ buttonType("submit", args: _*)(text)(fc, msgsProv)
+ def reset(args: (Symbol, Any)*)(text: => Html)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ buttonType("reset", args: _*)(text)(fc, msgsProv)
+ def button(args: (Symbol, Any)*)(text: => Html)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ buttonType("button", args: _*)(text)(fc, msgsProv)
+
+ def static(args: (Symbol, Any)*)(text: => Html)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ staticBasic(args: _*)(text)(fc, msgsProv)
+ def static(label: String, args: (Symbol, Any)*)(
+ text: => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ staticBasic(Args.withDefault(args, Symbol("_label") -> label): _*)(text)(fc, msgsProv)
+ def static(label: Html, args: (Symbol, Any)*)(
+ text: => Html
+ )(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ staticBasic(Args.withDefault(args, Symbol("_label") -> label): _*)(text)(fc, msgsProv)
+
+ def free(args: (Symbol, Any)*)(content: => Html)(implicit fc: B4FieldConstructor, msgsProv: MessagesProvider) =
+ freeFormGroup(args)(_ => content)(fc, msgsProv)
+
+}
diff --git a/play30-bootstrap4/module/app/views/b4/radioOption.scala.html b/play30-bootstrap4/module/app/views/b4/radioOption.scala.html
new file mode 100644
index 0000000..8176528
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/radioOption.scala.html
@@ -0,0 +1,18 @@
+@(inputValue: Any, label: Any, args: (Symbol, Any)*)(implicit extraInfo: (Boolean, Boolean, B4FieldInfo), fc: B4FieldConstructor, msgsProv: MessagesProvider)
+@displayLabelWithInput(inputClass: String, labelClass: String, fieldInfo: b4.B4FieldInfo) = {
+
+
+}
+@defining(extraInfo) { case (inline, disabled, fieldInfo) =>
+ @if(fieldInfo.isCustom) {
+
+}(fc, msgsProv)
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/textarea.scala.html b/play30-bootstrap4/module/app/views/b4/textarea.scala.html
new file mode 100644
index 0000000..d2ec352
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/textarea.scala.html
@@ -0,0 +1,4 @@
+@(field: Field, args: (Symbol,Any)*)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@inputFormGroup(field, withLabelFor = true, bs.Args.withAddingStringValue(args, Symbol("class"), "form-control")) { fieldInfo =>
+
+}(fc, msgsProv)
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/vertical/bsFieldConstructor.scala.html b/play30-bootstrap4/module/app/views/b4/vertical/bsFieldConstructor.scala.html
new file mode 100644
index 0000000..1808ed9
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/vertical/bsFieldConstructor.scala.html
@@ -0,0 +1,7 @@
+@(fieldInfo: b4.B4FieldInfo, inputHtml: Html)(implicit fc: b4.B4FieldConstructor, msgsProv: MessagesProvider)
+@b4.bsFieldConstructorCommon(fieldInfo, inputHtml) { content =>
+ @fieldInfo.labelOpt.map { label =>
+
+ }
+ @content
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/vertical/bsFormGroup.scala.html b/play30-bootstrap4/module/app/views/b4/vertical/bsFormGroup.scala.html
new file mode 100644
index 0000000..2fcae8c
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/vertical/bsFormGroup.scala.html
@@ -0,0 +1,7 @@
+@(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider)
+@b4.bsFormGroupCommon(contentHtml, argsMap) { content =>
+ @argsMap.get(Symbol("_label")).map { label =>
+
+ }
+ @content
+}
\ No newline at end of file
diff --git a/play30-bootstrap4/module/app/views/b4/vertical/package.scala b/play30-bootstrap4/module/app/views/b4/vertical/package.scala
new file mode 100644
index 0000000..a2bb559
--- /dev/null
+++ b/play30-bootstrap4/module/app/views/b4/vertical/package.scala
@@ -0,0 +1,72 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+package object vertical {
+
+ import play.twirl.api.Html
+ import play.api.mvc.{Call, RequestHeader}
+ import play.api.i18n.MessagesProvider
+ import views.html.helper._
+ import views.html.bs.Args.{inner, isTrue}
+
+ /** Declares the class for the Vertical FieldConstructor.
+ */
+ class VerticalFieldConstructor(val isCustom: Boolean = false, val withFeedbackTooltip: Boolean = false)
+ extends B4FieldConstructor {
+ /* Define the class of the corresponding form */
+ val formClass = "form-vertical"
+ /* Renders the corresponding template of the field constructor */
+ def apply(fieldInfo: B4FieldInfo, inputHtml: Html)(implicit msgsProv: MessagesProvider) =
+ bsFieldConstructor(fieldInfo, inputHtml)(this, msgsProv)
+ /* Renders the corresponding template of the form group */
+ def apply(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider) =
+ bsFormGroup(contentHtml, argsMap)(msgsProv)
+ }
+
+ /** Creates a new VerticalFieldConstructor to use for specific forms or scopes (don't use it as a default one). If a
+ * default B4FieldConstructor and a specific VerticalFieldConstructor are within the same scope, the more specific
+ * will be chosen.
+ */
+ def fieldConstructorSpecific(
+ isCustom: Boolean = false,
+ withFeedbackTooltip: Boolean = false
+ ): VerticalFieldConstructor =
+ new VerticalFieldConstructor(isCustom, withFeedbackTooltip)
+
+ /** Returns it as a B4FieldConstructor to use it as default within a template
+ */
+ def fieldConstructor(isCustom: Boolean = false, withFeedbackTooltip: Boolean = false): B4FieldConstructor =
+ fieldConstructorSpecific(isCustom, withFeedbackTooltip)
+
+ /** **********************************************************************************************************************************
+ * SHORTCUT HELPERS
+ * *********************************************************************************************************************************
+ */
+ def form(action: Call, args: (Symbol, Any)*)(body: VerticalFieldConstructor => Html) = {
+ val vfc = fieldConstructorSpecific(
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.form(action, inner(args): _*)(body(vfc))(vfc)
+ }
+ def formCSRF(action: Call, args: (Symbol, Any)*)(
+ body: VerticalFieldConstructor => Html
+ )(implicit request: RequestHeader) = {
+ val vfc = fieldConstructorSpecific(
+ isCustom = isTrue(args, Symbol("_custom")),
+ withFeedbackTooltip = isTrue(args, Symbol("_feedbackTooltip"))
+ )
+ views.html.b4.formCSRF(action, inner(args): _*)(body(vfc))(vfc, request)
+ }
+
+}
diff --git a/play30-bootstrap4/module/build.sbt b/play30-bootstrap4/module/build.sbt
new file mode 100644
index 0000000..95022ce
--- /dev/null
+++ b/play30-bootstrap4/module/build.sbt
@@ -0,0 +1,61 @@
+name := """play-bootstrap"""
+
+version := "1.7.0-P30-B4"
+
+scalaVersion := "2.13.17"
+
+crossScalaVersions := Seq("2.13.17", "3.7.3")
+
+resolvers ++= Seq(
+ "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases",
+ "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
+)
+
+libraryDependencies := libraryDependencies.value.filterNot(m => m.name == "twirl-api" || m.name == "play-server") ++ Seq(
+ playCore % "provided",
+ filters % "provided",
+ "com.adrianhurt" %% "play-bootstrap-core" % "1.7.0-P30",
+ specs2 % Test
+)
+
+lazy val root = (project in file(".")).enablePlugins(PlayScala).disablePlugins(PlayFilters, PlayLogback, PlayPekkoHttpServer)
+
+scalafmtOnCompile := true
+
+//*******************************
+// Maven settings
+//*******************************
+
+publishMavenStyle := true
+
+organization := "com.adrianhurt"
+
+description := "This is a collection of input helpers and field constructors for Play Framework to render Bootstrap HTML code."
+
+import xerial.sbt.Sonatype.*
+sonatypeProjectHosting := Some(GitHubHosting("playframework", "play-bootstrap", "contact@playframework.com"))
+
+homepage := Some(url("https://playframework.github.io/play-bootstrap"))
+
+licenses := Seq("Apache License" -> url("https://github.com/playframework/play-bootstrap/blob/master/LICENSE"))
+
+startYear := Some(2014)
+
+publishTo := {
+ val nexus = "https://oss.sonatype.org/"
+ if (isSnapshot.value)
+ Some("snapshots" at nexus + "content/repositories/snapshots")
+ else
+ Some("releases" at nexus + "service/local/staging/deploy/maven2")
+}
+
+Test / publishArtifact := false
+
+pomIncludeRepository := { _ => false }
+
+credentials += Credentials(Path.userHome / ".sbt" / "sonatype.credentials")
+
+publishConfiguration := publishConfiguration.value.withOverwrite(isSnapshot.value)
+PgpKeys.publishSignedConfiguration := PgpKeys.publishSignedConfiguration.value.withOverwrite(isSnapshot.value)
+publishLocalConfiguration := publishLocalConfiguration.value.withOverwrite(isSnapshot.value)
+PgpKeys.publishLocalSignedConfiguration := PgpKeys.publishLocalSignedConfiguration.value.withOverwrite(isSnapshot.value)
diff --git a/play30-bootstrap4/module/project/build.properties b/play30-bootstrap4/module/project/build.properties
new file mode 100644
index 0000000..01a16ed
--- /dev/null
+++ b/play30-bootstrap4/module/project/build.properties
@@ -0,0 +1 @@
+sbt.version=1.11.7
diff --git a/play30-bootstrap4/module/project/plugins.sbt b/play30-bootstrap4/module/project/plugins.sbt
new file mode 100644
index 0000000..40ffbea
--- /dev/null
+++ b/play30-bootstrap4/module/project/plugins.sbt
@@ -0,0 +1,8 @@
+// The Play plugin
+addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.9")
+
+addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2")
+
+addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1")
+
+addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5")
\ No newline at end of file
diff --git a/play30-bootstrap4/module/test/FieldConstructorsSpec.scala b/play30-bootstrap4/module/test/FieldConstructorsSpec.scala
new file mode 100644
index 0000000..0fbc99a
--- /dev/null
+++ b/play30-bootstrap4/module/test/FieldConstructorsSpec.scala
@@ -0,0 +1,262 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+import views.html.b4
+import TestUtils._
+import org.specs2.mutable.Specification
+import views.html.helper.FieldConstructor
+import play.api.data.Forms._
+import play.api.data._
+import play.api.{Configuration, Environment}
+import play.api.http.HttpConfiguration
+import play.api.i18n.{DefaultLangsProvider, DefaultMessagesApiProvider, MessagesProvider}
+import play.twirl.api.Html
+import play.api.mvc.Call
+
+class FieldConstructorsSpec extends Specification {
+
+ val environment = Environment.simple()
+ val conf = Configuration.reference
+ val langs = new DefaultLangsProvider(conf).get
+
+ val httpConfiguration = HttpConfiguration.fromConfiguration(conf, environment)
+ val messagesApi = new DefaultMessagesApiProvider(environment, conf, langs, httpConfiguration).get
+ implicit val msgsProv: MessagesProvider = messagesApi.preferred(Seq.empty)
+
+ def testFielConstructor(
+ fcDefault: B4FieldConstructor,
+ fcWithCustomFields: B4FieldConstructor,
+ fcWithFeedbackTooltip: B4FieldConstructor
+ ) = {
+ implicit val fc = fcDefault
+ val testInputString = ""
+ def testInput(field: Field, args: (Symbol, Any)*) =
+ b4.inputFormGroup(field, true, args) { _ => Html(testInputString) }
+
+ val fooForm = Form(single("foo" -> Forms.nonEmptyText(maxLength = 8)))
+ val fooField = fooForm("foo")
+
+ val simpleInput = testInput(fooField).body
+ def simpleInputWithArgs(args: (Symbol, Any)*)(implicit fc: B4FieldConstructor) = b4.text(fooField, args: _*).body
+ def simpleInputWithError(args: (Symbol, Any)*)(implicit fc: B4FieldConstructor) =
+ b4.text(fooForm.withError("foo", "test-error-0").withError("foo", "test-error-1")("foo"), args: _*).body
+
+ val fieldsetExtraClasses = fc match {
+ case hfc: b4.horizontal.HorizontalFieldConstructor => " row"
+ case _ => ""
+ }
+ val labelExtraClasses = fc match {
+ case hfc: b4.horizontal.HorizontalFieldConstructor => "col-form-label " + hfc.colLabel
+ case _ => ""
+ }
+
+ "have the basic structure" in {
+ simpleInput must contain("class=\"form-group")
+ simpleInput must not contain ("has-danger")
+ simpleInput must not contain ("aria-invalid")
+ simpleInput must contain(testInputString)
+ simpleInput must not contain ("class=\"-feedback\"")
+ simpleInput must not contain ("class=\"-tooltip\"")
+ simpleInput must not contain ("class=\"form-text text-muted\"")
+ }
+
+ "have a default id" in {
+ simpleInput must contain("id=\"foo_field\"")
+ }
+
+ "allow setting a custom id" in {
+ simpleInputWithArgs(Symbol("_id") -> "customid") must contain("id=\"customid\"")
+ }
+
+ "allow setting extra classes form-group" in {
+ clean(simpleInputWithArgs(Symbol("_class") -> "extra_class another_class")) must contain(
+ s"""
"theLabel")) must contain(
+ if (labelExtraClasses == "") """"""
+ else s""""""
+ )
+ }
+
+ "allow hide the label" in {
+ val labelString =
+ if (labelExtraClasses == "") """"""
+ else s""""""
+ clean(simpleInputWithArgs(Symbol("_label") -> "theLabel", Symbol("_hideLabel") -> true)) must contain(labelString)
+ clean(simpleInputWithArgs(Symbol("_hiddenLabel") -> "theLabel")) must contain(labelString)
+ }
+
+ "allow render without label" in {
+ simpleInputWithArgs() must not contain ("label")
+ }
+
+ "allow rendering errors" in {
+ val test = simpleInputWithError()
+ test must contain("has-danger")
+ test must contain("
test-error-0
")
+ test must contain("
test-error-1
")
+ }
+
+ "allow showing constraints" in {
+ val test = simpleInputWithArgs(Symbol("_showConstraints") -> true)
+ test must contain("")
+ test must contain("")
+ test must contain("class=\"form-text text-muted\">" + msgsProv.messages("constraint.required") + "")
+ test must contain("class=\"form-text text-muted\">" + msgsProv.messages("constraint.maxLength", 8) + "")
+ }
+
+ "localize placeholder property" in {
+ val test = simpleInputWithArgs(Symbol("placeholder") -> "simpleInputWithArgs.placeholder.value")
+ test must contain("placeholder=\"Placeholder value\"")
+ }
+
+ "allow showing help info" in {
+ simpleInputWithArgs(Symbol("_help") -> "test-help") must contain(
+ "test-help"
+ )
+ simpleInputWithArgs(Symbol("_success") -> "test-help") must contain(
+ "
"
+ )
+ }
+
+ "allow rendering erros and hide constraints when help info is present" in {
+ val test = simpleInputWithError(Symbol("_showConstraints") -> true, Symbol("_help") -> "test-help")
+ test must contain("
test-error-0
")
+ test must contain("
test-error-1
")
+ test must contain("test-help")
+ test must not contain (" true)
+ testStatus("success", Symbol("_success") -> "test-help")
+ testStatus("warning", Symbol("_warning") -> true)
+ testStatus("warning", Symbol("_warning") -> "test-help")
+ testStatus("danger", Symbol("_error") -> true)
+ testStatus("danger", Symbol("_error") -> "test-help")
+ }
+
+ "render aria attributes" in {
+ val test0 = simpleInputWithArgs()
+ test0 must not contain ("aria-invalid")
+ test0 must not contain ("aria-describedby")
+ test0 must not contain (" true)
+ test1 must contain("aria-invalid=\"true\"")
+ test1 must contain("aria-describedby=\"foo_error_0 foo_error_1 foo_info_0 foo_info_1\"")
+ test1 must contain(" "test-help")
+ test2 must contain("aria-describedby=\"foo_info_0\"")
+ test2 must contain(" true).body.trim
+ val customFile2 = b4.file(fooField)(fcWithCustomFields, msgsProv).body.trim
+ customFile1 must be equalTo customFile2
+
+ val boolField = Form(single("foo" -> Forms.boolean))("foo")
+ val customCheckbox1 = b4.checkbox(boolField, Symbol("_custom") -> true, Symbol("_text") -> "theText").body.trim
+ val customCheckbox2 = b4.checkbox(boolField, Symbol("_text") -> "theText")(fcWithCustomFields, msgsProv).body.trim
+ customCheckbox1 must be equalTo customCheckbox2
+
+ val fruits = Seq("A" -> "Apples", "P" -> "Pears", "B" -> "Bananas")
+ val customRadio1 = b4.radio(fooField, fruits, Symbol("_custom") -> true).body.trim
+ val customRadio2 = b4.radio(fooField, fruits)(fcWithCustomFields, msgsProv).body.trim
+ customRadio1 must be equalTo customRadio2
+
+ val customSelect1 = b4.select(fooField, fruits, Symbol("_custom") -> true).body.trim
+ val customSelect2 = b4.select(fooField, fruits)(fcWithCustomFields, msgsProv).body.trim
+ customSelect1 must be equalTo customSelect2
+ }
+
+ "allow rendering with feedback tooltips" in {
+ val test1 = simpleInputWithError()(fc)
+ val test2 = simpleInputWithError()(fcWithFeedbackTooltip)
+ test1.replaceAll("-feedback", "-tooltip") must be equalTo test2
+
+ val test3 = simpleInputWithArgs(Symbol("_success") -> "test-help")(fc)
+ val test4 = simpleInputWithArgs(Symbol("_success") -> "test-help")(fcWithFeedbackTooltip)
+ test3.replaceAll("-feedback", "-tooltip") must be equalTo test4
+
+ val test5 = simpleInputWithArgs(Symbol("_warning") -> "test-help")(fc)
+ val test6 = simpleInputWithArgs(Symbol("_warning") -> "test-help")(fcWithFeedbackTooltip)
+ test6.replaceAll("-feedback", "-tooltip") must be equalTo test6
+ }
+ }
+
+ "horizontal field constructor" should {
+ val (colLabel, colInput) = ("col-md-2", "col-md-10")
+ implicit val horizontalFieldConstructor = new b4.horizontal.HorizontalFieldConstructor(colLabel, colInput)
+ val fcWithCustomFields = new b4.horizontal.HorizontalFieldConstructor(colLabel, colInput, isCustom = true)
+ val fcWithFeedbackTooltip =
+ new b4.horizontal.HorizontalFieldConstructor(colLabel, colInput, withFeedbackTooltip = true)
+
+ testFielConstructor(horizontalFieldConstructor, fcWithCustomFields, fcWithFeedbackTooltip)
+
+ "render columns for horizontal form" in {
+ val body = b4.text(Form(single("foo" -> Forms.text))("foo"), Symbol("_label") -> "theLabel").body
+ body must contain(colLabel)
+ body must contain(colInput)
+ }
+ }
+
+ "vertical field constructor" should {
+ implicit val verticalFieldConstructor = new b4.vertical.VerticalFieldConstructor()
+ val fcWithCustomFields = new b4.vertical.VerticalFieldConstructor(isCustom = true)
+ val fcWithFeedbackTooltip = new b4.vertical.VerticalFieldConstructor(withFeedbackTooltip = true)
+ testFielConstructor(verticalFieldConstructor, fcWithCustomFields, fcWithFeedbackTooltip)
+ }
+
+ "inline field constructor" should {
+ implicit val inlineFieldConstructor = new b4.inline.InlineFieldConstructor()
+ val fcWithCustomFields = new b4.inline.InlineFieldConstructor(isCustom = true)
+ val fcWithFeedbackTooltip = new b4.inline.InlineFieldConstructor(withFeedbackTooltip = true)
+ testFielConstructor(inlineFieldConstructor, fcWithCustomFields, fcWithFeedbackTooltip)
+ }
+
+ "clear field constructor" should {
+ implicit val clearFieldConstructor = b4.clear.fieldConstructor()
+
+ "simply render the input" in {
+ val simpleInput = b4.text(Form(single("foo" -> Forms.text))("foo")).body.trim
+ simpleInput must startWith("")
+ // Make sure it doesn't have it twice
+ simpleInput.substring(simpleInput.indexOf(">") + 1) must not contain (">")
+ }
+ }
+}
diff --git a/play30-bootstrap4/module/test/FormsSpec.scala b/play30-bootstrap4/module/test/FormsSpec.scala
new file mode 100644
index 0000000..565b6b7
--- /dev/null
+++ b/play30-bootstrap4/module/test/FormsSpec.scala
@@ -0,0 +1,96 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+import views.html.b4
+import TestUtils._
+import org.specs2.mutable.Specification
+import play.api.data.Forms._
+import play.api.data._
+import play.api.{Configuration, Environment}
+import play.api.http.HttpConfiguration
+import play.api.i18n.{DefaultLangsProvider, DefaultMessagesApiProvider, MessagesProvider}
+import play.twirl.api.Html
+import play.api.mvc.Call
+
+class FormsSpec extends Specification {
+
+ val environment = Environment.simple()
+ val conf = Configuration.reference
+ val langs = new DefaultLangsProvider(conf).get
+
+ val httpConfiguration = HttpConfiguration.fromConfiguration(conf, environment)
+ val messagesApi = new DefaultMessagesApiProvider(environment, conf, langs, httpConfiguration).get
+ implicit val msgsProv: MessagesProvider = messagesApi.preferred(Seq.empty)
+
+ val vfc = b4.vertical.fieldConstructor()
+ val (colLabel, colInput) = ("col-md-2", "col-md-10")
+ val hfc = b4.horizontal.fieldConstructor(colLabel, colInput)
+ val ifc = b4.inline.fieldConstructor()
+ val cfc = b4.clear.fieldConstructor()
+
+ val testContentString = ""
+
+ val (method, action) = ("POST", "/handleRequest")
+ val fooCall = Call(method, action)
+ def fooFormBody(args: (Symbol, Any)*)(fc: b4.B4FieldConstructor) =
+ b4.form(fooCall, args: _*)(Html(testContentString))(fc).body
+
+ "@form" should {
+
+ val simple = fooFormBody()(vfc)
+
+ "have action and method" in {
+ simple must contain("action=\"" + action + "\"")
+ simple must contain("method=\"" + method + "\"")
+ }
+
+ "add default class for each field constructor" in {
+ fooFormBody()(vfc) must contain("class=\"form-vertical")
+ fooFormBody()(hfc) must contain("class=\"form-horizontal")
+ fooFormBody()(ifc) must contain("class=\"form-inline")
+ fooFormBody()(cfc) must contain("class=\"form-clear")
+ }
+
+ "allow setting custom class" in {
+ fooFormBody(Symbol("class") -> "customClass")(vfc) must contain("class=\"form-vertical customClass\"")
+ }
+
+ "allow disabling default class" in {
+ fooFormBody(Symbol("_disableDefaultClass") -> true)(vfc) must not contain ("form-vertical")
+ fooFormBody(Symbol("_disableDefaultClass") -> true, Symbol("class") -> "customClass")(vfc) must contain(
+ "class=\"customClass\""
+ )
+ }
+
+ "add form role as default" in {
+ simple must contain("role=\"form\"")
+ }
+
+ "allow setting extra arguments and remove those arguments with false values or with underscored names" in {
+ val body = fooFormBody(
+ Symbol("extra_attr") -> "test",
+ Symbol("true_attr") -> true,
+ Symbol("fase_attr") -> false,
+ Symbol("_underscored_attr") -> "test"
+ )(vfc)
+ body must contain("extra_attr=\"test\"")
+ body must contain("true_attr=\"true\"")
+ body must not contain ("false_attr=\"false\"")
+ body must not contain ("_underscored_attr=\"test\"")
+ }
+
+ "render the content body" in {
+ simple must contain("")
+ }
+ }
+}
diff --git a/play30-bootstrap4/module/test/HelpersSpec.scala b/play30-bootstrap4/module/test/HelpersSpec.scala
new file mode 100644
index 0000000..e7a5d2c
--- /dev/null
+++ b/play30-bootstrap4/module/test/HelpersSpec.scala
@@ -0,0 +1,858 @@
+/** Copyright 2019 Adrian Hurtado (adrianhurt)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package views.html.b4
+
+import views.html.b4
+import TestUtils._
+import org.specs2.mutable.Specification
+import views.html.helper._
+import play.api.data.Forms._
+import play.api.data._
+import play.api.{Configuration, Environment}
+import play.api.http.HttpConfiguration
+import play.api.i18n.{DefaultLangsProvider, DefaultMessagesApiProvider, MessagesProvider}
+import play.twirl.api.{Html, HtmlFormat}
+import play.api.mvc.Call
+
+class HelpersSpec extends Specification {
+
+ val environment = Environment.simple()
+ val conf = Configuration.reference
+ val langs = new DefaultLangsProvider(conf).get
+
+ val httpConfiguration = HttpConfiguration.fromConfiguration(conf, environment)
+ val messagesApi = new DefaultMessagesApiProvider(environment, conf, langs, httpConfiguration).get
+ implicit val msgsProv: MessagesProvider = messagesApi.preferred(Seq.empty)
+
+ val vfc = b4.vertical.fieldConstructor()
+ val (colLabel, colInput) = ("col-md-2", "col-md-10")
+ val hfc = b4.horizontal.fieldConstructor(colLabel, colInput)
+ val ifc = b4.inline.fieldConstructor()
+ val cfc = b4.clear.fieldConstructor()
+
+ /** A test field constructor that simply renders the input
+ */
+ implicit val testFieldConstructor = new B4FieldConstructor {
+ val formClass = ""
+ val isCustom = false
+ val withFeedbackTooltip = false
+ def apply(fieldInfo: B4FieldInfo, inputHtml: Html)(implicit msgsProv: MessagesProvider) = inputHtml
+ def apply(contentHtml: Html, argsMap: Map[Symbol, Any])(implicit msgsProv: MessagesProvider) = contentHtml
+ }
+
+ val fooField = Form(single("foo" -> Forms.text))("foo")
+ def fooFieldFilled(v: String) = Form(single("foo" -> Forms.text)).fill(v)("foo")
+
+ "@inputType" should {
+
+ "allow setting a custom id" in {
+ val body = b4.inputType("text", fooField, Symbol("id") -> "someid").body
+ val idAttr = "id=\"someid\""
+ body must contain(idAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(idAttr) + idAttr.length) must not contain (idAttr)
+ }
+
+ "allow setting a custom type" in {
+ val body = b4.inputType("email", fooField).body
+ val typeAttr = "type=\"email\""
+ body must contain(typeAttr)
+ // Make sure it doesn't contain it twice
+ body.substring(body.indexOf(typeAttr) + typeAttr.length) must not contain (typeAttr)
+ }
+
+ "add form-control class as default" in {
+ b4.inputType("text", fooField).body must contain("class=\"form-control\"")
+ }
+
+ "allow setting additional classes" in {
+ b4.inputType("text", fooField, Symbol("class") -> "extra_class").body must contain(
+ "class=\"form-control extra_class\""
+ )
+ }
+
+ "allow setting a default value" in {
+ val body = b4.inputType("text", fooField, Symbol("value") -> "defaultvalue").body
+ val valueAttr = "value=\"defaultvalue\""
+ body must contain(valueAttr)
+ // Make sure it doesn't contain it twice
+ body.substring(body.indexOf(valueAttr) + valueAttr.length) must not contain (valueAttr)
+ }
+
+ "allow being filled with a value" in {
+ val body = b4.inputType("text", fooFieldFilled("filledvalue"), Symbol("value") -> "defaultvalue").body
+ val valueAttr = "value=\"filledvalue\""
+ body must contain(valueAttr)
+ // Make sure it doesn't contain it twice
+ body.substring(body.indexOf(valueAttr) + valueAttr.length) must not contain (valueAttr)
+ // Make sure it doesn't contain the default value
+ body must not contain ("value=\"defaultvalue\"")
+ }
+
+ "allow setting extra arguments and remove those arguments with false values or with underscored names" in {
+ val body = b4
+ .inputType(
+ "text",
+ fooField,
+ Symbol("extra_attr") -> "test",
+ Symbol("true_attr") -> true,
+ Symbol("fase_attr") -> false,
+ Symbol("_underscored_attr") -> "test"
+ )
+ .body
+ body must contain("extra_attr=\"test\"")
+ body must contain("true_attr=\"true\"")
+ body must not contain ("false_attr=\"false\"")
+ body must not contain ("_underscored_attr=\"test\"")
+ }
+ }
+
+ val sampleArgs = Seq[(Symbol, Any)](Symbol("id") -> "someid", Symbol("foo") -> "fooValue")
+ def sampleInputTypeBody(theType: String) = b4.inputType(theType, fooField, sampleArgs: _*).body.trim
+
+ "@text" should {
+ "be equivalent to inputType with text type" in {
+ b4.text(fooField, sampleArgs: _*).body.trim must be equalTo sampleInputTypeBody("text")
+ }
+ }
+ "@password" should {
+ "be equivalent to inputType with password type" in {
+ b4.password(fooField, sampleArgs: _*).body.trim must be equalTo sampleInputTypeBody("password")
+ }
+ "not display its value" in {
+ b4.password(fooFieldFilled("barValue"), sampleArgs: _*).body.trim must be equalTo sampleInputTypeBody("password")
+ }
+ }
+ "@file" should {
+ "be equivalent to inputType with file type" in {
+ val file = b4.file(fooField, sampleArgs: _*).body.trim
+ file must contain(""" true) +: sampleArgs): _*).body.trim
+ customFile must contain("""
""")
+ customFile must contain(""" "someid").body
+ val idAttr = "id=\"someid\""
+ body must contain(idAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(idAttr) + idAttr.length) must not contain (idAttr)
+ }
+
+ "add form-control class as default" in {
+ b4.textarea(fooField).body must contain("class=\"form-control\"")
+ }
+
+ "allow setting additional classes" in {
+ b4.textarea(fooField, Symbol("class") -> "extra_class").body must contain("class=\"form-control extra_class\"")
+ }
+
+ "allow setting a default value" in {
+ val body = b4.textarea(fooField, Symbol("value") -> "defaultvalue").body
+ body must contain(">defaultvalue")
+ body must not contain ("value=\"defaultvalue\"")
+ }
+ }
+
+ "@checkbox" should {
+
+ val boolField = Form(single("foo" -> Forms.boolean))("foo")
+ def boolFieldFilled(v: Boolean) = Form(single("foo" -> Forms.boolean)).fill(v)("foo")
+ def stringFieldFilled(v: String) = Form(single("foo" -> Forms.text)).fill(v)("foo")
+
+ "allow setting a custom id" in {
+ val body = b4.checkbox(boolField, Symbol("id") -> "someid").body
+ val idAttr = "id=\"someid\""
+ body must contain(idAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(idAttr) + idAttr.length) must not contain (idAttr)
+ }
+
+ "be unchecked by default" in {
+ val body = b4.checkbox(boolField).body
+ body must not contain ("checked")
+ body must contain("value=\"true\"")
+ }
+
+ "allow setting a default custom value" in {
+ val body = b4.checkbox(boolField, Symbol("value") -> "bar").body
+ body must not contain ("checked")
+ body must contain("value=\"bar\"")
+ }
+
+ "allow setting a default value for checked attribute" in {
+ val body = b4.checkbox(boolField, Symbol("_default") -> true).body
+ body must contain("checked")
+ body must contain("value=\"true\"")
+ }
+
+ "allow setting a default value for checked attribute with a custom value" in {
+ val body = b4.checkbox(boolField, Symbol("value") -> "bar", Symbol("_default") -> true).body
+ body must contain("checked")
+ body must contain("value=\"bar\"")
+ }
+
+ "allow being filled with a value" in {
+ val body = b4.checkbox(boolFieldFilled(true)).body
+ body must contain("checked")
+ body must contain("value=\"true\"")
+ }
+
+ "allow being filled with a custom value" in {
+ val body = b4.checkbox(stringFieldFilled("bar"), Symbol("value") -> "bar").body
+ body must contain("checked")
+ body must contain("value=\"bar\"")
+ }
+
+ "ignore default checked value if it is filled" in {
+ val body1 = b4.checkbox(boolFieldFilled(false), Symbol("_default") -> true).body
+ body1 must not contain ("checked")
+ body1 must contain("value=\"true\"")
+ val body2 = b4.checkbox(stringFieldFilled(""), Symbol("value") -> "bar", Symbol("_default") -> true).body
+ body2 must not contain ("checked")
+ body2 must contain("value=\"bar\"")
+ }
+
+ "allow setting a forced value for checked attribute (always true)" in {
+ val body = b4.checkbox(boolField, Symbol("checked") -> true).body
+ body must contain("checked")
+ body must contain("value=\"true\"")
+ }
+ "allow setting a forced value for checked attribute (always false)" in {
+ val body = b4.checkbox(boolField, Symbol("checked") -> false).body
+ body must not contain ("checked")
+ body must contain("value=\"true\"")
+ }
+
+ "ignore default and filled checked value if it has forced checked" in {
+ val body1 = b4.checkbox(boolFieldFilled(false), Symbol("_default") -> false, Symbol("checked") -> true).body
+ body1 must contain("checked")
+ body1 must contain("value=\"true\"")
+ val body2 = b4.checkbox(boolFieldFilled(true), Symbol("_default") -> true, Symbol("checked") -> false).body
+ body2 must not contain ("checked")
+ body2 must contain("value=\"true\"")
+ val body3 = b4
+ .checkbox(
+ stringFieldFilled(""),
+ Symbol("value") -> "bar",
+ Symbol("_default") -> false,
+ Symbol("checked") -> true
+ )
+ .body
+ body3 must contain("checked")
+ body3 must contain("value=\"bar\"")
+ val body4 = b4
+ .checkbox(
+ stringFieldFilled("bar"),
+ Symbol("value") -> "bar",
+ Symbol("_default") -> true,
+ Symbol("checked") -> false
+ )
+ .body
+ body4 must not contain ("checked")
+ body4 must contain("value=\"bar\"")
+ }
+
+ "add support to readonly attribute" in {
+ val bodyWithoutReadonly = b4.checkbox(boolField, Symbol("value") -> true).body
+ bodyWithoutReadonly must contain("
false, Symbol("value") -> true).body
+ bodyReadonlyFalse must contain("")
+
+ val bodyReadonlyTrue = b4.checkbox(boolField, Symbol("readonly") -> true, Symbol("value") -> true).body
+ bodyReadonlyTrue must contain("
")
+ }
+
+ "render custom checkbox properly" in {
+ val body = clean(b4.checkbox(boolField, Symbol("_custom") -> true, Symbol("_text") -> "theText").body)
+ body must contain("""
""")
+ body must contain("""""")
+ body must contain("""""")
+ }
+ }
+
+ "@radio" should {
+
+ val fruits = Seq("A" -> "Apples", "P" -> "Pears", "B" -> "Bananas")
+
+ "allow setting a custom id" in {
+ val body = b4.radio(fooField, fruits, Symbol("id") -> "someid").body
+ body must contain("id=\"someid_A\"")
+ body must contain("id=\"someid_P\"")
+ body must contain("id=\"someid_B\"")
+ }
+
+ "be unchecked by default" in {
+ b4.radio(fooField, fruits).body must not contain ("checked")
+ }
+
+ "allow setting a default value" in {
+ val body = b4.radio(fooField, fruits, Symbol("value") -> "B").body
+ val checkedAttr = "checked"
+ body must contain(checkedAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(checkedAttr) + checkedAttr.length) must not contain (checkedAttr)
+ }
+
+ "allow being filled with a value" in {
+ val body = b4.radio(fooFieldFilled("B"), fruits).body
+ val checkedAttr = "checked"
+ body must contain(checkedAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(checkedAttr) + checkedAttr.length) must not contain (checkedAttr)
+ }
+
+ "not be inline by default" in {
+ b4.radio(fooField, fruits).body must not contain ("form-check-inline")
+ }
+
+ "allow be inline" in {
+ b4.radio(fooField, fruits, Symbol("_inline") -> true).body must contain("form-check-inline")
+ }
+
+ "add support to readonly attribute" in {
+ val bodyWithoutReadonly = b4.radio(fooField, fruits, Symbol("value") -> "B").body
+ bodyWithoutReadonly must not contain ("radio-group")
+ bodyWithoutReadonly must not contain ("disabled")
+ bodyWithoutReadonly must not contain (" false, Symbol("value") -> "B").body
+ bodyReadonlyFalse must contain("
")
+ bodyReadonlyFalse must not contain ("disabled=\"true\"")
+ bodyReadonlyFalse must contain("")
+
+ val bodyReadonlyTrue = b4.radio(fooField, fruits, Symbol("readonly") -> true, Symbol("value") -> "B").body
+ bodyReadonlyTrue must contain("
")
+ bodyReadonlyTrue must contain("disabled=\"true\"")
+ bodyReadonlyTrue must contain("
")
+ }
+
+ "render custom radio properly" in {
+ val body = clean(b4.radio(fooField, fruits, Symbol("_custom") -> true).body)
+ body must contain("""
""")
+ body must contain(""" "Apples", "P" -> "Pears", "B" -> "Bananas")
+
+ "allow setting a custom id" in {
+ val body = b4.select(fooField, fruits, Symbol("id") -> "someid").body
+ body must contain("id=\"someid\"")
+ }
+
+ "add form-control class as default" in {
+ b4.select(fooField, fruits).body must contain("class=\"form-control\"")
+ }
+
+ "allow setting additional classes" in {
+ b4.select(fooField, fruits, Symbol("class") -> "extra_class").body must contain(
+ "class=\"form-control extra_class\""
+ )
+ }
+
+ "be unselected by default" in {
+ b4.select(fooField, fruits).body must not contain ("selected")
+ }
+
+ "allow setting a default value" in {
+ val body = b4.select(fooField, fruits, Symbol("value") -> "B").body
+ val selectedAttr = "selected"
+ body must contain(selectedAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(selectedAttr) + selectedAttr.length) must not contain (selectedAttr)
+ }
+
+ "allow being filled with a value" in {
+ val body = b4.select(fooFieldFilled("B"), fruits).body
+ val selectedAttr = "selected"
+ body must contain(selectedAttr)
+ // Make sure it doesn't have it twice
+ body.substring(body.indexOf(selectedAttr) + selectedAttr.length) must not contain (selectedAttr)
+ }
+
+ "add support to readonly attribute" in {
+ val bodyWithoutReadonly = b4.select(fooField, fruits, Symbol("value") -> "B").body
+ bodyWithoutReadonly must not contain ("
")
+ bodyWithoutReadonly must not contain ("disabled")
+ bodyWithoutReadonly must not contain (" false, Symbol("value") -> "B").body
+ bodyReadonlyFalse must contain("
")
+ bodyReadonlyFalse must not contain ("disabled=\"true\"")
+ bodyReadonlyFalse must contain("")
+
+ val bodyReadonlyTrue = b4.select(fooField, fruits, Symbol("readonly") -> true, Symbol("value") -> "B").body
+ bodyReadonlyTrue must contain("
")
+ bodyReadonlyTrue must contain("disabled=\"true\"")
+ bodyReadonlyTrue must contain("")
+ }
+
+ "allow multiple" in {
+ val body = b4.select(fooField, fruits, Symbol("multiple") -> true, Symbol("value") -> "P,B").body
+ body must contain("multiple=\"true\"")
+ val selectedAttr = "selected"
+ body must contain(selectedAttr)
+ // Make sure it has it twice, but not more.
+ val restBody = body.substring(body.indexOf(selectedAttr) + selectedAttr.length)
+ restBody must contain(selectedAttr)
+ restBody.substring(restBody.indexOf(selectedAttr) + selectedAttr.length) must not contain (selectedAttr)
+ }
+
+ "render custom select properly" in {
+ val body = b4.select(fooField, fruits, Symbol("_custom") -> true).body
+ body must contain("""