From 1d51cb3e3158d3cfdfe853bca84ed4dea11f53e6 Mon Sep 17 00:00:00 2001 From: Bubka <858858+Bubka@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:37:59 +0200 Subject: [PATCH] Set up Form component & some form elements components --- resources/js_vue3/app.js | 18 +- .../components/formElements/Button.vue | 34 ++ .../components/formElements/FieldError.vue | 18 + .../js_vue3/components/formElements/Form.js | 318 ++++++++++++++++++ .../components/formElements/FormButtons.vue | 51 +++ .../components/formElements/FormErrors.js | 141 ++++++++ .../components/formElements/FormField.vue | 88 +++++ .../formElements/FormPasswordField.vue | 139 ++++++++ resources/js_vue3/composables/helpers.js | 34 ++ resources/js_vue3/layouts/FormWrapper.vue | 4 +- 10 files changed, 839 insertions(+), 6 deletions(-) create mode 100644 resources/js_vue3/components/formElements/Button.vue create mode 100644 resources/js_vue3/components/formElements/FieldError.vue create mode 100644 resources/js_vue3/components/formElements/Form.js create mode 100644 resources/js_vue3/components/formElements/FormButtons.vue create mode 100644 resources/js_vue3/components/formElements/FormErrors.js create mode 100644 resources/js_vue3/components/formElements/FormField.vue create mode 100644 resources/js_vue3/components/formElements/FormPasswordField.vue create mode 100644 resources/js_vue3/composables/helpers.js diff --git a/resources/js_vue3/app.js b/resources/js_vue3/app.js index fdb1ca25..bea7cd0e 100644 --- a/resources/js_vue3/app.js +++ b/resources/js_vue3/app.js @@ -42,13 +42,23 @@ app.use(Notifications) import ResponsiveWidthWrapper from '@/layouts/ResponsiveWidthWrapper.vue' import FormWrapper from '@/layouts/FormWrapper.vue' import Footer from '@/layouts/Footer.vue' +import VueButton from '@/components/formElements/Button.vue' +import FieldError from '@/components/formElements/FieldError.vue' +import FormField from '@/components/formElements/FormField.vue' +import FormPasswordField from '@/components/formElements/FormPasswordField.vue' +import FormButtons from '@/components/formElements/FormButtons.vue' // Components registration app - .component('font-awesome-icon', FontAwesomeIcon) - .component('responsive-width-wrapper', ResponsiveWidthWrapper) - .component('form-wrapper', FormWrapper) - .component('vue-footer', Footer) + .component('FontAwesomeIcon', FontAwesomeIcon) + .component('ResponsiveWidthWrapper', ResponsiveWidthWrapper) + .component('FormWrapper', FormWrapper) + .component('VueFooter', Footer) + .component('VueButton', VueButton) + .component('FieldError', FieldError) + .component('FormField', FormField) + .component('FormPasswordField', FormPasswordField) + .component('FormButtons', FormButtons) // App mounting app.mount('#app') diff --git a/resources/js_vue3/components/formElements/Button.vue b/resources/js_vue3/components/formElements/Button.vue new file mode 100644 index 00000000..9837a4b8 --- /dev/null +++ b/resources/js_vue3/components/formElements/Button.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/resources/js_vue3/components/formElements/FieldError.vue b/resources/js_vue3/components/formElements/FieldError.vue new file mode 100644 index 00000000..b4977b32 --- /dev/null +++ b/resources/js_vue3/components/formElements/FieldError.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/resources/js_vue3/components/formElements/Form.js b/resources/js_vue3/components/formElements/Form.js new file mode 100644 index 00000000..f5a2984d --- /dev/null +++ b/resources/js_vue3/components/formElements/Form.js @@ -0,0 +1,318 @@ +import { httpClientFactory } from '@/services/httpClientFactory' +import Errors from './FormErrors' + +class Form { + /** + * Create a new form instance. + * + * @param {Object} data + */ + constructor (data = {}) { + this.axios = httpClientFactory('web') + this.isBusy = false + this.isDisabled = false + // this.successful = false + this.errors = new Errors() + this.originalData = this.deepCopy(data) + + Object.assign(this, data) + } + + /** + * Fill form data. + * + * @param {Object} data + */ + fill (data) { + this.keys().forEach(key => { + this[key] = data[key] + }) + } + + /** + * Update original form data. + */ + setOriginal () { + Object.keys(this) + .filter(key => !Form.ignore.includes(key)) + .forEach(key => { + this.originalData[key] = this.deepCopy(this[key]) + }) + } + + /** + * Fill form data. + * + * @param {Object} data + */ + fillWithKeyValueObject (data) { + this.keys().forEach(key => { + const keyValueObject = data.find(s => s.key === key.toString()) + if(keyValueObject != undefined) { + this[key] = keyValueObject.value + } + }) + } + + /** + * 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.isBusy = true + // this.successful = false + } + + /** + * Finish processing the form. + */ + finishProcessing () { + this.isBusy = 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] = this.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 }) + this.axios.request({ url: this.route(url), method, data, ...config }) + .then(response => { + this.finishProcessing() + + resolve(response) + }) + .catch(error => { + this.isBusy = false + + if (error.response) { + this.errors.set(this.extractErrors(error.response)) + } + + reject(error) + }) + }) + } + + /** + * 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} + */ + upload (url, formData, config = {}) { + this.startProcessing() + + return new Promise((resolve, reject) => { + // (Form.axios || axios).request({ url: this.route(url), method, data, ...config }) + this.axios.request({ url: this.route(url), method: 'post', data: formData, header: {'Content-Type' : 'multipart/form-data'}, ...config }) + .then(response => { + this.finishProcessing() + + resolve(response) + }) + .catch(error => { + this.isBusy = 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) + } + } + + /** + * Deep copy the given object. + * + * @param {Object} obj + * @return {Object} + */ + deepCopy (obj) { + if (obj === null || typeof obj !== 'object') { + return obj + } + + const copy = Array.isArray(obj) ? [] : {} + + Object.keys(obj).forEach(key => { + copy[key] = this.deepCopy(obj[key]) + }) + + return copy + } +} + +Form.routes = {} +Form.errorMessage = 'Something went wrong. Please try again.' +Form.ignore = ['isBusy', 'isDisabled', 'errors', 'originalData'] + +export default Form diff --git a/resources/js_vue3/components/formElements/FormButtons.vue b/resources/js_vue3/components/formElements/FormButtons.vue new file mode 100644 index 00000000..731c29a1 --- /dev/null +++ b/resources/js_vue3/components/formElements/FormButtons.vue @@ -0,0 +1,51 @@ + + + \ No newline at end of file diff --git a/resources/js_vue3/components/formElements/FormErrors.js b/resources/js_vue3/components/formElements/FormErrors.js new file mode 100644 index 00000000..12c1194f --- /dev/null +++ b/resources/js_vue3/components/formElements/FormErrors.js @@ -0,0 +1,141 @@ + +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) + } +} + +/** + * If the given value is not an array, wrap it in one. + * + * @param {Any} value + * @return {Array} + */ +function arrayWrap (value) { + return Array.isArray(value) ? value : [value] +} \ No newline at end of file diff --git a/resources/js_vue3/components/formElements/FormField.vue b/resources/js_vue3/components/formElements/FormField.vue new file mode 100644 index 00000000..c7fa2be7 --- /dev/null +++ b/resources/js_vue3/components/formElements/FormField.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/resources/js_vue3/components/formElements/FormPasswordField.vue b/resources/js_vue3/components/formElements/FormPasswordField.vue new file mode 100644 index 00000000..6bc944b5 --- /dev/null +++ b/resources/js_vue3/components/formElements/FormPasswordField.vue @@ -0,0 +1,139 @@ + + + \ No newline at end of file diff --git a/resources/js_vue3/composables/helpers.js b/resources/js_vue3/composables/helpers.js new file mode 100644 index 00000000..d0a0664f --- /dev/null +++ b/resources/js_vue3/composables/helpers.js @@ -0,0 +1,34 @@ +// import { ref } from 'vue' + +export function useIdGenerator(fieldType, fieldName) { + let prefix + fieldName = fieldName.toString() + + switch (fieldType) { + case 'text': + prefix = 'txt' + break + case 'button': + prefix = 'btn' + break + case 'email': + prefix = 'eml' + break + case 'password': + prefix = 'pwd' + break + case 'radio': + prefix = 'rdo' + break + case 'label': + prefix = 'lbl' + break + default: + prefix = 'txt' + break + } + + return { + inputId: prefix + fieldName[0].toUpperCase() + fieldName.toLowerCase().slice(1) + } +} \ No newline at end of file diff --git a/resources/js_vue3/layouts/FormWrapper.vue b/resources/js_vue3/layouts/FormWrapper.vue index 81856793..83c27b58 100644 --- a/resources/js_vue3/layouts/FormWrapper.vue +++ b/resources/js_vue3/layouts/FormWrapper.vue @@ -1,9 +1,9 @@