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 + + + +