diff --git a/src/Framework/Framework/Controls/ValidationErrorsCount.cs b/src/Framework/Framework/Controls/ValidationErrorsCount.cs
new file mode 100644
index 0000000000..cbb0ee9ece
--- /dev/null
+++ b/src/Framework/Framework/Controls/ValidationErrorsCount.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using DotVVM.Framework.Binding;
+using DotVVM.Framework.Runtime;
+using DotVVM.Framework.Hosting;
+
+namespace DotVVM.Framework.Controls
+{
+ ///
+ /// Displays the count of validation errors from the current Validation.Target.
+ ///
+ [ControlMarkupOptions(AllowContent = false)]
+ public class ValidationErrorsCount : HtmlGenericControl
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ValidationErrorsCount()
+ : base("span", false)
+ {
+ }
+
+ ///
+ /// Gets or sets whether the errors from child objects in the viewmodel will be counted too.
+ /// Default is true (unlike which defaults to false).
+ ///
+ [MarkupOptions(AllowBinding = false)]
+ public bool IncludeErrorsFromChildren
+ {
+ get { return (bool)GetValue(IncludeErrorsFromChildrenProperty)!; }
+ set { SetValue(IncludeErrorsFromChildrenProperty, value); }
+ }
+ public static readonly DotvvmProperty IncludeErrorsFromChildrenProperty
+ = DotvvmProperty.Register(c => c.IncludeErrorsFromChildren, true);
+
+ ///
+ /// Gets or sets whether the errors from the object will be counted too.
+ ///
+ [MarkupOptions(AllowBinding = false)]
+ public bool IncludeErrorsFromTarget
+ {
+ get { return (bool)GetValue(IncludeErrorsFromTargetProperty)!; }
+ set { SetValue(IncludeErrorsFromTargetProperty, value); }
+ }
+ public static readonly DotvvmProperty IncludeErrorsFromTargetProperty
+ = DotvvmProperty.Register(c => c.IncludeErrorsFromTarget, true);
+
+ ///
+ /// Gets or sets the name of the tag that wraps the control.
+ ///
+ [MarkupOptions(AllowBinding = false)]
+ public string WrapperTagName
+ {
+ get { return (string)GetValue(WrapperTagNameProperty)!; }
+ set { SetValue(WrapperTagNameProperty, value); }
+ }
+ public static readonly DotvvmProperty WrapperTagNameProperty
+ = DotvvmProperty.Register(c => c.WrapperTagName, "span");
+
+ protected internal override void OnPreRender(IDotvvmRequestContext context)
+ {
+ TagName = WrapperTagName;
+ base.OnPreRender(context);
+ }
+
+ ///
+ /// Adds all attributes that should be added to the control begin tag.
+ ///
+ protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
+ {
+ base.AddAttributesToRender(writer, context);
+
+ if (false.Equals(this.GetValue(Validation.EnabledProperty)))
+ {
+ return;
+ }
+
+ var expression = this.GetValueBinding(Validation.TargetProperty)?.GetKnockoutBindingExpression(this) ?? "dotvvm.viewModelObservables.root";
+
+ var group = new KnockoutBindingGroup();
+ {
+ group.Add("target", expression);
+ group.Add("includeErrorsFromChildren", IncludeErrorsFromChildren.ToString().ToLowerInvariant());
+ group.Add("includeErrorsFromTarget", IncludeErrorsFromTarget.ToString().ToLowerInvariant());
+ }
+ writer.AddKnockoutDataBind("dotvvm-validationErrorsCount", group);
+
+ Validator.AddValidationOptionsBinding(writer, this);
+ }
+ }
+}
diff --git a/src/Framework/Framework/Controls/Validator.cs b/src/Framework/Framework/Controls/Validator.cs
index ea7140ee4c..8aafa248c5 100644
--- a/src/Framework/Framework/Controls/Validator.cs
+++ b/src/Framework/Framework/Controls/Validator.cs
@@ -110,6 +110,11 @@ private static void AddValidatedValue(IHtmlWriter writer, IDotvvmRequestContext
}
// render options
+ AddValidationOptionsBinding(writer, control);
+ }
+
+ public static void AddValidationOptionsBinding(IHtmlWriter writer, DotvvmControl control)
+ {
var bindingGroup = new KnockoutBindingGroup();
foreach (var property in ValidationOptionProperties)
{
diff --git a/src/Framework/Framework/Resources/Scripts/validation/validation.ts b/src/Framework/Framework/Resources/Scripts/validation/validation.ts
index 64d707d909..ca66676b73 100644
--- a/src/Framework/Framework/Resources/Scripts/validation/validation.ts
+++ b/src/Framework/Framework/Resources/Scripts/validation/validation.ts
@@ -22,6 +22,13 @@ type ValidationSummaryBinding = {
hideWhenValid: boolean
}
+type ValidationErrorsCountBinding = {
+ target: KnockoutObservable,
+ includeErrorsFromChildren: boolean,
+ includeErrorsFromTarget: boolean,
+ invalidCssClass?: string
+}
+
type DotvvmValidationErrorsChangedEventArgs = Partial & {
readonly allErrors: ValidationError[]
}
@@ -126,6 +133,37 @@ export function init() {
});
}
}
+
+ // ValidationErrorsCount
+ ko.bindingHandlers["dotvvm-validationErrorsCount"] = {
+ init: (element: HTMLElement, valueAccessor: () => ValidationErrorsCountBinding, allBindingsAccessor?: KnockoutAllBindingsAccessor) => {
+ const binding = valueAccessor();
+
+ const updateErrorCount = () => {
+ const errors = getValidationErrors(
+ binding.target,
+ binding.includeErrorsFromChildren,
+ binding.includeErrorsFromTarget
+ );
+ element.innerText = errors.length.toString();
+
+ const validationOptions = allBindingsAccessor!.get("dotvvm-validationOptions");
+ if (validationOptions) {
+ applyValidatorActionsCore(element, errors, validationOptions);
+ }
+ };
+
+ // Update initially to show current count
+ updateErrorCount();
+
+ // Subscribe to validation errors changes
+ validationErrorsChanged.subscribe(updateErrorCount);
+
+ ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
+ validationErrorsChanged.unsubscribe(updateErrorCount);
+ });
+ }
+ }
}
function validateViewModel(viewModel: any, path: string): void {
@@ -374,6 +412,14 @@ function applyValidatorActions(
validatorOptions: any): void {
const errors = getErrors(observable);
+ applyValidatorActionsCore(validator, errors, validatorOptions)
+}
+
+function applyValidatorActionsCore(
+ validator: HTMLElement,
+ errors: ValidationError[],
+ validatorOptions: any): void {
+
const errorMessages = errors.map(v => v.errorMessage);
for (const option of keys(validatorOptions)) {
elementActions[option](
diff --git a/src/Samples/Common/Views/ControlSamples/ValidationSummary/IncludeErrorsFromTarget_PropertyPathNotNull.dothtml b/src/Samples/Common/Views/ControlSamples/ValidationSummary/IncludeErrorsFromTarget_PropertyPathNotNull.dothtml
index 4e6a83b967..76f74305cb 100644
--- a/src/Samples/Common/Views/ControlSamples/ValidationSummary/IncludeErrorsFromTarget_PropertyPathNotNull.dothtml
+++ b/src/Samples/Common/Views/ControlSamples/ValidationSummary/IncludeErrorsFromTarget_PropertyPathNotNull.dothtml
@@ -9,12 +9,12 @@
+ data-ui="validationSummary" />
+
+
+
+
+
+
+
+
+
+
+ ");
+
+ check.CheckString(r.FormattedHtml, fileExtension: "html");
+ }
+
+ [TestMethod]
+ public async Task ValidationErrorsCount_WithValidationDisabled()
+ {
+ var r = await cth.RunPage(typeof(TestViewModel), @"
+ @viewModel DotVVM.Framework.Tests.ControlTests.ValidationErrorsCountTests.TestViewModel
+
+
+ ");
+
+ check.CheckString(r.FormattedHtml, fileExtension: "html");
+ }
+
+ public class TestViewModel : DotvvmViewModelBase
+ {
+ public PersonModel Person { get; set; } = new PersonModel();
+ }
+
+ public class PersonModel
+ {
+ [Required]
+ public string? Name { get; set; }
+
+ [Required]
+ [EmailAddress]
+ public string? Email { get; set; }
+ }
+ }
+}
diff --git a/src/Tests/ControlTests/testoutputs/ValidationErrorsCountTests.ValidationErrorsCount_BasicRendering.html b/src/Tests/ControlTests/testoutputs/ValidationErrorsCountTests.ValidationErrorsCount_BasicRendering.html
new file mode 100644
index 0000000000..0871366531
--- /dev/null
+++ b/src/Tests/ControlTests/testoutputs/ValidationErrorsCountTests.ValidationErrorsCount_BasicRendering.html
@@ -0,0 +1,20 @@
+
+
+
+ @viewModel DotVVM.Framework.Tests.ControlTests.ValidationErrorsCountTests.TestViewModel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tests/ControlTests/testoutputs/ValidationErrorsCountTests.ValidationErrorsCount_WithValidationDisabled.html b/src/Tests/ControlTests/testoutputs/ValidationErrorsCountTests.ValidationErrorsCount_WithValidationDisabled.html
new file mode 100644
index 0000000000..62db2d323d
--- /dev/null
+++ b/src/Tests/ControlTests/testoutputs/ValidationErrorsCountTests.ValidationErrorsCount_WithValidationDisabled.html
@@ -0,0 +1,8 @@
+
+
+
+ @viewModel DotVVM.Framework.Tests.ControlTests.ValidationErrorsCountTests.TestViewModel
+
+
+
+