Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed error messages for Required and EnforceClientFormat validators #1589

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
26 changes: 20 additions & 6 deletions src/Framework/Framework/Resources/Scripts/validation/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,29 @@ export function getErrors<T>(o: KnockoutObservable<T> | null): ValidationError[]
export class ValidationError {

private constructor(public errorMessage: string, public propertyPath: string, public validatedObservable: KnockoutObservable<any>) {
if (!errorMessage) {
throw new Error(`String "${errorMessage}" is not a valid ValidationError message.`);
}
}

public static attach(errorMessage: string, propertyPath: string, observable: KnockoutObservable<any>): ValidationError {
if (!errorMessage) {
throw new Error(`String "${errorMessage}" is not a valid ValidationError message.`);
let unwrapped = ValidationError.getOrCreateErrorsCollection(observable);
const error = new ValidationError(errorMessage, propertyPath, unwrapped);
unwrapped[errorsSymbol].push(error);
allErrors.push(error);
return error;
}

static attachIfNoErrors(errorMessage: string, propertyPath: string, observable: KnockoutObservable<any>) {
let unwrapped = ValidationError.getOrCreateErrorsCollection(observable);
if (unwrapped[errorsSymbol].length === 0) {
const error = new ValidationError(errorMessage, propertyPath, unwrapped);
unwrapped[errorsSymbol].push(error);
allErrors.push(error);
}
}

private static getOrCreateErrorsCollection(observable: KnockoutObservable<any>): KnockoutObservable<any> & { [errorsSymbol]: ValidationError[] } {
if (!observable) {
throw new Error(`ValidationError cannot be attached to "${observable}".`);
}
Expand All @@ -35,10 +52,7 @@ export class ValidationError {
if (!unwrapped.hasOwnProperty(errorsSymbol)) {
unwrapped[errorsSymbol] = [];
}
const error = new ValidationError(errorMessage, propertyPath, unwrapped);
unwrapped[errorsSymbol].push(error);
allErrors.push(error);
return error;
return unwrapped;
}

public detach(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ function validateViewModel(viewModel: any, path: string): void {
function validateRecursive(observable: KnockoutObservable<any>, propertyValue: any, type: TypeDefinition, propertyPath: string) {
const lastSetError: CoerceResult = (observable as any)[lastSetErrorSymbol];
if (lastSetError && lastSetError.isError) {
ValidationError.attach(lastSetError.message, propertyPath, observable);
ValidationError.attachIfNoErrors(lastSetError.message, propertyPath, observable);
}

if (Array.isArray(type)) {
Expand Down
26 changes: 22 additions & 4 deletions src/Framework/Framework/Resources/Scripts/validation/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@ export type DotvvmValidator = {
}

export const required: DotvvmValidator = {
isValid(value: any): boolean {
return !isEmpty(value);
isValid(value: any, context, property): boolean {
if (isEmpty(value)) {
return false;
}

// for value types, the observable may still hold the previous value - we need to look at element states if there is any element with invalid state and empty value
const metadata = getValidationMetadata(property);
if (metadata) {
for (const metaElement of metadata) {
if (!metaElement.elementValidationState && "value" in metaElement.element && isEmpty((metaElement.element as any)["value"])) {
return false;
}
}
}

return true;
}
}
export const regex: DotvvmValidator = {
Expand Down Expand Up @@ -40,8 +54,12 @@ export const enforceClientFormat: DotvvmValidator = {
const metadata = getValidationMetadata(property);
if (metadata) {
for (const metaElement of metadata) {
if (!metaElement.elementValidationState) {
valid = false;
if (!metaElement.elementValidationState && "value" in metaElement.element) {
// do not emit the error if the element value is empty and it is allowed to be empty
const elementValue = (metaElement.element as any).value as string;
if ((!allowEmptyStringOrWhitespaces && isEmpty(elementValue)) || (!allowEmptyString && elementValue === "") || !isEmpty(elementValue)) {
valid = false;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,27 @@
<p>
Nullable DateTime with disabled DotvvmClientFormat:
<dot:TextBox Validator.InvalidCssClass="has-error" Text="{value: Value1}" Validator.Value="{value: Value1}" />
<dot:Validator Value="{value: Value1}" ShowErrorMessageText="true" data-control="validator" />
</p>
<p>
Nullable DateTime:
<dot:TextBox Validator.InvalidCssClass="has-error" Text="{value: Value2}" Validator.Value="{value: Value2}" />
<dot:Validator Value="{value: Value2}" ShowErrorMessageText="true" data-control="validator" />
</p>
<p>
Nullable DateTime with Required:
<dot:TextBox Validator.InvalidCssClass="has-error" Text="{value: Value3}" Validator.Value="{value: Value3}" />
<dot:Validator Value="{value: Value3}" ShowErrorMessageText="true" data-control="validator" />
</p>
<p>
Non-nullable:
<dot:TextBox Validator.InvalidCssClass="has-error" Text="{value: Value4}" Validator.Value="{value: Value4}" />
<dot:Validator Value="{value: Value4}" ShowErrorMessageText="true" data-control="validator" />
</p>
<p>
Non-nullable with Required:
<dot:TextBox Validator.InvalidCssClass="has-error" Text="{value: Value5}" Validator.Value="{value: Value5}" />
<dot:Validator Value="{value: Value5}" ShowErrorMessageText="true" data-control="validator" />
</p>
<dot:Button Text="Validate" Click="{command: ValidateRequiredDateTime()}" ID="ValidateButton" />
</body>
Expand Down
16 changes: 9 additions & 7 deletions src/Samples/Tests/Tests/Feature/ValidationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public void Feature_Validation_DateTimeValidation()

var button = browser.First("input[type=button]");
var textBoxes = browser.FindElements("input[type=text]").ThrowIfDifferentCountThan(5);
var validators = browser.FindElements("span[data-control=validator]").ThrowIfDifferentCountThan(5);

void testValue(string value)
{
Expand All @@ -103,37 +104,38 @@ void testValue(string value)
}
button.Click();
}
void assertValidators(params bool[] states)
void assertValidators(params string[] errors)
{
if (states.Length != textBoxes.Count)
if (errors.Length != textBoxes.Count)
{
throw new ArgumentException("states");
throw new ArgumentException("errors");
}

for (int i = 0; i < textBoxes.Count; i++)
{
if (states[i])
if (!string.IsNullOrEmpty(errors[i]))
{
AssertUI.HasClass(textBoxes[i], "has-error");
}
else
{
AssertUI.HasNotClass(textBoxes[i], "has-error");
AssertUI.TextEquals(validators[i], errors[i]);
}
}
}

// empty field - Required validators should be triggered
testValue("");
assertValidators(false, false, true, true, true);
assertValidators("", "", "The Value3 field is required.", "Cannot coerce 'null' to type 'DateTime'.", "The Value5 field is required.");

// correct value - no error
testValue("06/14/2017 8:10:35 AM");
assertValidators(false, false, false, false, false);
assertValidators("", "", "", "", "");

// incorrect format - all fields should trigger errors except the one where DotvvmClientFormat is disabled
testValue("06-14-2017");
assertValidators(false, true, true, true, true);
assertValidators("", "The field Value2 is invalid.", "The Value3 field is required. The field Value3 is invalid.", "The field Value4 is invalid.", "The field Value5 is invalid.");
});
}

Expand Down