Skip to content

Commit 94e04b2

Browse files
authored
Merge pull request #31 from ministryofjustice/story/CCMSPUI-468-create-date-picker
CCMSPUI-468: Create MOJ date picker component
2 parents f101831 + eaa33d6 commit 94e04b2

File tree

8 files changed

+303
-9
lines changed

8 files changed

+303
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
# Custom Thymeleaf Dialect for GOV.UK Buttons
1+
# Custom Thymeleaf Dialect
22

33
## Introduction
4-
This project provides a custom Thymeleaf dialect to simplify the creation and customization of GOV.UK-styled buttons. Using this custom dialect, developers can generate button HTML elements with the GOV.UK Design System's standards, reducing repetitive boilerplate code and ensuring consistency.
4+
5+
This project provides a custom Thymeleaf dialect to simplify the creation and customization of GOV.UK-styled buttons.
6+
Using this custom dialect, developers can generate button HTML elements with the GOV.UK Design System's standards,
7+
reducing repetitive boilerplate code and ensuring consistency.
58

69
---
710

811
## Installation
12+
913
To use this custom Thymeleaf dialect, add the following dependency to your `build.gradle` file:
1014

1115
```groovy
@@ -15,19 +19,28 @@ implementation 'uk.gov.laa.ccms.springboot:laa-ccms-spring-boot-starter-govuk-di
1519
---
1620

1721
## How to use a Custom Dialect?
22+
1823
### 1. **Simplified Syntax**
19-
Writing GOV.UK-styled buttons often involves verbose and repetitive HTML, especially when handling attributes like `class`, `id`, `data-*`, or conditional rendering logic. With this custom dialect, you can declare buttons using clean, concise tags like:
24+
25+
Writing GOV.UK-styled buttons often involves verbose and repetitive HTML, especially when handling attributes like
26+
`class`, `id`, `data-*`, or conditional rendering logic. With this custom dialect, you can declare buttons using clean,
27+
concise tags like:
2028

2129
```html
30+
2231
<govuk:button th:text="'Click Me!'" href="'/path'" id="'button-id'" classes="'custom-class'"/>
2332
```
2433

25-
This simplifies templates and improves readability, making it easier for developers to focus on application logic rather than markup details.
34+
This simplifies templates and improves readability, making it easier for developers to focus on application logic rather
35+
than markup details.
2636

2737
### 2. **Dynamic Attribute Processing**
28-
This dialect dynamically processes attributes like `th:*`, resolving them using Thymeleaf's expression language. For example:
38+
39+
This dialect dynamically processes attributes like `th:*`, resolving them using Thymeleaf's expression language. For
40+
example:
2941

3042
```html
43+
3144
<govuk:button th:text="${buttonText}" th:href="${link}"/>
3245
```
3346

@@ -36,8 +49,11 @@ This ensures that all attributes, including conditional and computed values, are
3649
---
3750

3851
## Features
39-
- **Anchor and Button Elements:** Supports both `<a>` and `<button>` elements based on the presence of an `href` attribute.
40-
- **Dynamic Class Names:** Automatically includes the default `govuk-button` class and allows additional classes via the `classes` attribute.
52+
53+
- **Anchor and Button Elements:** Supports both `<a>` and `<button>` elements based on the presence of an `href`
54+
attribute.
55+
- **Dynamic Class Names:** Automatically includes the default `govuk-button` class and allows additional classes via the
56+
`classes` attribute.
4157
- **Accessibility:** Includes `aria-disabled` and other accessibility attributes for disabled buttons.
4258
- **Custom Attributes:** Supports GOV.UK-specific attributes like `data-module` and `data-prevent-double-click`.
4359

@@ -46,6 +62,7 @@ This ensures that all attributes, including conditional and computed values, are
4662
## Usage
4763

4864
### Prerequisites
65+
4966
- Thymeleaf 3.x
5067
- Spring Boot (for integration)
5168

@@ -57,26 +74,72 @@ This ensures that all attributes, including conditional and computed values, are
5774
<!DOCTYPE html>
5875
<html xmlns:govuk="http://www.gov.uk">
5976
<body>
60-
<govuk:button th:text="'Click Me!'" href="'/test'" id="'button-id'" classes="'custom-class'"/>
77+
<govuk:button th:text="'Click Me!'" href="'/test'" id="'button-id'" classes="'custom-class'"/>
6178
</body>
6279
</html>
6380
```
6481

6582
### Details Element Tag Processor
6683

67-
The `DetailsElementTagProcessor` is a custom Thymeleaf tag processor that enables the use of a `<govuk:details>` tag to generate a `<details>` HTML element styled with the GOV.UK Design System classes.
84+
The `DetailsElementTagProcessor` is a custom Thymeleaf tag processor that enables the use of a `<govuk:details>` tag to
85+
generate a `<details>` HTML element styled with the GOV.UK Design System classes.
6886

6987
#### Features
88+
7089
- Generates a `<details>` element with the `govuk-details` class.
7190
- Includes a `<summary>` element with a customizable summary text.
7291
- Includes a `<div>` element for detailed content.
7392

7493
#### Usage
94+
7595
To use this processor, define a `govuk:details` tag in your Thymeleaf templates and provide the following attributes:
7696

7797
- **`summaryText`**: The text displayed in the summary section of the `<details>` element.
7898
- **`text`**: The content displayed inside the `<div>` when the details are expanded.
7999

80100
#### Example
101+
81102
```html
103+
82104
<govuk:details summaryText="Click to view details" text="This is the detailed content."></govuk:details>
105+
```
106+
107+
### MOJ Date picker Element Tag Processor
108+
109+
The `moj:datepicker` custom tag renders a date picker component using the GOV.UK Design System styles and behavior. This
110+
component is useful for capturing date inputs in a standardized format.
111+
112+
---
113+
114+
### Parameters
115+
116+
| Parameter | Type | Description | Default Value |
117+
|----------------|--------|--------------------------------------------------------------------------|---------------|
118+
| `id` | String | The unique ID of the input field. | `"date"` |
119+
| `name` | String | The name attribute for the input field. | `"date"` |
120+
| `label` | String | The label text displayed above the date input. | `"Date"` |
121+
| `hint` | String | Hint text displayed below the label to guide the user. | `""` |
122+
| `errorMessage` | String | Error message displayed when the input field is invalid. | `""` |
123+
| `minDate` | String | The minimum date allowed in the date picker (ISO format: `YYYY-MM-DD`). | `""` |
124+
| `maxDate` | String | The maximum date allowed in the date picker (ISO format: `YYYY-MM-DD`). | `""` |
125+
| `value` | String | The pre-filled value of the date input field (ISO format: `YYYY-MM-DD`). | `""` |
126+
127+
---
128+
129+
### Usage
130+
131+
Add the `moj:datepicker` tag to your Thymeleaf template with the required parameters:
132+
133+
```html
134+
<moj:datepicker
135+
id="dob"
136+
name="dateOfBirth"
137+
label="Date of Birth"
138+
hint="For example, 01/01/2000."
139+
error="Please enter a valid date of birth."
140+
hasError="true"
141+
dataMinDate="2000-01-01"
142+
dataMaxDate="2025-12-31"
143+
value="2024-01-01">
144+
</moj:datepicker>
145+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package uk.gov.laa.ccms.springboot.dialect;
2+
3+
/**
4+
* DatePickerAttributes.
5+
*/
6+
public record DatePickerAttributes(
7+
String id,
8+
String name,
9+
String label,
10+
String hint,
11+
String errorMessage,
12+
String value,
13+
String minDate,
14+
String maxDate) {
15+
16+
public boolean hasError() {
17+
return errorMessage != null && !errorMessage.isBlank();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package uk.gov.laa.ccms.springboot.dialect;
2+
3+
import static org.springframework.util.StringUtils.hasText;
4+
5+
import java.util.Map;
6+
import org.thymeleaf.context.ITemplateContext;
7+
import org.thymeleaf.model.IModel;
8+
import org.thymeleaf.model.IModelFactory;
9+
import org.thymeleaf.model.IProcessableElementTag;
10+
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
11+
import org.thymeleaf.processor.element.IElementTagStructureHandler;
12+
import org.thymeleaf.templatemode.TemplateMode;
13+
14+
/**
15+
* Transforms <moj:datepicker/> elements into standard HTML button elements.
16+
*/
17+
public class DatePickerElementTagProcessor extends AbstractElementTagProcessor {
18+
19+
private static final String TAG_NAME = "datepicker";
20+
private static final int PRECEDENCE = 900;
21+
22+
public DatePickerElementTagProcessor() {
23+
super(TemplateMode.HTML, "moj", TAG_NAME, true, null, false, PRECEDENCE);
24+
}
25+
26+
@Override
27+
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
28+
IElementTagStructureHandler structureHandler) {
29+
30+
Map<String, String> attributes = ProcessorUtils.parseAttributes(context, tag);
31+
DatePickerAttributes datePickerAttributes = new DatePickerAttributes(
32+
attributes.getOrDefault("id", "date"),
33+
attributes.getOrDefault("name", "date"),
34+
attributes.getOrDefault("label", "Date"),
35+
attributes.get("hint"),
36+
attributes.get("errorMessage"),
37+
attributes.get("value"),
38+
attributes.get("minDate"),
39+
attributes.get("maxDate")
40+
);
41+
42+
String datePickerHtml = buildDatePickerHtml(datePickerAttributes);
43+
final IModelFactory modelFactory = context.getModelFactory();
44+
final IModel model = modelFactory.parse(context.getTemplateData(), datePickerHtml);
45+
structureHandler.replaceWith(model, false);
46+
47+
}
48+
49+
private String buildDatePickerHtml(DatePickerAttributes datePickerAttributes) {
50+
StringBuilder html = new StringBuilder();
51+
html.append("<div class=\"moj-datepicker\" data-module=\"moj-date-picker\"");
52+
53+
if (hasText(datePickerAttributes.minDate())) {
54+
html.append(" data-min-date=\"").append(datePickerAttributes.minDate()).append("\"");
55+
}
56+
if (hasText(datePickerAttributes.maxDate())) {
57+
html.append(" data-max-date=\"").append(datePickerAttributes.maxDate()).append("\"");
58+
}
59+
60+
html.append(">")
61+
.append("<div class=\"govuk-form-group");
62+
63+
if (datePickerAttributes.hasError()) {
64+
html.append(" govuk-form-group--error");
65+
}
66+
67+
html.append("\">")
68+
.append("<label class=\"govuk-label\" for=\"").append(datePickerAttributes.id())
69+
.append("\">")
70+
.append(datePickerAttributes.label())
71+
.append("</label>")
72+
.append("<div id=\"").append(datePickerAttributes.id())
73+
.append("-hint\" class=\"govuk-hint\">")
74+
.append(datePickerAttributes.hint())
75+
.append("</div>");
76+
77+
if (datePickerAttributes.hasError()) {
78+
html.append("<p id=\"").append(datePickerAttributes.id())
79+
.append("-error\" class=\"govuk-error-message\">")
80+
.append("<span class=\"govuk-visually-hidden\">Error:</span> ")
81+
.append(datePickerAttributes.errorMessage())
82+
.append("</p>");
83+
}
84+
85+
html.append("<input class=\"govuk-input moj-js-datepicker-input");
86+
87+
if (datePickerAttributes.hasError()) {
88+
html.append(" govuk-input--error");
89+
}
90+
91+
html.append("\" id=\"").append(datePickerAttributes.id())
92+
.append("\" name=\"").append(datePickerAttributes.name())
93+
.append("\" type=\"text\" aria-describedby=\"").append(datePickerAttributes.id())
94+
.append("-hint");
95+
96+
if (datePickerAttributes.hasError()) {
97+
html.append(" ").append(datePickerAttributes.id()).append("-error");
98+
}
99+
100+
if (hasText(datePickerAttributes.value())) {
101+
html.append("\" value=\"").append(datePickerAttributes.value());
102+
}
103+
104+
html.append("\" autocomplete=\"off\">")
105+
.append("</div>")
106+
.append("</div>");
107+
108+
return html.toString();
109+
}
110+
}

laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-govuk-dialect/src/main/java/uk/gov/laa/ccms/springboot/dialect/GovUkThymeleafDialectConfig.java

+5
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,9 @@ public GovUkDialect govUkDialect() {
1919
return new GovUkDialect();
2020
}
2121

22+
@Bean
23+
public MojCustomDialect mojCustomDialect() {
24+
return new MojCustomDialect();
25+
}
26+
2227
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package uk.gov.laa.ccms.springboot.dialect;
2+
3+
import java.util.Set;
4+
import org.thymeleaf.dialect.AbstractProcessorDialect;
5+
import org.thymeleaf.processor.IProcessor;
6+
import org.thymeleaf.standard.StandardDialect;
7+
8+
/**
9+
* Develops a custom MoJ dialect.
10+
*/
11+
public class MojCustomDialect extends AbstractProcessorDialect {
12+
13+
public MojCustomDialect() {
14+
super("MOJ Custom Dialect", "moj", StandardDialect.PROCESSOR_PRECEDENCE);
15+
}
16+
17+
@Override
18+
public Set<IProcessor> getProcessors(String dialectPrefix) {
19+
return Set.of(new DatePickerElementTagProcessor());
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package uk.gov.laa.ccms.springboot.dialect;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.test.context.SpringBootTest;
8+
import org.thymeleaf.context.Context;
9+
import org.thymeleaf.spring6.SpringTemplateEngine;
10+
11+
@SpringBootTest(classes = ThymeleafTestConfig.class)
12+
class MoJDatePickerElementTagProcessorTest {
13+
14+
@Autowired
15+
private SpringTemplateEngine templateEngine;
16+
17+
@Test
18+
void shouldRenderGovukButton() {
19+
20+
Context context = new Context();
21+
String renderedHtml = templateEngine.process("test-datepicker", context);
22+
assertThat(renderedHtml)
23+
.contains(
24+
"<div class=\"moj-datepicker\" data-module=\"moj-date-picker\" data-min-date=\"2000-01-01\" " +
25+
"data-max-date=\"2025-12-31\"><div class=\"govuk-form-group govuk-form-group--error\">" +
26+
"<label class=\"govuk-label\" for=\"dob\">Date of Birth</label><div id=\"dob-hint\" " +
27+
"class=\"govuk-hint\">For example, 01/01/2000.</div><p id=\"dob-error\" " +
28+
"class=\"govuk-error-message\"><span class=\"govuk-visually-hidden\">Error:</span> " +
29+
"Please enter a valid date of birth.</p><input class=\"govuk-input moj-js-datepicker-input " +
30+
"govuk-input--error\" id=\"dob\" name=\"dateOfBirth\" type=\"text\" " +
31+
"aria-describedby=\"dob-hint dob-error\" value=\"2024-01-01\" autocomplete=\"off\">" +
32+
"</div></div>")
33+
.contains(
34+
"<div class=\"moj-datepicker\" data-module=\"moj-date-picker\" " +
35+
"data-min-date=\"2000-01-01\" data-max-date=\"2025-12-31\">" +
36+
"<div class=\"govuk-form-group\"><label class=\"govuk-label\" " +
37+
"for=\"dob\">Date of Birth</label><div id=\"dob-hint\" " +
38+
"class=\"govuk-hint\">For example, 01/01/2000.</div><input " +
39+
"class=\"govuk-input moj-js-datepicker-input\" id=\"dob\" name=\"dateOfBirth\" " +
40+
"type=\"text\" aria-describedby=\"dob-hint\" value=\"2024-01-01\" " +
41+
"autocomplete=\"off\"></div></div>");
42+
43+
}
44+
45+
}

laa-ccms-spring-boot-starters/laa-ccms-spring-boot-starter-govuk-dialect/src/test/java/uk/gov/laa/ccms/springboot/dialect/ThymeleafTestConfig.java

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class ThymeleafTestConfig {
1313
public SpringTemplateEngine testTemplateEngine() {
1414
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
1515
templateEngine.addDialect(new GovUkDialect());
16+
templateEngine.addDialect(new MojCustomDialect());
1617
templateEngine.addTemplateResolver(templateResolver());
1718
return templateEngine;
1819
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html xmlns:moj="http://www.thymeleaf.org" lang="EN">
3+
<head>
4+
<title>Datepicker Test</title>
5+
</head>
6+
<body>
7+
8+
<moj:datepicker
9+
id="dob"
10+
name="dateOfBirth"
11+
label="Date of Birth"
12+
hint="For example, 01/01/2000."
13+
errorMessage="Please enter a valid date of birth."
14+
minDate="2000-01-01"
15+
maxDate="2025-12-31"
16+
value="2024-01-01">
17+
</moj:datepicker>
18+
19+
<moj:datepicker
20+
id="dob"
21+
name="dateOfBirth"
22+
label="Date of Birth"
23+
hint="For example, 01/01/2000."
24+
minDate="2000-01-01"
25+
maxDate="2025-12-31"
26+
value="2024-01-01">
27+
</moj:datepicker>
28+
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)