Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public form updates #2213

Draft
wants to merge 13 commits into
base: alpha
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Custom
.nx
playground
# playground
NOTES.md

# compiled output
Expand Down
182 changes: 170 additions & 12 deletions libs/angular-components/src/lib/public-form-utils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,182 @@
export type AppState = {
form: Record<string, Record<string, FieldsetItemState>[]>;
import { FieldValidator } from "./validation";

export type FormStatus = "not-started" | "incomplete" | "complete";

// Public type to define the state of the form
export type AppState<T> = {
form: Record<string, { heading: string; data: Record<string, FieldsetItemState>[] }>;
history: string[];
editting: string;
lastModified?: Date;
status: FormStatus;
currentFieldset?: { id: T; dispatchType: "change" | "continue" };
};

// Public type to define the state of the fieldset items
export type FieldsetItemState = {
name: string;
label: string;
// value: string | number | Date;
value: string;
};

export class PublicFormComponent<T> {
state?: AppState<T> | AppState<T>[];
_formData?: Record<string, string> = undefined;
_formRef?: HTMLElement = undefined;

// Obtain reference to the form element
init(e: Event) {
console.debug("Utils::init", e);
this._formRef = (e as CustomEvent).detail.el;

this.state = {
form: {},
history: [],
editting: "",
status: "not-started",
};
}

initList(e: Event) {
console.debug("Utils::initList", e);
this._formRef = (e as CustomEvent).detail.el;
this.state = [];
}

// Public method to allow for the initialization of the state
initState(state: string | AppState<T>) {
console.debug("Utils:initState", state);
relay(this._formRef, "external::init:state", state);
}

// Public method to allow for the updating of the state
updateState(e: Event) {
console.debug("Utils:updateState", e, { state: this.state });
if (!this.state) {
console.error("updateState: state has not yet been set");
return;
}

if (Array.isArray(this.state)) {
const state = (e as CustomEvent).detail as AppState<T>[];
this.state = state;
} else {
const state = (e as CustomEvent).detail as AppState<T>;
this.state = {
...this.state,
form: state.form,
currentFieldset: state.currentFieldset, // TODO: remember why this is here
};
}
}

getStateList(): Record<string, string>[] {
// getStateList(): unknown[] {
if (!Array.isArray(this.state)) {
console.error(
"Utils:getStateList: unable to update the state of a non-multi form type",
);
return [];
}

return this.state.map((s) => {
return Object.values(s.form)
.map((item) => item.data)
.reduce((acc, item) => {
for (const [key, value] of Object.entries(item)) {
// @ts-expect-error "ignore"
acc[key] = value.value;
}
return acc;
}, {} as Record<string, string>);
});
}

getStateItems(group: string): Record<string, FieldsetItemState>[] {
if (Array.isArray(this.state)) {
console.error(
"Utils:getStateItems: unable to update the state of a multi form type",
);
return [];
}
if (!this.state) {
console.error("Utils:getStateItems: state has not yet been set");
return [];
}

console.debug("Utils:getStateItems", this.state, { group });
return (this.state.form[group]?.data ?? []) as Record<string, FieldsetItemState>[];
}

// Public method to allow for the retrieval of the state value
getStateValue(group: string, key: string): string {
if (Array.isArray(this.state)) {
console.error("getStateValue: unable to update the state of a multi form type");
return "";
}
if (!this.state) {
console.error("getStateValue: state has not yet been set");
return "";
}

const data = this.state.form[group].data as Record<string, FieldsetItemState>[];
// @ts-expect-error "ignore"
return (data as Record<string, string>)[key]?.value ?? "";
}

// Public method to allow for the continuing to the next page
continueTo(name: T | undefined) {
console.debug("Utils:continueTo", { name, state: this.state });

if (!name) {
console.error("continueTo [name] is undefined");
return;
}
// Relay the continue message to the form element which will
// set the visibility of the fieldsets
relay<{ next: T }>(this._formRef, "external::continue", {
next: name,
});
}

// Public method to peform validation and send the appropriate messages to the form elements
validate(field: string, e: Event, validators: FieldValidator[]): [boolean, string] {
console.debug("Utils:validate", { field, e, validators });
const { el, state } = (e as CustomEvent).detail;
const value = state?.[field]?.value;

for (const validator of validators) {
const msg = validator(value);
this.#dispatchError(el, field, msg);
if (msg) {
return [false, ""];
}
}
return [true, value];
}

edit(index: number) {}

remove(index: number) {}

// Private method to dispatch the error message to the form element
#dispatchError(el: HTMLElement, name: string, msg: string) {
el.dispatchEvent(
new CustomEvent("msg", {
composed: true,
detail: {
action: "external::set:error",
data: {
name,
msg,
},
},
}),
);
}
}

// Public helper function to dispatch messages
export function dispatch<T>(
el: HTMLElement | Element | null | undefined,
eventName: string,
Expand All @@ -31,6 +196,7 @@ export function dispatch<T>(
);
}

// Public helper function to relay messages
export function relay<T>(
el: HTMLElement | Element | null | undefined,
eventName: string,
Expand All @@ -41,7 +207,7 @@ export function relay<T>(
console.error("dispatch element is null");
return;
}
// console.log(`RELAY(${eventName}):`, data, el);
console.debug(`RELAY(${eventName}):`, data, el);
el.dispatchEvent(
new CustomEvent<{ action: string; data?: T }>("msg", {
composed: true,
Expand All @@ -53,11 +219,3 @@ export function relay<T>(
}),
);
}

// TODO: Logic similar to this needs to be done on the React side as well i.e. an initial onMount
// event that passes a ref to the form,
export function continueTo(el: HTMLElement, name: string) {
relay<{ next: string }>(el, "external::continue", {
next: name,
});
}
68 changes: 65 additions & 3 deletions libs/angular-components/src/lib/validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { describe, it, expect } from "vitest";

import { SINValidator } from "./validation";
import { lengthValidator, SINValidator } from "./validation";
import { emailValidator, postalCodeValidator } from "./validation.ts";

describe("Validation", () => {
describe("Email", () => {
const validEmails = [
"",
"[email protected]",
"email#[email protected]",
"[email protected]",
Expand Down Expand Up @@ -56,7 +57,13 @@ describe("Validation", () => {
});

describe("SIN", () => {
const validSINs = ["130692544", "130 692 544", "130-692-544", "1 3 0 6 9 2 5 4 4"];
const validSINs = [
"",
"130692544",
"130 692 544",
"130-692-544",
"1 3 0 6 9 2 5 4 4",
];
const invalidSINs = ["130692543", "130 692 543", "130-692-543", "1 3 0 6 9 2 5 4 3"];

const validate = SINValidator();
Expand All @@ -77,7 +84,7 @@ describe("Validation", () => {
});

describe("Postal code", () => {
const validPostalCodes = ["M4B2J8", "M4B 2J8", "M4B-2J8"];
const validPostalCodes = ["", "M4B2J8", "M4B 2J8", "M4B-2J8"];
const invalidPostalCodes = ["T7D2HG", "T7D299", "T7D2H"];

const validate = postalCodeValidator();
Expand All @@ -96,4 +103,59 @@ describe("Validation", () => {
});
}
});

describe("Length", () => {
describe("Optional", () => {
const validValues = ["", "123456"];
const invalidValues = ["12345"];

const validate = lengthValidator({ min: 6, optional: true });

for (const val of validValues) {
it(`${val} should be valid`, () => {
const msg = validate(val);
expect(msg).toBe("");
});
}

for (const val of invalidValues) {
it(`${val} should be invalid`, () => {
const msg = validate(val);
expect(msg).not.toBe("");
});
}
});

describe("Required", () => {
const validValues = ["123456"];
const invalidValues = ["", "12345"];
const validate = lengthValidator({ min: 6, optional: false });

for (const val of validValues) {
it(`${val} should be valid`, () => {
const msg = validate(val);
expect(msg).toBe("");
});
}

for (const val of invalidValues) {
it(`${val} should be invalid`, () => {
const msg = validate(val);
expect(msg).not.toBe("");
});
}
});
});

describe("Date", () => {
it("needs a test");
});

describe("Regex", () => {
it("needs a test");
});

describe("Phone", () => {
it("needs a test");
});
});
Loading
Loading