import axios from 'axios' import { deepCopy, arrayWrap } from './functions' class Errors { /** * Create a new error bag instance. */ constructor () { this.errors = {} } /** * Set the errors object or field error messages. * * @param {Object|String} field * @param {Array|String|undefined} messages */ set (field, messages) { if (typeof field === 'object') { this.errors = field } else { this.set({ ...this.errors, [field]: arrayWrap(messages) }) } } /** * Get all the errors. * * @return {Object} */ all () { return this.errors } /** * Determine if there is an error for the given field. * * @param {String} field * @return {Boolean} */ has (field) { // eslint-disable-next-line no-prototype-builtins return this.errors.hasOwnProperty(field) } /** * Determine if there are any errors for the given fields. * * @param {...String} fields * @return {Boolean} */ hasAny (...fields) { return fields.some(field => this.has(field)) } /** * Determine if there are any errors. * * @return {Boolean} */ any () { return Object.keys(this.errors).length > 0 } /** * Get the first error message for the given field. * * @param String} field * @return {String|undefined} */ get (field) { if (this.has(field)) { return this.getAll(field)[0] } } /** * Get all the error messages for the given field. * * @param {String} field * @return {Array} */ getAll (field) { return arrayWrap(this.errors[field] || []) } /** * Get the error message for the given fields. * * @param {...String} fields * @return {Array} */ only (...fields) { const messages = [] fields.forEach((field) => { const message = this.get(field) if (message) { messages.push(message) } }) return messages } /** * Get all the errors in a flat array. * * @return {Array} */ flatten () { return Object.values(this.errors).reduce((a, b) => a.concat(b), []) } /** * Clear one or all error fields. * * @param {String|undefined} field */ clear (field) { const errors = {} if (field) { Object.keys(this.errors).forEach((key) => { if (key !== field) { errors[key] = this.errors[key] } }) } this.set(errors) } } class Form { /** * Create a new form instance. * * @param {Object} data */ constructor (data = {}) { this.busy = false this.successful = false this.errors = new Errors() this.originalData = deepCopy(data) Object.assign(this, data) } /** * Fill form data. * * @param {Object} data */ fill (data) { this.keys().forEach((key) => { this[key] = data[key] }) } /** * Get the form data. * * @return {Object} */ data () { return this.keys().reduce((data, key) => ( { ...data, [key]: this[key] } ), {}) } /** * Get the form data keys. * * @return {Array} */ keys () { return Object.keys(this) .filter(key => !Form.ignore.includes(key)) } /** * Start processing the form. */ startProcessing () { this.errors.clear() this.busy = true this.successful = false } /** * Finish processing the form. */ finishProcessing () { this.busy = false this.successful = true } /** * Clear the form errors. */ clear () { this.errors.clear() this.successful = false } /** * Reset the form fields. */ reset () { Object.keys(this) .filter(key => !Form.ignore.includes(key)) .forEach((key) => { this[key] = deepCopy(this.originalData[key]) }) } /** * Submit the form via a GET request. * * @param {String} url * @param {Object} config (axios config) * @return {Promise} */ get (url, config = {}) { return this.submit('get', url, config) } /** * Submit the form via a POST request. * * @param {String} url * @param {Object} config (axios config) * @return {Promise} */ post (url, config = {}) { return this.submit('post', url, config) } /** * Submit the form via a PATCH request. * * @param {String} url * @param {Object} config (axios config) * @return {Promise} */ patch (url, config = {}) { return this.submit('patch', url, config) } /** * Submit the form via a PUT request. * * @param {String} url * @param {Object} config (axios config) * @return {Promise} */ put (url, config = {}) { return this.submit('put', url, config) } /** * Submit the form via a DELETE request. * * @param {String} url * @param {Object} config (axios config) * @return {Promise} */ delete (url, config = {}) { return this.submit('delete', url, config) } /** * Submit the form data via an HTTP request. * * @param {String} method (get, post, patch, put) * @param {String} url * @param {Object} config (axios config) * @return {Promise} */ submit (method, url, config = {}) { this.startProcessing() const data = method === 'get' ? { params: this.data() } : this.data() return new Promise((resolve, reject) => { (Form.axios || axios).request({ url: this.route(url), method, data, ...config }) .then((response) => { this.finishProcessing() resolve(response) }) .catch((error) => { this.busy = false if (error.response) { this.errors.set(this.extractErrors(error.response)) } reject(error) }) }) } /** * Extract the errors from the response object. * * @param {Object} response * @return {Object} */ extractErrors (response) { if (!response.data || typeof response.data !== 'object') { return { error: Form.errorMessage } } if (response.data.errors) { return { ...response.data.errors } } if (response.data.message) { return { error: response.data.message } } return { ...response.data } } /** * Get a named route. * * @param {String} name * @return {Object} parameters * @return {String} */ route (name, parameters = {}) { let url = name // eslint-disable-next-line no-prototype-builtins if (Form.routes.hasOwnProperty(name)) { url = decodeURI(Form.routes[name]) } if (typeof parameters !== 'object') { parameters = { id: parameters } } Object.keys(parameters).forEach((key) => { url = url.replace(`{${key}}`, parameters[key]) }) return url } /** * Clear errors on keydown. * * @param {KeyboardEvent} event */ onKeydown (event) { if (event.target.name) { this.errors.clear(event.target.name) } } feedback (name) { if (this.errors.has(name)) { return this.errors.get(name) } } state (name) { if (this.errors.any() && this.errors.has(name)) { return false } return null } invalid (name) { const ret = this.state(name) if (ret === null) { return false } return true } } Form.routes = {} Form.errorMessage = 'Something went wrong. Please try again.' Form.ignore = ['busy', 'successful', 'errors', 'originalData'] export default Form