Form Validation While Filling New
This template explains how to validate form fields and show relevant error messages in an accessible manner while the user is filling a form. The moment to do this is when the user leaves a field (on blur). Never validate while the user is typing in a field as the screen reader user experience can become very bad.
Note that due to differeces between screen readers it is difficult to get consistent experience for all users when validation is done while filling. On-blur validation can also lead to a confusing situation, where the input fields' error messages push the submit button downwards and it registers only mousedown event, but not click. For these reason it is best to validate when the user submits (see another template for that) and validation while filling should be used only in rare special cases.
Hint: Press F on your keyboard to view both templates and components in fullscreen and ESC to exit the fullscreen mode. You can also open the template in a new browser window.
<!DOCTYPE html>
<html class="duet-bg-gradient duet-sticky-footer" lang="fi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>LähiTapiola</title>
<link rel="stylesheet" href="https://cdn.duetds.com/api/fonts/3.0.69/lib/localtapiola.css" integrity="sha384-5JYmtSD7nykpUvSmTW1CHMoBDkBZUpUmG0vuh+NUVtZag3F75Kr7+/JU3J7JV6Wq" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.duetds.com/api/css/5.1.4/lib/duet.min.css" integrity="sha384-IVvgkw6No32VCTlruRZKFObmOW/zmL9o9dLJ1DqtTdkfETYLJkFmT+b84iIdzcf+" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.duetds.com/api/tokens/5.1.4/lib/tokens.custom-properties.css" integrity="sha384-CLtvX3Hm5cg7+ALJG+wzT72Bmxt2YN1piNjcK8ILizklnRCExM6i3lvmYJKW4At4" crossorigin="anonymous" />
<script type="module" src="https://cdn.duetds.com/api/components/10.7.0/lib/duet/duet.esm.js" integrity="sha384-+mYGqSABNlw5C1P12JLkvceZfNumy8pEwtdwyoFDUGc5+O8EA3Vd520UaHGGB0mV" crossorigin="anonymous"></script>
</head>
<body>
<duet-header language="fi"></duet-header>
<duet-layout center>
<div slot="main">
<duet-card padding="large">
<duet-spacer breakpoint="x-small" size="medium"></duet-spacer>
<!-- The form has novalidate attribute to prevent browsers' native error messages appearing -->
<form id="example-form" action="#" method="post" novalidate>
<duet-heading level="h1" visual-level="h2">Ota yhteyttä</duet-heading>
<duet-paragraph>
Lähetä meille viesti kun haluat antaa palautetta tai pyytää yhteydenottoa.
Jos olet jo asiakkaamme, asiasi hoituu parhaiten, kun kirjaudut verkkopalveluun.
Täytä kaikki kentät, jotta voimme auttaa sinua mahdollisimman hyvin.
</duet-paragraph>
<duet-spacer size="medium"></duet-spacer>
<!--
The name validation pattern uses unicode classes to match all letters (including combining marks), not
just the latin alphabet. This is to allow names with diacritics, such as "Müller". Additionally we need to
allow spaces and some punctuation to match names like "Hélèn O'Neil" and "Matti M. Korhonen-Virtanen"
-->
<duet-input
label="Nimi"
name="name"
required
pattern="^[\p{Letter}\p{Mark} \.\-']+$"
data-value-missing="Pakollinen tieto"
data-pattern-mismatch="Numeroita ja erikoismerkkejä ei sallita"
accessible-live-error="off"
expand
></duet-input>
<duet-input
label="Sähköposti"
name="email"
type="email"
required
data-value-missing="Pakollinen tieto"
data-type-mismatch="Virheellinen muoto"
accessible-live-error="off"
expand
></duet-input>
<duet-select
label="Kaupunki"
name="city"
required
placeholder="Valitse"
data-value-missing="Pakollinen tieto"
accessible-live-error="off"
expand
></duet-select>
<duet-textarea
name="message"
label="Viesti asiakaspalveluun"
caption="Kuvaile asiaasi mahdollisimman tarkasti, niin voimme auttaa sinua paremmin."
data-value-missing="Pakollinen tieto"
required
accessible-live-error="off"
expand
></duet-textarea>
<duet-radio-group
required
label="Yhteydenoton syy"
direction="horizontal"
name="reason"
data-value-missing="Pakollinen tieto"
accessible-live-error="off"
>
<duet-radio label="Palaute" value="one"></duet-radio>
<duet-radio label="Yhteydenottopyyntö" value="two"></duet-radio>
</duet-radio-group>
<duet-spacer size="x-large"></duet-spacer>
<!-- Alert to show if the form was not submitted due to errors -->
<div id="alert-container"></div>
<duet-button submit variation="primary">Lähetä viesti</duet-button>
<duet-button>Keskeytä</duet-button>
</form>
</duet-card>
</div>
</duet-layout>
<duet-footer logo-href="#" language="fi"></duet-footer>
<template id="submit-error-message">
<duet-alert variation="danger">
<div class="visible"></div>
<!-- This is used to announce errors of each field for screen readers -->
<duet-visually-hidden></duet-visually-hidden>
</duet-alert>
<duet-spacer size="x-large"></duet-spacer>
</template>
<script>
// First select the form
const form = document.querySelector("#example-form")
// Then select the form fields
const formFields = {
name: form.querySelector("duet-input[name=name]"),
email: form.querySelector("duet-input[name=email]"),
city: form.querySelector("duet-select[name=city]"),
message: form.querySelector("duet-textarea"),
reason: form.querySelector("duet-radio-group[name=reason]"),
}
// Set the options for the city selection
formFields.city.items = [
{ label: "Helsinki", value: "1" },
{ label: "Tampere", value: "2" },
{ label: "Vantaa", value: "3" },
{ label: "Espoo", value: "4" }
]
for (const field in formFields) {
// Once the user types something in the name field, remove the error.
formFields[field].addEventListener("duetChange", function() {
clearSubmitErrorMessage()
formFields[field].error = ""
})
// Only validate if the value has changed (on focus store the old value and on blur compare to it).
// This prevents the error message from appearing when the user tabs through the form.
formFields[field].addEventListener("duetFocus", event => {
const input = event.target
input.previousValue = input.value
})
formFields[field].addEventListener("duetBlur", event => {
const input = event.target
if (input.previousValue !== input.value) {
validateField(input)
}
})
}
const errorMessages = {
general: {
invalid: "Virheellinen",
missing: "Puuttuu",
},
notSubmitted: "Lomaketta ei lähetetty, siinä on virheitä."
}
function setNotSubmittedAlert(visibleText, nonVisualText) {
const template = document.getElementById("submit-error-message")
const alert = document.importNode(template.content, true);
alert.querySelector(".visible").textContent = visibleText
alert.querySelector("duet-visually-hidden").textContent = nonVisualText
document.getElementById("alert-container").append(alert)
}
function clearSubmitErrorMessage() {
document.querySelectorAll("#alert-container > *").forEach(el => el.remove())
}
function validateField(input) {
input.error = ""
// For fields that have validity object
if (input.validity) {
const errorKeys = []
const inputErrors = []
if (!input.validity.valid) {
// Get the keys for invalidities (usually there is only one)
for (key in input.validity) {
if (key !== "valid" && input.validity[key]) {
errorKeys.push(key)
}
}
// Try getting error messages from inputs' data attributes
errorKeys.forEach(key => {
inputErrors.push(input.dataset[key])
})
// If not error messages found fall back to generic
input.error =
inputErrors.join(", ")
||
(input.validity.valueMissing ? errorMessages.general.missing : errorMessages.general.invalid)
}
}
// For fields that don't have validity object (radio group, select)
else if (input.required && !input.value) {
input.error = input.dataset.valueMissing || errorMessages.general.missing
}
}
// Validate form also on submit
// If there are errors, prevent the form from submitting
form.addEventListener("submit", function(event) {
clearSubmitErrorMessage()
const fieldsWithErrors = []
for (const field in formFields) {
const input = formFields[field]
validateField(input)
if (input.error) {
fieldsWithErrors.push(input)
}
}
if (fieldsWithErrors.length) {
// First parameter sets visible error the form was not submitted
// Second parameter sets announcement for screen readers for individual fields' errors
setNotSubmittedAlert(
errorMessages.notSubmitted,
fieldsWithErrors.map(field => `${field.label}${field.error}`).join(", ")
)
// NOTE: with some forms, you may want to focus the first error, but this is usually not a good idea
// as the focusing will interrupt the screan readers reading out of the errors.
// If you want to focus the first error, you can use the following code:
// if (errors[0].setFocus) {
// // If the field has a setFocus method, use it
// errors[0].setFocus()
// } else {
// // If the field is a radio or choice, focus the first option
// errors[0].querySelector("duet-radio, duet-choice")?.setFocus()
// }
// Prevent the form from submitting
event.preventDefault()
event.stopPropagation()
}
})
// Show data in the footer component (not part of the form validation)
const footer = document.querySelector("duet-footer")
footer.items = [
{ label: 'Hae korvausta', href: '#', icon: 'navigation-make-claim' },
{ label: 'Osta vakuutus', href: '#', icon: 'action-buy-insurance' },
{ label: 'Yhteystiedot', href: '#', icon: 'form-tel' }
]
footer.menu = [
{ label: 'Turvallisuus ja käyttöehdot', href: '#' },
{ label: 'Evästeet', href: '#' },
{ label: 'Henkilötietojen käsittely', href: '#' },
]
</script>
</body>
</html> Integration
To install this template’s dependencies into your project, run:
npm install @duetds/components
npm install @duetds/css
npm install @duetds/fonts For further guidelines, please see each package’s documentation.
Tutorials
Follow these practical tutorials to learn how to build simple page layouts using Duet’s CSS Framework, Web Components and other features:
Building Layouts
TutorialsUsing CLI Tools
TutorialsCreating Custom Patterns
TutorialsServer Side Rendering
TutorialsSharing Prototypes
TutorialsUsage With Markdown
Troubleshooting
If you experience any issues while using a template, please head over to the Support page for more guidelines and help.