Skip to main content

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:

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.

Submit
declare const form: HTMLBwFormElement;
form.addEventListener('dataSubmit', (event) => {
form.insertAdjacentHTML('afterend', `<p>Form data: ${JSON.stringify(event.detail)}</p>`);
});

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".

SubmitReset
<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.

Accept termsSubmit
import { createToast } from 'bluewater';

declare const form: HTMLBwFormElement;
form.addEventListener('submit', () => {
createToast({ variant: 'bar', message: 'Form submitted!', type: 'success' }).then(toast => toast.open = true)
});

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.

3419Submit
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)
})

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.

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]

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 types
  • dataPatch: Emits only the data that changed with correct types
  • submit: The native SubmitEvent that bubbles up from the inner HTMLFormElement
  • formData: [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 dataPatch to 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 originalValue property on all the form controls to get dataPatch to 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 their originalValue to 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.

Open Edit Form
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()
})
})

Comprehensive Example

Sign Up

Sign Up

Do you like hats?What kind of music do you like? music_noteRockAcid JazzSkaRap MetalRedBlueOther
Submit
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()
});