Skip to content

Commit

Permalink
feat: cancel esc button for cancel the form
Browse files Browse the repository at this point in the history
  • Loading branch information
danielo515 committed May 13, 2024
1 parent 1c3b1a7 commit ea350bf
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 43 deletions.
15 changes: 14 additions & 1 deletion src/FormModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,11 @@ export class FormModal extends Modal {
}
});

new Setting(contentEl).addButton((btn) =>
const buttons = new Setting(contentEl).addButton((btn) =>
btn.setButtonText("Cancel").onClick(this.formEngine.triggerCancel),
);

buttons.addButton((btn) =>
btn.setButtonText("Submit").setCta().onClick(this.formEngine.triggerSubmit),
);

Expand All @@ -303,7 +307,16 @@ export class FormModal extends Modal {
}
};

const cancelEscapeCallback = (evt: KeyboardEvent) => {
// We don't want to hande it if any modfier is pressed
if (!(evt.ctrlKey || evt.metaKey) && evt.key === "Escape") {
evt.preventDefault();
this.formEngine.triggerCancel();
}
};

contentEl.addEventListener("keydown", submitEnterCallback);
contentEl.addEventListener("keydown", cancelEscapeCallback);
}

onClose() {
Expand Down
57 changes: 24 additions & 33 deletions src/core/FormResult.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ isEmployed:: true`;
it("should return the data formatted as a string matching the provided template", () => {
const result = FormResult.make(formData, "ok");
const template = "My name is {{name}}, and I am {{age}} years old.";
const expectedOutput =
"My name is John Doe, and I am 30 years old.";
const expectedOutput = "My name is John Doe, and I am 30 years old.";
expect(result.asString(template)).toEqual(expectedOutput);
});
});
Expand All @@ -66,9 +65,7 @@ isEmployed:: true`;
const result = FormResult.make(formData, "ok");
const expectedOutput = `name:: John Doe
age:: 30`;
expect(
result.asDataviewProperties({ pick: ["name", "age"] }),
).toEqual(expectedOutput);
expect(result.asDataviewProperties({ pick: ["name", "age"] })).toEqual(expectedOutput);
});

it("should return the data as a string of dataview properties with all keys except the specified ones using options.omit", () => {
Expand Down Expand Up @@ -111,20 +108,18 @@ age:: 30`;
const result = FormResult.make(formData, "ok");
const expectedOutput = `name: John Doe
age: 30`;
expect(
result.asFrontmatterString({ pick: ["name", "age"] }).trim(),
).toEqual(expectedOutput);
expect(result.asFrontmatterString({ pick: ["name", "age"] }).trim()).toEqual(
expectedOutput,
);
});

it("should return the data as a YAML frontmatter string with all keys except the specified ones using options.omit", () => {
const result = FormResult.make(formData, "ok");
const expectedOutput = `name: John Doe
age: 30`;
expect(
result
.asFrontmatterString({ omit: ["hobbies", "isEmployed"] })
.trim(),
).toEqual(expectedOutput);
expect(result.asFrontmatterString({ omit: ["hobbies", "isEmployed"] }).trim()).toEqual(
expectedOutput,
);
});

it("should return the data as a YAML frontmatter string with only the specified keys using options.pick and ignoring options.omit", () => {
Expand Down Expand Up @@ -178,30 +173,26 @@ age: 30`;
expect(result.get("foo")).toEqual("");
});
});
describe('Shorthand proxied accessors', () => {
it('Should allow access to a value in the data directly using dot notation',
() => {
const result = FormResult.make(formData, "ok");
// @ts-ignore
expect(result.name.toString()).toEqual("John Doe");
})
it('Should allow access to a value in the data directly and allow to use shorthand methods on the returned value',
() => {
const result = FormResult.make(formData, "ok");
// @ts-ignore
expect(result.name.upper.toString()).toEqual("JOHN DOE");
}
)
it('proxied access to bullet list should return a bullet list', () => {
describe("Shorthand proxied accessors", () => {
it("Should allow access to a value in the data directly using dot notation", () => {
const result = FormResult.make(formData, "ok");
// @ts-ignore
expect(result.name.toString()).toEqual("John Doe");
});
it("Should allow access to a value in the data directly and allow to use shorthand methods on the returned value", () => {
const result = FormResult.make(formData, "ok");
// @ts-ignore
expect(result.name.upper.toString()).toEqual("JOHN DOE");
});
it("proxied access to bullet list should return a bullet list", () => {
const result = FormResult.make(formData, "ok");
// @ts-ignore
expect(result.hobbies.bullets).toEqual("- reading\n- swimming");
})
it('accessing a non existing key should return a safe ResultValue, letting chain without issues', () => {
});
it("accessing a non existing key should return a safe ResultValue, letting chain without issues", () => {
const result = FormResult.make(formData, "ok");
// @ts-ignore
expect(result.foo.upper.lower.toString()).toEqual("");
})
})

});
});
});
34 changes: 28 additions & 6 deletions src/store/formStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { makeFormEngine } from "./formStore";
describe("Form Engine", () => {
it("should update form fields correctly", () => {
const onSubmitMock = jest.fn();
const formEngine = makeFormEngine({ onSubmit: onSubmitMock });
const formEngine = makeFormEngine({ onSubmit: onSubmitMock, onCancel: console.log });

// Add fields to the form
const field1 = formEngine.addField({ name: "fieldName1" });
Expand All @@ -24,7 +24,7 @@ describe("Form Engine", () => {

it("should handle field errors correctly", () => {
const onSubmitMock = jest.fn();
const formEngine = makeFormEngine({ onSubmit: onSubmitMock });
const formEngine = makeFormEngine({ onSubmit: onSubmitMock, onCancel: console.log });
// Add a field to the form
const field1 = formEngine.addField({
name: "fieldName1",
Expand All @@ -43,7 +43,7 @@ describe("Form Engine", () => {
});
it("field errors should prefer field label over field name", () => {
const onSubmitMock = jest.fn();
const formEngine = makeFormEngine({ onSubmit: onSubmitMock });
const formEngine = makeFormEngine({ onSubmit: onSubmitMock, onCancel: console.log });
// Add a field to the form
const field1 = formEngine.addField({
name: "fieldName1",
Expand All @@ -63,7 +63,7 @@ describe("Form Engine", () => {
});
it("Clears the errors when a value is set", () => {
const onSubmitMock = jest.fn();
const formEngine = makeFormEngine({ onSubmit: onSubmitMock });
const formEngine = makeFormEngine({ onSubmit: onSubmitMock, onCancel: console.log });
// Add a field to the form
const field1 = formEngine.addField({
name: "fieldName1",
Expand Down Expand Up @@ -92,7 +92,11 @@ describe("Form Engine", () => {
fieldName1: "default1",
fieldName2: "default2",
};
const formEngine = makeFormEngine({ onSubmit: onSubmitMock, defaultValues });
const formEngine = makeFormEngine({
onSubmit: onSubmitMock,
defaultValues,
onCancel: console.log,
});

// Add fields to the form
const field1 = formEngine.addField({ name: "fieldName1" });
Expand All @@ -114,7 +118,11 @@ describe("Form Engine", () => {
const defaultValues = {
fieldName1: "default1",
};
const formEngine = makeFormEngine({ onSubmit: onSubmitMock, defaultValues });
const formEngine = makeFormEngine({
onSubmit: onSubmitMock,
defaultValues,
onCancel: console.log,
});

// Add fields to the form
const field1 = formEngine.addField({ name: "fieldName1" });
Expand All @@ -128,4 +136,18 @@ describe("Form Engine", () => {
fieldName1: "default1",
});
});
it("should flag the form as cancelled and call the onCancel callback when the cancel button is clicked", () => {
const onCancelMock = jest.fn();
const formEngine = makeFormEngine({ onSubmit: console.log, onCancel: onCancelMock });
// Add a field to the form
const field1 = formEngine.addField({ name: "fieldName1" });
// Update field value with an empty string
field1.value.set("");
// Trigger form submission
formEngine.triggerCancel();
// Assert that the onCancel callback is called
expect(onCancelMock).toHaveBeenCalled();
// Assert that the form is not valid
// expect(get(formEngine.isValid)).toBe(false); // is it worth checking this?
});
});
22 changes: 19 additions & 3 deletions src/store/formStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ type FieldFailed<T extends FieldValue> = {
function FieldFailed<T extends FieldValue>(field: Field<T>, failedRule: Rule): FieldFailed<T> {
return { ...field, rules: failedRule, errors: [failedRule.message] };
}
type FormStore<T extends FieldValue> = { fields: Record<string, Field<T>> };
type FormStore<T extends FieldValue> = {
fields: Record<string, Field<T>>;
status: "submitted" | "cancelled" | "draft";
};

// TODO: instead of making the whole engine generic, make just the addField method generic extending the type of the field value
// Then, the whole formEngine can be typed as FormEngine<FieldValue>
Expand Down Expand Up @@ -60,6 +63,12 @@ export interface FormEngine<T extends FieldValue> {
* If the form is invalid, the errors are updated and the onSubmit function is not called.
*/
triggerSubmit: () => void;
/**
* Cancels the form and calls the onCancel function.
* In the future we may add a confirmation dialog before calling the onCancel function.
* or even persist the form state to allow the user to resume later.
*/
triggerCancel: () => void;
}
function nonEmptyValue(s: FieldValue): Option<FieldValue> {
switch (typeof s) {
Expand Down Expand Up @@ -128,17 +137,19 @@ function parseForm<T extends FieldValue>(
}

export type OnFormSubmit<T> = (values: Record<string, T>) => void;

type makeFormEngineArgs<T> = {
onSubmit: OnFormSubmit<T>;
onCancel?: () => void;
onCancel: () => void;
defaultValues?: Record<string, T>;
};

export function makeFormEngine<T extends FieldValue>({
onSubmit,
onCancel,
defaultValues = {},
}: makeFormEngineArgs<T>): FormEngine<T> {
const formStore: Writable<FormStore<T>> = writable({ fields: {} });
const formStore: Writable<FormStore<T>> = writable({ fields: {}, status: "draft" });
// Creates helper functions to modify the store immutably
function setFormField(name: string) {
// Set the initial value of the field
Expand Down Expand Up @@ -198,6 +209,7 @@ export function makeFormEngine<T extends FieldValue>({
),
),
triggerSubmit() {
formStore.update((form) => ({ ...form, status: "submitted" }));
const formState = get(formStore);
// prettier-ignore
pipe(
Expand All @@ -206,6 +218,10 @@ export function makeFormEngine<T extends FieldValue>({
E.match(setErrors, onSubmit)
);
},
triggerCancel() {
formStore.update((form) => ({ ...form, status: "cancelled" }));
onCancel?.();
},
addField: (field) => {
const { initField: setField, setValue } = setFormField(field.name);
setField([], field.isRequired ? requiredRule(field.label || field.name) : undefined);
Expand Down

0 comments on commit ea350bf

Please sign in to comment.