Skip to content

Commit

Permalink
fix: updates to public-form related components
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisolsen committed Nov 13, 2024
1 parent 533f2a1 commit 41f11b9
Show file tree
Hide file tree
Showing 21 changed files with 853 additions and 466 deletions.
93 changes: 82 additions & 11 deletions libs/angular-components/src/lib/public-form-utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,93 @@
export type AppState = {
form: Record<string, Record<string, FieldsetItemState>[]>;
import { FieldValidator } from "./validation";

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

export class PublicFormComponent<T> {
state: AppState<T> = {
form: {},
history: [],
editting: "",
status: "not-started",
};
_formData?: Record<string, string> = undefined;
_formRef?: HTMLElement = undefined;

init(e: Event) {
this._formRef = (e as CustomEvent).detail.el;
relay(this._formRef, "init", {});
}

initState(state: string | AppState<T>) {
relay(this._formRef, "external::init:state", state);
}

updateState(e: Event) {
const state = (e as CustomEvent).detail as AppState<T>;
this.state = {
...this.state,
form: state.form,
currentFieldset: state.currentFieldset,
};
}

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

continueTo(name: T | undefined) {
if (!name) {
console.error("continueTo [name] is undefined");
return;
}
relay<{ next: T }>(this._formRef, "external::continue", {
next: name,
});
}

validate(field: string, e: Event, validators: FieldValidator[]): [boolean, string] {
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];
}

#dispatchError(el: HTMLElement, name: string, msg: string) {
el.dispatchEvent(
new CustomEvent("msg", {
composed: true,
detail: {
action: "external::set:error",
data: {
name,
msg,
},
},
}),
);
}
}

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" };
};

export type FieldsetItemState = {
name: string;
label: string;
// value: string | number | Date;
value: string;
};

Expand Down Expand Up @@ -53,11 +132,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");
});
});
48 changes: 11 additions & 37 deletions libs/angular-components/src/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,6 @@ import { FieldsetItemState } from "./public-form-utils";
export type FieldValidator = (value: unknown) => string;
export type FieldsetState = Record<string, FieldsetItemState>;

// TODO: move this into the form-utils file, since it is specific to the public form component
export function validate(
field: string,
fieldsetEl: HTMLElement,
fieldsetState: FieldsetState,
validators: FieldValidator[],
): [boolean, string] {
const value = fieldsetState?.[field]?.value;

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

// TODO: move this into the form-utils file, since it is specific to the public form component
function dispatchError(el: HTMLElement, name: string, msg: string) {
el.dispatchEvent(
new CustomEvent("msg", {
composed: true,
detail: {
action: "external::set:error",
data: {
name,
msg,
},
},
}),
);
}

export class FormValidator {
private validators: Record<string, FieldValidator[]>;
constructor(validators?: Record<string, FieldValidator[]>) {
Expand Down Expand Up @@ -135,10 +100,12 @@ export function emailValidator(msg?: string): FieldValidator {
return regexValidator(regex, msg || "Invalid email address");
}

// SIN# Generator: https://singen.ca
export function SINValidator(): FieldValidator {
return (value: unknown) => {
if (!value) return "";
const checkValue = "121121121".split("").map((c) => parseInt(c));

const checkValue = "121212121".split("").map((c) => parseInt(c));
const valueStr = (value as string).replace(/\D/g, "");

if (valueStr.length !== 9) return "SIN must contain 9 numbers";
Expand All @@ -154,7 +121,7 @@ export function SINValidator(): FieldValidator {
return `${val}`
.split("")
.map((c) => parseInt(c))
.reduce((acc, val) => acc * val, 1);
.reduce((acc, val) => acc + val, 0);
})
.reduce((acc, val) => acc + val, 0);

Expand Down Expand Up @@ -270,15 +237,22 @@ interface LengthValidatorOptions {
maxMsg?: string;
max?: number;
min?: number;
optional?: boolean;
}
export function lengthValidator({
invalidTypeMsg,
minMsg,
maxMsg,
min = -Number.MAX_VALUE,
max = Number.MAX_VALUE,
optional,
}: LengthValidatorOptions): FieldValidator {
return (value: unknown) => {
// valid if optional and blank
if (optional && `${value}`.length === 0) {
return "";
}

if (typeof value !== "string") {
return invalidTypeMsg || "Invalid type";
}
Expand Down
4 changes: 2 additions & 2 deletions libs/web-components/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ export function receive(
export function relay<T>(
el: HTMLElement | Element | null | undefined,
eventName: string,
data: T,
data?: T,
opts?: { bubbles?: boolean; cancelable?: boolean; timeout?: number },
) {
// console.log(`RELAY(${eventName}):`, data, el);

const dispatch = () => {
el?.dispatchEvent(
new CustomEvent<{ action: string; data: T }>("msg", {
new CustomEvent<{ action: string; data?: T }>("msg", {
composed: true,
bubbles: opts?.bubbles,
cancelable: opts?.cancelable,
Expand Down
Loading

0 comments on commit 41f11b9

Please sign in to comment.