From 337e8246d418400f581e787f7bd106c8dafbf425 Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Wed, 13 Mar 2019 21:55:37 +0530 Subject: [PATCH 1/8] first commit Signed-off-by: Ritesh Singh <1124ritesh@gmail.com> --- package.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..4287b0b --- /dev/null +++ b/package.json @@ -0,0 +1,4 @@ +{ + "name": "@imritesh/form", + "version": "1.0.0" +} \ No newline at end of file From 04a7bf5c7cf3f7e1bf3a7a2c19bf202b6972fa45 Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Wed, 13 Mar 2019 22:01:53 +0530 Subject: [PATCH 2/8] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2afb3ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Ritesh Singh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From f895af32f97cbe1ff13a5b004124280fa2ccc7ca Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Wed, 13 Mar 2019 22:03:05 +0530 Subject: [PATCH 3/8] first commit Signed-off-by: Ritesh Singh <1124ritesh@gmail.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ff291d..14363a1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # vue-laravel-form Vue Js Forms with laravel validation -![GitHub manifest version](https://img.shields.io/github/manifest-json/v/riteshsingh1/vue-laravel-form.svg) +![npm](https://img.shields.io/npm/v/@imritesh/form.svg) \ No newline at end of file From da8af12dc4c96e35e8c7a2a799943cbd3df5d6d5 Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Wed, 13 Mar 2019 22:09:57 +0530 Subject: [PATCH 4/8] form Signed-off-by: Ritesh Singh <1124ritesh@gmail.com> --- .DS_Store | Bin 0 -> 6148 bytes package.json | 9 +- src/Errors.js | 83 ++++++++ src/Form.js | 325 ++++++++++++++++++++++++++++++++ src/index.js | 3 + src/util/fieldNameValidation.js | 35 ++++ src/util/formData.js | 33 ++++ src/util/index.js | 3 + src/util/objects.js | 49 +++++ 9 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 .DS_Store create mode 100755 src/Errors.js create mode 100755 src/Form.js create mode 100755 src/index.js create mode 100755 src/util/fieldNameValidation.js create mode 100755 src/util/formData.js create mode 100755 src/util/index.js create mode 100755 src/util/objects.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..561a6af7c8cd8f1d6934e476bcb0fac28991e532 GIT binary patch literal 6148 zcmeHK&2G~`5S~o}u>*(*RcepEAaRILN}v@&NN$=SMS_BGfdin_j-%G#dZXAOf)L~j z4*_q$qwoYg2oC_?{-h{w5H}>~jx_s?XJ>Xj-zK|WB4X`HxJgteA`2?8WTROjqF?AK zDQQm?$mAG(dPw)EPrE=P{{jPI?XJl6>QEOe_iO$3s6$WCvrpIQn8IGn2SLmO#ET!x z9q6?mQeY8P=|1%k-K9P9s9T8sQi#4tBl2lLuhAaT5Hq}@lsF~Jd2GFqXVYWMu!A>y z09^52>|-ILjB&>_D2)4kbyP4-N4mzZ!6=T>tl9j=O6AJA^Hs~LS+z^Gm);SddYPY1 z(zZW-E}kCw*-@bFea_<(+Yg>b!)d?1w#$>ukCNe7CP(2AAupaqNyw*dK1sqbDH}Ccij^DnUy`O*hgi;_3e?Tdh zH7?*Y3{AotZ=A$DIfQ2sy@)bs7%&W+F$VM|U|l$4b(?Yx1BQV=Ga&W{2P)9jm@AZ9 z2O4<=09N3Z0$Y3@iX2Cyt1(xI9tcyZK$Xh$6@#gC_#GYRYRna?bYlAQ!Su{b-%yyG z9pgJPoS3W7w1xr0z%m16^;j3@fA{zI|K%j}WEe0E{8tRHO2_N8QIbAePZcN5S_gUn r6(Zttg(oR6 e.startsWith(`${field}.`) || e.startsWith(`${field}[`) + ); + + hasError = errors.length > 0; + } + + return hasError; + } + + first(field) { + return this.get(field)[0]; + } + + get(field) { + return this.errors[field] || []; + } + + /** + * Determine if we have any errors. + */ + any() { + return Object.keys(this.errors).length > 0; + } + + /** + * Record the new errors. + * + * @param {object} errors + */ + record(errors = {}) { + this.errors = errors; + } + + /** + * Clear a specific field, object or all error fields. + * + * @param {string|null} field + */ + clear(field) { + if (!field) { + this.errors = {}; + + return; + } + + let errors = Object.assign({}, this.errors); + + Object.keys(errors) + .filter(e => e === field || e.startsWith(`${field}.`) || e.startsWith(`${field}[`)) + .forEach(e => delete errors[e]); + + this.errors = errors; + } +} + +export default Errors; diff --git a/src/Form.js b/src/Form.js new file mode 100755 index 0000000..ab104ae --- /dev/null +++ b/src/Form.js @@ -0,0 +1,325 @@ +import Errors from './Errors'; +import { guardAgainstReservedFieldName, isArray, isFile, merge, objectToFormData } from './util'; + +class Form { + /** + * Create a new Form instance. + * + * @param {object} data + * @param {object} options + */ + constructor(data = {}, options = {}) { + this.processing = false; + this.successful = false; + + this.withData(data) + .withOptions(options) + .withErrors({}); + } + + withData(data) { + if (isArray(data)) { + data = data.reduce((carry, element) => { + carry[element] = ''; + return carry; + }, {}); + } + + this.setInitialValues(data); + + this.errors = new Errors(); + this.processing = false; + this.successful = false; + + for (const field in data) { + guardAgainstReservedFieldName(field); + + this[field] = data[field]; + } + + return this; + } + + withErrors(errors) { + this.errors = new Errors(errors); + + return this; + } + + withOptions(options) { + this.__options = { + resetOnSuccess: true, + }; + + if (options.hasOwnProperty('resetOnSuccess')) { + this.__options.resetOnSuccess = options.resetOnSuccess; + } + + if (options.hasOwnProperty('onSuccess')) { + this.onSuccess = options.onSuccess; + } + + if (options.hasOwnProperty('onFail')) { + this.onFail = options.onFail; + } + + const windowAxios = typeof window === 'undefined' ? false : window.axios + + this.__http = options.http || windowAxios || require('axios'); + + if (!this.__http) { + throw new Error( + 'No http library provided. Either pass an http option, or install axios.' + ); + } + + return this; + } + + /** + * Fetch all relevant data for the form. + */ + data() { + const data = {}; + + for (const property in this.initial) { + data[property] = this[property]; + } + + return data; + } + + /** + * Fetch specific data for the form. + * + * @param {array} fields + * @return {object} + */ + only(fields) { + return fields.reduce((filtered, field) => { + filtered[field] = this[field]; + return filtered; + }, {}); + } + + /** + * Reset the form fields. + */ + reset() { + merge(this, this.initial); + + this.errors.clear(); + } + + setInitialValues(values) { + this.initial = {}; + + merge(this.initial, values); + } + + populate(data) { + Object.keys(data).forEach(field => { + guardAgainstReservedFieldName(field); + + if (this.hasOwnProperty(field)) { + merge(this, { [field]: data[field] }); + } + }); + + return this; + } + + /** + * Clear the form fields. + */ + clear() { + for (const field in this.initial) { + this[field] = ''; + } + + this.errors.clear(); + } + + /** + * Send a POST request to the given URL. + * + * @param {string} url + */ + post(url) { + return this.submit('post', url); + } + + /** + * Send a PUT request to the given URL. + * + * @param {string} url + */ + put(url) { + return this.submit('put', url); + } + + /** + * Send a PATCH request to the given URL. + * + * @param {string} url + */ + patch(url) { + return this.submit('patch', url); + } + + /** + * Send a DELETE request to the given URL. + * + * @param {string} url + */ + delete(url) { + return this.submit('delete', url); + } + + /** + * Submit the form. + * + * @param {string} requestType + * @param {string} url + */ + submit(requestType, url) { + this.__validateRequestType(requestType); + this.errors.clear(); + this.processing = true; + this.successful = false; + + return new Promise((resolve, reject) => { + this.__http[requestType]( + url, + this.hasFiles() ? objectToFormData(this.data()) : this.data() + ) + .then(response => { + this.processing = false; + this.onSuccess(response.data); + + resolve(response.data); + }) + .catch(error => { + this.processing = false; + this.onFail(error); + + reject(error); + }); + }); + } + + /** + * @returns {boolean} + */ + hasFiles() { + for (const property in this.initial) { + if (this.hasFilesDeep(this[property])) { + return true; + } + } + + return false; + }; + + /** + * @param {Object|Array} object + * @returns {boolean} + */ + hasFilesDeep(object) { + if (object === null) { + return false; + } + + if (typeof object === 'object') { + for (const key in object) { + if (object.hasOwnProperty(key)) { + if (isFile(object[key])) { + return true; + } + } + } + } + + if (Array.isArray(object)) { + for (const key in object) { + if (object.hasOwnProperty(key)) { + return this.hasFilesDeep(object[key]); + } + } + } + + return isFile(object); + } + + /** + * Handle a successful form submission. + * + * @param {object} data + */ + onSuccess(data) { + this.successful = true; + + if (this.__options.resetOnSuccess) { + this.reset(); + } + } + + /** + * Handle a failed form submission. + * + * @param {object} data + */ + onFail(error) { + this.successful = false; + + if (error.response && error.response.data.errors) { + this.errors.record(error.response.data.errors); + } + } + + /** + * Get the error message(s) for the given field. + * + * @param field + */ + hasError(field) { + return this.errors.has(field); + } + + /** + * Get the first error message for the given field. + * + * @param {string} field + * @return {string} + */ + getError(field) { + return this.errors.first(field); + } + + /** + * Get the error messages for the given field. + * + * @param {string} field + * @return {array} + */ + getErrors(field) { + return this.errors.get(field); + } + + __validateRequestType(requestType) { + const requestTypes = ['get', 'delete', 'head', 'post', 'put', 'patch']; + + if (requestTypes.indexOf(requestType) === -1) { + throw new Error( + `\`${requestType}\` is not a valid request type, ` + + `must be one of: \`${requestTypes.join('`, `')}\`.` + ); + } + } + + static create(data = {}) { + return new Form().withData(data); + } +} + +export default Form; diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..804de2d --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +export { default } from './Form'; +export { default as Form } from './Form'; +export { default as Errors } from './Errors'; diff --git a/src/util/fieldNameValidation.js b/src/util/fieldNameValidation.js new file mode 100755 index 0000000..9c5e26b --- /dev/null +++ b/src/util/fieldNameValidation.js @@ -0,0 +1,35 @@ +export const reservedFieldNames = [ + '__http', + '__options', + '__validateRequestType', + 'clear', + 'data', + 'delete', + 'errors', + 'getError', + 'getErrors', + 'hasError', + 'initial', + 'onFail', + 'only', + 'onSuccess', + 'patch', + 'populate', + 'post', + 'processing', + 'successful', + 'put', + 'reset', + 'submit', + 'withData', + 'withErrors', + 'withOptions', +]; + +export function guardAgainstReservedFieldName(fieldName) { + if (reservedFieldNames.indexOf(fieldName) !== -1) { + throw new Error( + `Field name ${fieldName} isn't allowed to be used in a Form or Errors instance.` + ); + } +} diff --git a/src/util/formData.js b/src/util/formData.js new file mode 100755 index 0000000..a253b75 --- /dev/null +++ b/src/util/formData.js @@ -0,0 +1,33 @@ +export function objectToFormData(object, formData = new FormData(), parent = null) { + if (object === null || object === 'undefined' || object.length === 0) { + return formData.append(parent, object); + } + + for (const property in object) { + if (object.hasOwnProperty(property)) { + appendToFormData(formData, getKey(parent, property), object[property]); + } + } + + return formData; +} + +function getKey(parent, property) { + return parent ? parent + '[' + property + ']' : property; +} + +function appendToFormData(formData, key, value) { + if (value instanceof Date) { + return formData.append(key, value.toISOString()); + } + + if (value instanceof File) { + return formData.append(key, value, value.name); + } + + if (typeof value !== 'object') { + return formData.append(key, value); + } + + objectToFormData(value, formData, key); +} diff --git a/src/util/index.js b/src/util/index.js new file mode 100755 index 0000000..2288e88 --- /dev/null +++ b/src/util/index.js @@ -0,0 +1,3 @@ +export * from './objects'; +export * from './formData'; +export * from './fieldNameValidation'; diff --git a/src/util/objects.js b/src/util/objects.js new file mode 100755 index 0000000..1783d80 --- /dev/null +++ b/src/util/objects.js @@ -0,0 +1,49 @@ +export function isArray(object) { + return Object.prototype.toString.call(object) === '[object Array]'; +} + +export function isFile(object) { + return object instanceof File || object instanceof FileList; +} + +export function merge(a, b) { + for (const key in b) { + a[key] = cloneDeep(b[key]); + } +} + +export function cloneDeep(object) { + if (object === null) { + return null; + } + + if (isFile(object)) { + return object; + } + + if (Array.isArray(object)) { + const clone = []; + + for (const key in object) { + if (object.hasOwnProperty(key)) { + clone[key] = cloneDeep(object[key]); + } + } + + return clone; + } + + if (typeof object === 'object') { + const clone = {}; + + for (const key in object) { + if (object.hasOwnProperty(key)) { + clone[key] = cloneDeep(object[key]); + } + } + + return clone; + } + + return object; +} From c3da4cbd2dcef09aafaf51a99e923bd757df53c4 Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Wed, 13 Mar 2019 22:14:12 +0530 Subject: [PATCH 5/8] first release Signed-off-by: Ritesh Singh <1124ritesh@gmail.com> --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5623786..33f26f5 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,13 @@ "repository": { "type": "git", "url": "https://github.com/riteshsingh1/vue-laravel-form.git" - } + }, + "main": "src/index.js", + "keywords": [ + "vuejs-form", + "vue-form", + "laravel-form", + "validation", + "server" + ] } \ No newline at end of file From 614bb18627f9c192583142af9841ac52f407f55b Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Wed, 13 Mar 2019 22:14:14 +0530 Subject: [PATCH 6/8] 2.0.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 33f26f5..1e02007 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imritesh/form", - "version": "1.0.0", + "version": "2.0.0", "description": "An easy way to validate forms using laravel backend logic", "author": "Ritesh Singh", "license": "MIT", @@ -16,4 +16,4 @@ "validation", "server" ] -} \ No newline at end of file +} From f188cfdb6f37aae37096900623bc777ce14a413e Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Fri, 15 Mar 2019 11:56:00 +0530 Subject: [PATCH 7/8] updated readme .md Signed-off-by: Ritesh Singh <1124ritesh@gmail.com> --- README.md | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14363a1..b2b7e21 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,136 @@ # vue-laravel-form Vue Js Forms with laravel validation -![npm](https://img.shields.io/npm/v/@imritesh/form.svg) \ No newline at end of file +![npm](https://img.shields.io/npm/v/@imritesh/form.svg) + + +This package provides a Form class that can validation our forms with laravel backend validation logic. The class is meant to be used with a Laravel back end. + + +## Install + +You can install the package via yarn (or npm): +```bash +$ npm install @imritesh/form +``` +```bash +$ yarn add @imritesh/form +``` +By default, this package expects `axios` to be installed + +```bash +$ yarn add axios +``` + +## Usage + +```js + +import Form from 'form-backend-validation'; + +// Instantiate a form class with some values +const form = new Form({ + field1: 'value 1', + field2: 'value 2', + person: { + first_name: 'John', + last_name: 'Doe', + }, +}); + +// A form can also be initiated with an array +const form = new Form(['field1', 'field2']); + +// Submit the form, you can also use `.put`, `.patch` and `.delete` +form.post(anUrl) + .then(response => ...) + .catch(response => ...); + +// Returns true if request is being executed +form.processing; + +// If there were any validation errors, you easily access them + +// Example error response (json) +{ + "errors": { + "field1": ['Value is required'], + "field2": ['Value is required'] + } +} + +// Returns an object in which the keys are the field names +// and the values array with error message sent by the server +form.errors.all(); + +// Returns true if there were any error +form.errors.any(); + +// Returns true if there is an error for the given field name or object +form.errors.has(key); + +// Returns the first error for the given field name +form.errors.first(key); + +// Returns an array with errors for the given field name +form.errors.get(key); + +// Shortcut for getting the errors for the given field name +form.getError(key); + +// Clear all errors +form.errors.clear(); + +// Clear the error of the given field name or all errors on the given object +form.errors.clear(key); + +// Returns an object containing fields based on the given array of field names +form.only(keys); + +// Reset the values of the form to those passed to the constructor +form.reset(); + +// Set the values which should be used when calling reset() +form.setInitialValues(); + +// Populate a form after its instantiation, the populated fields won't override the initial fields +// Fields not present at instantiation will not be populated +const form = new Form({ + field1: '', + field2: '', +}); + +form.populate({ + field1: 'foo', + field2: 'bar', +}); + +``` + +### Options + +The `Form` class accepts a second `options` parameter. + +```js +const form = new Form({ + field1: 'value 1', + field2: 'value 2', +}, { + resetOnSuccess: false, +}); +``` + +You can also pass options via a `withOptions` method (this example uses the `create` factory method. + +``` +const form = Form.create() + .withOptions({ resetOnSuccess: false }) + .withData({ + field1: 'value 1', + field2: 'value 2', + }); +``` + +#### `resetOnSuccess: bool` + +Default: `true`. Set to `false` if you don't want the form to reset to its original values after a succesful submit. \ No newline at end of file From 38e32bd501bebf8ad582abe3b2cc553505b39042 Mon Sep 17 00:00:00 2001 From: Ritesh Singh <1124ritesh@gmail.com> Date: Fri, 15 Mar 2019 11:59:04 +0530 Subject: [PATCH 8/8] updated readme .md Signed-off-by: Ritesh Singh <1124ritesh@gmail.com> --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b2b7e21..7c0c7de 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,23 @@ $ yarn add axios ``` ## Usage - +```html +
+ + + +
+``` ```js import Form from 'form-backend-validation';