From 0f21d9e6602822efe03e7aa429e15ae7b3bc18d7 Mon Sep 17 00:00:00 2001 From: Sander Date: Tue, 15 Oct 2019 16:00:30 +0200 Subject: [PATCH] Better error messages for standalone contact forms Refs #20 --- changelog.md | 5 ++ docs/demo.css | 5 ++ src/contactForm.js | 110 ++++++++++++++++++++++++-- src/languageHelper.js | 15 +++- test/js-unit/recrasContactFormSpec.js | 71 ++++++++++++++++- 5 files changed, 196 insertions(+), 10 deletions(-) diff --git a/changelog.md b/changelog.md index 8db80db..900b626 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # Changelog +## unreleased +* Small styling improvement +* Option `showSubmit` for contact forms is now named `standalone`. The old option will remain as alias until the next major version +* Better error messages for standalone contact forms + ## 1.2.0 (2019-12-03) * Show error when input is higher than allowed * Allow selecting only a few package IDs diff --git a/docs/demo.css b/docs/demo.css index 42cc965..67d55aa 100644 --- a/docs/demo.css +++ b/docs/demo.css @@ -78,6 +78,11 @@ input[type="number"] { color: #a00; padding-left: 0.25em; } +.booking-error { + background: hsl(0, 25%, 96%); + border: 1px solid #a00; + padding: 0.5em; +} .recras-onlinebooking > *:not(:first-child) + * { border-top: 2px solid hsla(147, 25%, 25%, 0.25); diff --git a/src/contactForm.js b/src/contactForm.js index e409219..69ac075 100644 --- a/src/contactForm.js +++ b/src/contactForm.js @@ -77,6 +77,16 @@ class RecrasContactForm { return this.element.querySelectorAll(querystring); } + isStandalone(options) { + if (options.standalone) { + return true; + } + if (options.showSubmit) { + console.warn('Option "showSubmit" was renamed to "standalone". Please update your code.') + } + return false; + } + generateForm(extraOptions = {}) { let waitFor = []; @@ -88,14 +98,16 @@ class RecrasContactForm { RecrasCSSHelper.loadCSS('pikaday'); } return Promise.all(waitFor).then(() => { - let html = '
'; + const standalone = this.isStandalone(extraOptions); + const validateText = standalone ? 'novalidate' : ''; + let html = ``; if (extraOptions.voucherQuantitySelector) { html += this.quantitySelector(); } this.contactFormFields.forEach((field, idx) => { html += '
' + this.showField(field, idx) + '
'; }); - if (extraOptions.showSubmit) { + if (standalone) { html += this.submitButtonHtml(); } html += '
'; @@ -163,6 +175,30 @@ class RecrasContactForm { }); } + getInvalidFields() { + let invalid = []; + let required = this.getRequiredFields(); + + let els = this.findElements('.recras-contactform :invalid'); + for (let el of els) { + if (!required.includes(el)) { + invalid.push(el); + } + } + return invalid; + } + + getRequiredFields() { + let isEmpty = []; + let els = this.findElements('.recras-contactform :required'); + for (let el of els) { + if (el.value === undefined || el.value === '') { + isEmpty.push(el); + } + } + return isEmpty; + } + hasFieldOfType(identifier) { return this.contactFormFields.filter(field => { return field.field_identifier === identifier; @@ -178,6 +214,22 @@ class RecrasContactForm { return this.hasFieldOfType('boeking.arrangement'); } + isEmpty() { + let isEmpty = true; + let els = this.findElements('.recras-contactform input, .recras-contactform select, .recras-contactform textarea'); + let formValues = [...els].map(el => el.value); + for (let val of formValues) { + if (val !== '') { + isEmpty = false; + } + } + return isEmpty; + } + + isValid() { + return this.findElement('.recras-contactform').checkValidity(); + } + loadingIndicatorHide() { [...document.querySelectorAll('.recrasLoadingIndicator')].forEach(el => { el.parentNode.removeChild(el); @@ -195,12 +247,22 @@ class RecrasContactForm { return `
`; } + removeErrors(parentQuery = '') { + [...this.findElements(parentQuery + ' .booking-error')].forEach(el => { + el.parentNode.removeChild(el); + }); + } + removeWarnings() { [...this.findElements('.recrasError')].forEach(el => { el.parentNode.removeChild(el); }); } + requiredIsEmpty() { + return this.getRequiredFields().length > 0; + } + showField(field, idx) { if (field.soort_invoer === 'header') { return `

${ field.naam }

`; @@ -309,7 +371,7 @@ class RecrasContactForm { this.loadingIndicatorShow(this.element); return this.getContactFormFields() .then(() => this.generateForm({ - showSubmit: true, + standalone: true, })) .then(html => { this.appendHtml(html); @@ -330,6 +392,29 @@ class RecrasContactForm { }); } + showInlineErrors() { + for (let el of this.getRequiredFields()) { + const labelEl = el.parentNode.querySelector('label'); + const requiredText = this.languageHelper.translate('CONTACT_FORM_FIELD_REQUIRED', { + FIELD_NAME: labelEl.innerText, + }); + el.parentNode.insertAdjacentHTML( + 'afterend', + `
${ requiredText }
` + ); + } + for (let el of this.getInvalidFields()) { + const labelEl = el.parentNode.querySelector('label'); + const invalidText = this.languageHelper.translate('CONTACT_FORM_FIELD_INVALID', { + FIELD_NAME: labelEl.innerText, + }); + el.parentNode.insertAdjacentHTML( + 'afterend', + `
${ invalidText }
` + ); + } + } + showLabel(field, idx) { let labelText = field.naam; if (field.verplicht) { @@ -344,14 +429,27 @@ class RecrasContactForm { submitForm(e) { e.preventDefault(); - this.eventHelper.sendEvent(RecrasEventHelper.PREFIX_CONTACT_FORM, RecrasEventHelper.EVENT_CONTACT_FORM_SUBMIT, this.options.getFormId()); + let submitButton = this.findElement('.submitForm'); - let status = this.checkRequiredCheckboxes(); - if (!status) { + this.removeErrors('.recras-contactform'); + if (this.isEmpty()) { + submitButton.parentNode.insertAdjacentHTML( + 'afterend', + `
${ this.languageHelper.translate('ERR_CONTACT_FORM_EMPTY') }
` + ); + return false; + } else if (this.requiredIsEmpty() || !this.isValid()) { + this.showInlineErrors(); + return false; + } + + if (!this.checkRequiredCheckboxes()) { return false; } + this.eventHelper.sendEvent(RecrasEventHelper.PREFIX_CONTACT_FORM, RecrasEventHelper.EVENT_CONTACT_FORM_SUBMIT, this.options.getFormId()); + this.loadingIndicatorHide(); this.loadingIndicatorShow(submitButton); diff --git a/src/languageHelper.js b/src/languageHelper.js index ff3d93a..217931c 100644 --- a/src/languageHelper.js +++ b/src/languageHelper.js @@ -13,7 +13,7 @@ class RecrasLanguageHelper { ATTR_REQUIRED: 'Erforderlich', BOOKING_DISABLED_AGREEMENT: 'You have not agreed to the terms yet', BOOKING_DISABLED_AMOUNTS_INVALID: 'Programme amounts are invalid', - BOOKING_DISABLED_CONTACT_FORM_INVALID: 'Contact form is not filled in completely, or contains invalid values', + BOOKING_DISABLED_CONTACT_FORM_INVALID: 'Contact form is not filled in correctly', BOOKING_DISABLED_INVALID_DATE: 'No date selected', BOOKING_DISABLED_INVALID_TIME: 'No time selected', BOOKING_DISABLED_REQUIRED_PRODUCT: 'Required product not yet selected', @@ -21,6 +21,8 @@ class RecrasLanguageHelper { BUTTON_BUY_NOW: 'Jetzt kaufen', BUTTON_SUBMIT_CONTACT_FORM: 'Submit', CONTACT_FORM_CHECKBOX_REQUIRED: 'At least one option must be checked', + CONTACT_FORM_FIELD_INVALID: '"{FIELD_NAME}" is invalid', + CONTACT_FORM_FIELD_REQUIRED: '"{FIELD_NAME}" is a required field', CONTACT_FORM_SUBMIT_FAILED: 'The contact form could not be sent. Please try again later.', CONTACT_FORM_SUBMIT_SUCCESS: 'The contact form was sent successfully.', DATE: 'Datum', @@ -59,6 +61,7 @@ class RecrasLanguageHelper { DISCOUNT_TITLE: 'Rabattcode oder Gutschein', DISCOUNT_INVALID: 'Ungültiger Rabattcode oder Gutschein', ERR_AMOUNTS_NO_PACKAGE: 'Option "productAmounts" is set, but "package_id" is not set', + ERR_CONTACT_FORM_EMPTY: 'Contact form is not filled in', ERR_GENERAL: 'Etwas ist schief gelaufen:', ERR_INVALID_ELEMENT: 'Option "Element" ist kein gültiges Element', ERR_INVALID_HOSTNAME: 'Option "recras_hostname" ist ungültig.', @@ -93,7 +96,7 @@ class RecrasLanguageHelper { ATTR_REQUIRED: 'Required', BOOKING_DISABLED_AGREEMENT: 'You have not agreed to the terms yet', BOOKING_DISABLED_AMOUNTS_INVALID: 'Programme amounts are invalid', - BOOKING_DISABLED_CONTACT_FORM_INVALID: 'Contact form is not filled in completely, or contains invalid values', + BOOKING_DISABLED_CONTACT_FORM_INVALID: 'Contact form is not filled in correctly', BOOKING_DISABLED_INVALID_DATE: 'No date selected', BOOKING_DISABLED_INVALID_TIME: 'No time selected', BOOKING_DISABLED_REQUIRED_PRODUCT: 'Required product not yet selected', @@ -101,6 +104,8 @@ class RecrasLanguageHelper { BUTTON_BUY_NOW: 'Buy now', BUTTON_SUBMIT_CONTACT_FORM: 'Submit', CONTACT_FORM_CHECKBOX_REQUIRED: 'At least one option must be checked', + CONTACT_FORM_FIELD_INVALID: '"{FIELD_NAME}" is invalid', + CONTACT_FORM_FIELD_REQUIRED: '"{FIELD_NAME}" is a required field', CONTACT_FORM_SUBMIT_FAILED: 'The contact form could not be sent. Please try again later.', CONTACT_FORM_SUBMIT_SUCCESS: 'The contact form was sent successfully.', DATE: 'Date', @@ -139,6 +144,7 @@ class RecrasLanguageHelper { DISCOUNT_TITLE: 'Discount code or voucher', DISCOUNT_INVALID: 'Invalid discount code or voucher', ERR_AMOUNTS_NO_PACKAGE: 'Option "productAmounts" is set, but "package_id" is not set', + ERR_CONTACT_FORM_EMPTY: 'Contact form is not filled in', ERR_GENERAL: 'Something went wrong:', ERR_INVALID_ELEMENT: 'Option "element" is not a valid Element', ERR_INVALID_HOSTNAME: 'Option "recras_hostname" is invalid.', @@ -173,7 +179,7 @@ class RecrasLanguageHelper { ATTR_REQUIRED: 'Vereist', BOOKING_DISABLED_AGREEMENT: 'Je bent nog niet akkoord met de voorwaarden', BOOKING_DISABLED_AMOUNTS_INVALID: 'Aantallen in programma zijn ongeldig', - BOOKING_DISABLED_CONTACT_FORM_INVALID: 'Contactformulier is niet volledig ingevuld, of bevat ongeldige waardes', + BOOKING_DISABLED_CONTACT_FORM_INVALID: 'Contactformulier is niet correct ingevuld', BOOKING_DISABLED_INVALID_DATE: 'Geen datum geselecteerd', BOOKING_DISABLED_INVALID_TIME: 'Geen tijd geselecteerd', BOOKING_DISABLED_REQUIRED_PRODUCT: 'Vereist product nog niet geselecteerd', @@ -181,6 +187,8 @@ class RecrasLanguageHelper { BUTTON_BUY_NOW: 'Nu kopen', BUTTON_SUBMIT_CONTACT_FORM: 'Versturen', CONTACT_FORM_CHECKBOX_REQUIRED: 'Ten minste één optie moet aangevinkt worden', + CONTACT_FORM_FIELD_INVALID: '"{FIELD_NAME}" is ongeldig', + CONTACT_FORM_FIELD_REQUIRED: '"{FIELD_NAME}" is een verplicht veld', CONTACT_FORM_SUBMIT_FAILED: 'Het contactformulier kon niet worden verstuurd. Probeer het later nog eens.', CONTACT_FORM_SUBMIT_SUCCESS: 'Het contactformulier is succesvol verstuurd.', DATE: 'Datum', @@ -219,6 +227,7 @@ class RecrasLanguageHelper { DISCOUNT_TITLE: 'Kortingscode of tegoedbon', DISCOUNT_INVALID: 'Ongeldige kortingscode of tegoedbon', ERR_AMOUNTS_NO_PACKAGE: 'Optie "productAmounts" is ingesteld, maar "package_id" is niet ingesteld', + ERR_CONTACT_FORM_EMPTY: 'Contactformulier is niet ingevuld', ERR_GENERAL: 'Er ging iets mis:', ERR_INVALID_ELEMENT: 'Optie "element" is geen geldig Element', ERR_INVALID_HOSTNAME: 'Optie "recras_hostname" is ongeldig.', diff --git a/test/js-unit/recrasContactFormSpec.js b/test/js-unit/recrasContactFormSpec.js index 7fcb144..47d8747 100644 --- a/test/js-unit/recrasContactFormSpec.js +++ b/test/js-unit/recrasContactFormSpec.js @@ -219,6 +219,75 @@ describe('RecrasContactForm', () => { expect(sorted[1]).toEqual(packs[0]); expect(sorted[2]).toEqual(packs[1]); }); - }) + }); + + describe('getRequiredFields', () => { + let rc; + + beforeEach(async () => { + rc = new RecrasContactForm(new RecrasOptions({ + element: this.mainEl, + form_id: 1, + recras_hostname: 'demo.recras.nl', + })); + await rc.showForm(); + }); + + it('only returns required fields', () => { + const els = rc.getRequiredFields(); + expect(els.length).toBeGreaterThan(0); + for (const el of els) { + expect(el.getAttribute('required')).not.toBeNull(); + } + }); + }); + + describe('getInvalidFields', () => { + let rc; + + beforeEach(async () => { + rc = new RecrasContactForm(new RecrasOptions({ + element: this.mainEl, + form_id: 1, + recras_hostname: 'demo.recras.nl', + })); + await rc.showForm(); + }); + + it('returns invalid fields', () => { + let elEmail = rc.findElement('[type="email"]'); + elEmail.value = 'invalid'; + + let invalid = rc.getInvalidFields(); + expect(invalid.length).toBe(1); + expect(invalid[0]).toBe(elEmail); + + elEmail.value = 'info@recras.com'; + + invalid = rc.getInvalidFields(); + expect(invalid.length).toBe(0); + }); + }); + + describe('isEmpty', () => { + let rc; + + beforeEach(async () => { + rc = new RecrasContactForm(new RecrasOptions({ + element: this.mainEl, + form_id: 5, + recras_hostname: 'demo.recras.nl', + })); + await rc.showForm(); + }); + + it('returns if form is empty', () => { + expect(rc.isEmpty()).toBe(true); + let elEmail = rc.findElement('[type="email"]'); + elEmail.value = 'hi@example.com'; + + expect(rc.isEmpty()).toBe(false); + }); + }); }); });