Validation is based on Parsley.js
formValidator.data-parsley-trigger attribute.input, as they would otherwise display an error immediately upon entry.data-parsley-file-types attribute. The value must be a comma-separated list of file types (e.g. image/jpeg, image/png).data-parsley-max-file-size attribute. The value must be specified in kilobytes without a unit (e.g. 1024).Internet Explorer
There are problems with input fields of type “Date” in Internet Explorer (all versions) and Parsley.js, as Internet Explorer ignores the local format. The value of the field in IE always has the pattern “YYYY-MM-DD”, but Parsley expects, for example, “DD.MM.YYYY” in German and marks the field as erroneous.
This incompatibility is solved via a script that converts all fields of type “Date” to fields of type “Text” in Internet Explorer and tells Parsley via the data-parsley-pattern attribute to test it like a date (in the German notation).
<div class="formInset is-side-by-side">
<h2 class="formInset-title">Form title</h2>
<div class="formInset-description">
<h3>Ich bin eine Headline</h3>
<p><strong>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</strong></p>
<p>Bitters sartorial gastropub, hashtag four loko skateboard chillwave deep. Crucifix you probably haven’t heard of them pork belly tilde, direct trade migas cornhole meggings chambray Vice put a bird on it DIY brunch.</p>
</div>
<form class="formInset-form" method="POST" action="./" accept-charset="UTF-8">
<fieldset class="formFieldset">
<div class="formFieldset-fields">
<div class="formFieldset-field">
<div class="formField">
<label class="label is-optional" for="field-id">Bezeichnung</label>
<span class="formField-field">
<input class="field" type="text" id="field-id" name="fieldname" placeholder="" spellcheck="false" />
</span>
<div class="formMessage js-formValidator-message">
Ich bin ein Hinweis.
</div>
</div>
</div>
<div class="formFieldset-field">
<div class="formSelect">
<label class="label is-optional" for="field-select">Auswahlliste</label>
<span class="formSelect-input">
<span class="select">
<select id="field-select">
<option value="" disabled="disabled" selected="selected">Please select</option>
<hr />
<option value="-1">
Show all
</option>
<hr />
<option value="1">
Option 1
</option>
<option value="2">
Option 2
</option>
<option value="3">
Option 3
</option>
<option value="4" disabled="disabled">
Unavailable option
</option>
</select>
</span>
</span>
</div>
</div>
<div class="formFieldset-field">
<div class="formTextarea is-optional">
<label class="label is-optional" for="field-uniqueID-textarea">Textfeld</label>
<textarea class="formTextarea-field" id="field-uniqueID-textarea" name="textarea" placeholder="Aufforderung Nachricht zu schreiben"></textarea>
<div class="formMessage js-formValidator-message">
Ich bin ein Hinweis.
</div>
</div>
</div>
<div class="formFieldset-field">
<fieldset class="formToggleSet is-optional">
<legend class="formToggleSet-label">Gruppe mit Optionen</legend>
<div class="formToggleSet-options">
<div class="formToggleSet-option">
<div class="formToggle is-checkbox is-optional">
<label class="formToggle-label is-optional"><span class="toggle is-checkbox">
<input class="toggle-input" role="switch" type="checkbox" name="checkbox-group[]" value="" /><span class="toggle-marker"></span>
</span>
<span>Cardigan</span></label>
</div>
</div>
<div class="formToggleSet-option">
<div class="formToggle is-checkbox is-optional">
<label class="formToggle-label is-optional"><span class="toggle is-checkbox">
<input class="toggle-input" role="switch" type="checkbox" name="checkbox-group[]" value="" /><span class="toggle-marker"></span>
</span>
<span>Raw Denim</span></label>
</div>
</div>
<div class="formToggleSet-option">
<div class="formToggle is-checkbox is-optional">
<label class="formToggle-label is-optional"><span class="toggle is-checkbox">
<input class="toggle-input" role="switch" type="checkbox" name="checkbox-group[]" value="" /><span class="toggle-marker"></span>
</span>
<span>Flexitarian</span></label>
</div>
</div>
<div class="formToggleSet-option">
<div class="formToggle is-checkbox is-optional">
<label class="formToggle-label is-optional"><span class="toggle is-checkbox">
<input class="toggle-input" role="switch" type="checkbox" name="checkbox-group[]" value="" /><span class="toggle-marker"></span>
</span>
<span>Letterpress & Mustache</span></label>
</div>
</div>
<div class="formToggleSet-option">
<div class="formToggle is-checkbox is-optional">
<label class="formToggle-label is-optional"><span class="toggle is-checkbox">
<input class="toggle-input" role="switch" type="checkbox" name="checkbox-group[]" value="" /><span class="toggle-marker"></span>
</span>
<span>Gatekeep</span></label>
</div>
</div>
</div>
<div class="formMessage js-formValidator-message">
Wählen Sie mindestens eine Option aus.
</div>
</fieldset>
</div>
<div class="formFieldset-field">
<div class="formToggle is-checkbox is-required is-small">
<label class="formToggle-label is-required is-small"><span class="toggle is-checkbox is-small">
<input class="toggle-input" role="switch" type="checkbox" name="toggle" value="" required="" data-parsley-trigger="change" /><span class="toggle-marker"></span>
</span>
<span>Ich habe die <a href="#">Datenschutzhinweise</a>, die <a href="#">Teilnahmebedingungen</a> und die <a href="#">allgemeinen Geschäftsbedingungen</a> gelesen und akzeptiere sie.</span></label>
</div>
</div>
<div class="formFieldset-field">
<div class="formFieldset-note">
<strong class="label">Datenschutz</strong>
<small>Die Datenverarbeitung erfolgt wie in der <a href="#" target="_blank">Datenschutzerklärung für die Website</a> beschrieben.</small>
</div>
</div>
</div>
<div class="buttonGroup">
<button class="button is-secondary"><span class="button-label">Back</span></button>
<button class="button" type="submit"><span class="button-label">Send</span></button>
</div>
</fieldset>
</form>
</div>
<div class="formInset{{#modifier}} {{.}}{{/modifier}}">
{{#title}}
<h2 class="formInset-title">{{{.}}}</h2>
{{/title}}
{{#if description}}
<div class="formInset-description"{{#theme}} data-theme="{{.}}"{{/theme}}>
{{{description}}}
{{#figure}}
{{render '@figure' (contextData '@forminset' this) merge=true}}
{{/figure}}
{{#documentmockup}}
{{render '@documentmockup' (contextData '@forminset' this) merge=true}}
{{/documentmockup}}
</div>
{{/if}}
<form class="formInset-form{{#if validate}} formValidator{{/if}}" method="{{#method}}{{.}}{{/method}}{{^method}}POST{{/method}}" action="{{#action}}{{.}}{{/action}}{{^action}}./{{/action}}" accept-charset="UTF-8" {{#if validate}} data-parsley-validate data-parsley-trigger="input focusout"{{/if}}>
{{#formfield}}
<!-- Only for demonstration non-compliant markup -->
{{render '@formfield' (contextData '@forminset' this) merge=false}}
{{/formfield}}
{{#formhidden}}
<div>
<!-- Only for demonstration non-compliant markup -->
{{render '@formhidden' (contextData '@forminset' this) merge=false}}
</div>
{{/formhidden}}
{{#formfieldsets}}
{{render '@formfieldset' (contextData '@forminset' this) merge=false}}
{{/formfieldsets}}
{{#buttongroup}}
{{render '@buttongroup' (contextData '@forminset' this) merge=false}}
{{/buttongroup}}
</form>
</div>
{
"title": "Form title",
"description": "<h3>Ich bin eine Headline</h3>\n<p><strong>Selfies fixie next level trust fund jean shorts photo booth raw denim butcher mixtape ethical mustache.</strong></p>\n<p>Bitters sartorial gastropub, hashtag four loko skateboard chillwave deep. Crucifix you probably haven’t heard of them pork belly tilde, direct trade migas cornhole meggings chambray Vice put a bird on it DIY brunch.</p>\n",
"formfieldsets": {
"legend": null,
"fields": [
{
"formfield": {
"id": "id",
"name": "fieldname",
"label": "Bezeichnung",
"placeholder": "Beispiel für Inhalt",
"message": {
"content": "Ich bin ein Hinweis."
}
}
},
{
"formselect": {
"id": "select",
"label": "Auswahlliste",
"placeholder": "Please select",
"options": [
{
"label": null
},
{
"label": "Show all",
"value": -1
},
{
"label": null
},
{
"label": "Option 1",
"value": 1
},
{
"label": "Option 2",
"value": 2
},
{
"label": "Option 3",
"value": 3
},
{
"label": "Unavailable option",
"value": 4,
"is-disabled": true
}
]
}
},
{
"formtextarea": {
"id": "uniqueID-textarea",
"name": "textarea",
"label": "Textfeld",
"placeholder": "Aufforderung Nachricht zu schreiben",
"message": {
"content": "Ich bin ein Hinweis."
}
}
},
{
"formtoggleset": {
"type": "checkbox-group",
"label": "Gruppe mit Optionen",
"message": {
"content": "Wählen Sie mindestens eine Option aus."
},
"options": [
{
"type": "checkbox",
"name": "checkbox-group[]",
"label": "Cardigan"
},
{
"type": "checkbox",
"name": "checkbox-group[]",
"label": "Raw Denim"
},
{
"type": "checkbox",
"name": "checkbox-group[]",
"label": "Flexitarian"
},
{
"type": "checkbox",
"name": "checkbox-group[]",
"label": "Letterpress & Mustache"
},
{
"type": "checkbox",
"name": "checkbox-group[]",
"label": "Gatekeep"
}
]
}
},
{
"formtoggle": {
"type": "checkbox",
"id": "toggle",
"name": "toggle",
"label": "Ich habe die <a href=\"#\">Datenschutzhinweise</a>, die <a href=\"#\">Teilnahmebedingungen</a> und die <a href=\"#\">allgemeinen Geschäftsbedingungen</a> gelesen und akzeptiere sie.",
"is-required": true,
"modifier": "is-small"
}
},
{
"formnote": {
"label": "Datenschutz",
"content": "<small>Die Datenverarbeitung erfolgt wie in der <a href=\"#\" target=\"_blank\">Datenschutzerklärung für die Website</a> beschrieben.</small>"
}
}
]
},
"buttongroup": null,
"modifier": "is-side-by-side"
}
import formValidator from "./_formInset.script";
function addBusinessEmailValidator() {
function init(data) {
/*
* data {object}: Contains an array "list" with the blacklisted domains.
* If the top level domain is an asterisk, all possible top levels are filtered.
* {
* list: [
* "gmx.de",
* "gmail.*"
* ]
* }
*/
if (typeof data !== "object" || typeof data.list !== "object" || ! data.list.length) {
console.warn("formInset: No definition for business email is found.")
}
const domainsBlacklist = data.list;
window.Parsley.addValidator('businessEmail', {
validateString: function(value) {
const emailChunks = value.split('@');
if (emailChunks.length < 2) {
return true;
}
const domain = emailChunks[1].toLowerCase();
if (domain === "") {
return true;
}
const domainWithoutTopLevel = domain.replace(/\.[^.]+$/, ".*");
return ! domainsBlacklist.includes(domain) && ! domainsBlacklist.includes(domainWithoutTopLevel);
},
messages: {
en: 'Only business email addresses are accepted.',
de: 'Es werden nur geschäftliche E-Mail-Adressen akzeptiert.',
pl: 'Akceptujemy tylko firmowe adresy e-mail.',
},
});
}
fetch('/domainsBlacklist.json')
.then(response => response.json())
.then(data => {
init(data);
})
.catch(error => console.error('formInset: Can’t load JSON with definitions for business email.', error));
}
/**
* Telephone number validator for Parsley.js
*
* Regex: /^(\+?\d{2,4}\s?)?[\d\s]{8,20}$/
* Accepts: +49 894238438290, 0049 980502389, 04584 539894385, 894238438290
*/
window.Parsley.addValidator('telephone', {
validateString: function (value, requirement, instance) {
// Empty field is allowed (optional)
if (!value || value.trim() === '') {
return true;
}
// Regex: /^(\+?\d{2,4}\s?)?[\d\s]{8,20}$/
// Accepts: +49 894238438290, 0049 980502389, 04584 539894385, 894238438290
const phoneRegex = /^(\+?\d{2,4}\s?)?[\d\s]{8,20}$/,
isValid = phoneRegex.test(value.trim());
return isValid;
},
});
/**
* Automatically attach telephone validation to all input[type="tel"]
*/
function addTelephoneValidation() {
const telInputs = document.querySelectorAll('input[type="tel"]');
telInputs.forEach( input => {
input.setAttribute('data-parsley-telephone', 'true');
input.setAttribute('data-parsley-trigger', 'input focusout');
input.setAttribute('data-parsley-validation-threshold', '8');
});
}
/**
* Init
*/
addTelephoneValidation();
addBusinessEmailValidator();
formValidator.init({
lang: "en",
});
export default (function (){
var defaults = {
lang: "de",
selectors: {
forms: "form[data-parsley-validate]",
fieldContainer: '*[data-validate="true"]',
},
submit: {
selector: "",
disable: true,
},
};
var init = function(options){
var forms = $(defaults.selectors.forms);
if (forms.length === 0){
return;
}
const settings = Object.assign({}, defaults, options);
addValidators();
fixIEDateInput();
forms.each(function(){
new Validator(this, settings);
});
};
var Validator = function(form, settings){
var self = this;
this.settings = $.extend(true, {}, settings);
this.form = $(form);
this.submit = $('button[type="submit"]', this.form);
if (! $.isFunction($.fn.parsley)){
return;
}
this.form.parsley({
excluded: "input[type=button], input[type=submit], input[type=reset], input[type=hidden], [disabled]",
errorClass: "",
successClass: "",
errorsWrapper: '<ul class="parsley-errors-list"></ul>',
errorsContainer: function (field) { return getContainer(field) },
lang: this.settings.lang,
}).on("field:validated", function(formField){
self.updateStatus(self.form.parsley().isValid());
}).on("field:success", function(formField){
self.setSuccessStatus(formField);
}).on("field:error", function(formField){
self.setSuccessStatus(formField, false);
});
// Set language
if ( $("html").attr("lang") ){
var locale = $("html").attr("lang") || defaults.lang;
var matches = locale.match(/^[a-zA-Z]{2}/),
lang = matches[0] || this.settings.lang;
try {
window.Parsley.setLocale(lang);
} catch (e) {
console.log("formValidator: The set language is not available.");
}
}
this.updateStatus(false);
};
function getContainer(field){
var element = field.$element;
if (! element.hasClass(defaults.selectors.fieldContainer)){
return element.closest(defaults.selectors.fieldContainer);
}
return;
}
Validator.prototype.updateStatus = function(status){
this.form[0].dataset.valid = status;
}
Validator.prototype.getParent = function(element){
return element.parents(this.settings.selectors.fieldContainer);
}
Validator.prototype.setSuccessStatus = function(formField, valid){
if (typeof valid !== "boolean") {
valid = true;
}
formField.$element.attr("aria-invalid", ! valid);
const parent = formField.$element.parents(this.settings.selectors.fieldContainer);
parent.attr("data-invalid", ! valid);
const siblings = formField.$element.siblings();
siblings.attr("data-invalid", ! valid);
}
Validator.prototype.updateUI = function(refresh) {
};
Validator.prototype.validate = function(refresh) {
this.form.parsley().validate();
return this.form.parsley().isValid();
};
Validator.prototype.submit = function(callback){
var callback = (typeof callback === "function") ? callback : function(){};
if (! $.isFunction($.fn.parsley)){
callback();
return true;
}
var isValid = this.validate();
if (isValid){
callback();
}
return isValid;
};
var addValidators = function(){
window.Parsley.addValidator('maxFileSize', {
validateString: function(_value, maxSize, parsleyInstance) {
if (! window.FormData) {
return true;
}
var files = parsleyInstance.$element[0].files;
return files.length != 1 || files[0].size <= maxSize * 1024;
},
requirementType: 'integer',
messages: {
en: 'This file should not be larger than %s Kb.',
de: 'Die Datei darf nicht größer als %s KB sein.',
}
});
window.Parsley.addValidator('fileTypes', {
validateString: function(_value, types, parsleyInstance) {
if (! window.FormData || ! types) {
return true;
}
var types = types.split(/, ?/);
var files = parsleyInstance.$element[0].files;
return files.length != 1 || types.indexOf(files[0].type) !== -1;
},
requirementType: 'string',
messages: {
en: 'This file is not the right type.',
de: 'Die Datei ist vom falschen Typ.',
}
});
};
var fixIEDateInput = function(input){
function getInternetExplorerVersion(){
var rV = -1; // Return value assumes failure.
if (navigator.appName == 'Microsoft Internet Explorer' || navigator.appName == 'Netscape') {
var uA = navigator.userAgent;
var rE = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (rE.exec(uA) != null) {
rV = parseFloat(RegExp.$1);
}
else if (!!navigator.userAgent.match(/Trident.*rv\:11\./)) {
rV = 11;
}
}
return rV;
}
if (getInternetExplorerVersion() === -1){
return;
}
$('input[type="date"]').each(function(){
var input = $(this);
// Please regard: Pattern does not check if date exists,
// so eg. `31.02.2000` will be valid
var pattern = "([0-2][1-9]|3[0-1])\\.(0[1-9]|1[0-2])\\.(19|20)\\d{2}";
input.attr("type", "text")
.attr("data-parsley-pattern", pattern)
.attr("data-parsley-validation-threshold", "10");
});
}
return {
init: init
};
})();
@import "_formInset.settings";
@import "_formInset.styles";
%formInset {
.fileCard {
background-color: $_page-color;
}
.buttonGroup {
@include stack-spacing(large);
}
&-title + &-description {
@include stack-spacing(component);
text-align: center;
}
&-description {
> .figure {
@include stack-spacing(large);
}
}
.documentMockup {
@include stack-spacing(large);
background-color: $_BACKDROP-COLOR;
}
&-form {
box-shadow: none;
.buttonGroup {
@include stack-spacing(component);
}
}
&-title ~ &-form > .buttonGroup {
justify-content: center;
}
@include only-on-desktop(){
width: get-columns-width(8);
margin-left: auto;
margin-right: auto;
}
}
%formInset--side-by-side {
width: 100%;
.buttonGroup {
justify-content: flex-start;
}
%formInset-description {
text-align: left !important;
}
%formInset-description[data-theme] {
padding: var(--bp);
}
@include only-on-desktop(){
display: grid;
column-gap: var(--gg);
row-gap: var(--sp-component);
grid-template-columns: 1fr 1fr;
.formFieldset {
@include stack-spacing(0);
}
.formFieldset + .formFieldset {
padding-top: 0;
}
%formInset-title {
grid-column: 1 / span 2;
}
%formInset-description {
@include stack-spacing(0);
grid-column: 1 / span 1;
}
%formInset-form {
@include stack-spacing(0);
grid-column: 2 / span 1;
height: 100%;
border-radius: 0 var(--br) var(--br) 0;
padding: var(--bp);
background-color: $card_background-color;
display: flex;
flex-direction: column;
.formFieldset {
padding: 0;
}
.formFieldset:only-child {
height: 100%;
}
> *:last-child:not(:only-child),
.formFieldset > .buttonGroup:last-child {
flex-grow: 1;
align-items: flex-end;
}
> .buttonGroup {
@include stack-spacing(0);
padding: $formFieldset_stack-spacing 0 0;
}
}
%formInset-description[data-theme] {
padding: var(--bp-large);
margin-right: calc(-.5 * var(--gg));
border-top-left-radius: var(--br);
border-bottom-left-radius: var(--br);
}
%formInset-description[data-theme] + %formInset-form {
margin-left: calc(-.5 * var(--gg));
padding: var(--bp-large);
.formFieldset + .buttonGroup {
padding-top: $formFieldset_stack-spacing;
}
}
}
}
.formInset.is-side-by-side {
@extend %formInset--side-by-side;
}
// Highlighting non-compliant markup
.formInset-form:has(> *[class^="form"]:not(.formFieldset)) {
@extend %_not-compliant;
}
$formInset_button_style--invalid: (
) !default;
%formInset {
@include stack-spacing(section);
&-title {
@extend %sectionTitle;
@include stack-spacing(0);
}
&-description {
@extend %richtextBlock;
&:not(:first-child) {
@include stack-spacing();
}
> *:first-child {
@include stack-spacing(0);
}
}
&-form {
@include stack-spacing(0);
&:not(:first-child) {
@include stack-spacing(component);
}
.buttonGroup {
@include stack-spacing();
justify-content: flex-end;
}
.formFieldset:first-child {
@include stack-spacing(0);
}
}
}
.formInset {
@extend %formInset;
&-title {
@extend %formInset-title;
}
&-description {
@extend %formInset-description;
}
&-form {
@extend %formInset-form;
}
}
//***** Validation *****//
.formInset-form[data-parsley-validate] {
transition-duration: $_transition-duration--out;
*[data-invalid="true"] {
transition: color $_transition-duration--in;
.formMessage {
display: none;
}
}
label {
transition-property: color;
transition-duration: inherit
}
input {
transition-property: border-color;
transition-duration: inherit
}
&[data-valid="false"] .button[type="submit"] {
@include styles($formInset_button_style--invalid);
}
.formToggle-label {
flex-wrap: wrap;
span:not(.toggle) {
flex: 1 1;
}
}
}
.parsley-errors {
&-list {
list-style: none;
padding: 0;
margin-top: 0;
&:empty {
display: none;
}
li {
@extend %formMessage;
}
li + li {
@include stack-spacing(small);
}
.formToggle-label & {
order: 99;
width: 100%;
margin-top: $field_stack-spacing;
}
}
}