Skip to content

Commit

Permalink
feat: Adds ValidationError type into validator (#106)
Browse files Browse the repository at this point in the history
* Adds ValidationError type into validator

* Adds field name to default error messages
  • Loading branch information
SHession authored Sep 9, 2021
1 parent 9459ca3 commit fe94ca0
Show file tree
Hide file tree
Showing 17 changed files with 101 additions and 39 deletions.
5 changes: 3 additions & 2 deletions src/editorial-source-components/FieldWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ValidationError } from "../plugin/elementSpec";
import type { FieldView as TFieldView } from "../plugin/fieldViews/FieldView";
import type { Field } from "../plugin/types/Element";
import { FieldView } from "../renderers/react/FieldView";
Expand All @@ -6,7 +7,7 @@ import { InputHeading } from "./InputHeading";

type Props<F> = {
field: F;
errors: string[];
errors: ValidationError[];
label: string;
className?: string;
};
Expand All @@ -18,7 +19,7 @@ export const FieldWrapper = <F extends Field<TFieldView<unknown>>>({
className,
}: Props<F>) => (
<InputGroup className={className}>
<InputHeading label={label} errors={errors} />
<InputHeading label={label} errors={errors.map((e) => e.error)} />
<FieldView field={field} hasErrors={!!errors.length} />
</InputGroup>
);
3 changes: 2 additions & 1 deletion src/elements/code/CodeElementForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from "react";
import { FieldWrapper } from "../../editorial-source-components/FieldWrapper";
import type { FieldValidationErrors } from "../../plugin/elementSpec";
import type { FieldNameToField } from "../../plugin/types/Element";
import { CustomDropdownView } from "../../renderers/react/customFieldViewComponents/CustomDropdownView";
import type { codeFields } from "./CodeElementSpec";

type Props = {
errors: Record<string, string[]>;
errors: FieldValidationErrors;
fields: FieldNameToField<typeof codeFields>;
};

Expand Down
2 changes: 1 addition & 1 deletion src/elements/code/CodeElementSpec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ export const codeElement = createGuElementSpec(
(_, errors, __, fields) => {
return <CodeElementForm errors={errors} fields={fields} />;
},
createValidator({ html: [required()] })
createValidator({ html: [required("empty code field")] })
);
3 changes: 2 additions & 1 deletion src/elements/demo-image/DemoImageElementForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { FieldWrapper } from "../../editorial-source-components/FieldWrapper";
import { Label } from "../../editorial-source-components/Label";
import type { FieldValidationErrors } from "../../plugin/elementSpec";
import type { FieldNameToValueMap } from "../../plugin/fieldViews/helpers";
import type { CustomField, FieldNameToField } from "../../plugin/types/Element";
import { CustomDropdownView } from "../../renderers/react/customFieldViewComponents/CustomDropdownView";
Expand All @@ -10,7 +11,7 @@ import type { createImageFields, DemoSetMedia } from "./DemoImageElement";

type Props = {
fieldValues: FieldNameToValueMap<ReturnType<typeof createImageFields>>;
errors: Record<string, string[]>;
errors: FieldValidationErrors;
fields: FieldNameToField<ReturnType<typeof createImageFields>>;
};

Expand Down
3 changes: 2 additions & 1 deletion src/elements/embed/EmbedForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import { FieldWrapper } from "../../editorial-source-components/FieldWrapper";
import type { FieldValidationErrors } from "../../plugin/elementSpec";
import type { FieldNameToValueMap } from "../../plugin/fieldViews/helpers";
import type { FieldNameToField } from "../../plugin/types/Element";
import { CustomCheckboxView } from "../../renderers/react/customFieldViewComponents/CustomCheckboxView";
Expand All @@ -8,7 +9,7 @@ import type { embedFields } from "./EmbedSpec";

type Props = {
fieldValues: FieldNameToValueMap<typeof embedFields>;
errors: Record<string, string[]>;
errors: FieldValidationErrors;
fields: FieldNameToField<typeof embedFields>;
};

Expand Down
3 changes: 2 additions & 1 deletion src/elements/image/ImageElementForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SvgCamera } from "@guardian/src-icons";
import { Column, Columns, Inline } from "@guardian/src-layout";
import React from "react";
import { FieldWrapper } from "../../editorial-source-components/FieldWrapper";
import type { FieldValidationErrors } from "../../plugin/elementSpec";
import type { FieldNameToValueMap } from "../../plugin/fieldViews/helpers";
import type { CustomField, FieldNameToField } from "../../plugin/types/Element";
import { CustomCheckboxView } from "../../renderers/react/customFieldViewComponents/CustomCheckboxView";
Expand All @@ -24,7 +25,7 @@ const inlineStyles = css`

type Props = {
fieldValues: FieldNameToValueMap<ReturnType<typeof createImageFields>>;
errors: Record<string, string[]>;
errors: FieldValidationErrors;
fields: FieldNameToField<ReturnType<typeof createImageFields>>;
};

Expand Down
3 changes: 2 additions & 1 deletion src/elements/pullquote/PullquoteForm.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Column, Columns } from "@guardian/src-layout";
import React from "react";
import { FieldWrapper } from "../../editorial-source-components/FieldWrapper";
import type { FieldValidationErrors } from "../../plugin/elementSpec";
import type { FieldNameToField } from "../../plugin/types/Element";
import { CustomDropdownView } from "../../renderers/react/customFieldViewComponents/CustomDropdownView";
import type { pullquoteFields } from "./PullquoteSpec";

type Props = {
errors: Record<string, string[]>;
errors: FieldValidationErrors;
fields: FieldNameToField<typeof pullquoteFields>;
};

Expand Down
25 changes: 20 additions & 5 deletions src/plugin/__tests__/element.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,15 +360,19 @@ describe("buildElementPlugin", () => {
field1: { type: "richText" },
},
() => undefined,
() => ({ field1: ["Some error"] })
() => ({ field1: [{ error: "Some error", message: "" }] })
);

const testElementWithDifferentValidation = createElementSpec(
{
checkbox: { type: "checkbox" },
},
() => undefined,
() => ({ checkbox: ["Some other error"] })
() => ({
checkbox: [
{ error: "Some other error", message: "A human readable message" },
],
})
);

type ExternalData = { nestedElementValues: { field1: string } };
Expand Down Expand Up @@ -677,14 +681,23 @@ describe("buildElementPlugin", () => {
},
});

expect(errors).toEqual({ field1: ["Some error"] });
expect(errors).toEqual({
field1: [{ error: "Some error", message: "" }],
});

const otherErrors = validateElementData({
elementName: "testElementWithDifferentValidation",
values: { checkbox: true },
});

expect(otherErrors).toEqual({ checkbox: ["Some other error"] });
expect(otherErrors).toEqual({
checkbox: [
{
error: "Some other error",
message: "A human readable message",
},
],
});
});

it("should output undefined if there are no errors", () => {
Expand Down Expand Up @@ -749,7 +762,9 @@ describe("buildElementPlugin", () => {
)!
);

expect(errors).toEqual({ field1: ["Some error"] });
expect(errors).toEqual({
field1: [{ error: "Some error", message: "" }],
});
});
});
});
Expand Down
8 changes: 7 additions & 1 deletion src/plugin/elementSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ const createUpdater = <
};
};

export type ValidationError = {
error: string;
message: string;
};
export type FieldValidationErrors = Record<string, ValidationError[]>;

export type Validator<FDesc extends FieldDescriptions<string>> = (
fields: FieldNameToValueMap<FDesc>
) => undefined | Record<string, string[]>;
) => undefined | FieldValidationErrors;

export type Renderer<FDesc extends FieldDescriptions<string>> = (
validate: Validator<FDesc>,
Expand Down
3 changes: 2 additions & 1 deletion src/plugin/helpers/element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DOMSerializer, Node, Schema } from "prosemirror-model";
import type { FieldValidationErrors } from "../elementSpec";
import type { FieldNameToValueMap } from "../fieldViews/helpers";
import { fieldTypeToViewMap } from "../fieldViews/helpers";
import { createNodesForFieldValues, getFieldNameFromNode } from "../nodeSpec";
Expand Down Expand Up @@ -133,7 +134,7 @@ export const createElementDataValidator = <
elementName,
values,
}: ExtractDataTypeFromElementSpec<ESpec, ElementNames>):
| Record<string, string[]>
| FieldValidationErrors
| undefined => {
const element = elementTypeMap[elementName];

Expand Down
6 changes: 4 additions & 2 deletions src/plugin/helpers/validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ describe("Validation helpers", () => {

expect(result).toEqual({
field1: [],
field2: ["Too long: 7/5"],
field2: [
{ error: "Too long: 7/5", message: "field2 is too long: 7/5" },
],
});
});

Expand All @@ -28,7 +30,7 @@ describe("Validation helpers", () => {
});

expect(result).toEqual({
field1: ["Required"],
field1: [{ error: "Required", message: "field1 is required" }],
});
});
});
Expand Down
53 changes: 41 additions & 12 deletions src/plugin/helpers/validation.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,93 @@
import type { FieldValidationErrors, ValidationError } from "../elementSpec";
import type { FieldNameToValueMap } from "../fieldViews/helpers";
import type { FieldDescriptions } from "../types/Element";

type Validator = (fieldValue: unknown) => string[];
type Validator = (fieldValue: unknown, fieldName: string) => ValidationError[];

export const createValidator = (
fieldValidationMap: Record<string, Validator[]>
) => <FDesc extends FieldDescriptions<string>>(
fieldValues: FieldNameToValueMap<FDesc>
): Record<string, string[]> => {
const errors: Record<string, string[]> = {};
): FieldValidationErrors => {
const errors: FieldValidationErrors = {};

for (const fieldName in fieldValidationMap) {
const validators = fieldValidationMap[fieldName];
const value = fieldValues[fieldName];
const fieldErrors = validators.flatMap((validator) => validator(value));
const fieldErrors = validators.flatMap((validator) =>
validator(value, fieldName)
);
errors[fieldName] = fieldErrors;
}
return errors;
};

export const htmlMaxLength = (maxLength: number): Validator => (value) => {
export const htmlMaxLength = (
maxLength: number,
customMessage: string | undefined = undefined
): Validator => (value, field) => {
if (typeof value !== "string") {
throw new Error(`[htmlMaxLength]: value is not of type string`);
}
const el = document.createElement("div");
el.innerHTML = value;
if (el.innerText.length > maxLength) {
return [`Too long: ${el.innerText.length}/${maxLength}`];
return [
{
error: `Too long: ${value.length}/${maxLength}`,
message:
customMessage ?? `${field} is too long: ${value.length}/${maxLength}`,
},
];
}
return [];
};

export const maxLength = (maxLength: number): Validator => (value) => {
export const maxLength = (
maxLength: number,
customMessage: string | undefined = undefined
): Validator => (value, field) => {
if (typeof value !== "string") {
throw new Error(`[maxLength]: value is not of type string`);
}
if (value.length > maxLength) {
return [`Too long: ${value.length}/${maxLength}`];
return [
{
error: `Too long: ${value.length}/${maxLength}`,
message:
customMessage ?? `${field} is too long: ${value.length}/${maxLength}`,
},
];
}
return [];
};

export const htmlRequired = (): Validator => (value) => {
export const htmlRequired = (
customMessage: string | undefined = undefined
): Validator => (value, field) => {
if (typeof value !== "string") {
throw new Error(`[maxLength]: value is not of type string`);
}
const el = document.createElement("div");
el.innerHTML = value;
if (!el.innerText.length) {
return ["Required"];
return [
{ error: "Required", message: customMessage ?? `${field} is required` },
];
}
return [];
};

export const required = (): Validator => (value) => {
export const required = (
customMessage: string | undefined = undefined
): Validator => (value, field) => {
if (typeof value !== "string") {
throw new Error(`[maxLength]: value is not of type string`);
}
if (!value.length) {
return ["Required"];
return [
{ error: "Required", message: customMessage ?? `${field} is required` },
];
}
return [];
};
4 changes: 2 additions & 2 deletions src/plugin/types/Consumer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { FieldValidationErrors } from "../elementSpec";
import type { FieldNameToValueMap } from "../fieldViews/helpers";
import type { FieldDescriptions, FieldNameToField } from "./Element";
import type { Errors } from "./Errors";

export type Consumer<
ConsumerResult,
FDesc extends FieldDescriptions<string>
> = (
fieldValues: FieldNameToValueMap<FDesc>,
errors: Errors,
errors: FieldValidationErrors,
updateFields: (fieldValues: FieldNameToValueMap<FDesc>) => void,
fields: FieldNameToField<FDesc>
) => ConsumerResult;
1 change: 0 additions & 1 deletion src/plugin/types/Errors.ts

This file was deleted.

8 changes: 5 additions & 3 deletions src/renderers/react/ElementProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import type { ReactElement } from "react";
import React, { Component } from "react";
import type { Validator } from "../../plugin/elementSpec";
import type {
FieldValidationErrors,
Validator,
} from "../../plugin/elementSpec";
import type { FieldNameToValueMap } from "../../plugin/fieldViews/helpers";
import type { Commands } from "../../plugin/types/Commands";
import type { Consumer } from "../../plugin/types/Consumer";
import type {
FieldDescriptions,
FieldNameToField,
} from "../../plugin/types/Element";
import type { Errors } from "../../plugin/types/Errors";
import { ElementWrapper } from "./ElementWrapper";

const fieldErrors = <FDesc extends FieldDescriptions<string>>(
fields: FieldNameToValueMap<FDesc>,
errors: Errors | undefined
errors: FieldValidationErrors | undefined
) =>
Object.keys(fields).reduce(
(acc, key) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { CustomCheckbox } from "../../../editorial-source-components/CustomCheckbox";
import type { ValidationError } from "../../../plugin/elementSpec";
import type { CustomField } from "../../../plugin/types/Element";
import { getFieldViewTestId } from "../FieldView";
import { useCustomFieldState } from "../useCustomFieldViewState";

type CustomCheckboxViewProps = {
field: CustomField<boolean, boolean>;
errors: string[];
errors: ValidationError[];
label: string;
};

Expand All @@ -19,7 +20,7 @@ export const CustomCheckboxView = ({
<CustomCheckbox
checked={boolean}
text={label}
error={errors.join(", ")}
error={errors.map((e) => e.error).join(", ")}
onChange={() => {
setBoolean(!boolean);
}}
Expand Down
Loading

0 comments on commit fe94ca0

Please sign in to comment.