views/mdc/assets/js/components/events/errors.js in voom-presenters-0.2.0 vs views/mdc/assets/js/components/events/errors.js in voom-presenters-2.0.0
- old
+ new
@@ -1,142 +1,225 @@
+import 'core-js/features/array/flat';
+
+/**
+ * mapObject transforms an object's key-value pairs via the provided function.
+ * @param {Object} object
+ * @param {Function} fn A mapping function suitable for use with Array.map
+ * @return {Object}
+ */
+function mapObject(object, fn) {
+ return Object.entries(object)
+ .map(fn)
+ .reduce((obj, [k, v]) => Object.assign(obj, {[[k]]: v}), {});
+}
+
+/*
+ Attempt to interpret and serialize the following cases for display:
+
+ A: Rails errors:
+ 1. { "name": ["Requires name"] }
+
+ B: Validation errors:
+ 1. { :email => ["must be filled"] }
+ 2. { :fees => 0 => { :fee => ["must be filled", "must be greater than zero"] } }
+
+ C: Custom errors and client-side exceptions:
+ 1. { :email => "must be filled" }
+ 2. { exception: 'Something bad happened' }
+
+ D: Logical errors:
+ 1. "undefined method `map' for nil:NilClass"
+ */
+
export class VErrors {
- constructor(event) {
- this.event = event;
+ constructor(root, target) {
+ this.root = root;
+ this.target = target;
}
clearErrors() {
- var errorMessages = document.querySelectorAll('.v-error-message');
+ const errorMessages = this.root.querySelectorAll('.v-error-message');
- for (var i = 0; i < errorMessages.length; i++) {
+ for (let i = 0; i < errorMessages.length; i++) {
errorMessages[i].remove();
}
}
- // Rails errors
- // {
- // "name": [
- // "Requires name"
- // ]
- // }
+ /**
+ * normalize attempts to convert the various error structures described
+ * above into a single consistent structure by replacing error arrays
+ * with joined strings.
+ * @param {Object} errors
+ * @return {Object}
+ */
+ normalize(errors) {
+ if (!errors) {
+ return {};
+ }
- // Validation errors
- // { :email => ["must be filled"] }
+ // Normalize case D into case C-1:
+ if (typeof errors === 'string') {
+ errors = {error: errors};
+ }
- // Custom errors
- // { :email => "must be filled" }
+ return mapObject(errors, ([k, v]) => {
+ let result = null;
- // Exceptions
- // {exception: 'Something bad happened' }
+ // Case C, a single key-value pair:
+ if (typeof v === 'string') {
+ // Normalize case C into case A/B-1:
+ v = [v];
+ }
- stringsToArrays(value) {
- if (Array.isArray(value) || value.constructor === Object) {
- return value;
- }
- return new Array(value);
+ if (Array.isArray(v)) {
+ // Case A and B-1: an array of error messages:
+ result = v.join(', ');
+ } else if (v.constructor === Object) {
+ // Case B-2: a nested structure:
+ result = this.normalize(v);
+ } else {
+ throw new Error(`Cannot normalize value of type ${typeof v}`);
+ }
+
+ return [k, result];
+ });
}
- normalizeErrors(errors) {
- if (errors && errors.constructor === Object) {
- return Object.keys(errors).reduce((previous, key) => {
- previous[key] = this.stringsToArrays(errors[key]);
- return previous;
- }, {});
+ /**
+ * flatten attempts to extract all human-readable error messages from an
+ * arbitrary error structure, yielding a flat array of strings.
+ * @param {Object} errors
+ * @return {Array<String>}
+ */
+ flatten(errors) {
+ if (!errors) {
+ return [];
}
- return [];
+
+ // Normalize case D into case C-1:
+ if (typeof errors === 'string') {
+ errors = {error: errors};
+ }
+
+ const object = mapObject(errors, ([k, v]) => {
+ let result = null;
+
+ if (typeof v === 'string') {
+ result = v;
+ }
+ else if (v.constructor === Object) {
+ result = this.flatten(v);
+ }
+ else {
+ throw new Error(`Cannot flatten value of type ${typeof v}`);
+ }
+
+ return [k, result];
+ });
+
+ return Object.values(object).flat();
}
- // [http_status, content_type, resultText]
displayErrors(result) {
- var httpStatus = result.statusCode;
- var contentType = result.contentType;
- var resultText = result.content;
+ const {statusCode, contentType, content} = result;
- var responseErrors = null;
+ let responseErrors = null;
- if (contentType && contentType.indexOf("application/json") !== -1) {
- responseErrors = JSON.parse(resultText);
- } else if (contentType && contentType.indexOf("v/errors") !== -1) {
- responseErrors = resultText;
+ if (contentType && contentType.includes('application/json')) {
+ responseErrors = JSON.parse(content);
}
+ else if (contentType && contentType.includes('v/errors')) {
+ responseErrors = content;
+ }
if (responseErrors) {
if (!Array.isArray(responseErrors)) {
responseErrors = [responseErrors];
}
- for (var response of responseErrors) {
- var pageErrors = Object.values(this.normalizeErrors(response)).reduce(function (previous, value) {
- if (Array.isArray(value) && value.length > 0) {
- previous.push(value.join('<br/>'));
- }
- return previous;
- }, []);
- var fieldErrors = this.normalizeErrors(response.errors);
- for (var field in fieldErrors) {
- if (!this.displayInputError(field, fieldErrors[field])) {
- // Collect errors that can't be displayed at the field level
- pageErrors.push(fieldErrors[field]);
+ for (const response of responseErrors) {
+ const normalizedResponse = this.normalize(response);
+ const errors = normalizedResponse.errors ? normalizedResponse.errors : normalizedResponse;
+ if (errors.constructor === String) {
+ this.prependErrors([errors]);
+ }
+ else {
+ for (const key in errors) {
+ if (!this.displayInputError(key, errors[key])) {
+ // If not handled at the field level, display at the page level
+ if (errors[key].length > 0) {
+ this.prependErrors([errors[key]]);
+ }
+ }
}
}
- this.prependErrors(pageErrors);
}
- } else if (httpStatus === 0) {
- this.prependErrors(["Unable to contact server. Please check that you are online and retry."]);
- } else {
- this.prependErrors(["The server returned an unexpected response! Status:" + httpStatus]);
}
+ else if (statusCode === 0) {
+ this.prependErrors(
+ ['Unable to contact server. Please check that you are online and retry.']
+ );
+ }
+ else {
+ this.prependErrors(
+ [`The server returned an unexpected response! Status: ${statusCode}`]
+ );
+ }
}
// Sets the helper text on the field
// Returns true if it was able to set the error on the control
- displayInputError(divId, messages) {
- var currentEl = document.getElementById(divId);
- if (currentEl && currentEl.mdcComponent) {
- currentEl.mdcComponent.helperTextContent = messages.join(', ');
- var helperText = document.getElementById(divId + '-input-helper-text');
- helperText.classList.add('mdc-text-field--invalid',
- 'mdc-text-field-helper-text--validation-msg',
- 'mdc-text-field-helper-text--persistent');
- currentEl.mdcComponent.valid = false;
- return true;
+ displayInputError(id, message) {
+ const currentEl = this.root.getElementById(id) || this.root.getElementsByName(id)[0];
+
+ if (!currentEl) {
+ return false;
}
- return false;
+
+ const helperText = this.root.getElementById(`${currentEl.id}-helper-text`);
+ if (!helperText) {
+ return false;
+ }
+
+ helperText.innerHTML = message;
+ currentEl.classList.add('mdc-text-field--invalid');
+ helperText.classList.add('mdc-text-field-helper-text--validation-msg');
+ helperText.classList.remove('v-hidden');
+
+ return true;
}
// Creates a div before the element with the same id as the error
- // Used to display an error message without their being an input field to attach the error to
+ // Used to display an error message without their being an input field to
+ // attach the error to
prependErrors(messages) {
- var errorsDiv = this.findNearestErrorDiv();
- // create a new div element
- var newDiv = document.createElement("div");
- newDiv.className = 'v-error-message';
- // and give it some content
+ const errorsDiv = this.findNearestErrorDiv();
- for (var message of messages) {
- var newContent = document.createTextNode(message);
- newDiv.appendChild(newContent);
- let br = document.createElement('br');
- // add the text node to the newly created div
- newDiv.appendChild(br);
+ if (!errorsDiv) {
+ console.error('Unable to display Errors! ', messages);
+
+ return false;
}
+ const newDiv = document.createElement('div');
+
+ newDiv.classList.add('v-error-message');
+ newDiv.insertAdjacentHTML('beforeend', messages.join('<br>'));
+
// add the newly created element and its content into the DOM
- if (errorsDiv) {
- errorsDiv.parentElement.insertBefore(newDiv, errorsDiv);
- return true;
- } else {
- console.error("Unable to display Errors! ", messages);
+ if (errorsDiv.clientTop < 10) {
+ errorsDiv.scrollIntoView();
}
- return false;
+
+ errorsDiv.insertAdjacentElement('beforebegin', newDiv);
+
+ return true;
}
findNearestErrorDiv() {
- let errorsDiv = null;
- const currentDiv = this.event.target;
- if(currentDiv) {
- errorsDiv = currentDiv.closest('.v-errors')
- }else{
- errorsDiv = document.querySelector('.v-errors');
+ if (this.target) {
+ return this.target.closest('.v-errors');
}
- return errorsDiv;
+
+ return this.root.querySelector('.v-errors');
}
}