Files

160 lines
4.0 KiB
TypeScript
Raw Permalink Normal View History

import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-input.js';
import './idp-checkbox.js';
export type TIdpFormStatus = 'idle' | 'pending' | 'success' | 'error';
export type TIdpFormData = Record<string, string | boolean>;
export interface IIdpFormSubmitEventDetail {
data: TIdpFormData;
form: IdpForm;
}
type TFormControl = HTMLElement & {
name?: string;
key?: string;
value?: string;
checked?: boolean;
validate?: () => boolean;
};
declare global {
interface HTMLElementTagNameMap {
'idp-form': IdpForm;
}
}
@customElement('idp-form')
export class IdpForm extends DeesElement {
public static demo = () => html`
<idp-form>
<idp-input name="email" label="Email" type="email" required></idp-input>
<idp-form-submit>Continue</idp-form-submit>
</idp-form>
`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String, reflect: true })
public accessor status: TIdpFormStatus = 'idle';
@property({ type: String })
public accessor statusMessage = '';
@property({ type: Boolean, reflect: true })
public accessor disabled = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
form {
display: flex;
flex-direction: column;
gap: 16px;
}
::slotted(idp-form-submit) {
margin-top: 2px;
}
.status {
padding: 11px 13px;
border-radius: 10px;
border: 1px solid var(--idp-border);
background: var(--idp-muted);
color: var(--idp-muted-fg);
font-size: 13px;
line-height: 1.45;
}
.status.pending {
border-color: color-mix(in srgb, var(--idp-accent), transparent 60%);
color: var(--idp-accent);
}
.status.success {
border-color: color-mix(in srgb, var(--idp-ok), transparent 60%);
color: var(--idp-ok);
}
.status.error {
border-color: color-mix(in srgb, var(--idp-destructive), transparent 60%);
color: var(--idp-destructive);
}
`,
];
public setStatus(statusArg: TIdpFormStatus, messageArg = '') {
this.status = statusArg;
this.statusMessage = messageArg;
}
public resetStatus() {
this.setStatus('idle', '');
}
public submit() {
this.handleSubmit();
}
private getControls() {
return Array.from(this.querySelectorAll('idp-input, idp-checkbox')) as TFormControl[];
}
private validateControls() {
return this.getControls().every((controlArg) => controlArg.validate ? controlArg.validate() : true);
}
private getFormData(): TIdpFormData {
const data: TIdpFormData = {};
for (const control of this.getControls()) {
const name = control.name || control.key;
if (!name) {
continue;
}
if (typeof control.checked === 'boolean') {
data[name] = control.checked;
} else {
data[name] = control.value || '';
}
}
return data;
}
private handleSubmitRequest(eventArg: Event) {
eventArg.preventDefault();
eventArg.stopPropagation();
this.handleSubmit();
}
private handleSubmit(eventArg?: Event) {
eventArg?.preventDefault();
if (this.disabled || this.status === 'pending') {
return;
}
if (!this.validateControls()) {
this.setStatus('error', 'Please check the highlighted fields.');
return;
}
this.dispatchEvent(new CustomEvent<IIdpFormSubmitEventDetail>('idp-submit', {
detail: {
data: this.getFormData(),
form: this,
},
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
return html`
<form novalidate @submit=${this.handleSubmit} @idp-form-submit-request=${this.handleSubmitRequest}>
<slot></slot>
${this.statusMessage
? html`<div class="status ${this.status}" role="status">${this.statusMessage}</div>`
: html``}
</form>
`;
}
}