UNPKG

vira

Version:

A simple and highly versatile design system using element-vir.

363 lines (355 loc) 14.9 kB
import { getObjectTypedEntries } from '@augment-vir/common'; import { css, defineElementEvent, html, listen, nothing, testId, } from 'element-vir'; import { viraFormCssVars } from '../styles/form-styles.js'; import { defineViraElement } from '../util/define-vira-element.js'; import { applyRequiredLabel, areFormFieldsValid, ViraFormFieldType, } from '../util/vira-form-fields.js'; import { ViraCheckbox } from './vira-checkbox.element.js'; import { ViraDateInput } from './vira-date-input.element.js'; import { ViraInput, ViraInputType } from './vira-input.element.js'; import { ViraSelect } from './vira-select.element.js'; import { ViraTextArea } from './vira-text-area.element.js'; /** * A form element. * * @category Elements * @see https://electrovir.github.io/vira/book/elements/vira-form */ export const ViraForm = defineViraElement()({ tagName: 'vira-form', state() { return { lastIsValid: false, }; }, events: { valueChange: defineElementEvent(), validChange: defineElementEvent(), }, styles: css ` :host { display: flex; } form { display: flex; flex-grow: 1; flex-direction: column; align-items: stretch; gap: 10px; > * { width: unset; } } .horizontal-fields { width: 100%; border-collapse: separate; border-spacing: 0 10px; & th, & td { padding: 0; } & th { padding: 0 8px; vertical-align: middle; white-space: nowrap; font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value}; text-align: right; } & td { width: 100%; vertical-align: top; & > ${ViraCheckbox}, & > ${ViraDateInput}, & > ${ViraInput}, & > ${ViraSelect}, & > ${ViraTextArea} { width: 100%; } } } `, render({ inputs, dispatch, events, state, updateState }) { const currentIsValid = areFormFieldsValid(inputs.fields); if (currentIsValid !== state.lastIsValid) { updateState({ lastIsValid: currentIsValid, }); dispatch(new events.validChange({ allFieldsAreValid: currentIsValid, })); } function wrapFormField({ fieldTemplate, label, }) { if (inputs.useHorizontalLabels) { return html ` <tr> <th scope="row">${label}</th> <td>${fieldTemplate}</td> </tr> `; } else { return fieldTemplate; } } const formFieldTemplates = getObjectTypedEntries(inputs.fields).map(([key, field,]) => { const isDisabled = !!(inputs.isDisabled || field.isDisabled); const showRequiredMarker = !!field.isRequired && !inputs.hideRequiredMarkers; if (field.isHidden) { return nothing; } else if (field.type === ViraFormFieldType.Checkbox) { const checkboxLabel = showRequiredMarker ? html ` ${field.label}* ` : field.label; return wrapFormField({ label: checkboxLabel, fieldTemplate: html ` <${ViraCheckbox.assign({ value: field.value || false, isDisabled: !!(isDisabled || inputs.isReadonly), hasError: field.hasError, useHorizontalLabel: inputs.horizontalCheckboxes, fillWhenChecked: field.fillWhenChecked, fillWhenUnchecked: field.fillWhenUnchecked, })} ${field.testId ? testId(field.testId) : nothing} ${listen(ViraCheckbox.events.valueChange, (event) => { dispatch(new events.valueChange({ key, ...field, value: event.detail, })); })} > ${inputs.useHorizontalLabels ? nothing : html ` <span slot=${ViraCheckbox.slotNames['vira-checkbox-label']} > ${checkboxLabel} </span> `} </${ViraCheckbox}> `, }); } const label = applyRequiredLabel(field.label, showRequiredMarker); const childLabel = inputs.useHorizontalLabels ? undefined : label; const horizontalLabelAttributes = inputs.useHorizontalLabels && label ? { 'aria-label': label, } : {}; if (field.type === ViraFormFieldType.Select) { return wrapFormField({ label, fieldTemplate: html ` <${ViraSelect.assign({ options: field.options, value: field.value, placeholder: field.placeholder, disabled: isDisabled, isReadonly: inputs.isReadonly, label: childLabel, hasError: field.hasError, icon: field.icon, ...(inputs.useHorizontalLabels && label ? { attributePassthrough: { select: horizontalLabelAttributes, }, } : {}), })} ${field.testId ? testId(field.testId) : nothing} ${listen(ViraSelect.events.valueChange, (event) => { dispatch(new events.valueChange({ key, ...field, value: event.detail, })); })} ></${ViraSelect}> `, }); } else if (field.type === ViraFormFieldType.TextArea) { return wrapFormField({ label, fieldTemplate: html ` <${ViraTextArea.assign({ value: field.value || '', disabled: isDisabled, hasError: field.hasError, isReadonly: inputs.isReadonly, label: childLabel, placeholder: field.placeholder, rows: field.rows, preventResize: field.preventResize, attributePassthrough: horizontalLabelAttributes, })} ${field.testId ? testId(field.testId) : nothing} ${listen(ViraTextArea.events.valueChange, (event) => { dispatch(new events.valueChange({ key, ...field, value: event.detail, })); })} ></${ViraTextArea}> `, }); } else if (field.type === ViraFormFieldType.Number) { return wrapFormField({ label, fieldTemplate: html ` <${ViraInput.assign({ value: field.value?.toString() || '', disabled: isDisabled, allowedInputs: /\d/, hasError: field.hasError, icon: field.icon, isReadonly: inputs.isReadonly, label: childLabel, placeholder: field.placeholder, showClearButton: inputs.showClearButtons, type: ViraInputType.Number, attributePassthrough: { ...horizontalLabelAttributes, ...(field.min === undefined ? {} : { min: String(field.min), }), ...(field.max === undefined ? {} : { max: String(field.max), }), ...(field.step === undefined ? {} : { step: String(field.step), }), }, })} ${field.testId ? testId(field.testId) : nothing} ${listen(ViraInput.events.valueChange, (event) => { const numericValue = event.detail === '' ? undefined : Number(event.detail); dispatch(new events.valueChange({ key, ...field, value: numericValue, })); })} ></${ViraInput}> `, }); } else if (field.type === ViraFormFieldType.Date) { return wrapFormField({ label, fieldTemplate: html ` <${ViraDateInput.assign({ value: field.value, min: field.min, max: field.max, timezone: field.timezone, showDateOnly: field.showDateOnly, showTimeOnly: field.showTimeOnly, isDisabled, hasError: field.hasError, isReadonly: inputs.isReadonly, label: childLabel, })} ${field.testId ? testId(field.testId) : nothing} ${listen(ViraDateInput.events.valueChange, (event) => { dispatch(new events.valueChange({ key, ...field, value: event.detail, })); })} ></${ViraDateInput}> `, }); } else { return wrapFormField({ label, fieldTemplate: html ` <${ViraInput.assign({ value: field.value || '', disabled: isDisabled, hasError: field.hasError, icon: field.icon, isReadonly: inputs.isReadonly, label: childLabel, placeholder: field.placeholder, showClearButton: inputs.showClearButtons, attributePassthrough: { ...horizontalLabelAttributes, ...(field.isUsername ? { autocomplete: 'username', } : field.type === ViraFormFieldType.NewPassword ? { autocomplete: 'new-password', } : field.type === ViraFormFieldType.ExistingPassword ? { autocomplete: 'password', } : field.type === ViraFormFieldType.Email ? { autocomplete: 'email', } : {}), }, type: [ ViraFormFieldType.NewPassword, ViraFormFieldType.ExistingPassword, ViraFormFieldType.PlainPassword, ].includes(field.type) ? ViraInputType.Password : field.type === ViraFormFieldType.Email ? ViraInputType.Email : ViraInputType.Default, })} ${field.testId ? testId(field.testId) : nothing} ${listen(ViraInput.events.valueChange, (event) => { dispatch(new events.valueChange({ key, ...field, value: event.detail, })); })} ></${ViraInput}> `, }); } }); const formFieldsWrapper = inputs.useHorizontalLabels ? html ` <table class="horizontal-fields"> <tbody>${formFieldTemplates}</tbody> </table> ` : formFieldTemplates; return html ` <form ${listen('submit', (event) => event.preventDefault())}> ${formFieldsWrapper} <slot></slot> </form> `; }, });