Skip to content

Commit

Permalink
feat: field handle isValidating flag (#26)
Browse files Browse the repository at this point in the history
* perf: smarter resolveIsValid impl

* fix(core): clear errors to corresponding array items when shrinking array

* feat(validation): keep track of validation start and end for each field

* feat(core): keep track of validating state of each fielda

Co-authored-by: Mikołaj Klaman <mklaman@virtuslab.com>
  • Loading branch information
mixvar and Mikołaj Klaman authored Oct 30, 2020
1 parent 292b3b3 commit e299289
Show file tree
Hide file tree
Showing 17 changed files with 633 additions and 113 deletions.
156 changes: 122 additions & 34 deletions src/core/builders/create-form-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "" as any;

const validation = await validate([Schema.string], getValue);
const validation = await validate({ fields: [Schema.string], getValue });

expect(validation).toEqual([{ field: Schema.string, error: "REQUIRED" }]);
});
Expand All @@ -88,7 +88,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "defined string" as any;

const validation = await validate([Schema.string], getValue);
const validation = await validate({ fields: [Schema.string], getValue });

expect(validation).toEqual([{ field: Schema.string, error: null }]);
});
Expand All @@ -106,7 +106,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "" as any;

const validation = await validate([Schema.string], getValue);
const validation = await validate({ fields: [Schema.string], getValue });

expect(validation).toEqual([{ field: Schema.string, error: "REQUIRED" }]);
});
Expand All @@ -124,7 +124,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "ab" as any;

const validation = await validate([Schema.string], getValue);
const validation = await validate({ fields: [Schema.string], getValue });

expect(validation).toEqual([{ field: Schema.string, error: "TOO_SHORT" }]);
});
Expand All @@ -142,7 +142,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "abcd" as any;

const validation = await validate([Schema.string], getValue);
const validation = await validate({ fields: [Schema.string], getValue });

expect(validation).toEqual([{ field: Schema.string, error: null }]);
});
Expand All @@ -156,7 +156,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "A" as any;

const validation = await validate([Schema.choice], getValue);
const validation = await validate({ fields: [Schema.choice], getValue });

expect(validation).toEqual([
{ field: Schema.choice, error: "INVALID_VALUE" },
Expand All @@ -172,7 +172,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => "C" as any;

const validation = await validate([Schema.choice], getValue);
const validation = await validate({ fields: [Schema.choice], getValue });

expect(validation).toEqual([{ field: Schema.choice, error: null }]);
});
Expand All @@ -186,7 +186,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => null as any;

const validation = await validate([Schema.instance], getValue);
const validation = await validate({ fields: [Schema.instance], getValue });

expect(validation).toEqual([{ field: Schema.instance, error: "REQUIRED" }]);
});
Expand All @@ -203,7 +203,10 @@ describe("createFormValidator", () => {
]);
const getValue = () => ["ok", "very-ok", "invalid", "still-ok"] as any;

const validation = await validate([Schema.arrayString], getValue);
const validation = await validate({
fields: [Schema.arrayString],
getValue,
});

expect(validation).toEqual([
{ field: Schema.arrayString, error: "INVALID_VALUE" },
Expand All @@ -220,7 +223,10 @@ describe("createFormValidator", () => {
const getValue = () =>
[["ok"], ["very-ok"], ["invalid"], ["still-ok"]] as any;

const validation = await validate([Schema.arrayArrayString], getValue);
const validation = await validate({
fields: [Schema.arrayArrayString],
getValue,
});

expect(validation).toEqual([
{ field: Schema.arrayArrayString, error: null },
Expand All @@ -236,7 +242,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => null as any;

const validation = await validate([Schema.object], getValue);
const validation = await validate({ fields: [Schema.object], getValue });

expect(validation).toEqual([
{ field: Schema.object, error: "INVALID_VALUE" },
Expand All @@ -252,7 +258,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => null as any;

const validation = await validate([Schema.object], getValue);
const validation = await validate({ fields: [Schema.object], getValue });

expect(validation).toEqual([{ field: Schema.object, error: "REQUIRED" }]);
});
Expand All @@ -266,7 +272,7 @@ describe("createFormValidator", () => {
]);
const getValue = () => null as any;

const validation = await validate([Schema.object], getValue);
const validation = await validate({ fields: [Schema.object], getValue });

expect(validation).toEqual([{ field: Schema.object, error: null }]);
});
Expand Down Expand Up @@ -295,15 +301,15 @@ describe("createFormValidator", () => {
}
};

const validation = await validate(
[
const validation = await validate({
fields: [
Schema.arrayObjectString.nth(0),
Schema.arrayObjectString.nth(1),
Schema.arrayObjectString.nth(2),
Schema.arrayObjectString.nth(3),
],
getValue
);
getValue,
});

expect(validation).toEqual([
{ field: Schema.arrayObjectString.nth(0), error: "TOO_SHORT" },
Expand Down Expand Up @@ -338,15 +344,15 @@ describe("createFormValidator", () => {
}
};

const validation = await validate(
[
const validation = await validate({
fields: [
Schema.arrayChoice.nth(1),
Schema.arrayChoice.nth(0),
Schema.arrayObjectString.nth(0),
Schema.arrayObjectString.nth(1),
],
getValue
);
getValue,
});

expect(validation).toEqual([
{ field: Schema.arrayChoice.nth(1), error: null },
Expand Down Expand Up @@ -378,11 +384,11 @@ describe("createFormValidator", () => {
}
};

const validation = await validate(
[Schema.string, Schema.choice],
const validation = await validate({
fields: [Schema.string, Schema.choice],
getValue,
"change"
);
trigger: "change",
});

expect(validation).toEqual([{ field: Schema.choice, error: "REQUIRED" }]);
});
Expand Down Expand Up @@ -443,11 +449,11 @@ describe("createFormValidator", () => {
}
};

const validation = await validate(
[Schema.string, Schema.number, Schema.choice],
const validation = await validate({
fields: [Schema.string, Schema.number, Schema.choice],
getValue,
"change"
);
trigger: "change",
});

expect(validation).toEqual([
{ field: Schema.string, error: "TOO_SHORT" },
Expand Down Expand Up @@ -500,21 +506,103 @@ describe("createFormValidator", () => {
}
};

const validation = await validate(
[
const result = await validate({
fields: [
Schema.arrayString.nth(0),
Schema.arrayString.nth(1),
Schema.arrayString.nth(2),
Schema.string,
],
getValue
);
getValue,
});

expect(validation).toEqual([
expect(result).toEqual([
{ field: Schema.arrayString.nth(0), error: "INVALID_VALUE" },
{ field: Schema.arrayString.nth(1), error: null },
{ field: Schema.arrayString.nth(2), error: "TOO_SHORT" },
{ field: Schema.string, error: "TOO_SHORT" },
]);
});

it("calls callback functions to signal start and end of validation for every affected field", async () => {
const pass = <T>(_val: T) => wait(null);

const { validate } = createFormValidator(Schema, validate => [
validate({
field: Schema.string,
rules: () => [pass],
}),
validate({
field: Schema.string,
rules: () => [pass],
}),
validate.each({
field: Schema.arrayString,
rules: () => [pass],
}),
validate({
field: Schema.arrayString.nth(0),
rules: () => [pass],
}),
]);

const onFieldValidationStart = jest.fn();
const onFieldValidationEnd = jest.fn();

const fields = [
Schema.number,
Schema.arrayString.nth(0),
Schema.arrayString.nth(1),
Schema.arrayString.nth(2),
Schema.string,
];

await validate({
fields,
getValue: () => "foo" as any,
onFieldValidationStart,
onFieldValidationEnd,
});

const expectField = (desc: FieldDescriptor<any>) =>
expect.objectContaining({ __path: impl(desc).__path });

expect(onFieldValidationStart).toHaveBeenCalledWith(
expectField(Schema.arrayString.nth(0))
);
expect(onFieldValidationStart).toHaveBeenCalledWith(
expectField(Schema.arrayString.nth(1))
);
expect(onFieldValidationStart).toHaveBeenCalledWith(
expectField(Schema.arrayString.nth(2))
);
expect(onFieldValidationStart).toHaveBeenCalledWith(
expectField(Schema.string)
);
expect(onFieldValidationStart).not.toHaveBeenCalledWith(
expectField(Schema.number)
);
expect(onFieldValidationStart).not.toHaveBeenCalledWith(
expectField(Schema.arrayString)
);

expect(onFieldValidationEnd).toHaveBeenCalledWith(
expectField(Schema.arrayString.nth(0))
);
expect(onFieldValidationEnd).toHaveBeenCalledWith(
expectField(Schema.arrayString.nth(1))
);
expect(onFieldValidationEnd).toHaveBeenCalledWith(
expectField(Schema.arrayString.nth(2))
);
expect(onFieldValidationEnd).toHaveBeenCalledWith(
expectField(Schema.string)
);
expect(onFieldValidationEnd).not.toHaveBeenCalledWith(
expectField(Schema.number)
);
expect(onFieldValidationEnd).not.toHaveBeenCalledWith(
expectField(Schema.arrayString)
);
});
});
42 changes: 26 additions & 16 deletions src/core/builders/create-form-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,13 @@ export const createFormValidator = <Values extends object, Err>(
};

const formValidator: FormValidator<Values, Err> = {
validate: (fields, getValue, trigger) => {
validate: ({
fields,
trigger,
getValue,
onFieldValidationStart,
onFieldValidationEnd,
}) => {
const fieldsToValidate = fields
.map(field => ({
field,
Expand All @@ -72,13 +78,16 @@ export const createFormValidator = <Values extends object, Err>(
.filter(x => x.validators.length > 0);

return Promise.all(
fieldsToValidate.map(async ({ field, validators }) => {
fieldsToValidate.map(({ field, validators }) => {
const value = getValue(field);
const error = await firstNonNullPromise(validators, x =>
runValidationForField(x, value)
);

return { field, error };
onFieldValidationStart?.(field);
return firstNonNullPromise(validators, v =>
runValidationForField(v, value)
).then(error => {
onFieldValidationEnd?.(field);
return { field, error };
});
})
);
},
Expand All @@ -103,28 +112,29 @@ validate.each = config => ({
dependencies: config.dependencies,
});

const runValidationForField = async <Value, Err>(
const runValidationForField = <Value, Err>(
validator: FieldValidator<Value, Err, unknown[]>,
value: Value
): Promise<Err | null> => {
const rules = validator
.validators([] as any)
.filter(x => !isFalsy(x)) as Validator<Value, Err>[];

return firstNonNullPromise(rules, async rule => await rule(value));
return firstNonNullPromise(rules, rule => Promise.resolve(rule(value)));
};

const firstNonNullPromise = async <T, V>(
const firstNonNullPromise = <T, V>(
list: T[],
mapper: (x: T) => Promise<V | null>
provider: (x: T) => Promise<V | null>
): Promise<V | null> => {
for (const x of list) {
const result = await mapper(x);
if (result != null) {
return result;
}
if (list.length === 0) {
return Promise.resolve(null);
}
return null;

const [el, ...rest] = list;
return provider(el).then(result =>
result != null ? result : firstNonNullPromise(rest, provider)
);
};

// TODO rethink
Expand Down
Loading

0 comments on commit e299289

Please sign in to comment.