Skip to content

Commit

Permalink
feat: handle change method (#58)
Browse files Browse the repository at this point in the history
* fix(examples): minor example code improvements

* refactor(core): decoder refactor & fixes

- simplifu decoder return type
- allow nulls in instanceOf decoder
- expose constructor from isntanceOf decoder

* feat(core): replace 'instaceOf' decoder with 'date'

* feat(core): enable type coercion for decoders

* feat(core): decode change event function

* feat(core): implement FieldHandle handleChange fn

* feat(examples): simplify code using handleChange method

* feat(doc): improve decoders jsdoc

Co-authored-by: Mikołaj Klaman <mklaman@virtuslab.com>
  • Loading branch information
mixvar and Mikołaj Klaman authored Jan 18, 2021
1 parent ee33dde commit a013729
Show file tree
Hide file tree
Showing 46 changed files with 1,494 additions and 356 deletions.
6 changes: 2 additions & 4 deletions examples/basic-forms/1-basic-form-naive/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const Example: React.FC = () => {
<input
id={name.id}
value={name.value}
onChange={e => name.setValue(e.target.value)}
onChange={name.handleChange}
onBlur={name.handleBlur}
autoComplete="off"
/>
Expand All @@ -45,9 +45,7 @@ const Example: React.FC = () => {
type="number"
id={age.id}
value={age.value}
onChange={e =>
age.setValue(e.target.value === "" ? "" : +e.target.value)
}
onChange={age.handleChange}
onBlur={age.handleBlur}
autoComplete="off"
/>
Expand Down
6 changes: 2 additions & 4 deletions examples/basic-forms/2-basic-form-improved/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const NameField: React.FC = () => {
<input
id={field.id}
value={field.value}
onChange={e => field.setValue(e.target.value)}
onChange={field.handleChange}
onBlur={field.handleBlur}
autoComplete="off"
/>
Expand All @@ -60,9 +60,7 @@ const AgeField: React.FC = () => {
type="number"
id={field.id}
value={field.value}
onChange={e =>
field.setValue(e.target.value === "" ? "" : +e.target.value)
}
onChange={field.handleChange}
onBlur={field.handleBlur}
autoComplete="off"
/>
Expand Down
6 changes: 2 additions & 4 deletions examples/basic-forms/3-basic-form-validation/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const NameField: React.FC = () => {
<input
id={field.id}
value={field.value}
onChange={e => field.setValue(e.target.value)}
onChange={field.handleChange}
onBlur={field.handleBlur}
autoComplete="off"
/>
Expand All @@ -80,9 +80,7 @@ const AgeField: React.FC = () => {
type="number"
id={field.id}
value={field.value}
onChange={e =>
field.setValue(e.target.value === "" ? "" : +e.target.value)
}
onChange={field.handleChange}
onBlur={field.handleBlur}
autoComplete="off"
/>
Expand Down
6 changes: 2 additions & 4 deletions examples/basic-forms/4-basic-form-mui/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const NameField: React.FC = () => {
label="Name:"
id={field.id}
value={field.value}
onChange={e => field.setValue(e.target.value)}
onChange={field.handleChange}
onBlur={field.handleBlur}
error={field.error != null}
helperText={field.error}
Expand All @@ -83,9 +83,7 @@ const AgeField: React.FC = () => {
label="Age:"
id={field.id}
value={field.value}
onChange={e =>
field.setValue(e.target.value === "" ? "" : +e.target.value)
}
onChange={field.handleChange}
onBlur={field.handleBlur}
error={field.error != null}
helperText={field.error}
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs/checkbox-group-input/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const ColorsCheckboxGroup: React.FC = () => {
name={colors.id}
checked={field.value}
onBlur={field.handleBlur}
onChange={e => field.setValue(e.target.checked)}
onChange={field.handleChange}
/>
<label htmlFor={field.id}>{labels[key]}</label>
</div>
Expand Down
2 changes: 1 addition & 1 deletion examples/inputs/field-array-input/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const PromoCodesArrayInput: React.FC = () => {
<input
key={field.id}
value={field.value}
onChange={e => field.setValue(e.target.value)}
onChange={field.handleChange}
onBlur={field.handleBlur}
autoComplete="off"
/>
Expand Down
5 changes: 3 additions & 2 deletions examples/inputs/mui-date-picker-input/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ReactDOM from "react-dom";
import "../index.css";

const Schema = createFormSchema(fields => ({
date: fields.instanceOf(Date),
date: fields.date(),
}));

const Example: React.FC = () => {
Expand All @@ -44,7 +44,8 @@ const DatePickerInput: React.FC = () => {
variant="inline"
label="Choose your favourite date"
value={dateField.value}
onChange={date => dateField.setValue(date as Date)}
onChange={dateField.setValue}
onBlur={dateField.handleBlur}
fullWidth
autoOk
/>
Expand Down
5 changes: 1 addition & 4 deletions examples/inputs/mui-multi-select-input/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,7 @@ const ColorsMultiSelectInput: React.FC = () => {
<Select
labelId={colors.id}
value={colors.value}
onChange={e => {
// TODO: simplify when field.handleChange is implemented
colors.setValue(e.target.value as Array<"red" | "green" | "blue">);
}}
onChange={colors.handleChange}
input={<Input />}
renderValue={selected => (selected as string[]).join(", ")}
multiple
Expand Down
5 changes: 3 additions & 2 deletions examples/inputs/radio-group-input/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ const ColorsRadioGroup: React.FC = () => {
<legend>Choose your favourite color:</legend>

{Object.values(colors.options).map(option => (
<div key={colors.id}>
<div key={option}>
<input
id={option}
type="radio"
name={colors.id}
value={option}
checked={colors.value === option}
onBlur={colors.handleBlur}
onChange={e => e.target.checked && colors.setValue(option)}
onChange={colors.handleChange}
/>
<label htmlFor={option}>{labels[option]}</label>
</div>
Expand Down
6 changes: 1 addition & 5 deletions examples/inputs/select-input/src/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,9 @@ const ColorSelectInput: React.FC = () => {
<select
id={field.id}
value={field.value}
onChange={e => field.setValue(e.target.value as Color)}
onChange={field.handleChange}
onBlur={field.handleBlur}
>
{/*
we need custom-typed Object.keys in order for type of `option`
to be inferred properly to "red" | "green" | "blue" in this example
*/}
{Object.values(field.options).map(option => (
<option key={option} value={option}>
{labels[option]}
Expand Down
40 changes: 18 additions & 22 deletions src/core/builders/create-form-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("createFormSchema", () => {
choice: fields.choice("A", "B", "C"),
num: fields.number(),
bool: fields.bool(),
instance: fields.instanceOf(Date),
date: fields.date(),
arrayString: fields.array(fields.string()),
arrayChoice: fields.array(fields.choice("a", "b", "c")),
arrayArrayString: fields.array(fields.array(fields.string())),
Expand All @@ -35,7 +35,7 @@ describe("createFormSchema", () => {
choice: "A" | "B" | "C";
num: number | "";
bool: boolean;
instance: Date | null;
date: Date | null;
arrayString: string[];
arrayChoice: Array<"a" | "b" | "c">;
arrayArrayString: string[][];
Expand All @@ -59,7 +59,7 @@ describe("createFormSchema", () => {
choice: fields.choice("A", "B", "C"),
num: fields.number(),
bool: fields.bool(),
instance: fields.instanceOf(Date),
date: fields.date(),
arrayString: fields.array(fields.string()),
arrayChoice: fields.array(fields.choice("a", "b", "c")),
arrayArrayString: fields.array(fields.array(fields.string())),
Expand All @@ -86,7 +86,7 @@ describe("createFormSchema", () => {
choice: "A" | "B" | "C";
num: number | "";
bool: boolean;
instance: Date | null;
date: Date | null;
arrayString: string[];
arrayChoice: Array<"a" | "b" | "c">;
arrayArrayString: string[][];
Expand Down Expand Up @@ -115,7 +115,7 @@ describe("createFormSchema", () => {
choice: fields.choice("A", "B", "C"),
num: fields.number(),
bool: fields.bool(),
instance: fields.instanceOf(Date),
date: fields.date(),
arrayString: fields.array(fields.string()),
arrayChoice: fields.array(fields.choice("a", "b", "c")),
arrayArrayString: fields.array(fields.array(fields.string())),
Expand All @@ -136,7 +136,7 @@ describe("createFormSchema", () => {
"choice",
"num",
"bool",
"instance",
"date",
"arrayString",
"arrayChoice",
"arrayArrayString",
Expand All @@ -153,7 +153,7 @@ describe("createFormSchema", () => {
choice: fields.choice("A", "B", "C"),
num: fields.number(),
bool: fields.bool(),
instance: fields.instanceOf(Date),
date: fields.date(),
arrayString: fields.array(fields.string()),
arrayChoice: fields.array(fields.choice("a", "b", "c")),
arrayArrayString: fields.array(fields.array(fields.string())),
Expand All @@ -174,7 +174,7 @@ describe("createFormSchema", () => {
choice: expect.objectContaining({ __path: "choice" }),
num: expect.objectContaining({ __path: "num" }),
bool: expect.objectContaining({ __path: "bool" }),
instance: expect.objectContaining({ __path: "instance" }),
date: expect.objectContaining({ __path: "date" }),
arrayString: expect.objectContaining({
__path: "arrayString",
nth: expect.any(Function),
Expand Down Expand Up @@ -227,7 +227,7 @@ describe("createFormSchema", () => {
expect(descriptor.__decoder.fieldType).toBe("string");
expect(descriptor.__decoder.init()).toBe("");
expect(descriptor.__decoder.decode("foo").ok).toBe(true);
expect(descriptor.__decoder.decode(42).ok).toBe(false);
expect(descriptor.__decoder.decode({}).ok).toBe(false);
});

it("creates field descriptor for choice field", () => {
Expand Down Expand Up @@ -263,7 +263,7 @@ describe("createFormSchema", () => {
expect(descriptor.__decoder.fieldType).toBe("number");
expect(descriptor.__decoder.init()).toBe("");
expect(descriptor.__decoder.decode(666).ok).toBe(true);
expect(descriptor.__decoder.decode("42").ok).toBe(false);
expect(descriptor.__decoder.decode("foo").ok).toBe(false);
});

it("creates field descriptor for bool field", () => {
Expand All @@ -277,25 +277,21 @@ describe("createFormSchema", () => {
expect(descriptor.__decoder.fieldType).toBe("bool");
expect(descriptor.__decoder.init()).toBe(false);
expect(descriptor.__decoder.decode(true).ok).toBe(true);
expect(descriptor.__decoder.decode("true").ok).toBe(false);
expect(descriptor.__decoder.decode("foo").ok).toBe(false);
});

it("creates field descriptor for instanceOf field", () => {
class MyClass {
constructor(public foo: string) {}
}

it("creates field descriptor for date field", () => {
const Schema = createFormSchema(fields => ({
theClass: fields.instanceOf(MyClass),
theDate: fields.date(),
}));

const descriptor = impl(Schema.theClass);
const descriptor = impl(Schema.theDate);

expect(descriptor.__path).toBe("theClass");
expect(descriptor.__decoder.fieldType).toBe("class");
expect(descriptor.__path).toBe("theDate");
expect(descriptor.__decoder.fieldType).toBe("date");
expect(descriptor.__decoder.init()).toBe(null);
expect(descriptor.__decoder.decode(new MyClass("42")).ok).toBe(true);
expect(descriptor.__decoder.decode({ foo: "42" }).ok).toBe(false);
expect(descriptor.__decoder.decode(new Date()).ok).toBe(true);
expect(descriptor.__decoder.decode(new Date("foo")).ok).toBe(false);
});

it("creates field descriptor for array field", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/core/builders/create-form-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const createFieldDescriptor = (
case "bool":
case "number":
case "string":
case "class":
case "date":
case "choice":
return rootDescriptor;

Expand Down
10 changes: 5 additions & 5 deletions src/core/builders/create-form-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe("createFormValidator", () => {
string: fields.string(),
number: fields.number(),
choice: fields.choice("A", "B", "C"),
instance: fields.instanceOf(Date),
date: fields.date(),
arrayString: fields.array(fields.string()),
arrayChoice: fields.array(fields.choice("a", "b", "c")),
arrayArrayString: fields.array(fields.array(fields.string())),
Expand Down Expand Up @@ -178,18 +178,18 @@ describe("createFormValidator", () => {
expect(validation).toEqual([{ field: Schema.choice, error: null }]);
});

it("should return ERR for failing single-rule on instance field ", async () => {
it("should return ERR for failing single-rule on date field ", async () => {
const { validate } = createFormValidator(Schema, validate => [
validate({
field: Schema.instance,
field: Schema.date,
rules: () => [x => (x === null ? "REQUIRED" : null)],
}),
]);
const getValue = () => null as any;

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

expect(validation).toEqual([{ field: Schema.instance, error: "REQUIRED" }]);
expect(validation).toEqual([{ field: Schema.date, error: "REQUIRED" }]);
});

it("should return ERR for failing multiple-rule on string array field ", async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/core/decoders/array.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("array decoder", () => {
const decoder = impl(array(string()));
const value = [null, "foo", 42, "", []];

expect(decoder.decode(value)).toEqual({ ok: false, value });
expect(decoder.decode(value)).toEqual({ ok: false });
});
});

Expand All @@ -64,7 +64,7 @@ describe("array decoder", () => {
const decoder = impl(array(string()));
const value = [null, "foo", 42, "", []];

expect(decoder.decode(value)).toEqual({ ok: false, value });
expect(decoder.decode(value)).toEqual({ ok: false });
});
});

Expand All @@ -87,7 +87,7 @@ describe("array decoder", () => {
const decoder = impl(array(string()));
const value = [null, "foo", 42, "", []];

expect(decoder.decode(value)).toEqual({ ok: false, value });
expect(decoder.decode(value)).toEqual({ ok: false });
});
});
});
6 changes: 3 additions & 3 deletions src/core/decoders/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { impl, opaque } from "../types/type-mapper-util";

/**
* Define array field with elements of type defined by provided `innerDecoder`.
* Will check if value is array and if each element matches expected type at runtime.
* Default initial value will be `[]`
* Accepts empty arrays and arrays containing elements which are valid in respect to rules imposed by `innerDecoder`.
*
* @example
* ```
Expand All @@ -23,7 +23,7 @@ export const array = <E>(
init: () => [],

decode: value => {
if (Array.isArray(value)) {
if (value && Array.isArray(value)) {
const decodeResults = value.map(impl(innerDecoder).decode);
if (decodeResults.every(result => result.ok)) {
return {
Expand All @@ -32,7 +32,7 @@ export const array = <E>(
};
}
}
return { ok: false, value };
return { ok: false };
},
};

Expand Down
Loading

0 comments on commit a013729

Please sign in to comment.