Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCMSPUI-468: Create MOJ date picker component #31

Merged
merged 5 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Custom Thymeleaf Dialect for GOV.UK Buttons
# Custom Thymeleaf Dialect

## Introduction
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.

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.

---

## Installation

To use this custom Thymeleaf dialect, add the following dependency to your `build.gradle` file:

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

## How to use a Custom Dialect?

### 1. **Simplified Syntax**
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:

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:

```html

<govuk:button th:text="'Click Me!'" href="'/path'" id="'button-id'" classes="'custom-class'"/>
```

This simplifies templates and improves readability, making it easier for developers to focus on application logic rather than markup details.
This simplifies templates and improves readability, making it easier for developers to focus on application logic rather
than markup details.

### 2. **Dynamic Attribute Processing**
This dialect dynamically processes attributes like `th:*`, resolving them using Thymeleaf's expression language. For example:

This dialect dynamically processes attributes like `th:*`, resolving them using Thymeleaf's expression language. For
example:

```html

<govuk:button th:text="${buttonText}" th:href="${link}"/>
```

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

## Features
- **Anchor and Button Elements:** Supports both `<a>` and `<button>` elements based on the presence of an `href` attribute.
- **Dynamic Class Names:** Automatically includes the default `govuk-button` class and allows additional classes via the `classes` attribute.

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

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

### Prerequisites

- Thymeleaf 3.x
- Spring Boot (for integration)

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

### Details Element Tag Processor

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.
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.

#### Features

- Generates a `<details>` element with the `govuk-details` class.
- Includes a `<summary>` element with a customizable summary text.
- Includes a `<div>` element for detailed content.

#### Usage

To use this processor, define a `govuk:details` tag in your Thymeleaf templates and provide the following attributes:

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

#### Example

```html

<govuk:details summaryText="Click to view details" text="This is the detailed content."></govuk:details>
```

### MOJ Date picker Element Tag Processor

The `moj:datepicker` custom tag renders a date picker component using the GOV.UK Design System styles and behavior. This
component is useful for capturing date inputs in a standardized format.

---

### Parameters

| Parameter | Type | Description | Default Value |
|----------------|--------|--------------------------------------------------------------------------|---------------|
| `id` | String | The unique ID of the input field. | `"date"` |
| `name` | String | The name attribute for the input field. | `"date"` |
| `label` | String | The label text displayed above the date input. | `"Date"` |
| `hint` | String | Hint text displayed below the label to guide the user. | `""` |
| `errorMessage` | String | Error message displayed when the input field is invalid. | `""` |
| `minDate` | String | The minimum date allowed in the date picker (ISO format: `YYYY-MM-DD`). | `""` |
| `maxDate` | String | The maximum date allowed in the date picker (ISO format: `YYYY-MM-DD`). | `""` |
| `value` | String | The pre-filled value of the date input field (ISO format: `YYYY-MM-DD`). | `""` |

---

### Usage

Add the `moj:datepicker` tag to your Thymeleaf template with the required parameters:

```html
<moj:datepicker
id="dob"
name="dateOfBirth"
label="Date of Birth"
hint="For example, 01/01/2000."
error="Please enter a valid date of birth."
hasError="true"
dataMinDate="2000-01-01"
dataMaxDate="2025-12-31"
value="2024-01-01">
</moj:datepicker>

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package uk.gov.laa.ccms.springboot.dialect;

/**
* DatePickerAttributes.
*/
public record DatePickerAttributes(
String id,
String name,
String label,
String hint,
String errorMessage,
String value,
String minDate,
String maxDate) {

public boolean hasError() {
return errorMessage != null && !errorMessage.isBlank();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package uk.gov.laa.ccms.springboot.dialect;

import static org.springframework.util.StringUtils.hasText;

import java.util.Map;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IModelFactory;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.templatemode.TemplateMode;

/**
* Transforms <moj:datepicker/> elements into standard HTML button elements.
*/
public class DatePickerElementTagProcessor extends AbstractElementTagProcessor {

private static final String TAG_NAME = "datepicker";
private static final int PRECEDENCE = 900;

public DatePickerElementTagProcessor() {
super(TemplateMode.HTML, "moj", TAG_NAME, true, null, false, PRECEDENCE);
}

@Override
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
IElementTagStructureHandler structureHandler) {

Map<String, String> attributes = ProcessorUtils.parseAttributes(context, tag);
DatePickerAttributes datePickerAttributes = new DatePickerAttributes(
attributes.getOrDefault("id", "date"),
attributes.getOrDefault("name", "date"),
attributes.getOrDefault("label", "Date"),
attributes.get("hint"),
attributes.get("errorMessage"),
attributes.get("value"),
attributes.get("minDate"),
attributes.get("maxDate")
);

String datePickerHtml = buildDatePickerHtml(datePickerAttributes);
final IModelFactory modelFactory = context.getModelFactory();
final IModel model = modelFactory.parse(context.getTemplateData(), datePickerHtml);
structureHandler.replaceWith(model, false);

}

private String buildDatePickerHtml(DatePickerAttributes datePickerAttributes) {
StringBuilder html = new StringBuilder();
html.append("<div class=\"moj-datepicker\" data-module=\"moj-date-picker\"");

if (hasText(datePickerAttributes.minDate())) {
html.append(" data-min-date=\"").append(datePickerAttributes.minDate()).append("\"");
}
if (hasText(datePickerAttributes.maxDate())) {
html.append(" data-max-date=\"").append(datePickerAttributes.maxDate()).append("\"");
}

html.append(">")
.append("<div class=\"govuk-form-group");

if (datePickerAttributes.hasError()) {
html.append(" govuk-form-group--error");
}

html.append("\">")
.append("<label class=\"govuk-label\" for=\"").append(datePickerAttributes.id())
.append("\">")
.append(datePickerAttributes.label())
.append("</label>")
.append("<div id=\"").append(datePickerAttributes.id())
.append("-hint\" class=\"govuk-hint\">")
.append(datePickerAttributes.hint())
.append("</div>");

if (datePickerAttributes.hasError()) {
html.append("<p id=\"").append(datePickerAttributes.id())
.append("-error\" class=\"govuk-error-message\">")
.append("<span class=\"govuk-visually-hidden\">Error:</span> ")
.append(datePickerAttributes.errorMessage())
.append("</p>");
}

html.append("<input class=\"govuk-input moj-js-datepicker-input");

if (datePickerAttributes.hasError()) {
html.append(" govuk-input--error");
}

html.append("\" id=\"").append(datePickerAttributes.id())
.append("\" name=\"").append(datePickerAttributes.name())
.append("\" type=\"text\" aria-describedby=\"").append(datePickerAttributes.id())
.append("-hint");

if (datePickerAttributes.hasError()) {
html.append(" ").append(datePickerAttributes.id()).append("-error");
}

if (hasText(datePickerAttributes.value())) {
html.append("\" value=\"").append(datePickerAttributes.value());
}

html.append("\" autocomplete=\"off\">")
.append("</div>")
.append("</div>");

return html.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ public GovUkDialect govUkDialect() {
return new GovUkDialect();
}

@Bean
public MojCustomDialect mojCustomDialect() {
return new MojCustomDialect();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package uk.gov.laa.ccms.springboot.dialect;

import java.util.Set;
import org.thymeleaf.dialect.AbstractProcessorDialect;
import org.thymeleaf.processor.IProcessor;
import org.thymeleaf.standard.StandardDialect;

/**
* Develops a custom MoJ dialect.
*/
public class MojCustomDialect extends AbstractProcessorDialect {

public MojCustomDialect() {
super("MOJ Custom Dialect", "moj", StandardDialect.PROCESSOR_PRECEDENCE);
}

@Override
public Set<IProcessor> getProcessors(String dialectPrefix) {
return Set.of(new DatePickerElementTagProcessor());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package uk.gov.laa.ccms.springboot.dialect;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

@SpringBootTest(classes = ThymeleafTestConfig.class)
class MoJDatePickerElementTagProcessorTest {

@Autowired
private SpringTemplateEngine templateEngine;

@Test
void shouldRenderGovukButton() {

Context context = new Context();
String renderedHtml = templateEngine.process("test-datepicker", context);
assertThat(renderedHtml)
.contains(
"<div class=\"moj-datepicker\" data-module=\"moj-date-picker\" data-min-date=\"2000-01-01\" " +
"data-max-date=\"2025-12-31\"><div class=\"govuk-form-group govuk-form-group--error\">" +
"<label class=\"govuk-label\" for=\"dob\">Date of Birth</label><div id=\"dob-hint\" " +
"class=\"govuk-hint\">For example, 01/01/2000.</div><p id=\"dob-error\" " +
"class=\"govuk-error-message\"><span class=\"govuk-visually-hidden\">Error:</span> " +
"Please enter a valid date of birth.</p><input class=\"govuk-input moj-js-datepicker-input " +
"govuk-input--error\" id=\"dob\" name=\"dateOfBirth\" type=\"text\" " +
"aria-describedby=\"dob-hint dob-error\" value=\"2024-01-01\" autocomplete=\"off\">" +
"</div></div>")
.contains(
"<div class=\"moj-datepicker\" data-module=\"moj-date-picker\" " +
"data-min-date=\"2000-01-01\" data-max-date=\"2025-12-31\">" +
"<div class=\"govuk-form-group\"><label class=\"govuk-label\" " +
"for=\"dob\">Date of Birth</label><div id=\"dob-hint\" " +
"class=\"govuk-hint\">For example, 01/01/2000.</div><input " +
"class=\"govuk-input moj-js-datepicker-input\" id=\"dob\" name=\"dateOfBirth\" " +
"type=\"text\" aria-describedby=\"dob-hint\" value=\"2024-01-01\" " +
"autocomplete=\"off\"></div></div>");

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ThymeleafTestConfig {
public SpringTemplateEngine testTemplateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addDialect(new GovUkDialect());
templateEngine.addDialect(new MojCustomDialect());
templateEngine.addTemplateResolver(templateResolver());
return templateEngine;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html xmlns:moj="http://www.thymeleaf.org" lang="EN">
<head>
<title>Datepicker Test</title>
</head>
<body>

<moj:datepicker
id="dob"
name="dateOfBirth"
label="Date of Birth"
hint="For example, 01/01/2000."
errorMessage="Please enter a valid date of birth."
minDate="2000-01-01"
maxDate="2025-12-31"
value="2024-01-01">
</moj:datepicker>

<moj:datepicker
id="dob"
name="dateOfBirth"
label="Date of Birth"
hint="For example, 01/01/2000."
minDate="2000-01-01"
maxDate="2025-12-31"
value="2024-01-01">
</moj:datepicker>

</body>
</html>
Loading