Skip to content

Commit

Permalink
fix: add ability to create looping subforms within a parent form
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisolsen committed Jan 15, 2025
1 parent bf1f5b7 commit 8add27f
Show file tree
Hide file tree
Showing 26 changed files with 1,470 additions and 493 deletions.
17 changes: 6 additions & 11 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
{
"name": "Attach to Chrome",
"port": 9222,
"request": "attach",
"type": "chrome"
}
]
}
256 changes: 218 additions & 38 deletions libs/angular-components/src/lib/public-form-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,235 @@ import { FieldValidator } from "./validation";

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

export class PublicFormComponent<T> {
state: AppState<T> = {
form: {},
history: [],
editting: "",
status: "not-started",
};
// Public type to define the state of the form
export type AppState<T> = {
uuid: string;
form: Record<string, Fieldset<T>>;
history: string[];
editting: string;
lastModified?: Date;
status: FormStatus;
currentFieldset?: { id: T; dispatchType: "change" | "continue" };
};

export type Fieldset<T> = {
heading: string;
skipSummary?: boolean;
data:
| { type: "details"; fieldsets: Record<string, FieldsetItemState> }
| { type: "list"; items: AppState<T>[] };
};

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

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

constructor(private type: "details" | "list") {
console.log("constructor", this.type);
// nothing here
}

// Obtain reference to the form element
init(e: Event) {
// FIXME: This condition should not be needed, but currently it is the only way to get things working
if (this._formRef) {
console.error("init: form element has already been set");
return;
}
this._formRef = (e as CustomEvent).detail.el;

this.state = {
uuid: crypto.randomUUID(),
form: {},
history: [],
editting: "",
status: "not-started",
};
}

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

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

updateState(e: Event) {
const state = (e as CustomEvent).detail as AppState<T>;
// ISSUE:
// The issue is that all the logic below is happening outside the form component, thereby resulting
// in the "fixed" data not being in sync with the data within the form component.
//

// Public method to allow for the updating of the state
// updateState(e: Event) {
// console.debug(
// "Utils:updateState",
// this.type,
// { state: this.state },
// (e as CustomEvent).detail,
// );
// if (!this.state) {
// console.error("updateState: state has not yet been set");
// return;
// }
//
// const detail = (e as CustomEvent).detail;
// } else if (this.type === "details" && Array.isArray(detail.data)) {
// this.#updateObjectListState(detail);
// } else {
// this.#updateObjectState(detail);
// }
// }

updateListState(e: Event) {
const detail = (e as CustomEvent).detail;

if (!Array.isArray(detail.data)) {
return;
}

this.state = detail.data;
}

#updateObjectListState(detail: { data: AppState<T>[]; index: number; id: string }) {
if (!Array.isArray(detail.data)) {
return;
}

if (Array.isArray(this.state)) {
return;
}

this.state = {
...this.state,
form: state.form,
currentFieldset: state.currentFieldset,
};
form: {
...(this.state?.form || {}),
[detail.id]: detail.data,
},
} as AppState<T>;
}

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

const detail = (e as CustomEvent).detail;
if (detail.type === "list") {
// form state being updated with subform array data
this.state = {
...this.state,
form: { ...(this.state?.form || {}), [detail.id]: detail.data },
// currentFieldset: newState.currentFieldset,
// history: detail.data.,
} as AppState<T>;
} else {
// form state being updated with form data
this.state = {
...this.state,
form: { ...(this.state?.form || {}), ...detail.data.form },
// currentFieldset: newState.currentFieldset,
history: detail.data.history,
} as AppState<T>;
}
}

continueTo(name: T | undefined) {
if (!name) {
getStateList(): Record<string, string>[] {
if (!this.state) {
return [];
}
if (!Array.isArray(this.state)) {
console.warn(
"Utils:getStateList: unable to update the state of a non-multi form type",
this.state,
);
return [];
}
if (this.state.length === 0) {
return [];
}

return this.state.map((s) => {
return Object.values(s.form)
.filter((item) => {
return item?.data?.type === "details";
})
.map((item) => {
return (item.data.type === "details" && item.data?.fieldsets) || {};
})
.reduce(
(acc, item) => {
for (const [key, value] of Object.entries(item)) {
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(next: T | undefined) {
if (!next) {
console.error("continueTo [name] is undefined");
return;
}
relay<{ next: T }>(this._formRef, "external::continue", {
next: name,
// Relay the continue message to the form element which will
// set the visibility of the fieldsets
console.log("continueTo: TYPE", {
type: this.type,
state: this.state,
formRef: this._formRef,
next,
});
// FIXME: this makes a call to the subform instead of the form
relay<{ next: T }>(this._formRef, "external::continue", { next });
}

// Public method to perform validation and send the appropriate messages to the form elements
validate(field: string, e: Event, validators: FieldValidator[]): [boolean, string] {
const { el, state } = (e as CustomEvent).detail;
const value = state?.[field]?.value;
Expand All @@ -60,6 +245,15 @@ export class PublicFormComponent<T> {
return [true, value];
}

edit(index: number) {
relay(this._formRef, "external::alter:state", { index, operation: "edit" });
}

remove(index: number) {
relay(this._formRef, "external::alter:state", { index, operation: "remove" });
}

// Private method to dispatch the error message to the form element
#dispatchError(el: HTMLElement, name: string, msg: string) {
el.dispatchEvent(
new CustomEvent("msg", {
Expand All @@ -76,21 +270,7 @@ export class PublicFormComponent<T> {
}
}

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

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

// Public helper function to relay messages
export function relay<T>(
el: HTMLElement | Element | null | undefined,
eventName: string,
Expand All @@ -120,7 +301,6 @@ export function relay<T>(
console.error("dispatch element is null");
return;
}
// console.log(`RELAY(${eventName}):`, data, el);
el.dispatchEvent(
new CustomEvent<{ action: string; data?: T }>("msg", {
composed: true,
Expand Down
8 changes: 6 additions & 2 deletions libs/angular-components/src/lib/validation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { describe, it, expect } from "vitest";

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

describe("Validation", () => {
describe("Email", () => {
Expand Down
6 changes: 3 additions & 3 deletions libs/react-components/src/lib/fieldset/fieldset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ declare global {
namespace JSX {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IntrinsicElements {
"goa-fieldset": WCProps & React.HTMLAttributes<HTMLElement>;
"goa-public-form-page": WCProps & React.HTMLAttributes<HTMLElement>;
}
}
}
Expand Down Expand Up @@ -63,7 +63,7 @@ export function GoabFieldset({
}, [ref.current, onContinue])

return (
<goa-fieldset
<goa-public-form-page
ref={ref}
id={id}
first={first}
Expand All @@ -76,7 +76,7 @@ export function GoabFieldset({
ml={ml}
>
{children}
</goa-fieldset>
</goa-public-form-page>
);
}

Expand Down
Loading

0 comments on commit 8add27f

Please sign in to comment.