Skip to content

Commit

Permalink
Add label-class attribute to form field tag helpers
Browse files Browse the repository at this point in the history
It's relatively common to have the label child tag helper specified just
to add an additional CSS class. This removes the need to do that by
adding a `label-class` attribute at the top-level.
  • Loading branch information
gunndabad committed Jun 7, 2023
1 parent 693bd72 commit 4df77f2
Show file tree
Hide file tree
Showing 18 changed files with 72 additions and 25 deletions.
1 change: 1 addition & 0 deletions docs/components/character-count.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Check out the [max words validator](../validation/maxwords.md) for adding server
| `form-group-*` | | Additional attributes to add to the generated form-group wrapper element. |
| `id` | `string` | The `id` attribute for the generated `textarea` element. If not specified then a value is generated from the `name` attribute. |
| `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. |
| `label-class` | `string` | Additional classes for the generated `label` element. |
| `max-length` | `int?` | The maximum number of characters the generated `textarea` may contain. Required unless the `max-words` attribute is specified. |
| `max-words` | `int?` | The maximum number of words the generated `textarea` may contain. Required unless the `max-length` attribute is specified. |
| `name` | `string` | The `name` attribute for the generated `textarea` element. Required unless the `asp-for` attribute is specified. |
Expand Down
1 change: 1 addition & 0 deletions docs/components/file-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| `id` | `string` | The `id` attribute for the generated `input` element. If not specified then a value is generated from the `name` attribute. |
| `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. |
| `input-*` | | Additional attributes to add to the generated `input` element. |
| `label-class` | `string` | Additional classes for the generated `label` element. |
| `name` | `string` | The `name` attribute for the generated `input` element. Required unless the `asp-for` attribute is specified. |

### `<govuk-file-upload-label>`
Expand Down
1 change: 1 addition & 0 deletions docs/components/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
| `disabled` | `bool` | Whether the element should be disabled. The default is `false`. |
| `id` | `string` | The `id` attribute for the generated `select` element. If not specified then a value is generated from the `name` attribute. |
| `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. |
| `label-class` | `string` | Additional classes for the generated `label` element. |
| `name` | `string` | The `name` attribute for the generated `select` element. Required unless the `asp-for` attribute is specified. |
| `select-*` | | Additional attributes to add to the generated `select` element. |

Expand Down
1 change: 1 addition & 0 deletions docs/components/text-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The content is the HTML to use within the generated component.
| `id` | `string` | The `id` attribute for the generated `input` element. If not specified then a value is generated from the `name` attribute. |
| `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. |
| `input-*` | | Additional attributes to add to the generated `input` element. |
| `label-class` | `string` | Additional classes for the generated `label` element. |
| `name` | `string` | The `name` attribute for the generated `input` element. Required unless the `asp-for` attribute is specified. |
| `inputmode` | `string` | The `inputmode` attribute for the generated `input` element. |
| `pattern` | `string` | The `pattern` attribute for the generated `input` element. |
Expand Down
1 change: 1 addition & 0 deletions docs/components/textarea.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
| `disabled` | `bool` | Whether the textarea should be disabled. The default is `false`. |
| `id` | `string` | The `id` attribute for the generated `textarea` element. If not specified then a value is generated from the `name` attribute. |
| `ignore-modelstate-errors` | `bool` | Whether ModelState errors on the ModelExpression specified by the `asp-for` attribute should be ignored when generating an error message. The default is `false`. |
| `label-class` | `string` | Additional classes for the generated `label` element. |
| `name` | `string` | The `name` attribute for the generated `textarea` element. Required unless the `asp-for` attribute is specified. |
| `rows` | `int` | The `rows` attribute for the generated `textarea` element. The default is `5`. |
| `spellcheck` | `bool?` | The `spellcheck` attribute for the generated `textarea` element. The default is `null`. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class AttributeDictionaryExtensions
/// </summary>
/// <param name="attributeDictionary">The <see cref="AttributeDictionary"/> to add the CSS class name to.</param>
/// <param name="value">The CSS class name to add.</param>
public static void MergeCssClass(this AttributeDictionary attributeDictionary, string value)
public static void MergeCssClass(this AttributeDictionary attributeDictionary, string? value)
{
Guard.ArgumentNotNull(nameof(attributeDictionary), attributeDictionary);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class CharacterCountTagHelper : FormGroupTagHelperBase
private const string DisabledAttributeName = "disabled";
private const string FormGroupAttributesPrefix = "form-group-";
private const string IdAttributeName = "id";
private const string LabelClassAttributeName = "label-class";
private const string MaxLengthAttributeName = "max-length";
private const string MaxWordsLengthAttributeName = "max-words";
private const string NameAttributeName = "name";
Expand Down Expand Up @@ -86,6 +87,12 @@ internal CharacterCountTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IMod
[HtmlAttributeName(IdAttributeName)]
public string? Id { get; set; }

/// <summary>
/// Additional classes for the generated <c>label</c> element.
/// </summary>
[HtmlAttributeName(LabelClassAttributeName)]
public string? LabelClass { get; set; }

/// <summary>
/// The maximum number of characters the generated <c>textarea</c> may contain.
/// </summary>
Expand Down Expand Up @@ -217,7 +224,7 @@ private protected override IHtmlContent GenerateFormGroupContent(
{
var contentBuilder = new HtmlContentBuilder();

var label = GenerateLabel(formGroupContext);
var label = GenerateLabel(formGroupContext, LabelClass);
contentBuilder.AppendHtml(label);

var hint = GenerateHint(tagHelperContext, formGroupContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class FileUploadTagHelper : FormGroupTagHelperBase

private const string AttributesPrefix = "input-";
private const string IdAttributeName = "id";
private const string LabelClassAttributeName = "label-class";
private const string NameAttributeName = "name";

/// <summary>
Expand Down Expand Up @@ -52,6 +53,12 @@ internal FileUploadTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHe
[HtmlAttributeName(DictionaryAttributePrefix = AttributesPrefix)]
public IDictionary<string, string?> InputAttributes { get; set; } = new Dictionary<string, string?>();

/// <summary>
/// Additional classes for the generated <c>label</c> element.
/// </summary>
[HtmlAttributeName(LabelClassAttributeName)]
public string? LabelClass { get; set; }

/// <summary>
/// The <c>name</c> attribute for the generated <c>input</c> element.
/// </summary>
Expand All @@ -72,7 +79,7 @@ private protected override IHtmlContent GenerateFormGroupContent(
{
var contentBuilder = new HtmlContentBuilder();

var label = GenerateLabel(formGroupContext);
var label = GenerateLabel(formGroupContext, LabelClass);
contentBuilder.AppendHtml(label);

var hint = GenerateHint(tagHelperContext, formGroupContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ void AddErrorToFormErrorContext()
}
}

internal IHtmlContent GenerateLabel(FormGroupContext formGroupContext)
internal IHtmlContent GenerateLabel(FormGroupContext formGroupContext, string? labelClass)
{
// We need some content for the label; if AspFor is null then label content must have been specified
if (AspFor == null && formGroupContext.Label?.Content == null)
Expand All @@ -206,7 +206,9 @@ internal IHtmlContent GenerateLabel(FormGroupContext formGroupContext)

var isPageHeading = formGroupContext.Label?.IsPageHeading ?? ComponentGenerator.LabelDefaultIsPageHeading;
var content = formGroupContext.Label?.Content;
var attributes = formGroupContext.Label?.Attributes;

var attributes = formGroupContext.Label?.Attributes?.ToAttributeDictionary() ?? new();
attributes.MergeCssClass(labelClass);

var resolvedContent = content ??
new HtmlString(HtmlEncoder.Default.Encode(ModelHelper.GetDisplayName(ViewContext!, AspFor!.ModelExplorer, AspFor.Name) ?? string.Empty));
Expand Down
9 changes: 8 additions & 1 deletion src/GovUk.Frontend.AspNetCore/TagHelpers/SelectTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class SelectTagHelper : FormGroupTagHelperBase
private const string DescribedByAttributeName = "described-by";
private const string DisabledAttributeName = "disabled";
private const string IdAttributeName = "id";
private const string LabelClassAttributeName = "label-class";
private const string NameAttributeName = "name";

/// <summary>
Expand Down Expand Up @@ -65,6 +66,12 @@ internal SelectTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHelper
[HtmlAttributeName(IdAttributeName)]
public string? Id { get; set; }

/// <summary>
/// Additional classes for the generated <c>label</c> element.
/// </summary>
[HtmlAttributeName(LabelClassAttributeName)]
public string? LabelClass { get; set; }

/// <summary>
/// The <c>name</c> attribute for the generated <c>select</c> element.
/// </summary>
Expand Down Expand Up @@ -93,7 +100,7 @@ private protected override IHtmlContent GenerateFormGroupContent(

var contentBuilder = new HtmlContentBuilder();

var label = GenerateLabel(formGroupContext);
var label = GenerateLabel(formGroupContext, LabelClass);
contentBuilder.AppendHtml(label);

var hint = GenerateHint(tagHelperContext, formGroupContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class TextAreaTagHelper : FormGroupTagHelperBase
private const string AutocompleteAttributeName = "autocomplete";
private const string DisabledAttributeName = "disabled";
private const string IdAttributeName = "id";
private const string LabelClassAttributeName = "label-class";
private const string NameAttributeName = "name";
private const string RowsAttributeName = "rows";
private const string SpellcheckAttributeName = "spellcheck";
Expand Down Expand Up @@ -64,6 +65,12 @@ internal TextAreaTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHelp
[HtmlAttributeName(IdAttributeName)]
public string? Id { get; set; }

/// <summary>
/// Additional classes for the generated <c>label</c> element.
/// </summary>
[HtmlAttributeName(LabelClassAttributeName)]
public string? LabelClass { get; set; }

/// <summary>
/// The <c>name</c> attribute for the generated <c>textarea</c> element.
/// </summary>
Expand Down Expand Up @@ -105,7 +112,7 @@ private protected override IHtmlContent GenerateFormGroupContent(
{
var contentBuilder = new HtmlContentBuilder();

var label = GenerateLabel(formGroupContext);
var label = GenerateLabel(formGroupContext, LabelClass);
contentBuilder.AppendHtml(label);

var hint = GenerateHint(tagHelperContext, formGroupContext);
Expand Down
13 changes: 10 additions & 3 deletions src/GovUk.Frontend.AspNetCore/TagHelpers/TextInputTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class TextInputTagHelper : FormGroupTagHelperBase
private const string DisabledAttributeName = "disabled";
private const string IdAttributeName = "id";
private const string InputModeAttributeName = "inputmode";
private const string LabelClassAttributeName = "label-class";
private const string NameAttributeName = "name";
private const string PatternAttributeName = "pattern";
private const string SpellcheckAttributeName = "spellcheck";
Expand All @@ -46,8 +47,8 @@ public TextInputTagHelper()

internal TextInputTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHelper? modelHelper = null)
: base(
htmlGenerator ?? new ComponentGenerator(),
modelHelper ?? new DefaultModelHelper())
htmlGenerator ?? new ComponentGenerator(),
modelHelper ?? new DefaultModelHelper())
{
}

Expand Down Expand Up @@ -88,6 +89,12 @@ internal TextInputTagHelper(IGovUkHtmlGenerator? htmlGenerator = null, IModelHel
[HtmlAttributeName(DictionaryAttributePrefix = AttributesPrefix)]
public IDictionary<string, string?>? InputAttributes { get; set; } = new Dictionary<string, string?>();

/// <summary>
/// Additional classes for the generated <c>label</c> element.
/// </summary>
[HtmlAttributeName(LabelClassAttributeName)]
public string? LabelClass { get; set; }

/// <summary>
/// The <c>name</c> attribute for the generated <c>input</c> element.
/// </summary>
Expand Down Expand Up @@ -160,7 +167,7 @@ private protected override IHtmlContent GenerateFormGroupContent(

var contentBuilder = new HtmlContentBuilder();

var label = GenerateLabel(formGroupContext);
var label = GenerateLabel(formGroupContext, LabelClass);
contentBuilder.AppendHtml(label);

var hint = GenerateHint(tagHelperContext, formGroupContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public async Task ProcessAsync_WithMaxWords_GeneratesExpectedOutput()
Id = "my-id",
Name = "my-name",
MaxWords = 10,
Threshold = 90
Threshold = 90,
LabelClass = "additional-label-class"
};

// Act
Expand All @@ -61,7 +62,7 @@ public async Task ProcessAsync_WithMaxWords_GeneratesExpectedOutput()
var expectedHtml = @"
<div class=""govuk-character-count"" data-module=""govuk-character-count"" data-maxwords=""10"" data-threshold=""90"">
<div class=""govuk-form-group"">
<label class=""govuk-label"" for=""my-id"">The label</label>
<label class=""govuk-label additional-label-class"" for=""my-id"">The label</label>
<div class=""govuk-hint"" id=""my-id-hint"">The hint</div>
<textarea class=""govuk-textarea govuk-js-character-count"" id=""my-id"" name=""my-name"" rows=""5"" aria-describedby=""my-id-hint""></textarea>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
{
Id = "my-id",
DescribedBy = "describedby",
Name = "my-id"
Name = "my-id",
LabelClass = "additional-label-class"
};

// Act
Expand All @@ -54,7 +55,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
// Assert
var expectedHtml = @"
<div class=""govuk-form-group"">
<label for=""my-id"" class=""govuk-label"">The label</label>
<label for=""my-id"" class=""govuk-label additional-label-class"">The label</label>
<div id=""my-id-hint"" class=""govuk-hint"">The hint</div>
<input aria-describedby=""describedby my-id-hint"" class=""govuk-file-upload"" id=""my-id"" name=""my-id"" type=""file"">
</div>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ public void GenerateLabel_NoLabelContentOnContextOrAspFor_ThrowsInvalidOperation
};

// Act
var ex = Record.Exception(() => tagHelper.GenerateLabel(formGroupContext));
var ex = Record.Exception(() => tagHelper.GenerateLabel(formGroupContext, labelClass: null));

// Assert
Assert.IsType<InvalidOperationException>(ex);
Expand All @@ -533,7 +533,7 @@ public void GenerateLabel_LabelContentOnContext_ReturnsContentFromContext()
};

// Act
var label = tagHelper.GenerateLabel(formGroupContext);
var label = tagHelper.GenerateLabel(formGroupContext, labelClass: null);

// Assert
Assert.NotNull(label);
Expand Down Expand Up @@ -562,7 +562,7 @@ public void GenerateLabel_NoLabelContentOnContext_ReturnsContentFromAspFor()
};

// Act
var label = tagHelper.GenerateLabel(formGroupContext);
var label = tagHelper.GenerateLabel(formGroupContext, labelClass: null);

// Assert
Assert.NotNull(label);
Expand Down Expand Up @@ -592,7 +592,7 @@ public void GenerateLabel_LabelContentOnContextAndAspFor_ReturnsContentFromConte
};

// Act
var label = tagHelper.GenerateLabel(formGroupContext);
var label = tagHelper.GenerateLabel(formGroupContext, labelClass: null);

// Assert
Assert.NotNull(label);
Expand Down Expand Up @@ -751,7 +751,7 @@ private protected override IHtmlContent GenerateFormGroupContent(
{
var contentBuilder = new HtmlContentBuilder();

var label = GenerateLabel(formGroupContext);
var label = GenerateLabel(formGroupContext, labelClass: null);
contentBuilder.AppendHtml(label);

var hint = GenerateHint(tagHelperContext, formGroupContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
{
Id = "my-id",
DescribedBy = "describedby",
Name = "my-name"
Name = "my-name",
LabelClass = "additional-label-class"
};

// Act
Expand All @@ -79,7 +80,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
// Assert
var expectedHtml = @"
<div class=""govuk-form-group"">
<label for=""my-id"" class=""govuk-label"">The label</label>
<label for=""my-id"" class=""govuk-label additional-label-class"">The label</label>
<div id=""my-id-hint"" class=""govuk-hint"">The hint</div>
<select aria-describedby=""describedby my-id-hint"" class=""govuk-select"" id=""my-id"" name=""my-name"">
<option>First</option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
var tagHelper = new TextAreaTagHelper()
{
Id = "my-id",
Name = "my-name"
Name = "my-name",
LabelClass = "additional-label-class"
};

// Act
Expand All @@ -57,7 +58,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
// Assert
var expectedHtml = @"
<div class=""govuk-form-group"">
<label class=""govuk-label"" for=""my-id"">The label</label>
<label class=""govuk-label additional-label-class"" for=""my-id"">The label</label>
<div class=""govuk-hint"" id=""my-id-hint"">The hint</div>
<textarea class=""govuk-textarea govuk-js-textarea"" id=""my-id"" name=""my-name"" rows=""5"" aria-describedby=""my-id-hint""></textarea>
</div>";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
InputMode = "numeric",
Pattern = "[0-9]*",
Type = "number",
Value = "42"
Value = "42",
LabelClass = "additional-label-class"
};

// Act
Expand All @@ -59,7 +60,7 @@ public async Task ProcessAsync_GeneratesExpectedOutput()
// Assert
var expectedHtml = @"
<div class=""govuk-form-group"">
<label for=""my-id"" class=""govuk-label"">The label</label>
<label for=""my-id"" class=""govuk-label additional-label-class"">The label</label>
<div id=""my-id-hint"" class=""govuk-hint"">The hint</div>
<input
aria-describedby=""describedby my-id-hint""
Expand Down

0 comments on commit 4df77f2

Please sign in to comment.