vira
Version:
A simple and highly versatile design system using element-vir.
363 lines (355 loc) • 14.9 kB
JavaScript
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>
`;
},
});