From d8bb41a04af547498a31e86ecee2d25c6f3251f9 Mon Sep 17 00:00:00 2001 From: Bubka <858858+Bubka@users.noreply.github.com> Date: Fri, 17 Jan 2020 18:21:47 +0100 Subject: [PATCH] Add Form component --- resources/js/components/form/errors.js | 135 ++++++++++ .../js/components/form/errors/FieldError.vue | 21 ++ .../js/components/form/errors/FormError.vue | 26 ++ .../js/components/form/errors/FormErrors.vue | 26 ++ resources/js/components/form/form.js | 238 ++++++++++++++++++ resources/js/components/form/index.js | 14 ++ resources/js/components/form/utils.js | 32 +++ resources/js/components/index.js | 8 +- 8 files changed, 498 insertions(+), 2 deletions(-) create mode 100644 resources/js/components/form/errors.js create mode 100644 resources/js/components/form/errors/FieldError.vue create mode 100644 resources/js/components/form/errors/FormError.vue create mode 100644 resources/js/components/form/errors/FormErrors.vue create mode 100644 resources/js/components/form/form.js create mode 100644 resources/js/components/form/index.js create mode 100644 resources/js/components/form/utils.js diff --git a/resources/js/components/form/errors.js b/resources/js/components/form/errors.js new file mode 100644 index 00000000..26d9397c --- /dev/null +++ b/resources/js/components/form/errors.js @@ -0,0 +1,135 @@ +// Original component by Cretu Eusebiu +// https://github.com/cretueusebiu/vform + +import { arrayWrap } from './utils' + +export default 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) { + 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) + } +} diff --git a/resources/js/components/form/errors/FieldError.vue b/resources/js/components/form/errors/FieldError.vue new file mode 100644 index 00000000..0aa891c5 --- /dev/null +++ b/resources/js/components/form/errors/FieldError.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/components/form/errors/FormError.vue b/resources/js/components/form/errors/FormError.vue new file mode 100644 index 00000000..8ba47057 --- /dev/null +++ b/resources/js/components/form/errors/FormError.vue @@ -0,0 +1,26 @@ + + + diff --git a/resources/js/components/form/errors/FormErrors.vue b/resources/js/components/form/errors/FormErrors.vue new file mode 100644 index 00000000..6d42487f --- /dev/null +++ b/resources/js/components/form/errors/FormErrors.vue @@ -0,0 +1,26 @@ + + + diff --git a/resources/js/components/form/form.js b/resources/js/components/form/form.js new file mode 100644 index 00000000..3fffe4b2 --- /dev/null +++ b/resources/js/components/form/form.js @@ -0,0 +1,238 @@ +// Original component by Cretu Eusebiu +// https://github.com/cretueusebiu/vform + +import axios from 'axios' +import Errors from './errors' +import { deepCopy } from './utils' + +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 from via a GET request. + * + * @param {String} url + * @return {Promise} + */ + get (url) { + return this.submit('get', url) + } + + /** + * Submit the from via a POST request. + * + * @param {String} url + * @return {Promise} + */ + post (url) { + return this.submit('post', url) + } + + /** + * Submit the from via a PATCH request. + * + * @param {String} url + * @return {Promise} + */ + patch (url) { + return this.submit('patch', url) + } + + /** + * Submit the from via a PUT request. + * + * @param {String} url + * @return {Promise} + */ + put (url) { + return this.submit('put', url) + } + + /** + * Submit the from via a DELETE request. + * + * @param {String} url + * @return {Promise} + */ + delete (url) { + return this.submit('delete', url) + } + + /** + * 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) => { + 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 + + 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) + } + } +} + +Form.routes = {} +Form.errorMessage = 'Something went wrong. Please try again.' +Form.ignore = ['busy', 'successful', 'errors', 'originalData'] + +export default Form diff --git a/resources/js/components/form/index.js b/resources/js/components/form/index.js new file mode 100644 index 00000000..90e2ab7b --- /dev/null +++ b/resources/js/components/form/index.js @@ -0,0 +1,14 @@ +import Form from './form' +import Errors from './errors' +import FieldError from './errors/FieldError' +import FormError from './errors/FormError' +import FormErrors from './errors/FormErrors' + +export { + Form, + Errors, + FieldError, + FormError, + FormErrors, + Form as default +} \ No newline at end of file diff --git a/resources/js/components/form/utils.js b/resources/js/components/form/utils.js new file mode 100644 index 00000000..34ce2a17 --- /dev/null +++ b/resources/js/components/form/utils.js @@ -0,0 +1,32 @@ +// Original component by Cretu Eusebiu +// https://github.com/cretueusebiu/vform + +/** + * Deep copy the given object. + * + * @param {Object} obj + * @return {Object} + */ +export function deepCopy (obj) { + if (obj === null || typeof obj !== 'object') { + return obj + } + + const copy = Array.isArray(obj) ? [] : {} + + Object.keys(obj).forEach(key => { + copy[key] = deepCopy(obj[key]) + }) + + return copy +} + +/** + * If the given value is not an array, wrap it in one. + * + * @param {Any} value + * @return {Array} + */ +export function arrayWrap (value) { + return Array.isArray(value) ? value : [value] +} \ No newline at end of file diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 61735760..161f2158 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -1,9 +1,13 @@ import Vue from 'vue' import Button from './Button' +import { FormError, FormErrors, FieldError } from './form' // Components that are registered globaly. [ - Button, + Button, + FormError, + FormErrors, + FieldError ].forEach(Component => { - Vue.component(Component.name, Component) + Vue.component(Component.name, Component) })