web: extract form processing (#7298)
* web: break circular dependency between AKElement & Interface. This commit changes the way the root node of the web application shell is discovered by child components, such that the base class shared by both no longer results in a circular dependency between the two models. I've run this in isolation and have seen no failures of discovery; the identity token exists as soon as the Interface is constructed and is found by every item on the page. * web: fix broken typescript references This built... and then it didn't? Anyway, the current fix is to provide type information the AkInterface for the data that consumers require. * web: extract the form processing from the form submission process Our forms have a lot of customized value handling, and the function `serializeForm` takes our input structures and creates a JSON object ready for submission across the wire for the various models provided by the API. That function was embedded in the `ak-form` object, but it has no actual dependencies on the state of that object; aside from identifying the input elements, which is done at the very start of processing, this large block of code stands alone. Separating out the "processing the form" from "identifying the form" allows us to customize our form handling and preserve form information on the client for transactional purposes such as our wizard. w
This commit is contained in:
parent
7f2d03dcd0
commit
a52e4a3262
|
@ -31,6 +31,93 @@ export interface KeyUnknown {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
|
||||||
|
*/
|
||||||
|
function assignValue(element: HTMLInputElement, value: unknown, json: KeyUnknown): void {
|
||||||
|
let parent = json;
|
||||||
|
if (!element.name?.includes(".")) {
|
||||||
|
parent[element.name] = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nameElements = element.name.split(".");
|
||||||
|
for (let index = 0; index < nameElements.length - 1; index++) {
|
||||||
|
const nameEl = nameElements[index];
|
||||||
|
// Ensure all nested structures exist
|
||||||
|
if (!(nameEl in parent)) parent[nameEl] = {};
|
||||||
|
parent = parent[nameEl] as { [key: string]: unknown };
|
||||||
|
}
|
||||||
|
parent[nameElements[nameElements.length - 1]] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the elements of the form to JSON.[4]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function serializeForm<T extends KeyUnknown>(
|
||||||
|
elements: NodeListOf<HorizontalFormElement>,
|
||||||
|
): T | undefined {
|
||||||
|
const json: { [key: string]: unknown } = {};
|
||||||
|
elements.forEach((element) => {
|
||||||
|
element.requestUpdate();
|
||||||
|
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
||||||
|
if (element.hidden || !inputElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip elements that are writeOnly where the user hasn't clicked on the value
|
||||||
|
if (element.writeOnly && !element.writeOnlyActivated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
inputElement.tagName.toLowerCase() === "select" &&
|
||||||
|
"multiple" in inputElement.attributes
|
||||||
|
) {
|
||||||
|
const selectElement = inputElement as unknown as HTMLSelectElement;
|
||||||
|
assignValue(
|
||||||
|
inputElement,
|
||||||
|
Array.from(selectElement.selectedOptions).map((v) => v.value),
|
||||||
|
json,
|
||||||
|
);
|
||||||
|
} else if (inputElement.tagName.toLowerCase() === "input" && inputElement.type === "date") {
|
||||||
|
assignValue(inputElement, inputElement.valueAsDate, json);
|
||||||
|
} else if (
|
||||||
|
inputElement.tagName.toLowerCase() === "input" &&
|
||||||
|
inputElement.type === "datetime-local"
|
||||||
|
) {
|
||||||
|
assignValue(inputElement, new Date(inputElement.valueAsNumber), json);
|
||||||
|
} else if (
|
||||||
|
inputElement.tagName.toLowerCase() === "input" &&
|
||||||
|
"type" in inputElement.dataset &&
|
||||||
|
inputElement.dataset["type"] === "datetime-local"
|
||||||
|
) {
|
||||||
|
// Workaround for Firefox <93, since 92 and older don't support
|
||||||
|
// datetime-local fields
|
||||||
|
assignValue(inputElement, new Date(inputElement.value), json);
|
||||||
|
} else if (
|
||||||
|
inputElement.tagName.toLowerCase() === "input" &&
|
||||||
|
inputElement.type === "checkbox"
|
||||||
|
) {
|
||||||
|
assignValue(inputElement, inputElement.checked, json);
|
||||||
|
} else if ("selectedFlow" in inputElement) {
|
||||||
|
assignValue(inputElement, inputElement.value, json);
|
||||||
|
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
|
||||||
|
const select = inputElement as unknown as SearchSelect<unknown>;
|
||||||
|
try {
|
||||||
|
const value = select.toForm();
|
||||||
|
assignValue(inputElement, value, json);
|
||||||
|
} catch (exc) {
|
||||||
|
if (exc instanceof PreventFormSubmit) {
|
||||||
|
throw new PreventFormSubmit(exc.message, element);
|
||||||
|
}
|
||||||
|
throw exc;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assignValue(inputElement, inputElement.value, json);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return json as unknown as T;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form
|
* Form
|
||||||
*
|
*
|
||||||
|
@ -177,95 +264,13 @@ export abstract class Form<T> extends AKElement {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
serializeForm(): T | undefined {
|
serializeForm(): T | undefined {
|
||||||
const elements =
|
const elements = this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
"ak-form-element-horizontal",
|
||||||
"ak-form-element-horizontal",
|
);
|
||||||
) || [];
|
if (!elements) {
|
||||||
const json: { [key: string]: unknown } = {};
|
return {} as T;
|
||||||
elements.forEach((element) => {
|
|
||||||
element.requestUpdate();
|
|
||||||
const inputElement = element.querySelector<HTMLInputElement>("[name]");
|
|
||||||
if (element.hidden || !inputElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Skip elements that are writeOnly where the user hasn't clicked on the value
|
|
||||||
if (element.writeOnly && !element.writeOnlyActivated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
inputElement.tagName.toLowerCase() === "select" &&
|
|
||||||
"multiple" in inputElement.attributes
|
|
||||||
) {
|
|
||||||
const selectElement = inputElement as unknown as HTMLSelectElement;
|
|
||||||
this.assignValue(
|
|
||||||
inputElement,
|
|
||||||
Array.from(selectElement.selectedOptions).map((v) => v.value),
|
|
||||||
json,
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
inputElement.tagName.toLowerCase() === "input" &&
|
|
||||||
inputElement.type === "date"
|
|
||||||
) {
|
|
||||||
this.assignValue(inputElement, inputElement.valueAsDate, json);
|
|
||||||
} else if (
|
|
||||||
inputElement.tagName.toLowerCase() === "input" &&
|
|
||||||
inputElement.type === "datetime-local"
|
|
||||||
) {
|
|
||||||
this.assignValue(inputElement, new Date(inputElement.valueAsNumber), json);
|
|
||||||
} else if (
|
|
||||||
inputElement.tagName.toLowerCase() === "input" &&
|
|
||||||
"type" in inputElement.dataset &&
|
|
||||||
inputElement.dataset["type"] === "datetime-local"
|
|
||||||
) {
|
|
||||||
// Workaround for Firefox <93, since 92 and older don't support
|
|
||||||
// datetime-local fields
|
|
||||||
this.assignValue(inputElement, new Date(inputElement.value), json);
|
|
||||||
} else if (
|
|
||||||
inputElement.tagName.toLowerCase() === "input" &&
|
|
||||||
inputElement.type === "checkbox"
|
|
||||||
) {
|
|
||||||
this.assignValue(inputElement, inputElement.checked, json);
|
|
||||||
} else if ("selectedFlow" in inputElement) {
|
|
||||||
this.assignValue(inputElement, inputElement.value, json);
|
|
||||||
} else if (inputElement.tagName.toLowerCase() === "ak-search-select") {
|
|
||||||
const select = inputElement as unknown as SearchSelect<unknown>;
|
|
||||||
try {
|
|
||||||
const value = select.toForm();
|
|
||||||
this.assignValue(inputElement, value, json);
|
|
||||||
} catch (exc) {
|
|
||||||
if (exc instanceof PreventFormSubmit) {
|
|
||||||
throw new PreventFormSubmit(exc.message, element);
|
|
||||||
}
|
|
||||||
throw exc;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.assignValue(inputElement, inputElement.value, json);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return json as unknown as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
|
|
||||||
*/
|
|
||||||
private assignValue(
|
|
||||||
element: HTMLInputElement,
|
|
||||||
value: unknown,
|
|
||||||
json: { [key: string]: unknown },
|
|
||||||
): void {
|
|
||||||
let parent = json;
|
|
||||||
if (!element.name?.includes(".")) {
|
|
||||||
parent[element.name] = value;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const nameElements = element.name.split(".");
|
return serializeForm(elements) as T;
|
||||||
for (let index = 0; index < nameElements.length - 1; index++) {
|
|
||||||
const nameEl = nameElements[index];
|
|
||||||
// Ensure all nested structures exist
|
|
||||||
if (!(nameEl in parent)) parent[nameEl] = {};
|
|
||||||
parent = parent[nameEl] as { [key: string]: unknown };
|
|
||||||
}
|
|
||||||
parent[nameElements[nameElements.length - 1]] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Reference in New Issue