Skip to content

Commit

Permalink
PLATUI-2916: Add beforeInput(s) and afterInput(s) options to form gro…
Browse files Browse the repository at this point in the history
…ups in play-frontend (#297)
  • Loading branch information
kyle-bowden committed Apr 22, 2024
1 parent 57a2517 commit 4b01f1d
Show file tree
Hide file tree
Showing 60 changed files with 730 additions and 37 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Expand Up @@ -8,12 +8,28 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
For compatibility information see `govukFrontendVersion` and `hmrcFrontendVersion` in
[LibDependencies](project/LibDependencies.scala)

## [9.7.0] - 2024-04-19

### Changed

- Added beforeInput(s) and afterInput(s) options to form groups

### Compatible with

- [hmrc/hmrc-frontend v6.15.0](https://github.com/hmrc/hmrc-frontend/releases/tag/v6.15.0)
- [alphagov/govuk-frontend v5.3.0](https://github.com/alphagov/govuk-frontend/releases/tag/v5.3.0)

## [9.6.1] - 2024-04-15

### Changed

- Added in caching with a max-age of 60 minutes for hmrc-frontend resources

### Compatible with

- [hmrc/hmrc-frontend v6.15.0](https://github.com/hmrc/hmrc-frontend/releases/tag/v6.15.0)
- [alphagov/govuk-frontend v5.3.0](https://github.com/alphagov/govuk-frontend/releases/tag/v5.3.0)

## [9.6.0] - 2024-04-10

### Changed
Expand Down
Expand Up @@ -18,7 +18,6 @@ package uk.gov.hmrc.hmrcfrontend.controllers

import org.scalatest.TestData
import org.scalatest.matchers.must.Matchers
import org.scalatest.matchers._
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.play.guice.GuiceOneAppPerTest
import play.api.Application
Expand Down
Expand Up @@ -19,10 +19,12 @@ package uk.gov.hmrc.govukfrontend.views.viewmodels
import play.api.libs.functional.syntax._
import play.api.libs.json._
import uk.gov.hmrc.govukfrontend.views.viewmodels.content.Content
import uk.gov.hmrc.govukfrontend.views.viewmodels.content.Content._

case class FormGroup(
classes: Option[String] = None,
attributes: Map[String, String] = Map.empty,
beforeInput: Option[Content] = None,
afterInput: Option[Content] = None
)

Expand All @@ -34,14 +36,30 @@ object FormGroup {
(
(__ \ "classes").readNullable[String] and
(__ \ "attributes").readWithDefault[Map[String, String]](Map.empty) and
(__ \ "beforeInput").readNullable[Content] and
(__ \ "afterInput").readNullable[Content]
)(FormGroup.apply _)

implicit def jsonWrites: OWrites[FormGroup] =
(
(__ \ "classes").writeNullable[String] and
(__ \ "attributes").write[Map[String, String]] and
(__ \ "beforeInput").writeNullable[Content] and
(__ \ "afterInput").writeNullable[Content]
)(unlift(FormGroup.unapply))

def jsonReadsForMultipleInputs: Reads[FormGroup] = (
(__ \ "classes").readNullable[String] and
(__ \ "attributes").readWithDefault[Map[String, String]](Map.empty) and
(__ \ "beforeInputs").readNullable[Content] and
(__ \ "afterInputs").readNullable[Content]
)(FormGroup.apply _)

def jsonWritesForMultipleInputs: OWrites[FormGroup] =
(
(__ \ "classes").writeNullable[String] and
(__ \ "attributes").write[Map[String, String]] and
(__ \ "beforeInputs").writeNullable[Content] and
(__ \ "afterInputs").writeNullable[Content]
)(unlift(FormGroup.unapply))
}
Expand Up @@ -21,6 +21,7 @@ import play.api.libs.json._
import uk.gov.hmrc.govukfrontend.views.html.components._
import uk.gov.hmrc.govukfrontend.views.viewmodels.CommonJsonFormats._
import uk.gov.hmrc.govukfrontend.views.viewmodels.FormGroup
import uk.gov.hmrc.govukfrontend.views.viewmodels.FormGroup.{jsonReadsForMultipleInputs, jsonWritesForMultipleInputs}

/** Parameters to `GovukCheckboxes` Twirl template
*
Expand Down Expand Up @@ -61,7 +62,7 @@ object Checkboxes {
(__ \ "fieldset").readNullable[Fieldset] and
(__ \ "hint").readNullable[Hint] and
(__ \ "errorMessage").readNullable[ErrorMessage] and
(__ \ "formGroup").readWithDefault[FormGroup](defaultObject.formGroup) and
(__ \ "formGroup").readWithDefault[FormGroup](defaultObject.formGroup)(jsonReadsForMultipleInputs) and
(__ \ "idPrefix").readNullable[String] and
(__ \ "name").readWithDefault[String](defaultObject.name) and
(__ \ "items").readWithDefault[Seq[CheckboxItem]](defaultObject.items)(forgivingSeqReads[CheckboxItem]) and
Expand All @@ -76,7 +77,7 @@ object Checkboxes {
(__ \ "fieldset").writeNullable[Fieldset] and
(__ \ "hint").writeNullable[Hint] and
(__ \ "errorMessage").writeNullable[ErrorMessage] and
(__ \ "formGroup").write[FormGroup] and
(__ \ "formGroup").write[FormGroup](jsonWritesForMultipleInputs) and
(__ \ "idPrefix").writeNullable[String] and
(__ \ "name").write[String] and
(__ \ "items").write[Seq[CheckboxItem]] and
Expand Down
Expand Up @@ -20,6 +20,7 @@ import play.api.libs.functional.syntax._
import play.api.libs.json._
import uk.gov.hmrc.govukfrontend.views.html.components._
import uk.gov.hmrc.govukfrontend.views.viewmodels.FormGroup
import uk.gov.hmrc.govukfrontend.views.viewmodels.FormGroup.{jsonReadsForMultipleInputs, jsonWritesForMultipleInputs}

/** Parameters to `GovukDateInput` Twirl template
*
Expand Down Expand Up @@ -56,7 +57,7 @@ object DateInput {
(__ \ "items").readWithDefault[Seq[InputItem]](defaultObject.items) and
(__ \ "hint").readNullable[Hint] and
(__ \ "errorMessage").readNullable[ErrorMessage] and
(__ \ "formGroup").readWithDefault[FormGroup](defaultObject.formGroup) and
(__ \ "formGroup").readWithDefault[FormGroup](defaultObject.formGroup)(jsonReadsForMultipleInputs) and
(__ \ "fieldset").readNullable[Fieldset] and
(__ \ "classes").readWithDefault[String](defaultObject.classes) and
(__ \ "attributes").readWithDefault[Map[String, String]](defaultObject.attributes)
Expand All @@ -69,7 +70,7 @@ object DateInput {
(__ \ "items").write[Seq[InputItem]] and
(__ \ "hint").writeNullable[Hint] and
(__ \ "errorMessage").writeNullable[ErrorMessage] and
(__ \ "formGroup").write[FormGroup] and
(__ \ "formGroup").write[FormGroup](jsonWritesForMultipleInputs) and
(__ \ "fieldset").writeNullable[Fieldset] and
(__ \ "classes").write[String] and
(__ \ "attributes").write[Map[String, String]]
Expand Down
Expand Up @@ -21,6 +21,7 @@ import play.api.libs.json._
import uk.gov.hmrc.govukfrontend.views.html.components._
import uk.gov.hmrc.govukfrontend.views.viewmodels.CommonJsonFormats._
import uk.gov.hmrc.govukfrontend.views.viewmodels.FormGroup
import uk.gov.hmrc.govukfrontend.views.viewmodels.FormGroup.{jsonReadsForMultipleInputs, jsonWritesForMultipleInputs}

/** Parameters to `GovukRadios` Twirl template
*
Expand Down Expand Up @@ -58,7 +59,7 @@ object Radios {
(__ \ "fieldset").readNullable[Fieldset] and
(__ \ "hint").readNullable[Hint] and
(__ \ "errorMessage").readNullable[ErrorMessage] and
(__ \ "formGroup").readWithDefault[FormGroup](defaultObject.formGroup) and
(__ \ "formGroup").readWithDefault[FormGroup](defaultObject.formGroup)(jsonReadsForMultipleInputs) and
(__ \ "idPrefix").readNullable[String] and
(__ \ "name").readWithDefault[String](defaultObject.name) and
(__ \ "items").readWithDefault[Seq[RadioItem]](defaultObject.items)(forgivingSeqReads[RadioItem]) and
Expand All @@ -72,7 +73,7 @@ object Radios {
(__ \ "fieldset").writeNullable[Fieldset] and
(__ \ "hint").writeNullable[Hint] and
(__ \ "errorMessage").writeNullable[ErrorMessage] and
(__ \ "formGroup").write[FormGroup] and
(__ \ "formGroup").write[FormGroup](jsonWritesForMultipleInputs) and
(__ \ "idPrefix").writeNullable[String] and
(__ \ "name").write[String] and
(__ \ "items").write[Seq[RadioItem]] and
Expand Down
Expand Up @@ -83,6 +83,7 @@
formGroup = formGroup.copy(
classes = Some(toClasses("govuk-character-count", formGroup.classes.getOrElse(""))),
attributes = dataAttributes ++ formGroup.attributes,
beforeInput = formGroup.beforeInput,
afterInput = Some(HtmlContent(characterCountMessagePlusOptionalAfterInput))
),
classes = toClasses("govuk-js-character-count", if (classes.isEmpty) "" else s" $classes"),
Expand Down
Expand Up @@ -57,6 +57,7 @@

@divSnippet(describedBy: String) = {
<div class="@toClasses("govuk-checkboxes", classes)"@toAttributes(attributes) data-module="govuk-checkboxes">
@{formGroup.beforeInput.map(_.asHtml)}
@for((item, index) <- items.zip(LazyList from 1)) {
@if(item.divider.exists(_.nonEmpty)) {
<div class="govuk-checkboxes__divider">@item.divider</div>
Expand Down Expand Up @@ -89,6 +90,7 @@
}
}
}
@{formGroup.afterInput.map(_.asHtml)}
</div>
}

Expand Down
Expand Up @@ -60,6 +60,7 @@

@divSnippet(unusedDescribedBy: String) = {
<div class="@toClasses("govuk-date-input", classes)"@toAttributes(attributes) id="@id">
@{formGroup.beforeInput.map(_.asHtml)}
@for(item <- dateInputItems) {
<div class="govuk-date-input__item">
@govukInput(Input(
Expand All @@ -80,6 +81,7 @@
))
</div>
}
@{formGroup.afterInput.map(_.asHtml)}
</div>
}

Expand Up @@ -37,6 +37,8 @@
@defining {
id="@id" name="@name" type="file"@value.mapNonEmpty { value => value="@value" }@if(disabled.getOrElse(false)) { disabled}@if(describedBy.nonEmpty) { aria-describedby="@describedBy"}@toAttributes(attributes)
} { attrs =>
@formGroup.beforeInput.map(_.asHtml)
<input class="@toClasses("govuk-file-upload", classes, errorMessage.fold("")(_ => "govuk-file-upload--error"))" @attrs>
@formGroup.afterInput.map(_.asHtml)
}
}
Expand Up @@ -33,24 +33,24 @@
@govukHintAndErrorMessage(id, describedBy, hint, errorMessage)(inputSnippet)
}

@inputPrefix(prefix: Option[PrefixOrSuffix], hasPrefix: Boolean, hasSuffix: Boolean, hasAfterInput: Boolean) = {@maybeWrapperStart(hasPrefix, hasSuffix, hasAfterInput)@maybePrefix(prefix, hasPrefix)}
@inputPrefix(prefix: Option[PrefixOrSuffix], hasPrefix: Boolean, hasSuffix: Boolean, hasBeforeInput: Boolean, hasAfterInput: Boolean) = {@maybeWrapperStart(hasPrefix, hasSuffix, hasBeforeInput, hasAfterInput)@maybePrefix(prefix, hasPrefix)}

@maybePrefix(prefix: Option[PrefixOrSuffix], hasPrefix: Boolean) ={@if(hasPrefix) {<div class="@toClasses("govuk-input__prefix", prefix.get.classes)" aria-hidden="true"@toAttributes(prefix.get.attributes)>@prefix.get.content.asHtml</div>
}}

@maybeWrapperStart(hasPrefix: Boolean, hasSuffix: Boolean, hasAfterInput: Boolean) = {@if(hasPrefix || hasSuffix || hasAfterInput) {
<div class="@toClasses("govuk-input__wrapper", inputWrapper.classes.getOrElse(""))"@toAttributes(inputWrapper.attributes)>
@maybeWrapperStart(hasPrefix: Boolean, hasSuffix: Boolean, hasBeforeInput: Boolean, hasAfterInput: Boolean) = {@if(hasPrefix || hasSuffix || hasBeforeInput || hasAfterInput) {
<div class="@toClasses("govuk-input__wrapper", inputWrapper.classes.getOrElse(""))"@toAttributes(inputWrapper.attributes)> @{formGroup.beforeInput.map(_.asHtml)}
}}

@inputSuffix(suffix: Option[PrefixOrSuffix], hasPrefix: Boolean, hasSuffix: Boolean) = {
@maybeSuffix(suffix, hasSuffix)@maybeWrapperEnd(hasPrefix: Boolean, hasSuffix: Boolean)
@inputSuffix(suffix: Option[PrefixOrSuffix], hasPrefix: Boolean, hasSuffix: Boolean, hasBeforeInput: Boolean, hasAfterInput: Boolean) = {
@maybeSuffix(suffix, hasSuffix)@maybeWrapperEnd(hasPrefix: Boolean, hasSuffix: Boolean, hasBeforeInput: Boolean, hasAfterInput: Boolean)
}

@maybeSuffix(suffix: Option[PrefixOrSuffix], hasSuffix: Boolean) = {@if(hasSuffix) {
<div class="@toClasses("govuk-input__suffix", suffix.get.classes)" aria-hidden="true"@toAttributes(suffix.get.attributes)>@suffix.get.content.asHtml</div>
}}

@maybeWrapperEnd(hasPrefix: Boolean, hasSuffix: Boolean) = {@if(hasPrefix || hasSuffix) {</div>}}
@maybeWrapperEnd(hasPrefix: Boolean, hasSuffix: Boolean, hasBeforeInput: Boolean, hasAfterInput: Boolean) = {@if(hasPrefix || hasSuffix || hasBeforeInput || hasAfterInput) {@{formGroup.afterInput.map(_.asHtml)} </div>}}

@inputSnippet(describedBy: String) = {
@defining {
Expand All @@ -60,6 +60,6 @@
@defining {
@autocomplete.mapNonEmpty { autocomplete => autocomplete="@autocomplete"}@pattern.mapNonEmpty { pattern => pattern="@pattern"}@inputmode.mapNonEmpty { inputmode => inputmode="@inputmode"}@autocapitalize.map {v => autocapitalize="@v"}@toAttributes(attributes)
} { otherAttrs =>
@(inputPrefix(params.prefix, params.prefix.nonEmpty, params.suffix.nonEmpty, formGroup.afterInput.nonEmpty))<input class="@toClasses("govuk-input", classes, errorMessage.fold("")(_ => "govuk-input--error"))" @attrs@otherAttrs>@(inputSuffix(params.suffix, params.prefix.nonEmpty, params.suffix.nonEmpty))@formGroup.afterInput.map(_.asHtml)
@(inputPrefix(params.prefix, params.prefix.nonEmpty, params.suffix.nonEmpty, formGroup.beforeInput.filter(_.nonEmpty), formGroup.afterInput.filter(_.nonEmpty))) <input class="@toClasses("govuk-input", classes, errorMessage.fold("")(_ => "govuk-input--error"))" @attrs@otherAttrs> @(inputSuffix(params.suffix, params.prefix.nonEmpty, params.suffix.nonEmpty, formGroup.beforeInput.filter(_.nonEmpty), formGroup.afterInput.filter(_.nonEmpty)))
}}
}
Expand Up @@ -52,6 +52,7 @@

@divSnippet(unusedDescribedBy: String) = {
<div class="@toClasses("govuk-radios", classes)"@toAttributes(attributes) data-module="govuk-radios">
@{formGroup.beforeInput.map(_.asHtml)}
@for((item, index) <- items.zip(LazyList from 1)) {
@defining(itemId(item, index)) { id =>
@defining(s"conditional-$id") { conditionalId =>
Expand Down Expand Up @@ -94,6 +95,7 @@
}
}}
}
@{formGroup.afterInput.map(_.asHtml)}
</div>
}

Expand Down
Expand Up @@ -40,6 +40,7 @@
@defining {
id="@id" name="@name"@if(disabled.getOrElse(false)) { disabled} @describedBy.toOption.map { describedBy => aria-describedby="@describedBy"}@toAttributes(attributes)
} { attrs =>
@{formGroup.beforeInput.map(_.asHtml)}
<select class="@toClasses("govuk-select", classes, errorMessage.fold("")(_ => "govuk-select--error"))" @attrs>
@for(item <- items) {
@defining((value.nonEmpty && item.value == value) || item.selected) { isSelected =>
Expand All @@ -52,5 +53,6 @@
}
}
</select>
@{formGroup.afterInput.map(_.asHtml)}
}
}
Expand Up @@ -41,6 +41,6 @@
@defining {
id="@id" name="@name" rows="@if(rows != 0) {@rows} else {5}"@if(describedBy.nonEmpty) { aria-describedby="@describedBy"}@autocompleteAttr@if(spellcheck.nonEmpty){spellcheck="@if(spellcheck.getOrElse(true)){true}else{false}"}@if(disabled.getOrElse(false)) { disabled} @toAttributes(attributes)
} { attrs =>
<textarea class="@toClasses("govuk-textarea", errorMessage.fold("")(_ => "govuk-textarea--error"), classes)" @attrs>@value.mapNonEmpty { value =>@HtmlFormat.escape(value)}</textarea>
@formGroup.beforeInput.map(_.asHtml) <textarea class="@toClasses("govuk-textarea", errorMessage.fold("")(_ => "govuk-textarea--error"), classes)" @attrs>@value.mapNonEmpty { value =>@HtmlFormat.escape(value)}</textarea>
}}
}
@@ -0,0 +1,3 @@
{
"name" : "govukCharacterCount"
}
@@ -0,0 +1,16 @@
{
"id" : "with-formgroup",
"name" : "with-formgroup",
"maxlength" : 10,
"label" : {
"text" : "With formgroup"
},
"formGroup" : {
"beforeInput": { "html": "<p>test before</p>"},
"afterInput": { "html": "<p>test after</p>"},
"attributes" : {
"some-data-attribute": "true",
"some-other-data-attribute": "eleven"
}
}
}
@@ -0,0 +1,11 @@
<div class="govuk-form-group govuk-character-count" data-module="govuk-character-count" data-maxlength="10" some-data-attribute="true" some-other-data-attribute="eleven">
<label class="govuk-label" for="with-formgroup">
With formgroup
</label>
<p>test before</p>
<textarea class="govuk-textarea govuk-js-character-count" id="with-formgroup" name="with-formgroup" rows="5" aria-describedby="with-formgroup-info"></textarea>
<div id="with-formgroup-info" class="govuk-hint govuk-character-count__message">
You can enter up to 10 characters
</div>
<p>test after</p>
</div>
@@ -0,0 +1,3 @@
{
"name" : "govukCharacterCount"
}
@@ -0,0 +1,16 @@
{
"id" : "with-formgroup",
"name" : "with-formgroup",
"maxlength" : 10,
"label" : {
"text" : "With formgroup"
},
"formGroup" : {
"beforeInput": { "text": "test before"},
"afterInput": { "text": "test after"},
"attributes" : {
"some-data-attribute": "true",
"some-other-data-attribute": "eleven"
}
}
}
@@ -0,0 +1,11 @@
<div class="govuk-form-group govuk-character-count" data-module="govuk-character-count" data-maxlength="10" some-data-attribute="true" some-other-data-attribute="eleven">
<label class="govuk-label" for="with-formgroup">
With formgroup
</label>
test before
<textarea class="govuk-textarea govuk-js-character-count" id="with-formgroup" name="with-formgroup" rows="5" aria-describedby="with-formgroup-info"></textarea>
<div id="with-formgroup-info" class="govuk-hint govuk-character-count__message">
You can enter up to 10 characters
</div>
test after
</div>
@@ -0,0 +1,3 @@
{
"name" : "govukCheckboxes"
}
@@ -0,0 +1,37 @@
{
"name" : "how-contacted-checked",
"idPrefix" : "how-contacted-checked",
"formGroup" : {
"beforeInputs": { "html": "<p>test before</p>"},
"afterInputs": { "html": "<p>test after</p>"},
"attributes" : {
"some-data-attribute": "true",
"some-other-data-attribute": "eleven"
}
},
"fieldset" : {
"legend" : {
"text" : "How do you want to be contacted?"
}
},
"items" : [ {
"value" : "email",
"text" : "Email",
"conditional" : {
"html" : "<label class=\"govuk-label\" for=\"context-email\">Email address</label>\n<input class=\"govuk-input govuk-!-width-one-third\" name=\"context-email\" type=\"text\" id=\"context-email\">\n"
}
}, {
"value" : "phone",
"text" : "Phone",
"checked" : true,
"conditional" : {
"html" : "<label class=\"govuk-label\" for=\"contact-phone\">Phone number</label>\n<span id=\"contact-phone-error\" class=\"govuk-error-message\">Problem with input</span>\n<input class=\"govuk-input govuk-input--error govuk-!-width-one-third\" name=\"contact-phone\" type=\"text\" id=\"contact-phone\" aria-describedby=\"contact-phone-error\">\n"
}
}, {
"value" : "text",
"text" : "Text message",
"conditional" : {
"html" : "<label class=\"govuk-label\" for=\"contact-text-message\">Mobile phone number</label>\n<input class=\"govuk-input govuk-!-width-one-third\" name=\"contact-text-message\" type=\"text\" id=\"contact-text-message\">\n"
}
} ]
}

0 comments on commit 4b01f1d

Please sign in to comment.