Skip to content

Commit 04c72fd

Browse files
gunndabadCopilot
andauthored
Infer input type from model metadata (#448)
* Deduce input type from model metadata * Add unit tests for input type deduction from model metadata (#449) * Initial plan * Add unit tests for input type deduction - WIP Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> * Complete unit tests for input type deduction from model metadata Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> * Fix code formatting in TestModelMetadata Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com> * Add CHANGELOG entry --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: gunndabad <2041280+gunndabad@users.noreply.github.com>
1 parent 0f8bb5b commit 04c72fd

File tree

4 files changed

+417
-11
lines changed

4 files changed

+417
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ The deprecated `asp-for` attribute has been removed; the `for` attribute should
1010

1111
Adds support for the password tag helper's show/hide customisation.
1212

13+
Infer `type` attribute on `<govuk-input>` from the model metadata when `type` is not specified.
14+
1315
### Fixes
1416

1517
Fix error summary when items are specified with an explicit `href` attribute.

src/GovUk.Frontend.AspNetCore/TagHelpers/TextInputTagHelper.cs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ public class TextInputTagHelper : TagHelper
5555
private const string TypeAttributeName = "type";
5656
private const string ValueAttributeName = "value";
5757

58+
private static readonly HashSet<Type> _numberModelTypes = new()
59+
{
60+
typeof(byte),
61+
typeof(sbyte),
62+
typeof(short),
63+
typeof(ushort),
64+
typeof(int),
65+
typeof(uint),
66+
typeof(long),
67+
typeof(ulong)
68+
};
69+
5870
private readonly IComponentGenerator _componentGenerator;
5971
private readonly IModelHelper _modelHelper;
6072

@@ -233,6 +245,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
233245
var name = ResolveName();
234246
var id = ResolveId(name);
235247
var value = ResolveValue();
248+
var type = ResolveType();
236249
var labelOptions = textInputContext.GetLabelOptions(For, ViewContext!, _modelHelper, id, ForAttributeName);
237250
var hintOptions = textInputContext.GetHintOptions(For, _modelHelper);
238251
var errorMessageOptions = textInputContext.GetErrorMessageOptions(For, ViewContext!, _modelHelper, IgnoreModelStateErrors);
@@ -279,7 +292,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
279292
{
280293
Id = id,
281294
Name = name,
282-
Type = Type,
295+
Type = type,
283296
InputMode = InputMode,
284297
Value = value,
285298
Disabled = Disabled,
@@ -316,17 +329,73 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
316329
private string ResolveId(string name) =>
317330
Id ?? TagBuilder.CreateSanitizedId(name, Constants.IdAttributeDotReplacement);
318331

319-
private string ResolveName()
320-
{
321-
return Name is null && For is null
332+
private string ResolveName() =>
333+
Name is null && For is null
322334
? throw ExceptionHelper.AtLeastOneOfAttributesMustBeProvided(
323335
NameAttributeName,
324336
ForAttributeName)
325337
: Name ?? _modelHelper.GetFullHtmlFieldName(ViewContext!, For!.Name);
326-
}
327338

328-
private string? ResolveValue()
339+
private string? ResolveValue() =>
340+
_valueSpecified ? _value : For is not null ? _modelHelper.GetModelValue(ViewContext!, For.ModelExplorer, For.Name) : null;
341+
342+
private TemplateString ResolveType()
329343
{
330-
return _valueSpecified ? _value : For is not null ? _modelHelper.GetModelValue(ViewContext!, For.ModelExplorer, For.Name) : null;
344+
if (Type is not null)
345+
{
346+
return Type;
347+
}
348+
349+
if (For is not null)
350+
{
351+
if (For.Metadata.TemplateHint is { } templateHint && TryGetInputType(templateHint, out var inputType))
352+
{
353+
return inputType;
354+
}
355+
356+
if (For.Metadata.DataTypeName is { } dataTypeName && TryGetInputType(dataTypeName, out inputType))
357+
{
358+
return inputType;
359+
}
360+
361+
var underlyingModelType = For.Metadata.UnderlyingOrModelType;
362+
if (_numberModelTypes.Contains(underlyingModelType))
363+
{
364+
return "number";
365+
}
366+
}
367+
368+
return "text";
369+
370+
bool TryGetInputType(string dataTypeName, out string inputType)
371+
{
372+
switch (dataTypeName)
373+
{
374+
case "EmailAddress":
375+
inputType = "email";
376+
return true;
377+
case "Month":
378+
inputType = "month";
379+
return true;
380+
case "Password":
381+
inputType = "password";
382+
return true;
383+
case "PhoneNumber":
384+
inputType = "tel";
385+
return true;
386+
case "Search":
387+
inputType = "search";
388+
return true;
389+
case "Url":
390+
inputType = "url";
391+
return true;
392+
case "Week":
393+
inputType = "week";
394+
return true;
395+
default:
396+
inputType = "";
397+
return false;
398+
}
399+
}
331400
}
332401
}

0 commit comments

Comments
 (0)