diff --git a/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml b/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml
index f56984d..9ed4030 100644
--- a/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml
+++ b/src/HtmlTags.AspNetCore.TestSite/Views/Home/Index.cshtml
@@ -91,30 +91,34 @@
Html.LinkButton("Cancel", nameof(HomeController.Index))))
Input Tag value:
-
+
Editor for value:
@Html.EditorFor(m => m.Value)
Input tag blarg:
-
+
Editor for blarg:
@Html.EditorFor(m => m.Blarg)
Input tag blorg.blorg:
-
+
Editor for blorg.blorg:
@Html.EditorFor(m => m.Blorg.Blorg)
Input tag Blorgs[0].Blarg:
-
+
Editor for Blorgs[0].Blarg:
@Html.EditorFor(m => m.Blorgs[0].Blorg)
@Html.Input(m => m.Value).AddClass("foo")
Display tag for Value:
-
+
Display for Vlaue:
@Html.Display(m => m.Value)
Label tag for Value:
-
+
Label for Value:
@Html.Label(m => m.Value)
+ Valiidator tag for Value:
+
+ Validation for Value:
+ @Html.ValidationMessage(m => m.Value)
@{
var tag = new HtmlTag("canvas");
diff --git a/src/HtmlTags.AspNetCore/Conventions/Elements/Builders/ValidationMessageBuilder.cs b/src/HtmlTags.AspNetCore/Conventions/Elements/Builders/ValidationMessageBuilder.cs
new file mode 100644
index 0000000..6a64409
--- /dev/null
+++ b/src/HtmlTags.AspNetCore/Conventions/Elements/Builders/ValidationMessageBuilder.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
+
+namespace HtmlTags.Conventions.Elements.Builders
+{
+ public class DefaultValidationMessageBuilder : IElementBuilder
+ {
+ public HtmlTag Build(ElementRequest request)
+ {
+ var viewContext = request.Get() ?? throw new InvalidOperationException("Validation messages require a ViewContext");
+
+ var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
+ if (!viewContext.ViewData.ModelState.ContainsKey(request.ElementId) && formContext == null)
+ {
+ return HtmlTag.Empty();
+ }
+
+ var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(request.ElementId, out var entry);
+ var modelErrors = tryGetModelStateResult ? entry.Errors : null;
+
+ ModelError modelError = null;
+ if (modelErrors != null && modelErrors.Count != 0)
+ {
+ modelError = modelErrors.FirstOrDefault(m => !string.IsNullOrEmpty(m.ErrorMessage)) ?? modelErrors[0];
+ }
+
+ if (modelError == null && formContext == null)
+ {
+ return HtmlTag.Empty();
+ }
+
+ var tag = new HtmlTag(viewContext.ValidationMessageElement);
+
+ var className = modelError != null ?
+ HtmlHelper.ValidationMessageCssClassName :
+ HtmlHelper.ValidationMessageValidCssClassName;
+ tag.AddClass(className);
+
+ if (modelError != null)
+ {
+ var modelExplorer = request.Get() ?? throw new InvalidOperationException("Validation messages require a ModelExplorer");
+ tag.Text(ValidationHelpers.GetModelErrorMessageOrDefault(modelError, entry, modelExplorer));
+ }
+
+ if (formContext != null)
+ {
+ tag.Attr("data-valmsg-for", request.ElementId);
+
+ tag.Attr("data-valmsg-replace", "true");
+ }
+
+ return tag;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs b/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs
index d4b2631..bd2eb5a 100644
--- a/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs
+++ b/src/HtmlTags.AspNetCore/HtmlHelperExtensions.cs
@@ -23,6 +23,13 @@ public static HtmlTag Input(this IHtmlHelper helper, Expression(this IHtmlHelper helper, Expression> expression)
+ where T : class
+ {
+ var generator = GetGenerator(helper, expression);
+ return generator.ValidationMessageFor(expression);
+ }
+
public static HtmlTag Label(this IHtmlHelper helper, Expression> expression)
where T : class
{
diff --git a/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs b/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs
index f9e3fe2..cc14090 100644
--- a/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs
+++ b/src/HtmlTags.AspNetCore/ModelMetadataTagExtensions.cs
@@ -11,7 +11,7 @@ namespace HtmlTags
{
public static class ModelMetadataTagExtensions
{
- public static void ModelMetadata(this HtmlConventionRegistry registry)
+ public static HtmlConventionRegistry ModelMetadata(this HtmlConventionRegistry registry)
{
registry.Labels.Modifier();
registry.Displays.Modifier();
@@ -19,6 +19,8 @@ public static void ModelMetadata(this HtmlConventionRegistry registry)
registry.Editors.Modifier();
registry.Editors.Modifier();
registry.Editors.Modifier();
+
+ return registry;
}
private class DisplayNameElementModifier : IElementModifier
diff --git a/src/HtmlTags.AspNetCore/ModelStateTagExtensions.cs b/src/HtmlTags.AspNetCore/ModelStateTagExtensions.cs
new file mode 100644
index 0000000..e5abca6
--- /dev/null
+++ b/src/HtmlTags.AspNetCore/ModelStateTagExtensions.cs
@@ -0,0 +1,17 @@
+using HtmlTags.Conventions;
+using HtmlTags.Conventions.Elements;
+using HtmlTags.Conventions.Elements.Builders;
+
+namespace HtmlTags
+{
+ public static class ModelStateTagExtensions
+ {
+ public static HtmlConventionRegistry ModelState(this HtmlConventionRegistry registry)
+ {
+ registry.ValidationMessages.Always.BuildBy();
+ registry.ValidationMessages.NamingConvention(new DotNotationElementNamingConvention());
+
+ return registry;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs b/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs
index e6574c6..f4f284f 100644
--- a/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs
+++ b/src/HtmlTags.AspNetCore/ServiceCollectionExtensions.cs
@@ -6,8 +6,20 @@
public static class ServiceCollectionExtensions
{
+ ///
+ /// Configures HtmlTags without ASP.NET Core defaults without modifying the library
+ ///
+ /// Service collection
+ /// Convention library
+ /// Service collection
public static IServiceCollection AddHtmlTags(this IServiceCollection services, HtmlConventionLibrary library) => services.AddSingleton(library);
+ ///
+ /// Configures HtmlTags with ASP.NET Core defaults
+ ///
+ /// Service collection
+ /// Custom convention registries
+ /// Service collection
public static IServiceCollection AddHtmlTags(this IServiceCollection services, params HtmlConventionRegistry[] registries)
{
var library = new HtmlConventionLibrary();
@@ -15,18 +27,29 @@ public static IServiceCollection AddHtmlTags(this IServiceCollection services, p
{
registry.Apply(library);
}
+
+ var defaultRegistry = new HtmlConventionRegistry()
+ .Defaults()
+ .ModelMetadata()
+ .ModelState();
+
+ defaultRegistry.Apply(library);
+
return services.AddHtmlTags(library);
}
+ ///
+ /// Configures HtmlTags with ASP.NET Core defaults
+ ///
+ /// Service collection
+ /// Additional configuration callback
+ /// Service collection
public static IServiceCollection AddHtmlTags(this IServiceCollection services, Action config)
{
var registry = new HtmlConventionRegistry();
config(registry);
- registry.Defaults();
- registry.ModelMetadata();
-
return services.AddHtmlTags(registry);
}
}
diff --git a/src/HtmlTags.AspNetCore/ValidationMessageTagHelper.cs b/src/HtmlTags.AspNetCore/ValidationMessageTagHelper.cs
new file mode 100644
index 0000000..42993cc
--- /dev/null
+++ b/src/HtmlTags.AspNetCore/ValidationMessageTagHelper.cs
@@ -0,0 +1,11 @@
+namespace HtmlTags
+{
+ using Conventions.Elements;
+ using Microsoft.AspNetCore.Razor.TagHelpers;
+
+ [HtmlTargetElement("validation-message-tag", Attributes = ForAttributeName, TagStructure = TagStructure.WithoutEndTag)]
+ public class ValidationMessageTagHelper : HtmlTagTagHelper
+ {
+ protected override string Category { get; } = ElementConstants.ValidationMessage;
+ }
+}
\ No newline at end of file
diff --git a/src/HtmlTags/Cache.cs b/src/HtmlTags/Cache.cs
index b6c4db3..2b05fda 100755
--- a/src/HtmlTags/Cache.cs
+++ b/src/HtmlTags/Cache.cs
@@ -42,9 +42,11 @@ public Cache(IDictionary dictionary)
_values = dictionary;
}
- public Func OnMissing { set { _onMissing = value; } }
+ public Func OnMissing { set => _onMissing = value; }
- public Func GetKey { get { return _getKey; } set { _getKey = value; } }
+ public Func GetKey { get => _getKey;
+ set => _getKey = value;
+ }
public int Count => _values.Count;
diff --git a/src/HtmlTags/Conventions/ElementGenerator.cs b/src/HtmlTags/Conventions/ElementGenerator.cs
index 3c2deae..ffe0340 100644
--- a/src/HtmlTags/Conventions/ElementGenerator.cs
+++ b/src/HtmlTags/Conventions/ElementGenerator.cs
@@ -17,7 +17,7 @@ private ElementGenerator(ITagGenerator tags)
public static ElementGenerator For(HtmlConventionLibrary library, Func serviceLocator = null, T model = null)
{
- serviceLocator = serviceLocator ?? (Activator.CreateInstance);
+ serviceLocator = serviceLocator ?? Activator.CreateInstance;
var tags = new TagGenerator(library.TagLibrary, new ActiveProfile(), serviceLocator);
@@ -33,6 +33,9 @@ public HtmlTag LabelFor(Expression> expression, string
public HtmlTag InputFor(Expression> expression, string profile = null, T model = null)
=> Build(expression, ElementConstants.Editor, profile, model);
+ public HtmlTag ValidationMessageFor(Expression> expression, string profile = null, T model = null)
+ => Build(expression, ElementConstants.ValidationMessage, profile, model);
+
public HtmlTag DisplayFor(Expression> expression, string profile = null, T model = null)
=> Build(expression, ElementConstants.Display, profile, model);
@@ -41,8 +44,8 @@ public HtmlTag TagFor(Expression> expression, string c
public T Model
{
- get { return _model.Value; }
- set { _model = new Lazy(() => value); }
+ get => _model.Value;
+ set => _model = new Lazy(() => value);
}
public ElementRequest GetRequest(Expression> expression, T model = null)
@@ -70,6 +73,8 @@ private HtmlTag Build(ElementRequest request, string category, string profile =
public HtmlTag InputFor(ElementRequest request, string profile = null, T model = null) => Build(request, ElementConstants.Editor, profile, model);
+ public HtmlTag ValidationMessageFor(ElementRequest request, string profile = null, T model = null) => Build(request, ElementConstants.ValidationMessage, profile, model);
+
public HtmlTag DisplayFor(ElementRequest request, string profile = null, T model = null) => Build(request, ElementConstants.Display, profile, model);
public HtmlTag TagFor(ElementRequest request, string category, string profile = null, T model = null) => Build(request, category, profile, model);
diff --git a/src/HtmlTags/Conventions/Elements/Builders/TextboxBuilder.cs b/src/HtmlTags/Conventions/Elements/Builders/TextboxBuilder.cs
index e50ab45..4e5195a 100644
--- a/src/HtmlTags/Conventions/Elements/Builders/TextboxBuilder.cs
+++ b/src/HtmlTags/Conventions/Elements/Builders/TextboxBuilder.cs
@@ -2,8 +2,6 @@ namespace HtmlTags.Conventions.Elements.Builders
{
public class TextboxBuilder : IElementBuilder
{
- public bool Matches(ElementRequest subject) => true;
-
public HtmlTag Build(ElementRequest request)
{
return new TextboxTag().Attr("value", (request.RawValue ?? string.Empty).ToString());
diff --git a/src/HtmlTags/Conventions/Elements/ElementConstants.cs b/src/HtmlTags/Conventions/Elements/ElementConstants.cs
index 1f0270c..7182995 100644
--- a/src/HtmlTags/Conventions/Elements/ElementConstants.cs
+++ b/src/HtmlTags/Conventions/Elements/ElementConstants.cs
@@ -5,6 +5,7 @@ public static class ElementConstants
public static readonly string Label = "Label";
public static readonly string Display = "Display";
public static readonly string Editor = "Editor";
+ public static readonly string ValidationMessage = "ValidationMessage";
public static readonly string Templates = "Templates";
}
diff --git a/src/HtmlTags/Conventions/Elements/IElementGenerator.cs b/src/HtmlTags/Conventions/Elements/IElementGenerator.cs
index ef97b48..62b93a9 100644
--- a/src/HtmlTags/Conventions/Elements/IElementGenerator.cs
+++ b/src/HtmlTags/Conventions/Elements/IElementGenerator.cs
@@ -8,11 +8,13 @@ public interface IElementGenerator where T : class
T Model { get; set; }
HtmlTag LabelFor(Expression> expression, string profile = null, T model = null);
HtmlTag InputFor(Expression> expression, string profile = null, T model = null);
+ HtmlTag ValidationMessageFor(Expression> expression, string profile = null, T model = null);
HtmlTag DisplayFor(Expression> expression, string profile = null, T model = null);
HtmlTag TagFor(Expression> expression, string category, string profile = null, T model = null);
HtmlTag LabelFor(ElementRequest request, string profile = null, T model = null);
HtmlTag InputFor(ElementRequest request, string profile = null, T model = null);
+ HtmlTag ValidationMessageFor(ElementRequest request, string profile = null, T model = null);
HtmlTag DisplayFor(ElementRequest request, string profile = null, T model = null);
HtmlTag TagFor(ElementRequest request, string category, string profile = null, T model = null);
}
diff --git a/src/HtmlTags/Conventions/Formatting/GetStringRequest.cs b/src/HtmlTags/Conventions/Formatting/GetStringRequest.cs
index 8937ffd..0934038 100644
--- a/src/HtmlTags/Conventions/Formatting/GetStringRequest.cs
+++ b/src/HtmlTags/Conventions/Formatting/GetStringRequest.cs
@@ -73,7 +73,7 @@ public Type PropertyType
return _propertyType;
}
- set { _propertyType = value; }
+ set => _propertyType = value;
}
public PropertyInfo Property { get; }
diff --git a/src/HtmlTags/Conventions/HtmlConventionRegistryExtensions.cs b/src/HtmlTags/Conventions/HtmlConventionRegistryExtensions.cs
index abedca5..d0b3849 100644
--- a/src/HtmlTags/Conventions/HtmlConventionRegistryExtensions.cs
+++ b/src/HtmlTags/Conventions/HtmlConventionRegistryExtensions.cs
@@ -5,7 +5,7 @@ namespace HtmlTags.Conventions
public static class HtmlConventionRegistryExtensions
{
- public static void Defaults(this HtmlConventionRegistry registry)
+ public static HtmlConventionRegistry Defaults(this HtmlConventionRegistry registry)
{
registry.Editors.BuilderPolicy();
@@ -21,7 +21,7 @@ public static void Defaults(this HtmlConventionRegistry registry)
registry.Labels.Always.BuildBy();
-
+ return registry;
}
}
}
\ No newline at end of file
diff --git a/src/HtmlTags/Conventions/ProfileExpression.cs b/src/HtmlTags/Conventions/ProfileExpression.cs
index 94a6fba..f443868 100644
--- a/src/HtmlTags/Conventions/ProfileExpression.cs
+++ b/src/HtmlTags/Conventions/ProfileExpression.cs
@@ -21,6 +21,8 @@ public ProfileExpression(HtmlConventionLibrary library, string profileName)
public ElementCategoryExpression Editors => new ElementCategoryExpression(BuildersFor(ElementConstants.Editor));
+ public ElementCategoryExpression ValidationMessages => new ElementCategoryExpression(BuildersFor(ElementConstants.ValidationMessage));
+
public void Apply(HtmlConventionLibrary library) => library.Import(Library);
}
}
\ No newline at end of file
diff --git a/src/HtmlTags/HtmlDocument.cs b/src/HtmlTags/HtmlDocument.cs
index 387243c..a4ea016 100755
--- a/src/HtmlTags/HtmlDocument.cs
+++ b/src/HtmlTags/HtmlDocument.cs
@@ -27,7 +27,9 @@ public HtmlDocument()
public HtmlTag RootTag { get; }
public HtmlTag Head { get; }
public HtmlTag Body { get; }
- public string Title { get { return _title.Text(); } set { _title.Text(value); } }
+ public string Title { get => _title.Text();
+ set => _title.Text(value);
+ }
public HtmlTag Current => _currentStack.Any() ? _currentStack.Peek() : Body;
public HtmlTag Last { get; private set; }
diff --git a/src/HtmlTags/Reflection/Expressions/StringStartsWithPropertyOperation.cs b/src/HtmlTags/Reflection/Expressions/StringStartsWithPropertyOperation.cs
index 655ff3d..8277d16 100644
--- a/src/HtmlTags/Reflection/Expressions/StringStartsWithPropertyOperation.cs
+++ b/src/HtmlTags/Reflection/Expressions/StringStartsWithPropertyOperation.cs
@@ -17,10 +17,7 @@ public StringStartsWithPropertyOperation()
{
}
- public override string Text
- {
- get { return "starts with"; }
- }
+ public override string Text => "starts with";
}
public class CollectionContainsPropertyOperation : IPropertyOperation
diff --git a/src/HtmlTags/Reflection/MethodValueGetter.cs b/src/HtmlTags/Reflection/MethodValueGetter.cs
index 90f71ee..a07a3ad 100644
--- a/src/HtmlTags/Reflection/MethodValueGetter.cs
+++ b/src/HtmlTags/Reflection/MethodValueGetter.cs
@@ -34,20 +34,11 @@ public string Name
}
}
- public Type DeclaringType
- {
- get { return _methodInfo.DeclaringType; }
- }
+ public Type DeclaringType => _methodInfo.DeclaringType;
- public Type ValueType
- {
- get { return _methodInfo.ReturnType; }
- }
+ public Type ValueType => _methodInfo.ReturnType;
- public Type ReturnType
- {
- get { return _methodInfo.ReturnType; }
- }
+ public Type ReturnType => _methodInfo.ReturnType;
public Expression ChainExpression(Expression body)
{
diff --git a/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs b/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs
index 94d758c..d25da0a 100644
--- a/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs
+++ b/test/HtmlTags.AspNetCore.Testing/ModelMetadataTagExtensionsTester.cs
@@ -129,6 +129,31 @@ public void ShouldAddClientSideValidationClasses()
editor.Attr("data-val-maxlength-max").ShouldNotBeNullOrEmpty();
}
+ [Fact]
+ public void ShouldBuildValidationMessage()
+ {
+ var subject = new Subject { Value = null };
+ var helper = GetHtmlHelper(subject);
+
+ var validationMessage = helper.ValidationMessage(s => s.Value);
+
+ validationMessage.TagName().ShouldBe("span");
+ validationMessage.Text().ShouldNotBeEmpty();
+ validationMessage.HasClass(HtmlHelper.ValidationMessageCssClassName).ShouldBeTrue();
+ }
+
+ [Fact]
+ public void ShouldHaveEmptyValidationTagWhenNotInvalid()
+ {
+ var subject = new Subject { Value = "value" };
+ var helper = GetHtmlHelper(subject);
+
+ var validationMessage = helper.ValidationMessage(s => s.Value);
+
+ validationMessage.TagName().ShouldBe("span");
+ validationMessage.Text().ShouldBeEmpty();
+ validationMessage.HasClass(HtmlHelper.ValidationMessageValidCssClassName).ShouldBeTrue();
+ }
[Fact]
public void ShouldNotAddClientSideValidationClassesWhenNoClientValidationEnabled()
@@ -268,13 +293,6 @@ private static HtmlHelper GetHtmlHelper(
innerHelper = innerHelperWrapper(innerHelper);
}
- var registry = new HtmlConventionRegistry();
- registry.ModelMetadata();
- registry.Defaults();
-
- var library = new HtmlConventionLibrary();
- registry.Apply(library);
-
var serviceCollection = new ServiceCollection();
serviceCollection
@@ -284,7 +302,7 @@ private static HtmlHelper GetHtmlHelper(
.AddSingleton(innerHelper)
.AddSingleton()
.AddSingleton(attributeProvider)
- .AddHtmlTags(library);
+ .AddHtmlTags();
var serviceProvider = serviceCollection.BuildServiceProvider();