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)
})