From aa9cdf6efbe7bbc20dfffd287e7d06228ee3d6f0 Mon Sep 17 00:00:00 2001 From: Chris Olsen Date: Thu, 16 Jan 2025 12:15:15 -0700 Subject: [PATCH] chore: consolidate duplicate react/angular files into common --- libs/angular-components/package.json | 1 + libs/angular-components/src/index.ts | 3 +- libs/common/package.json | 3 +- libs/common/src/index.ts | 2 + libs/common/src/lib/common.ts | 4 +- .../src/lib/public-form-controller.ts} | 9 +- .../src/lib/validation.spec.ts | 32 ++- .../src/lib/validators.ts} | 38 +-- .../src/experimental/form/form.tsx | 2 +- .../src/experimental/validators.ts | 253 ------------------ .../src/lib/form-item/form-item.tsx | 4 +- .../src/lib/radio-group/radio-group.tsx | 2 +- tsconfig.base.json | 4 +- 13 files changed, 68 insertions(+), 289 deletions(-) rename libs/{angular-components/src/lib/public-form-utils.ts => common/src/lib/public-form-controller.ts} (97%) rename libs/{angular-components => common}/src/lib/validation.spec.ts (82%) rename libs/{angular-components/src/lib/validation.ts => common/src/lib/validators.ts} (93%) delete mode 100644 libs/react-components/src/experimental/validators.ts diff --git a/libs/angular-components/package.json b/libs/angular-components/package.json index 13a022554..4ec012339 100644 --- a/libs/angular-components/package.json +++ b/libs/angular-components/package.json @@ -17,6 +17,7 @@ "directory": "libs/angular-components" }, "peerDependencies": { + "@abgov/ui-components-common": "^0.0.0 || ^1.0.0", "@angular/forms": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/common": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "@angular/core": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/libs/angular-components/src/index.ts b/libs/angular-components/src/index.ts index ed67baa18..55aebfe4b 100644 --- a/libs/angular-components/src/index.ts +++ b/libs/angular-components/src/index.ts @@ -1,4 +1,3 @@ export * from "./lib/angular-components.module"; export * from "./lib/components"; -export * from "./lib/validation"; -export * from "./lib/public-form-utils"; +export * from "@abgov/ui-components-common"; diff --git a/libs/common/package.json b/libs/common/package.json index 3f837caf9..abfb759d3 100644 --- a/libs/common/package.json +++ b/libs/common/package.json @@ -21,5 +21,6 @@ "module": "./index.js", "publishConfig": { "access": "public" - } + }, + "semantic-release": "semantic-release" } diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index f1154d871..79f0d8960 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -1 +1,3 @@ export * from "./lib/common"; +export * from "./lib/validators"; +export * from "./lib/public-form-controller"; diff --git a/libs/common/src/lib/common.ts b/libs/common/src/lib/common.ts index da6349268..f8f746b5c 100644 --- a/libs/common/src/lib/common.ts +++ b/libs/common/src/lib/common.ts @@ -218,7 +218,7 @@ export type GoabSkeletonSize = "1" | "2" | "3" | "4"; export type GoabRadioGroupOrientation = "horizontal" | "vertical"; -export interface GoabRadioGroup extends Margins { +export interface GoabRadioGroupProps extends Margins { name: string; value?: string; disabled?: boolean; @@ -228,7 +228,7 @@ export interface GoabRadioGroup extends Margins { ariaLabel?: string; } -export interface GoabRadioItem { +export interface GoabRadioItemProps { value?: string; label?: string; name?: string; diff --git a/libs/angular-components/src/lib/public-form-utils.ts b/libs/common/src/lib/public-form-controller.ts similarity index 97% rename from libs/angular-components/src/lib/public-form-utils.ts rename to libs/common/src/lib/public-form-controller.ts index d896f5879..1bd28d4c3 100644 --- a/libs/angular-components/src/lib/public-form-utils.ts +++ b/libs/common/src/lib/public-form-controller.ts @@ -1,4 +1,4 @@ -import { FieldValidator } from "./validation"; +import { FieldsetItemState, FieldValidator } from "./validators"; export type FormStatus = "not-started" | "incomplete" | "complete"; @@ -21,13 +21,6 @@ export type Fieldset = { | { type: "list"; items: AppState[] }; }; -// Public type to define the state of the fieldset items -export type FieldsetItemState = { - name: string; - label: string; - value: string; -}; - export class PublicFormController { state?: AppState | AppState[]; _formData?: Record = undefined; diff --git a/libs/angular-components/src/lib/validation.spec.ts b/libs/common/src/lib/validation.spec.ts similarity index 82% rename from libs/angular-components/src/lib/validation.spec.ts rename to libs/common/src/lib/validation.spec.ts index c75db71be..307c3f260 100644 --- a/libs/angular-components/src/lib/validation.spec.ts +++ b/libs/common/src/lib/validation.spec.ts @@ -5,7 +5,8 @@ import { SINValidator, emailValidator, postalCodeValidator, -} from "./validation"; + dateValidator, +} from "./validators"; describe("Validation", () => { describe("Email", () => { @@ -153,6 +154,35 @@ describe("Validation", () => { describe("Date", () => { it("needs a test"); + // 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("Start date", () => { + // const start = new Date(2025, 0, 1); + // const validator = dateValidator({ min: start }); + // const validDate = new Date(2025, 2, 1); + it("needs a test"); + }); + + // describe("End date", () => { + // + // }) }); describe("Regex", () => { diff --git a/libs/angular-components/src/lib/validation.ts b/libs/common/src/lib/validators.ts similarity index 93% rename from libs/angular-components/src/lib/validation.ts rename to libs/common/src/lib/validators.ts index b8ebf7d6d..5e4b8269e 100644 --- a/libs/angular-components/src/lib/validation.ts +++ b/libs/common/src/lib/validators.ts @@ -1,10 +1,15 @@ -import { FieldsetItemState } from "./public-form-utils"; +export type FieldsetItemState = { + name: string; + label: string; + value: string; +}; export type FieldValidator = (value: unknown) => string; export type FieldsetState = Record; export class FormValidator { private readonly validators: Record; + constructor(validators?: Record) { this.validators = validators || {}; } @@ -32,10 +37,6 @@ export class FormValidator { } } -// ********** -// Validators -// ********** - export function birthDayValidator(): FieldValidator[] { return [ requiredValidator("Day is required"), @@ -155,17 +156,18 @@ export function regexValidator(regex: RegExp, msg: string): FieldValidator { interface DateValidatorOptions { invalidMsg?: string; - startMsg?: string; - endMsg?: string; - start?: Date; - end?: Date; + minMsg?: string; + maxMsg?: string; + min?: Date; + max?: Date; } + export function dateValidator({ invalidMsg, - startMsg, - endMsg, - start, - end, + minMsg, + maxMsg, + min, + max, }: DateValidatorOptions): FieldValidator { return (date: unknown) => { let _date: Date = new Date(0); @@ -181,11 +183,11 @@ export function dateValidator({ return invalidMsg || "Invalid date"; } - if (_date && start && _date < start) { - return startMsg || `Must be after ${start}`; + if (_date && min && _date < min) { + return minMsg || `Must be after ${min}`; } - if (_date && end && _date > end) { - return endMsg || `Must be before ${end}`; + if (_date && max && _date > max) { + return maxMsg || `Must be before ${max}`; } return ""; @@ -199,6 +201,7 @@ interface NumericValidatorOptions { min?: number; max?: number; } + export function numericValidator({ invalidTypeMsg, minMsg, @@ -239,6 +242,7 @@ interface LengthValidatorOptions { min?: number; optional?: boolean; } + export function lengthValidator({ invalidTypeMsg, minMsg, diff --git a/libs/react-components/src/experimental/form/form.tsx b/libs/react-components/src/experimental/form/form.tsx index cf2e5edd4..febdec506 100644 --- a/libs/react-components/src/experimental/form/form.tsx +++ b/libs/react-components/src/experimental/form/form.tsx @@ -1,6 +1,6 @@ import { ReactNode, useEffect, useRef } from "react"; import { Margins, GoabFormStorageType, GoabFormOnMountDetail, GoabFormOnStateChange } from "@abgov/ui-components-common"; -import { relay } from "../validators"; +import { relay } from "@abgov/ui-components-common"; interface WCProps extends Margins { ref?: React.MutableRefObject; diff --git a/libs/react-components/src/experimental/validators.ts b/libs/react-components/src/experimental/validators.ts deleted file mode 100644 index 4749d3bbf..000000000 --- a/libs/react-components/src/experimental/validators.ts +++ /dev/null @@ -1,253 +0,0 @@ -export type FieldValidator = (value: unknown) => string; - -export class FormValidator { - private validators: Record; - constructor(validators?: Record) { - this.validators = validators || {}; - } - - add(fieldName: string, ...validators: FieldValidator[]) { - this.validators[fieldName] = validators; - } - - validate(data: Record): { - errors: Record; - valid: boolean; - } { - const errors: Record = {}; - - Object.entries(this.validators).forEach(([name, validators]) => { - const err = validators - .map((validatorFn) => { - const errMsg = validatorFn(data[name]); - return errMsg; - }) - .find((msg) => !!msg); - if (err) { - errors[name] = err; - } - }); - - return { errors, valid: Object.keys(errors).length === 0 }; - } -} - -export function relay( - el: HTMLElement | Element | null | undefined, - eventName: string, - data: T, - opts?: { bubbles?: boolean }, -) { - if (!el) { - console.error("dispatch element is null"); - return; - } - el.dispatchEvent( - new CustomEvent<{ action: string; data: T }>("msg", { - composed: true, - bubbles: opts?.bubbles, - detail: { - action: eventName, - data, - }, - }), - ); -} - -export type RelayedError = { - name: string; - msg: string; -}; - -export function relayErrors(el: HTMLElement, errors: Record) { - for (const [name, msg] of Object.entries(errors)) { - relay(el, "external::set:error", { name, msg }); - } -} - -// ********** -// Validators -// ********** - -export function birthDayValidator(): FieldValidator[] { - return [ - requiredValidator("Day is required"), - numericValidator({ - min: 1, - max: 31, - minMsg: "Day must be between 1 and 31", - maxMsg: "Day must be between 1 and 31", - }), - ]; -} - -export function birthMonthValidator(): FieldValidator[] { - return [ - requiredValidator("Month is required"), - numericValidator({ - min: 1, - max: 12, - minMsg: "Month must be between Jan and Dec", - maxMsg: "Month must be between Jan and Dec", - }), - ]; -} - -export function birthYearValidator(): FieldValidator[] { - const maxYear = new Date().getFullYear(); - return [ - requiredValidator("Year is required"), - numericValidator({ - min: 1900, - max: maxYear, - minMsg: "Year must be greater than 1900", - maxMsg: `Year must be less than ${maxYear}`, - }), - ]; -} - -export function requiredValidator(msg?: string): FieldValidator { - return (value: unknown) => { - msg = msg || "Required"; - - if (typeof value === "number" && !isNaN(value)) { - return ""; - } - if (value) { - return ""; - } - return msg; - }; -} - -export function phoneNumberValidator(msg?: string): FieldValidator { - const regex = new RegExp(/^\+?[\d-() ]{10,18}$/); - return regexValidator(regex, msg || "Invalid phone number"); -} - -export function emailValidator(msg?: string): FieldValidator { - const regex = new RegExp(/^[\w+-.]+@([\w-]+.)+[\w-]{2,4}$/); - return regexValidator(regex, msg || "Invalid email address"); -} - -export function regexValidator(regex: RegExp, msg: string): FieldValidator { - return (value: unknown) => { - if (!value) { - return ""; - } - if ((value as string).match(regex)) { - return ""; - } - - return msg; - }; -} - -interface DateValidatorOptions { - invalidMsg?: string; - startMsg?: string; - endMsg?: string; - start?: Date; - end?: Date; -} -export function dateValidator({ - invalidMsg, - startMsg, - endMsg, - start, - end, -}: DateValidatorOptions): FieldValidator { - return (date: unknown) => { - let _date: Date = new Date(0); - - if (typeof date === "string") { - _date = new Date(date); - } - if ((date as Date).toDateString) { - _date = date as Date; - } - - if (_date.toString() === "Invalid Date" || _date.getTime() === 0) { - return invalidMsg || "Invalid date"; - } - - if (_date && start && _date < start) { - return startMsg || `Must be after ${start}`; - } - if (_date && end && _date > end) { - return endMsg || `Must be before ${end}`; - } - - return ""; - }; -} - -interface NumericValidatorOptions { - invalidTypeMsg?: string; - minMsg?: string; - maxMsg?: string; - min?: number; - max?: number; -} -export function numericValidator({ - invalidTypeMsg, - minMsg, - maxMsg, - min = -Number.MAX_VALUE, - max = Number.MAX_VALUE, -}: NumericValidatorOptions): FieldValidator { - return (value: unknown) => { - let _value: number = Number.MAX_VALUE; - - if (typeof value === "string") { - _value = parseFloat(value); - } - if (typeof value === "number") { - _value = value; - } - - if (isNaN(_value)) { - return invalidTypeMsg || "Must be a numeric value"; - } - - if (_value > max) { - return maxMsg || `Must be less than or equal to ${max}`; - } - if (_value < min) { - return minMsg || `Must be greater than or equal to ${min}`; - } - - return ""; - }; -} - -interface LengthValidatorOptions { - invalidTypeMsg?: string; - minMsg?: string; - maxMsg?: string; - max?: number; - min?: number; -} -export function lengthValidator({ - invalidTypeMsg, - minMsg, - maxMsg, - min = -Number.MAX_VALUE, - max = Number.MAX_VALUE, -}: LengthValidatorOptions): FieldValidator { - return (value: unknown) => { - if (typeof value !== "string") { - return invalidTypeMsg || "Invalid type"; - } - - if (value.length > max) { - return maxMsg || `Must be less than ${max} characters`; - } - - if (value.length < min) { - return minMsg || `Must be greater than ${min} characters`; - } - - return ""; - }; -} diff --git a/libs/react-components/src/lib/form-item/form-item.tsx b/libs/react-components/src/lib/form-item/form-item.tsx index 4e3a737cb..befb26529 100644 --- a/libs/react-components/src/lib/form-item/form-item.tsx +++ b/libs/react-components/src/lib/form-item/form-item.tsx @@ -24,7 +24,7 @@ declare global { } } -export interface GoabFormItemprops extends Margins { +export interface GoabFormItemProps extends Margins { label?: string; labelSize?: GoabFormItemLabelSize; requirement?: GoabFormItemRequirement; @@ -50,7 +50,7 @@ export function GoabFormItem({ ml, testId, id, -}: GoabFormItemprops): JSX.Element { +}: GoabFormItemProps): JSX.Element { return ( void; + onChange: (detail: GoabRadioGroupOnChangeDetail) => void; } export function GoabRadioGroup({ diff --git a/tsconfig.base.json b/tsconfig.base.json index d22b7a119..d78368e58 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -19,7 +19,9 @@ "@abgov/react-components": ["libs/react-components/src/index.ts"], "@abgov/react-components/experimental": ["libs/react-components/src/experimental/index.ts"], "@abgov/style": ["dist/libs/web-components/index.css"], - "@abgov/ui-components-common": ["dist/libs/common/index.d.ts"], + "@abgov/ui-components-common": [ + "libs/common/src/index.ts" + ], "@abgov/web-components": ["dist/libs/web-components/index.js"] } },