Skip to content

Commit

Permalink
fix(form): set field values synchronously [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel910 committed May 10, 2024
1 parent 8cd450f commit c977398
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 46 deletions.
4 changes: 2 additions & 2 deletions packages/form/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ export class FormAPI<T> {
value: this.presenter.getFieldValue(props.name),
form: this as FormAPI<any>,
onChange: async (value: unknown) => {
await this.presenter.setFieldValue(props.name, value);
this.presenter.setFieldValue(props.name, value);

if (this.shouldValidate()) {
await this.presenter.validateField(props.name);
this.presenter.validateField(props.name);
}
}
};
Expand Down
29 changes: 22 additions & 7 deletions packages/form/src/FormField.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import lodashNoop from "lodash/noop";
import { BindComponentProps, FormAPI, FormValidationOptions } from "~/types";
import { Validator } from "@webiny/validation/types";
import { FieldValidationResult, FormFieldValidator } from "./FormFieldValidator";
Expand All @@ -6,6 +7,10 @@ interface BeforeChange {
(value: unknown, cb: (value: unknown) => void): void;
}

const defaultBeforeChange: BeforeChange = (value, cb) => cb(value);

const defaultAfterChange = lodashNoop;

export class FormField {
private readonly name: string;
private readonly defaultValue: unknown;
Expand All @@ -14,7 +19,7 @@ export class FormField {
private readonly afterChange?: (value: unknown, form: FormAPI) => void;
private validation: FieldValidationResult | undefined = undefined;

constructor(props: BindComponentProps) {
private constructor(props: BindComponentProps) {
this.name = props.name;
this.defaultValue = props.defaultValue;
this.beforeChange = props.beforeChange;
Expand All @@ -38,6 +43,10 @@ export class FormField {
return newField;
}

static create(props: BindComponentProps) {
return new FormField(props);
}

async validate(
value: unknown,
options?: FormValidationOptions
Expand All @@ -46,10 +55,6 @@ export class FormField {
return this.validation;
}

resetValidation() {
this.validation = undefined;
}

isValid() {
return this.validation ? this.validation.isValid : undefined;
}
Expand All @@ -63,14 +68,24 @@ export class FormField {
}

getBeforeChange() {
return this.beforeChange;
return this.beforeChange ?? defaultBeforeChange;
}

getAfterChange() {
return this.afterChange;
return this.afterChange ?? defaultAfterChange;
}

getValidation() {
return this.validation;
}

setValue(value: unknown, cb: (value: unknown) => void) {
const beforeChange = this.getBeforeChange();
const afterChange = this.getAfterChange();

beforeChange(value, value => {
cb(value);
afterChange(value);
});
}
}
8 changes: 3 additions & 5 deletions packages/form/src/FormPresenter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ describe("FormPresenter", () => {
presenter.registerField({
name: "firstName",
validators: [validation.create("required")],
beforeChange: async (value, cb) => {
// Simulate an async callback
await new Promise(resolve => setTimeout(resolve, 100));
beforeChange: (value, cb) => {
beforeChangeSpy(value);
cb(value);
},
Expand All @@ -56,8 +54,8 @@ describe("FormPresenter", () => {
// Assert
expect(vm.formFields.get("firstName")).toBeInstanceOf(FormField);

await presenter.setFieldValue("firstName", "John");
await presenter.setFieldValue("settings.email", "test@example.com");
presenter.setFieldValue("firstName", "John");
presenter.setFieldValue("settings.email", "test@example.com");

expect(presenter.vm.data).toEqual({
firstName: "John",
Expand Down
61 changes: 29 additions & 32 deletions packages/form/src/FormPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import { FormField } from "./FormField";
import { FormValidator } from "./FormValidator";
import { FieldValidationResult } from "./FormFieldValidator";

interface BeforeChange {
(value: unknown, cb: (value: unknown) => void): void;
}

interface FormInvalidFields {
[name: string]: string;
}
Expand All @@ -33,9 +29,6 @@ export interface InvalidFormFields {
[name: string]: FieldValidationResult;
}

const defaultBeforeChange: BeforeChange = (value, cb) => cb(value);
const defaultAfterChange = lodashNoop;

export class FormPresenter<T extends GenericFormData = GenericFormData> {
/* Holds the current form data. */
private data: T;
Expand Down Expand Up @@ -100,32 +93,20 @@ export class FormPresenter<T extends GenericFormData = GenericFormData> {
);
}

async setFieldValue(name: string, value: unknown) {
setFieldValue(name: string, value: unknown) {
const field = this.formFields.get(name);
if (!field) {
return;
}

const beforeChange = field.getBeforeChange() ?? defaultBeforeChange;
const afterChange = field.getAfterChange() ?? defaultAfterChange;

const newValue = await new Promise(resolve => {
beforeChange(value, value => {
resolve(value);
});
});

runInAction(() => {
this.commitValueToData(name, newValue);
afterChange(newValue);
/**
* We delegate field value handling to the FormField class.
*/
field.setValue(value, value => {
this.commitValueToData(name, value);
});
}

private commitValueToData = (name: string, value: unknown) => {
lodashSet(this.data, name, value);
this.onFormChange(toJS(this.data));
};

async validateField(name: string, options?: FormValidationOptions) {
const field = this.formFields.get(name);
if (!field) {
Expand Down Expand Up @@ -160,21 +141,32 @@ export class FormPresenter<T extends GenericFormData = GenericFormData> {
}

registerField(props: BindComponentProps) {
let field = this.formFields.get(props.name);
if (field) {
field = FormField.createFrom(field, props);
const existingField = this.formFields.get(props.name);

let field;
if (existingField) {
field = FormField.createFrom(existingField, props);
} else {
field = new FormField(props);
field = FormField.create(props);
}

this.formFields.set(props.name, field);

// Set field's default value
// We only want to handle default field value for new fields.
if (existingField) {
return;
}

// Set field's default value.
const fieldName = field.getName();
const currentFieldValue = lodashGet(this.data, fieldName);
const defaultValue = field.getDefaultValue();
if (!currentFieldValue && defaultValue) {
this.setFieldValue(fieldName, defaultValue);
if (currentFieldValue === undefined && defaultValue !== undefined) {
// We need to postpone the state update, because `registerField` is called within a render cycle.
// You can't set a new state, while the previous state is being rendered.
requestAnimationFrame(() => {
this.setFieldValue(fieldName, defaultValue);
});
}
}

Expand All @@ -195,4 +187,9 @@ export class FormPresenter<T extends GenericFormData = GenericFormData> {

return isValid;
}

private commitValueToData = (name: string, value: unknown) => {
lodashSet(this.data, name, value);
this.onFormChange(toJS(this.data));
};
}

0 comments on commit c977398

Please sign in to comment.