Skip to content

Commit f68b2cf

Browse files
authored
Merge pull request #94 from HtmlTags/validators
Validator support
2 parents 90e2dee + acf3c8a commit f68b2cf

19 files changed

+186
-45
lines changed

src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,30 +91,34 @@
9191
Html.LinkButton("Cancel", nameof(HomeController.Index))))
9292

9393
Input Tag value:
94-
<input-tag for="Value" class="foo"></input-tag>
94+
<input-tag for="Value" class="foo" />
9595
Editor for value:
9696
@Html.EditorFor(m => m.Value)
9797
Input tag blarg:
98-
<input-tag for="Blarg"></input-tag>
98+
<input-tag for="Blarg" />
9999
Editor for blarg:
100100
@Html.EditorFor(m => m.Blarg)
101101
Input tag blorg.blorg:
102-
<input-tag for="Blorg.Blorg"></input-tag>
102+
<input-tag for="Blorg.Blorg" />
103103
Editor for blorg.blorg:
104104
@Html.EditorFor(m => m.Blorg.Blorg)
105105
Input tag Blorgs[0].Blarg:
106-
<input-tag for="Blorgs[0].Blorg"></input-tag>
106+
<input-tag for="Blorgs[0].Blorg" />
107107
Editor for Blorgs[0].Blarg:
108108
@Html.EditorFor(m => m.Blorgs[0].Blorg)
109109
@Html.Input(m => m.Value).AddClass("foo")
110110
Display tag for Value:
111-
<display-tag for="Value"></display-tag>
111+
<display-tag for="Value" />
112112
Display for Vlaue:
113113
@Html.Display(m => m.Value)
114114
Label tag for Value:
115-
<label-tag for="Value"></label-tag>
115+
<label-tag for="Value" />
116116
Label for Value:
117117
@Html.Label(m => m.Value)
118+
Valiidator tag for Value:
119+
<validation-message-tag for="Value" />
120+
Validation for Value:
121+
@Html.ValidationMessage(m => m.Value)
118122

119123
@{
120124
var tag = new HtmlTag("canvas");
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Linq;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using Microsoft.AspNetCore.Mvc.Rendering;
5+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
6+
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
7+
8+
namespace HtmlTags.Conventions.Elements.Builders
9+
{
10+
public class DefaultValidationMessageBuilder : IElementBuilder
11+
{
12+
public HtmlTag Build(ElementRequest request)
13+
{
14+
var viewContext = request.Get<ViewContext>() ?? throw new InvalidOperationException("Validation messages require a ViewContext");
15+
16+
var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
17+
if (!viewContext.ViewData.ModelState.ContainsKey(request.ElementId) && formContext == null)
18+
{
19+
return HtmlTag.Empty();
20+
}
21+
22+
var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(request.ElementId, out var entry);
23+
var modelErrors = tryGetModelStateResult ? entry.Errors : null;
24+
25+
ModelError modelError = null;
26+
if (modelErrors != null && modelErrors.Count != 0)
27+
{
28+
modelError = modelErrors.FirstOrDefault(m => !string.IsNullOrEmpty(m.ErrorMessage)) ?? modelErrors[0];
29+
}
30+
31+
if (modelError == null && formContext == null)
32+
{
33+
return HtmlTag.Empty();
34+
}
35+
36+
var tag = new HtmlTag(viewContext.ValidationMessageElement);
37+
38+
var className = modelError != null ?
39+
HtmlHelper.ValidationMessageCssClassName :
40+
HtmlHelper.ValidationMessageValidCssClassName;
41+
tag.AddClass(className);
42+
43+
if (modelError != null)
44+
{
45+
var modelExplorer = request.Get<ModelExplorer>() ?? throw new InvalidOperationException("Validation messages require a ModelExplorer");
46+
tag.Text(ValidationHelpers.GetModelErrorMessageOrDefault(modelError, entry, modelExplorer));
47+
}
48+
49+
if (formContext != null)
50+
{
51+
tag.Attr("data-valmsg-for", request.ElementId);
52+
53+
tag.Attr("data-valmsg-replace", "true");
54+
}
55+
56+
return tag;
57+
}
58+
}
59+
}

src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ public static HtmlTag Input<T, TResult>(this IHtmlHelper<T> helper, Expression<F
2323
return generator.InputFor(expression);
2424
}
2525

26+
public static HtmlTag ValidationMessage<T, TResult>(this IHtmlHelper<T> helper, Expression<Func<T, TResult>> expression)
27+
where T : class
28+
{
29+
var generator = GetGenerator(helper, expression);
30+
return generator.ValidationMessageFor(expression);
31+
}
32+
2633
public static HtmlTag Label<T, TResult>(this IHtmlHelper<T> helper, Expression<Func<T, TResult>> expression)
2734
where T : class
2835
{

src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ namespace HtmlTags
1111
{
1212
public static class ModelMetadataTagExtensions
1313
{
14-
public static void ModelMetadata(this HtmlConventionRegistry registry)
14+
public static HtmlConventionRegistry ModelMetadata(this HtmlConventionRegistry registry)
1515
{
1616
registry.Labels.Modifier<DisplayNameElementModifier>();
1717
registry.Displays.Modifier<MetadataModelDisplayModifier>();
1818
registry.Editors.Modifier<MetadataModelEditModifier>();
1919
registry.Editors.Modifier<PlaceholderElementModifier>();
2020
registry.Editors.Modifier<ModelStateErrorsModifier>();
2121
registry.Editors.Modifier<ClientSideValidationModifier>();
22+
23+
return registry;
2224
}
2325

2426
private class DisplayNameElementModifier : IElementModifier
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using HtmlTags.Conventions;
2+
using HtmlTags.Conventions.Elements;
3+
using HtmlTags.Conventions.Elements.Builders;
4+
5+
namespace HtmlTags
6+
{
7+
public static class ModelStateTagExtensions
8+
{
9+
public static HtmlConventionRegistry ModelState(this HtmlConventionRegistry registry)
10+
{
11+
registry.ValidationMessages.Always.BuildBy<DefaultValidationMessageBuilder>();
12+
registry.ValidationMessages.NamingConvention(new DotNotationElementNamingConvention());
13+
14+
return registry;
15+
}
16+
}
17+
}

src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,50 @@
66

77
public static class ServiceCollectionExtensions
88
{
9+
/// <summary>
10+
/// Configures HtmlTags without ASP.NET Core defaults without modifying the library
11+
/// </summary>
12+
/// <param name="services">Service collection</param>
13+
/// <param name="library">Convention library</param>
14+
/// <returns>Service collection</returns>
915
public static IServiceCollection AddHtmlTags(this IServiceCollection services, HtmlConventionLibrary library) => services.AddSingleton(library);
1016

17+
/// <summary>
18+
/// Configures HtmlTags with ASP.NET Core defaults
19+
/// </summary>
20+
/// <param name="services">Service collection</param>
21+
/// <param name="registries">Custom convention registries</param>
22+
/// <returns>Service collection</returns>
1123
public static IServiceCollection AddHtmlTags(this IServiceCollection services, params HtmlConventionRegistry[] registries)
1224
{
1325
var library = new HtmlConventionLibrary();
1426
foreach (var registry in registries)
1527
{
1628
registry.Apply(library);
1729
}
30+
31+
var defaultRegistry = new HtmlConventionRegistry()
32+
.Defaults()
33+
.ModelMetadata()
34+
.ModelState();
35+
36+
defaultRegistry.Apply(library);
37+
1838
return services.AddHtmlTags(library);
1939
}
2040

41+
/// <summary>
42+
/// Configures HtmlTags with ASP.NET Core defaults
43+
/// </summary>
44+
/// <param name="services">Service collection</param>
45+
/// <param name="config">Additional configuration callback</param>
46+
/// <returns>Service collection</returns>
2147
public static IServiceCollection AddHtmlTags(this IServiceCollection services, Action<HtmlConventionRegistry> config)
2248
{
2349
var registry = new HtmlConventionRegistry();
2450

2551
config(registry);
2652

27-
registry.Defaults();
28-
registry.ModelMetadata();
29-
3053
return services.AddHtmlTags(registry);
3154
}
3255
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace HtmlTags
2+
{
3+
using Conventions.Elements;
4+
using Microsoft.AspNetCore.Razor.TagHelpers;
5+
6+
[HtmlTargetElement("validation-message-tag", Attributes = ForAttributeName, TagStructure = TagStructure.WithoutEndTag)]
7+
public class ValidationMessageTagHelper : HtmlTagTagHelper
8+
{
9+
protected override string Category { get; } = ElementConstants.ValidationMessage;
10+
}
11+
}

src/HtmlTags/Cache.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,11 @@ public Cache(IDictionary<TKey, TValue> dictionary)
4242
_values = dictionary;
4343
}
4444

45-
public Func<TKey, TValue> OnMissing { set { _onMissing = value; } }
45+
public Func<TKey, TValue> OnMissing { set => _onMissing = value; }
4646

47-
public Func<TValue, TKey> GetKey { get { return _getKey; } set { _getKey = value; } }
47+
public Func<TValue, TKey> GetKey { get => _getKey;
48+
set => _getKey = value;
49+
}
4850

4951
public int Count => _values.Count;
5052

src/HtmlTags/Conventions/ElementGenerator.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ private ElementGenerator(ITagGenerator tags)
1717

1818
public static ElementGenerator<T> For(HtmlConventionLibrary library, Func<Type, object> serviceLocator = null, T model = null)
1919
{
20-
serviceLocator = serviceLocator ?? (Activator.CreateInstance);
20+
serviceLocator = serviceLocator ?? Activator.CreateInstance;
2121

2222
var tags = new TagGenerator(library.TagLibrary, new ActiveProfile(), serviceLocator);
2323

@@ -33,6 +33,9 @@ public HtmlTag LabelFor<TResult>(Expression<Func<T, TResult>> expression, string
3333
public HtmlTag InputFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null)
3434
=> Build(expression, ElementConstants.Editor, profile, model);
3535

36+
public HtmlTag ValidationMessageFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null)
37+
=> Build(expression, ElementConstants.ValidationMessage, profile, model);
38+
3639
public HtmlTag DisplayFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null)
3740
=> Build(expression, ElementConstants.Display, profile, model);
3841

@@ -41,8 +44,8 @@ public HtmlTag TagFor<TResult>(Expression<Func<T, TResult>> expression, string c
4144

4245
public T Model
4346
{
44-
get { return _model.Value; }
45-
set { _model = new Lazy<T>(() => value); }
47+
get => _model.Value;
48+
set => _model = new Lazy<T>(() => value);
4649
}
4750

4851
public ElementRequest GetRequest<TResult>(Expression<Func<T, TResult>> expression, T model = null)
@@ -70,6 +73,8 @@ private HtmlTag Build(ElementRequest request, string category, string profile =
7073

7174
public HtmlTag InputFor(ElementRequest request, string profile = null, T model = null) => Build(request, ElementConstants.Editor, profile, model);
7275

76+
public HtmlTag ValidationMessageFor(ElementRequest request, string profile = null, T model = null) => Build(request, ElementConstants.ValidationMessage, profile, model);
77+
7378
public HtmlTag DisplayFor(ElementRequest request, string profile = null, T model = null) => Build(request, ElementConstants.Display, profile, model);
7479

7580
public HtmlTag TagFor(ElementRequest request, string category, string profile = null, T model = null) => Build(request, category, profile, model);

src/HtmlTags/Conventions/Elements/Builders/TextboxBuilder.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ namespace HtmlTags.Conventions.Elements.Builders
22
{
33
public class TextboxBuilder : IElementBuilder
44
{
5-
public bool Matches(ElementRequest subject) => true;
6-
75
public HtmlTag Build(ElementRequest request)
86
{
97
return new TextboxTag().Attr("value", (request.RawValue ?? string.Empty).ToString());

src/HtmlTags/Conventions/Elements/ElementConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public static class ElementConstants
55
public static readonly string Label = "Label";
66
public static readonly string Display = "Display";
77
public static readonly string Editor = "Editor";
8+
public static readonly string ValidationMessage = "ValidationMessage";
89

910
public static readonly string Templates = "Templates";
1011
}

src/HtmlTags/Conventions/Elements/IElementGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ public interface IElementGenerator<T> where T : class
88
T Model { get; set; }
99
HtmlTag LabelFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null);
1010
HtmlTag InputFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null);
11+
HtmlTag ValidationMessageFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null);
1112
HtmlTag DisplayFor<TResult>(Expression<Func<T, TResult>> expression, string profile = null, T model = null);
1213
HtmlTag TagFor<TResult>(Expression<Func<T, TResult>> expression, string category, string profile = null, T model = null);
1314

1415
HtmlTag LabelFor(ElementRequest request, string profile = null, T model = null);
1516
HtmlTag InputFor(ElementRequest request, string profile = null, T model = null);
17+
HtmlTag ValidationMessageFor(ElementRequest request, string profile = null, T model = null);
1618
HtmlTag DisplayFor(ElementRequest request, string profile = null, T model = null);
1719
HtmlTag TagFor(ElementRequest request, string category, string profile = null, T model = null);
1820
}

src/HtmlTags/Conventions/Formatting/GetStringRequest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public Type PropertyType
7373

7474
return _propertyType;
7575
}
76-
set { _propertyType = value; }
76+
set => _propertyType = value;
7777
}
7878

7979
public PropertyInfo Property { get; }

src/HtmlTags/Conventions/HtmlConventionRegistryExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace HtmlTags.Conventions
55

66
public static class HtmlConventionRegistryExtensions
77
{
8-
public static void Defaults(this HtmlConventionRegistry registry)
8+
public static HtmlConventionRegistry Defaults(this HtmlConventionRegistry registry)
99
{
1010
registry.Editors.BuilderPolicy<CheckboxBuilder>();
1111

@@ -21,7 +21,7 @@ public static void Defaults(this HtmlConventionRegistry registry)
2121

2222
registry.Labels.Always.BuildBy<DefaultLabelBuilder>();
2323

24-
24+
return registry;
2525
}
2626
}
2727
}

src/HtmlTags/Conventions/ProfileExpression.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public ProfileExpression(HtmlConventionLibrary library, string profileName)
2121

2222
public ElementCategoryExpression Editors => new ElementCategoryExpression(BuildersFor(ElementConstants.Editor));
2323

24+
public ElementCategoryExpression ValidationMessages => new ElementCategoryExpression(BuildersFor(ElementConstants.ValidationMessage));
25+
2426
public void Apply(HtmlConventionLibrary library) => library.Import(Library);
2527
}
2628
}

src/HtmlTags/HtmlDocument.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ public HtmlDocument()
2727
public HtmlTag RootTag { get; }
2828
public HtmlTag Head { get; }
2929
public HtmlTag Body { get; }
30-
public string Title { get { return _title.Text(); } set { _title.Text(value); } }
30+
public string Title { get => _title.Text();
31+
set => _title.Text(value);
32+
}
3133

3234
public HtmlTag Current => _currentStack.Any() ? _currentStack.Peek() : Body;
3335
public HtmlTag Last { get; private set; }

src/HtmlTags/Reflection/Expressions/StringStartsWithPropertyOperation.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ public StringStartsWithPropertyOperation()
1717
{
1818
}
1919

20-
public override string Text
21-
{
22-
get { return "starts with"; }
23-
}
20+
public override string Text => "starts with";
2421
}
2522

2623
public class CollectionContainsPropertyOperation : IPropertyOperation

src/HtmlTags/Reflection/MethodValueGetter.cs

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,11 @@ public string Name
3434
}
3535
}
3636

37-
public Type DeclaringType
38-
{
39-
get { return _methodInfo.DeclaringType; }
40-
}
37+
public Type DeclaringType => _methodInfo.DeclaringType;
4138

42-
public Type ValueType
43-
{
44-
get { return _methodInfo.ReturnType; }
45-
}
39+
public Type ValueType => _methodInfo.ReturnType;
4640

47-
public Type ReturnType
48-
{
49-
get { return _methodInfo.ReturnType; }
50-
}
41+
public Type ReturnType => _methodInfo.ReturnType;
5142

5243
public Expression ChainExpression(Expression body)
5344
{

0 commit comments

Comments
 (0)