Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/Framework/Framework/Controls/ValidationErrorsCount.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Displays the count of validation errors from the current Validation.Target.
/// </summary>
[ControlMarkupOptions(AllowContent = false)]
public class ValidationErrorsCount : HtmlGenericControl
{
/// <summary>
/// Initializes a new instance of the <see cref="ValidationErrorsCount"/> class.
/// </summary>
public ValidationErrorsCount()
: base("span", false)
{
}

/// <summary>
/// Gets or sets whether the errors from child objects in the viewmodel will be counted too.
/// Default is <c>true</c> (unlike <see cref="ValidationSummary"/> which defaults to <c>false</c>).
/// </summary>
[MarkupOptions(AllowBinding = false)]
public bool IncludeErrorsFromChildren
{
get { return (bool)GetValue(IncludeErrorsFromChildrenProperty)!; }
set { SetValue(IncludeErrorsFromChildrenProperty, value); }
}
public static readonly DotvvmProperty IncludeErrorsFromChildrenProperty
= DotvvmProperty.Register<bool, ValidationErrorsCount>(c => c.IncludeErrorsFromChildren, true);

/// <summary>
/// Gets or sets whether the errors from the <see cref="Validation.TargetProperty"/> object will be counted too.
/// </summary>
[MarkupOptions(AllowBinding = false)]
public bool IncludeErrorsFromTarget
{
get { return (bool)GetValue(IncludeErrorsFromTargetProperty)!; }
set { SetValue(IncludeErrorsFromTargetProperty, value); }
}
public static readonly DotvvmProperty IncludeErrorsFromTargetProperty
= DotvvmProperty.Register<bool, ValidationErrorsCount>(c => c.IncludeErrorsFromTarget, true);

/// <summary>
/// Gets or sets the name of the tag that wraps the control.
/// </summary>
[MarkupOptions(AllowBinding = false)]
public string WrapperTagName
{
get { return (string)GetValue(WrapperTagNameProperty)!; }
set { SetValue(WrapperTagNameProperty, value); }
}
public static readonly DotvvmProperty WrapperTagNameProperty
= DotvvmProperty.Register<string, ValidationErrorsCount>(c => c.WrapperTagName, "span");

protected internal override void OnPreRender(IDotvvmRequestContext context)
{
TagName = WrapperTagName;
base.OnPreRender(context);
}

/// <summary>
/// Adds all attributes that should be added to the control begin tag.
/// </summary>
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);
}
}
}
5 changes: 5 additions & 0 deletions src/Framework/Framework/Controls/Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
46 changes: 46 additions & 0 deletions src/Framework/Framework/Resources/Scripts/validation/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ type ValidationSummaryBinding = {
hideWhenValid: boolean
}

type ValidationErrorsCountBinding = {
target: KnockoutObservable<any>,
includeErrorsFromChildren: boolean,
includeErrorsFromTarget: boolean,
invalidCssClass?: string
}

type DotvvmValidationErrorsChangedEventArgs = Partial<PostbackOptions> & {
readonly allErrors: ValidationError[]
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
<label Validator.Value="{value: Login.Nick}">
Nick:
<dot:TextBox Text="{value: Login.Nick}"
data-ui="nick-textbox"/>
data-ui="nick-textbox" />
</label>
<label Validator.Value="{value: Login.Password}">
Password:
<dot:TextBox Text="{value: Login.Password}"
data-ui="password-textbox"/>
data-ui="password-textbox" />
</label>
<div Visible="{value: !Login.IsLoggedIn}">
<dot:Button Text="Log In"
Expand All @@ -24,7 +24,12 @@
</div>
<dot:ValidationSummary IncludeErrorsFromTarget="true"
IncludeErrorsFromChildren="true"
data-ui="validationSummary"/>
data-ui="validationSummary" />
<dot:ValidationErrorsCount IncludeErrorsFromTarget="true"
IncludeErrorsFromChildren="true"
data-ui="validationSummary"
Validator.InvalidCssClass="has-error"
Validator.HideWhenValid="true"/>
</form>
<div Visible="{value: Login.IsLoggedIn}">
<dot:Button Text="Log Out"
Expand Down
68 changes: 68 additions & 0 deletions src/Tests/ControlTests/ValidationErrorsCountTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using CheckTestOutput;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Controls;
using DotVVM.Framework.Tests.Binding;
using DotVVM.Framework.ViewModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using DotVVM.Framework.Testing;

namespace DotVVM.Framework.Tests.ControlTests
{
[TestClass]
public class ValidationErrorsCountTests
{
static readonly ControlTestHelper cth = new ControlTestHelper();
OutputChecker check = new OutputChecker("testoutputs");

[TestMethod]
public async Task ValidationErrorsCount_BasicRendering()
{
var r = await cth.RunPage(typeof(TestViewModel), @"
@viewModel DotVVM.Framework.Tests.ControlTests.ValidationErrorsCountTests.TestViewModel
<!-- default settings -->
<dot:ValidationErrorsCount />
<!-- with validation target -->
<dot:ValidationErrorsCount Validation.Target={value: Person} />
<!-- with custom wrapper tag -->
<dot:ValidationErrorsCount WrapperTagName='div' />
<!-- not including errors from children -->
<dot:ValidationErrorsCount IncludeErrorsFromChildren=false />
<!-- not including errors from target -->
<dot:ValidationErrorsCount IncludeErrorsFromTarget=false />
");

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
<!-- validation disabled -->
<dot:ValidationErrorsCount Validation.Enabled=false />
");

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; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<html>
<head></head>
<body>
@viewModel DotVVM.Framework.Tests.ControlTests.ValidationErrorsCountTests.TestViewModel
<!-- default settings -->
<span></span>

<!-- with validation target -->
<span></span>

<!-- with custom wrapper tag -->
<div></div>

<!-- not including errors from children -->
<span></span>

<!-- not including errors from target -->
<span></span>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head></head>
<body>
@viewModel DotVVM.Framework.Tests.ControlTests.ValidationErrorsCountTests.TestViewModel
<!-- validation disabled -->
<span></span>
</body>
</html>
Loading