Forms
The Blue Water Design System provides a comprehensive set of form controls and utilities to create consistent, accessible forms across your applications. This guide covers form usage in both Angular and non-Angular projects.
Form Controls
The library provides the following form controls:
- bw-input - Text input with various types and validation
- bw-select - Dropdown select with search and multi-select support
- bw-checkbox - Single checkbox
- bw-checkbox-group - Group of checkboxes
- bw-radio-group - Group of radio buttons
- bw-toggle - Toggle switch
- bw-control - Create new form controls
Basic Form Structure
Wrap your forms using the BwForm component. It is a wrapper over the native form element that adds some quality of life features.
- Typescript
- HTML
declare const form: HTMLBwFormElement;
form.addEventListener('dataSubmit', (event) => {
form.insertAdjacentHTML('afterend', `<p>Form data: ${JSON.stringify(event.detail)}</p>`);
});
<bw-form id="form">
<bw-input label="Name" name="name" required></bw-input>
<bw-input label="Email" name="email"></bw-input>
<bw-button type="submit">Submit</bw-button>
</bw-form>
Angular
Since we rely on the native form element, very little changes when authoring forms in Angular.
import { Component } from '@angular/core';
import { BwInput, BwForm, BwButton } from 'bluewater-angular';
@Component({
imports: [BwInput, BwForm, BwButton],
template: `
<bw-form (dataSubmit)="onFormData($event)">
<bw-input label="Name" name="name" required></bw-input>
<bw-input label="Email" name="email"></bw-input>
<bw-button type="submit">Submit</bw-button>
</bw-form>
`
})
export class AppComponent {
onFormData(event: CustomEvent) {
console.log(event.detail);
}
}
Note: Since there is little difference between angular and vanilla, the remainder of this tutorial will be in vanilla.
Submitting and Resetting
Use buttons with the type="submit" attribute to submit the form. To reset the form, use type="reset".
<bw-form>
<bw-input label="Name" required></bw-input>
<div class="flex-row gap padding-vertical">
<bw-button type="submit">Submit</bw-button>
<bw-button type="reset">Reset</bw-button>
</div>
</bw-form>
Validation
Validation is built into the form controls. BwForm will not submit if the form is invalid. The first invalid element will be focused upon the submission of an invalid form.
Required
Some of the controls have a required attribute that will require a truthy value to be valid.
- Typescript
- HTML
import { createToast } from 'bluewater';
declare const form: HTMLBwFormElement;
form.addEventListener('submit', () => {
createToast({ variant: 'bar', message: 'Form submitted!', type: 'success' }).then(toast => toast.open = true)
});
<bw-form id="form">
<bw-input label="Name" name="name" required></bw-input>
<bw-checkbox name="accept" required>Accept terms</bw-checkbox>
<bw-button type="submit">Submit</bw-button>
</bw-form>
Built-in Attributes
Some of the validation attributes that exist on the native input element also work on BwInput.
<bw-input placeholder="Type less than 9 characters" minlength="9"></bw-input>
<bw-divider></bw-divider>
<bw-input placeholder="Type a special character" pattern="^[A-Za-z0-9]*$"></bw-input>
<bw-divider></bw-divider>
<bw-input placeholder="Type a number less than 5" type="number" min="5"></bw-input>
<bw-divider></bw-divider>
Custom Errors
Most of the form controls have a setCustomValidity method that allows you to force the control to be invalid. You can use this for custom logic.
BwInput and BwToggle are the only controls that do not have this method. This is because inputs have validator functions you can use instead, and toggles can only be off or on, there is no validation to be done. Use a BwCheckbox if you need to attach some logic to a boolean form control.
- Typescript
- HTML
import { createToast } from 'bluewater';
declare const form: HTMLBwFormElement;
declare const radioGroup: HTMLBwRadioGroupElement;
form.addEventListener('submit', () => {
createToast({ variant: 'bar', message: 'Form submitted!', type: 'success' }).then(toast => toast.open = true)
});
radioGroup.addEventListener('valueChange', ({detail: value}) => {
const error = value === "4" ? null : `2 + 2 is not ${value}!`
radioGroup.setCustomValidity(error)
})
<bw-form id="form">
<bw-radio-group id="radioGroup" label="What is 2 + 2?" required>
<bw-radio value="3">3</bw-radio>
<bw-radio value="4">4</bw-radio>
<bw-radio value="19">19</bw-radio>
</bw-radio-group>
<bw-button type="submit">Submit</bw-button>
</bw-form>
Validator Functions
BwInput and BwControl have a validators property where you can pass in an array of functions that return an array of error messages. Use this for complex logic that can't be done with the built in attributes.
- Typescript
- HTML
async function fetchValueExists(value) {
await new Promise(res => setTimeout(res), 50)
return ['blue', 'red', 'green'].includes(value)
}
async function validate(value) {
const errors = []
//simulate making an api call
const valueExists = await fetchValueExists(value)
if (valueExists) {
errors.push('Value already exists')
}
return errors
}
const input = document.querySelector('bw-input')
input.validators = [validate]
<bw-input label="Type a color other than red, blue, or green"></bw-input>
Custom Controls
BwControl is a blank component that you can use to create complex controls that can participate in the form. Common use cases include needing to represent an array/object in the form, or having a complex value that requires multiple controls to create. A full example of how to use BwControl is here.
Events
BwForm has multiple events you can listen for to know when a user has completed the form.
dataSubmit: Emits the data with correct typesdataPatch: Emits only the data that changed with correct typessubmit: The native SubmitEvent that bubbles up from the inner HTMLFormElementformData: [DEPRECATED] Emits the data with all the values converted to strings
CSS States
The form controls have various states. The full list is below.
- BwInput & BwSelect
- --required
- --optional
- --invalid
- --valid
- --user-invalid
- --user-valid
- BwControl
- --invalid
- --valid
- --user-invalid
- --user-valid
- BwRadioGroup, BwCheckbox, & BwCheckboxGroup
- --required
- --optional
- --invalid
- --valid
--user-invalid and --user-valid mean the control has been touched by the user.
BwToggle has no states because it has no validation functionality. It is simply an on/off switch
bw-input:state(--user-valid) {
--border: solid 1px var(--bw-green-500);
}
Edit Forms
To make a form work for both adding and updating an entity, you must:
- Set the value property on all the controls to be the entities current value
- Listen for the
dataPatchto get only the controls whose value is different from it's original value
Note: If you are loading in the original values asynchronously, you may need to also set the
originalValueproperty on all the form controls to getdataPatchto work as expected. This is because if the controls have completed their initial render before the orginal values have been fetched, the controls will set theiroriginalValueto undefined because the value was not present yet. Another alternative is to not render the form controls until you have finished fetching the inital values.
- Typescript
- HTML
import { createDialog } from 'bluewater';
const modal = document.querySelector('bw-modal')!
const modalTemplate = document.querySelector<HTMLTemplateElement>('#modalContent')!
const formTemplate = document.querySelector<HTMLTemplateElement>('#formContent')!
async function fetchInitialValues() {
//simulate fetching data
await new Promise(res => setTimeout(res, 1500))
return {
field1: "value1",
field2: "value2",
field3: "value3"
}
}
async function setInputs() {
const values = await fetchInitialValues()
document.querySelector<HTMLBwInputElement>('bw-input[name=field1]')!.value = values.field1
document.querySelector<HTMLBwInputElement>('bw-input[name=field2]')!.value = values.field2
document.querySelector<HTMLBwInputElement>('bw-input[name=field3]')!.value = values.field3
}
modal.init = () => {
const content = modalTemplate.content.cloneNode(true)
modal.append(content)
}
modal.destroy = () => {
const content = modal.querySelector('.bw-overlay-container')
content.remove()
}
modal.addEventListener('modalPresent', async () => {
modal.querySelector('.bw-overlay-container')!.remove()
modal.append(formTemplate.content.cloneNode(true))
await setInputs()
const closeButton = modal.querySelector('bw-icon-button')
closeButton.onclick = () => modal.dismiss()
const form = modal.querySelector('bw-form')
form.addEventListener('dataPatch', ({detail}) => {
modal.dismiss()
const dialog = createDialog({
header: 'Form patched',
message: JSON.stringify(detail),
buttons: [
{
text: 'Ok',
handler: el => el.dismiss()
}
]
})
dialog.present()
})
})
<template id="formContent">
<div class="bw-overlay-container">
<bw-toolbar class="bw-overlay-header">
<h2>Edit Form</h2>
<bw-icon-button icon="close" slot="end"></bw-icon-button>
</bw-toolbar>
<div class="bw-overlay-content flex-column gap">
<bw-form id="editForm">
<bw-input name="field1" label="Field 1"></bw-input>
<bw-input name="field2" label="Field 2"></bw-input>
<bw-input name="field3" label="Field 3"></bw-input>
</bw-form>
</div>
<bw-toolbar class="bw-overlay-footer">
<bw-button slot="end" type="submit" form="editForm">Submit</bw-button>
</bw-toolbar>
</div>
</template>
<template id="modalContent">
<div class="bw-overlay-container">
<bw-toolbar class="bw-overlay-header">
<h2>Edit Form</h2>
<bw-icon-button icon="close" slot="end"></bw-icon-button>
</bw-toolbar>
<div class="bw-overlay-content flex-column gap">
<div class="flex-column gap-3xs">
<bw-skeleton-text style="height: 20px; width: 42px; --border-radius: var(--bw-radius-sm);"></bw-skeleton-text>
<bw-skeleton-text style="height: 30px; width: 100%; --border-radius: var(--bw-radius-sm);"></bw-skeleton-text>
</div>
<div class="flex-column gap-3xs">
<bw-skeleton-text style="height: 20px; width: 42px; --border-radius: var(--bw-radius-sm);"></bw-skeleton-text>
<bw-skeleton-text style="height: 30px; width: 100%; --border-radius: var(--bw-radius-sm);"></bw-skeleton-text>
</div>
<div class="flex-column gap-3xs">
<bw-skeleton-text style="height: 20px; width: 42px; --border-radius: var(--bw-radius-sm);"></bw-skeleton-text>
<bw-skeleton-text style="height: 30px; width: 100%; --border-radius: var(--bw-radius-sm);"></bw-skeleton-text>
</div>
</div>
</div>
</template>
<bw-modal size="sm" backdrop-dismiss="false">
<bw-button slot="trigger">Open Edit Form</bw-button>
</bw-modal>
Comprehensive Example
Sign Up
- Typescript
- HTML
import { createDialog } from 'bluewater';
const drawer = document.querySelector('bw-drawer')!
const form = document.querySelector('bw-form')!
const pwInput = document.querySelector<HTMLBwInputElement>('bw-input[name=pw]')!
const discard = document.querySelector('bw-discard-form')!
pwInput.validators = [input => input.length < 9 ? ['Must be at least 9 characters long'] : []]
discard.addEventListener('discard', () => {
drawer.open = false
})
form.addEventListener('dataSubmit', (event) => {
drawer.dismiss()
const dialog = createDialog({
header: 'Form submitted',
message: JSON.stringify(event.detail),
buttons: [
{
text: 'Ok',
handler: el => el.dismiss()
}
]
})
dialog.present()
});
<bw-form>
<bw-drawer backdrop-dismiss="false">
<bw-button slot="trigger">Sign Up</bw-button>
<bw-toolbar slot="header">
<h2>Sign Up</h2>
<bw-discard-form slot="end">
<bw-icon-button icon="close"></bw-icon-button>
</bw-discard-form>
</bw-toolbar>
<div slot="content" class="flex-column gap">
<bw-input name="email" label="Email" required></bw-input>
<bw-input name="pw" label="Password" type="password" required></bw-input>
<bw-input name="fullName" label="Full Name"></bw-input>
<bw-input name="dob" label="Date of Birth" type="date" transform="date"></bw-input>
<bw-input name="favoriteNumber" label="Favorite Number (1 - 9)" max="9" min="1" required type="number" transform="number"></bw-input>
<bw-checkbox name="likesHats">Do you like hats?</bw-checkbox>
<bw-checkbox-group name="favMusic" required>
<bw-label required slot="label">
What kind of music do you like? <bw-icon size="sm">music_note</bw-icon>
</bw-label>
<bw-checkbox-option value="rock">Rock</bw-checkbox-option>
<bw-checkbox-option value="acid jazz">Acid Jazz</bw-checkbox-option>
<bw-checkbox-option value="ska">Ska</bw-checkbox-option>
<bw-checkbox-option value="rap metal">Rap Metal</bw-checkbox-option>
</bw-checkbox-group>
<bw-radio-group name="favColor" label="Favorite Color">
<bw-radio-button value="red">Red</bw-radio-button>
<bw-radio-button value="blue">Blue</bw-radio-button>
<bw-radio-button value="other">Other</bw-radio-button>
</bw-radio-group>
</div>
<bw-toolbar slot="footer">
<bw-button slot="end" type="submit">Submit</bw-button>
</bw-toolbar>
</bw-drawer>
</bw-form>