web: laying the groundwork for future expansion (#7045)
* web: laying the groundwork for future expansion This commit is a hodge-podge of updates and changes to the web. Functional changes: - Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately calculated if `help` was included rather than in-lined. - ESLint: Modified the "unused vars" rule so that variables starting with an underline are not considered by the rule. This allows for elided variables in event handlers. It's not a perfect solution-- a better one would be to use Typescript's function-specialization typing, but there are too many places where we elide or ignore some variables in a function's usage that switching over to specialization would be a huge lift. - locale: It turns out, lit-locale does its own context management. We don't need to have a context at all in this space, and that's one less listener we need to attach t othe DOM. - ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over rendering and reduces the number of actual renders of the page. - FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word "Details." Specializing this field will both help users of screen readers in the future, and will allow test suites to find specific form groups now. - RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the events to the outside world, and we set the value locally so that the current `Form.ts` continues to behave as expected. We also prevent the "button lost value" event from propagating; this presents a unified select-like interface to users of the RadioButtonGroup. The current value semantics are preserved; other clients of the RadioButton do not see a change in behavior. - EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics for forwarding it; just send it as-is. - Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of comments into the code, describing APIs, interfaces, class and function signatures, to better document the behavior inside and as signposts for future work. * web: permit arrays to be sent in custom events without interpolation. * actually use assignValue or rather serializeFieldRecursive Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -31,6 +31,37 @@ export interface KeyUnknown {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
* The base form element for interacting with user inputs.
|
||||
*
|
||||
* All forms either[1] inherit from this class and implement the `renderInlineForm()` method to
|
||||
* produce the actual form, or include the form in-line as a slotted element. Bizarrely, this form
|
||||
* will not render at all if it's not actually in the viewport?[2]
|
||||
*
|
||||
* @element ak-form
|
||||
*
|
||||
* @slot - Where the form goes if `renderInlineForm()` returns undefined.
|
||||
* @fires eventname - description
|
||||
*
|
||||
* @csspart partname - description
|
||||
*/
|
||||
|
||||
/* TODO:
|
||||
*
|
||||
* 1. Specialization: Separate this component into three different classes:
|
||||
* - The base class
|
||||
* - The "use `renderInlineForm` class
|
||||
* - The slotted class.
|
||||
* 2. There is already specialization-by-type throughout all of our code.
|
||||
* Consider refactoring serializeForm() so that the conversions are on
|
||||
* the input types, rather than here. (i.e. "Polymorphism is better than
|
||||
* switch.")
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
@customElement("ak-form")
|
||||
export abstract class Form<T> extends AKElement {
|
||||
abstract send(data: T): Promise<unknown>;
|
||||
@ -61,6 +92,10 @@ export abstract class Form<T> extends AKElement {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the render function. Blocks rendering the form if the form is not within the
|
||||
* viewport.
|
||||
*/
|
||||
get isInViewport(): boolean {
|
||||
const rect = this.getBoundingClientRect();
|
||||
return !(rect.x + rect.y + rect.width + rect.height === 0);
|
||||
@ -70,6 +105,11 @@ export abstract class Form<T> extends AKElement {
|
||||
return this.successMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* After rendering the form, if there is both a `name` and `slug` element within the form,
|
||||
* events the `name` element so that the slug will always have a slugified version of the
|
||||
* `name.`. This duplicates functionality within ak-form-element-horizontal.
|
||||
*/
|
||||
updated(): void {
|
||||
this.shadowRoot
|
||||
?.querySelectorAll("ak-form-element-horizontal[name=name]")
|
||||
@ -103,6 +143,12 @@ export abstract class Form<T> extends AKElement {
|
||||
form?.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the form elements that may contain filenames. Not sure why this is quite so
|
||||
* convoluted. There is exactly one case where this is used:
|
||||
* `./flow/stages/prompt/PromptStage: 147: case PromptTypeEnum.File.`
|
||||
* Consider moving this functionality to there.
|
||||
*/
|
||||
getFormFiles(): { [key: string]: File } {
|
||||
const files: { [key: string]: File } = {};
|
||||
const elements =
|
||||
@ -126,6 +172,10 @@ export abstract class Form<T> extends AKElement {
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the elements of the form to JSON.[4]
|
||||
*
|
||||
*/
|
||||
serializeForm(): T | undefined {
|
||||
const elements =
|
||||
this.shadowRoot?.querySelectorAll<HorizontalFormElement>(
|
||||
@ -147,17 +197,21 @@ export abstract class Form<T> extends AKElement {
|
||||
"multiple" in inputElement.attributes
|
||||
) {
|
||||
const selectElement = inputElement as unknown as HTMLSelectElement;
|
||||
json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value);
|
||||
this.assignValue(
|
||||
inputElement,
|
||||
Array.from(selectElement.selectedOptions).map((v) => v.value),
|
||||
json,
|
||||
);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "date"
|
||||
) {
|
||||
json[element.name] = inputElement.valueAsDate;
|
||||
this.assignValue(inputElement, inputElement.valueAsDate, json);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "datetime-local"
|
||||
) {
|
||||
json[element.name] = new Date(inputElement.valueAsNumber);
|
||||
this.assignValue(inputElement, new Date(inputElement.valueAsNumber), json);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
"type" in inputElement.dataset &&
|
||||
@ -165,19 +219,19 @@ export abstract class Form<T> extends AKElement {
|
||||
) {
|
||||
// Workaround for Firefox <93, since 92 and older don't support
|
||||
// datetime-local fields
|
||||
json[element.name] = new Date(inputElement.value);
|
||||
this.assignValue(inputElement, new Date(inputElement.value), json);
|
||||
} else if (
|
||||
inputElement.tagName.toLowerCase() === "input" &&
|
||||
inputElement.type === "checkbox"
|
||||
) {
|
||||
json[element.name] = inputElement.checked;
|
||||
this.assignValue(inputElement, inputElement.checked, json);
|
||||
} else if ("selectedFlow" in inputElement) {
|
||||
json[element.name] = inputElement.value;
|
||||
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();
|
||||
json[element.name] = value;
|
||||
this.assignValue(inputElement, value, json);
|
||||
} catch (exc) {
|
||||
if (exc instanceof PreventFormSubmit) {
|
||||
throw new PreventFormSubmit(exc.message, element);
|
||||
@ -185,13 +239,16 @@ export abstract class Form<T> extends AKElement {
|
||||
throw exc;
|
||||
}
|
||||
} else {
|
||||
this.serializeFieldRecursive(inputElement, inputElement.value, json);
|
||||
this.assignValue(inputElement, inputElement.value, json);
|
||||
}
|
||||
});
|
||||
return json as unknown as T;
|
||||
}
|
||||
|
||||
private serializeFieldRecursive(
|
||||
/**
|
||||
* Recursively assign `value` into `json` while interpreting the dot-path of `element.name`
|
||||
*/
|
||||
private assignValue(
|
||||
element: HTMLInputElement,
|
||||
value: unknown,
|
||||
json: { [key: string]: unknown },
|
||||
@ -211,6 +268,12 @@ export abstract class Form<T> extends AKElement {
|
||||
parent[nameElements[nameElements.length - 1]] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and send the form to the destination. The `send()` method must be overridden for
|
||||
* this to work. If processing the data results in an error, we catch the error, distribute
|
||||
* field-levels errors to the fields, and send the rest of them to the Notifications.
|
||||
*
|
||||
*/
|
||||
async submit(ev: Event): Promise<unknown | undefined> {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user