@htmlguyllc/jpack
Advanced tools
Comparing version 1.0.12 to 1.0.13
@@ -8,7 +8,4 @@ import axios from 'axios'; | ||
//default options for form.fromURL() | ||
let from_url_defaults = { | ||
incomingElementSelector: null, //the form element or wrapper that you want to retrieve from the URL | ||
insertIntoElement: null, //what element to put the form into | ||
onload: function(form){ return this; }, //once the form is loaded onto the page | ||
//defaults for the XHRForm class | ||
const XHRFormDefaults = { | ||
xhrSubmit: true, //submit the form using XHR instead of the default action | ||
@@ -19,23 +16,36 @@ submitURL:null, //will be grabbed from the form's action attribute, or fallback to the URL the form was retrieved from | ||
onSuccess: function(response, form){ }, //called when the form is submitted successfully | ||
//validate the form, display any errors and return false to block submission | ||
validateForm: function(form){ | ||
//add .was-validated for bootstrap to show errors | ||
form.classList.add('was-validated'); | ||
//if there are any :invalid elements, the form is not valid | ||
const is_valid = !form.querySelector(':invalid'); | ||
//if it's valid, clear the validation indicators | ||
if( is_valid ) form.classList.remove('was-validated'); | ||
return is_valid; | ||
} | ||
}; | ||
//defaults for the FormFromURL class | ||
const FormFromURLDefaults = { | ||
incomingElementSelector: null, //the form element or wrapper that you want to retrieve from the URL | ||
insertIntoElement: null, //what element to put the form into | ||
onload: function(form){ return this; }, //once the form is loaded onto the page | ||
}; | ||
/** | ||
* Form based helpers | ||
* | ||
* @type {{fromURL: form.fromURL}} | ||
* This class allows you to submit a form via XHR and easily handle the results | ||
*/ | ||
export const form = { | ||
export class XHRForm { | ||
/** | ||
* Grabs a form from a URL and returns it to the current page | ||
* Form can be just about any datatype - uses dom.getElement() | ||
* | ||
* Also handles form submission using XHR and can open a modal to display the form | ||
* | ||
* Only methods that require notes or are commonly used externally are commented below | ||
* | ||
* @param url - string | ||
* @param options - object{incomingElementSelector,insertIntoElement, onload} | ||
* @param form | ||
* @param options | ||
*/ | ||
fromURL: function(url, options){ | ||
if( typeof url !== "string" ) throw `${url} is not a string`; | ||
constructor(form, options){ | ||
@@ -45,419 +55,513 @@ //if options are undefined, set them | ||
//make sure options is an object (empty or not) with only the keys set in the defaults | ||
type_checks.isDataObject(options, Object.keys(from_url_defaults), false, true, true); | ||
//make sure options is an object (empty or not) | ||
type_checks.isDataObject(options, Object.keys(XHRFormDefaults), false, false, true); | ||
//extend defaults with provided options | ||
options = {...from_url_defaults, ...options}; | ||
options = {...XHRFormDefaults, ...options}; | ||
/** | ||
* Set the URL from which the form will be retrieved | ||
* | ||
* @param url | ||
* @returns {form} | ||
*/ | ||
this.setURL = function(url){ | ||
if( typeof url !== 'string' ) throw `${url} is not a string`; | ||
this._url = url; | ||
return this; | ||
}; | ||
this.getURL = function(){ | ||
return this._url; | ||
}; | ||
//set it immediately from the provided string | ||
this.setURL(url); | ||
this.setForm(form); | ||
this.setValidateCallback(options.validateForm); | ||
this.setXHRSubmit(options.xhrSubmit); | ||
this.setSubmitMethod(options.submitMethod); | ||
this.setSubmitURL(options.submitURL); | ||
this.onSuccess(options.onSuccess); | ||
this.onError(options.onError); | ||
} | ||
/** | ||
* If the URL provided returns HTML, this selector will be used to pull the form out | ||
* | ||
* If left null, it will assume the entire response is the form's HTML | ||
* | ||
* @param selector: string|null | ||
* @returns {form} | ||
*/ | ||
this.setIncomingElementSelector = function(selector){ | ||
if( selector !== null && typeof selector !== 'string' ) throw `${selector} is not a string or null value`; | ||
this._incomingElementSelector = selector; | ||
return this; | ||
}; | ||
this.getIncomingElementSelector = function(){ | ||
return this._incomingElementSelector; | ||
}; | ||
//set it immediately from options | ||
this.setIncomingElementSelector(options.incomingElementSelector); | ||
//validation callback | ||
_validateCallback = null; | ||
/** | ||
* Allows you to set a parent element that the form will be inserted into using the default insertForm method | ||
* Alternatively, you can leave this and override insertForm() and have more control over where it should go | ||
* | ||
* Uses dom.getElement() so you can pass a string, jQuery object, object, etc | ||
* However if more than 1 element is detected, an error will be thrown | ||
* | ||
* @param element | ||
*/ | ||
this.setInsertIntoElement = function(element){ | ||
this._insertIntoElement = element; | ||
}; | ||
this.getInsertIntoElement = function(){ | ||
return this._insertIntoElement; | ||
}; | ||
//set it immediately from the options | ||
this.setInsertIntoElement(options.insertIntoElement); | ||
/** | ||
* | ||
* @param callback | ||
* @returns {XHRForm} | ||
*/ | ||
setValidateCallback(callback){ | ||
if( typeof callback !== "function" ) throw `${callback} is not a function`; | ||
this._validateCallback = callback; | ||
return this; | ||
} | ||
/** | ||
* Get the form from the URL and pass to insertForm | ||
* | ||
* There are three main ways to provide the form from your server: | ||
* 1) Straight HTML. The entire response is the form and that's it. | ||
* 2) Straight HTML, but the form is only a part of the response so it needs to be parsed out based on a selector. | ||
* 3) A JSON object containing the key "html" like this: {"html":"<form>your form here</form>"} | ||
* | ||
*/ | ||
this.getForm = function(){ | ||
var self = this; | ||
/** | ||
* Runs the validate callback and passes the form | ||
* | ||
* @returns {null} | ||
*/ | ||
validate(form){ | ||
if( typeof form === "undefined" ) form = this.getForm(); | ||
return this._validateCallback(form); | ||
} | ||
navigation.showLoader(); | ||
axios.get(this.getURL()).then(function (response) { | ||
navigation.hideLoader(); | ||
/** | ||
* Set the form element | ||
* | ||
* @param form | ||
* @returns {XHRForm} | ||
*/ | ||
setForm(form){ | ||
if( !form || typeof form === 'undefined' ) throw `Form element is required`; | ||
let data = response.data; | ||
form = dom.getElement(form, true, true); | ||
if( !form ) throw `Invalid form element received`; | ||
//just in case the server returned the wrong response type and it's actually JSON - ignore errors | ||
try{ data = typeof data === 'string' ? JSON.parse(data) : data; } catch(e){ } | ||
this._form = form; | ||
//if the response is a string (probably HTML) | ||
if( typeof data === 'string' ){ | ||
if( typeof self.getIncomingElementSelector() === 'string' ){ | ||
//parse the incoming HTML | ||
const parsed = navigation.parseHTML(data, self.getIncomingElementSelector()); | ||
//provide the form's HTML in an object containing other details like the route and the full response to insertForm | ||
return self.insertForm(parsed, data); | ||
} | ||
//otherwise the entire response is assumed to be the form | ||
return self.insertForm({html:data}); | ||
} | ||
//if the response is an object (probably JSON) | ||
else if( typeof data === 'object' ){ | ||
//if HTML was provided in the object | ||
if( typeof data.html !== "undefined" ){ | ||
return self.insertForm({html:data.html}, data); | ||
} | ||
} | ||
return this; | ||
} | ||
throw `Unexpected server response ${data}`; | ||
}) | ||
.catch(function (error) { | ||
navigation.hideLoader(); | ||
throw error; | ||
}); | ||
}; | ||
/** | ||
* Get the form element | ||
* | ||
* @returns {*|Element|HTMLDocument} | ||
*/ | ||
getForm(){ | ||
return this._form; | ||
} | ||
/** | ||
* Use this method to modify the form immediately after it's displayed | ||
* | ||
* You'll likely want to attach plugins for datepickers/dropdowns, or maybe hide a field based on the value of another | ||
* | ||
* @param onload | ||
* @returns {form} | ||
*/ | ||
this.setOnload = function(onload){ | ||
if( typeof onload !== 'function' ) throw `${onload} is not a function`; | ||
this._onload = onload; | ||
return this; | ||
}; | ||
this.triggerOnload = function(form){ | ||
return this._onload(form); | ||
}; | ||
//set it immediately from the options | ||
this.setOnload(options.onload); | ||
/** | ||
* Whether or not you want the form to be submitted using an XHR request | ||
* | ||
* @param enabled - bool | ||
*/ | ||
setXHRSubmit(enabled){ | ||
this._xhrSubmit = !!enabled; | ||
return this; | ||
} | ||
/** | ||
* Attaches the on submit handler (only if xhrSubmit is true) | ||
* | ||
* Pass the form or form selector | ||
*/ | ||
this.attachSubmitHandler = function(form){ | ||
if( !this._xhrSubmit ) return; | ||
/** | ||
* How to submit the form - if set to null, the method will be pulled from the form's | ||
* method attribute or fallback to "POST" | ||
* | ||
* @param method | ||
* @returns {form} | ||
*/ | ||
setSubmitMethod(method){ | ||
if( typeof method !== "string" && method !== null ) throw `${method} is not a string or null`; | ||
this._submitMethod = method; | ||
return this; | ||
} | ||
//just incase you didn't provide the actual Element | ||
/** | ||
* Gets the form submission method (POST, GET, etc) | ||
* | ||
* @returns {*|string} | ||
*/ | ||
getSubmitMethod(){ | ||
return this._submitMethod; | ||
} | ||
/** | ||
* The URL to submit the form to | ||
* | ||
* If null, the form's action attribute will be used. | ||
* Use a function if you want to dynamically generate the URL just prior to the request | ||
* - the function will receive the form as a param | ||
* Generally speaking a string is sufficient | ||
* | ||
* @param url | ||
* @returns {form} | ||
*/ | ||
setSubmitURL(url){ | ||
if( typeof url !== "string" | ||
&& typeof url !== "function" | ||
&& url !== null ) throw `${url} is not a string, function, or null`; | ||
this._submitURL = url; | ||
return this; | ||
} | ||
/** | ||
* Gets the URL the form will be submitted to | ||
* | ||
* @param form | ||
* @returns {*|string|*} | ||
*/ | ||
getSubmitURL(form){ | ||
//if a function, run it | ||
if( typeof this._submitURL === "function" ) return this._submitURL(form); | ||
return this._submitURL; | ||
} | ||
/** | ||
* Attaches the on submit handler (only if xhrSubmit is true) | ||
* | ||
* Pass the form or form selector | ||
*/ | ||
attachSubmitHandler(form){ | ||
if( !this._xhrSubmit ) return; | ||
//if not passed, get it from this object | ||
if( typeof form === "undefined" ) { | ||
form = this.getForm(); | ||
}else { | ||
form = dom.getElement(form); | ||
} | ||
var self = this; | ||
if( !form ) throw `Form element not received, cannot attach submit handler`; | ||
form.addEventListener('submit', function(e){ | ||
e.preventDefault(); | ||
self.submitForm(form); | ||
return false; | ||
}); | ||
}; | ||
var self = this; | ||
/** | ||
* Whether or not you want the form to be submitted using an XHR request | ||
* | ||
* @param enabled - bool | ||
*/ | ||
this.setXHRSubmit = function(enabled){ | ||
this._xhrSubmit = !!enabled; | ||
return this; | ||
}; | ||
//set it immediately from the options | ||
this.setXHRSubmit(options.xhrSubmit); | ||
form.addEventListener('submit', function(e){ | ||
//if xhr submit is disabled, don't block the default action | ||
if( !self._xhrSubmit ) return true; | ||
e.preventDefault(); | ||
self.submitForm(form); | ||
return false; | ||
}); | ||
/** | ||
* How to submit the form - if set to null, the method will be pulled from the form's | ||
* method attribute or fallback to "POST" | ||
* | ||
* @param method | ||
* @returns {form} | ||
*/ | ||
this.setSubmitMethod = function(method){ | ||
if( typeof method !== "string" && method !== null ) throw `${method} is not a string or null`; | ||
this._submitMethod = method; | ||
return this; | ||
}; | ||
this.getSubmitMethod = function(){ | ||
return this._submitMethod; | ||
}; | ||
//set it immediately from the options | ||
this.setSubmitMethod(options.submitMethod); | ||
return this; | ||
} | ||
/** | ||
* The URL to submit the form to | ||
* | ||
* If null, the form's action attribute will be used. | ||
* Use a function if you want to dynamically generate the URL just prior to the request | ||
* - the function will receive the form as a param | ||
* Generally speaking a string is sufficient | ||
* | ||
* @param url | ||
* @returns {form} | ||
*/ | ||
this.setSubmitURL = function(url){ | ||
if( typeof url !== "string" | ||
&& typeof url !== "function" | ||
&& url !== null ) throw `${url} is not a string, function, or null`; | ||
/** | ||
* Set a callback function to run when the form is submitted successfully | ||
* | ||
* Your function will receive 2 params, the first is the response from the server and the second is the form on the page | ||
* | ||
* @param callback | ||
* @returns {form} | ||
*/ | ||
onSuccess(callback){ | ||
if( typeof callback !== "function" ) throw `${callback} is not a function`; | ||
this._onSuccess.push(callback); | ||
return this; | ||
} | ||
this._submitURL = url; | ||
return this; | ||
}; | ||
this.getSubmitURL = function(form){ | ||
//if a function, run it | ||
if( typeof this._submitURL === "function" ) return this._submitURL(form); | ||
/** | ||
* Removes all onSuccess callbacks you've set | ||
*/ | ||
clearOnSuccessCallbacks(){ | ||
this._onSuccess = []; | ||
return this; | ||
} | ||
return this._submitURL; | ||
}; | ||
//set it immediately from the options | ||
this.setSubmitURL(options.submitURL); | ||
//stores all onSuccess callbacks | ||
_onSuccess = []; | ||
/** | ||
* Returns an object containing all form values to be submitted | ||
* | ||
* Override/extend this if you want to manipulate the data prior to submission | ||
* | ||
* @returns FormData | ||
*/ | ||
this.getFormValues = function(form){ | ||
return new FormData(form); | ||
}; | ||
/** | ||
* Triggers all onSuccess callbacks | ||
* | ||
* @param response | ||
* @param form | ||
*/ | ||
triggerOnSuccess(response, form){ | ||
this._onSuccess.forEach(function(onSuccess){ | ||
onSuccess(response, form); | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Set a callback function to run when the form is submitted successfully | ||
* | ||
* Your function will receive 2 params, the first is the response from the server and the second is the form on the page | ||
* | ||
* @param onSuccess | ||
* @returns {form} | ||
*/ | ||
this.setOnSuccess = function(onSuccess){ | ||
if( typeof onSuccess !== "function" ) throw `${onSuccess} is not a function`; | ||
this._onSuccess = onSuccess; | ||
return this; | ||
}; | ||
this.triggerOnSuccess = function(response, form){ | ||
return this._onSuccess(response, form); | ||
}; | ||
//set immediately from options | ||
this.setOnSuccess(options.onSuccess); | ||
/** | ||
* Add a callback function to run when the form is submitted successfully | ||
* | ||
* @param callback | ||
* @returns {FormFromURL} | ||
*/ | ||
onError(callback){ | ||
if( typeof callback !== "function" ) throw `${callback} is not a function`; | ||
this._onError.push(callback); | ||
return this; | ||
} | ||
this.setOnError = function(onError){ | ||
if( typeof onError !== "function" ) throw `${onError} is not a function`; | ||
this._onError = onError; | ||
return this; | ||
}; | ||
this.triggerOnError = function(error, response, form){ | ||
this._onError(error, response, form); | ||
return this; | ||
}; | ||
//set immediately from options | ||
this.setOnError(options.onError); | ||
/** | ||
* Clears all onError callbacks you've set | ||
* @returns {XHRForm} | ||
*/ | ||
clearOnErrorCallbacks(){ | ||
this._onError = []; | ||
return this; | ||
} | ||
//stores all onError callbacks | ||
_onError = []; | ||
/** | ||
* Triggers the onError callbacks | ||
* | ||
* @param error | ||
* @param response | ||
* @param form | ||
* @returns {XHRForm} | ||
*/ | ||
triggerOnError(error, response, form){ | ||
this._onError.forEach(function(onError){ | ||
onError(error, response, form); | ||
}); | ||
return this; | ||
} | ||
}; | ||
/** | ||
* Allows you to insert the form wherever you want on the page | ||
* Override this method to customize where the form is inserted | ||
* (maybe you want to open a modal first and place it there?) | ||
* | ||
* parsed_content.html will always be the HTML | ||
* | ||
* parsed_content may contain other data like route and title if the form was pulled out of | ||
* a full HTML page which contains those items | ||
* | ||
* response is the full server response (html string or object from JSON - not provided if the response is only the form's HTML) | ||
* | ||
* form is provided if this is after the form was submitted and HTML was returned form the server | ||
* | ||
* @param parsed_content | ||
* @param response | ||
* @param form | ||
* @returns {*|Element|HTMLDocument} | ||
*/ | ||
form.fromURL.prototype.insertForm = function(parsed_content, response, form) { | ||
//selector for where the form will go | ||
let el = this.getInsertIntoElement(); | ||
/** | ||
* Submits the form using XHR | ||
* | ||
* 1) Determines the URL | ||
* 2) Determines the method (GET, POST, PATCH, etc) | ||
* 3) Determines if the form is valid | ||
* 4) Gets the form's values | ||
* 5) Submits the form | ||
* 6) Replaces the form, runs onError, or runs onSuccess based on the response (see next line) | ||
* Response Type = Action Taken | ||
* string html with form inside = replace form | ||
* string html with incomingElementSelector set, but not found = kickoff onError | ||
* string - replace form on page with entire response | ||
* object.html = replace form | ||
* object.error = kickoff onError | ||
* object in general = kickoff onSuccess | ||
* | ||
* @param form | ||
* @returns {form|boolean} | ||
*/ | ||
submitForm(form) { | ||
//cache for use inside other scopes | ||
var self = this; | ||
//if not provided | ||
if( el === null ) throw 'Cannot determine where to insert form. Overwrite insertForm() or provide insertIntoElement'; | ||
//get the provided submit URL | ||
let url = this.getSubmitURL(form); | ||
//if the URL is null, grab from the form | ||
if( url === null ){ | ||
if( form.attributes.action ){ //check that it was set explicitly | ||
url = form.action; //grab JUST the value | ||
} | ||
} | ||
//default to the URL used to grab the form if it's not provided | ||
url = !url ? this.getURL() : url; | ||
//get the container element - error if not found | ||
el = dom.getElement(el, true); | ||
//get the provided submit method | ||
let method = this.getSubmitMethod(); | ||
//if it's null, grab it from the form | ||
if( method === null ){ | ||
if( typeof form.attributes.method !== 'undefined' ){ //check that it was set explicitly | ||
method = form.method; //grab JUST the value | ||
} | ||
} | ||
//default to post if we still don't have a method and lowercase anything that was provided | ||
method = !method ? 'post' : method.toLowerCase(); | ||
//put the form in the container element | ||
el.innerHTML = parsed_content.html; | ||
//if not valid, stop here until they resubmit | ||
if (!this.validate(form)) return false; | ||
//find the newly added form | ||
form = el.querySelector('form'); | ||
navigation.showLoader(); | ||
//attach an on-submit listener to send the form's values via XHR | ||
this.attachSubmitHandler(form); | ||
//get form values | ||
const form_values = Array.from( | ||
this.getFormValues(form), | ||
e => e.map(encodeURIComponent).join('=') | ||
).join('&'); | ||
//run the onload callback now that the form is there | ||
this.triggerOnload(form); | ||
axios({ | ||
url: url, | ||
method: method, | ||
data: form_values, | ||
}).then(function (response) { | ||
navigation.hideLoader(); | ||
return el; | ||
}; | ||
let data = response.data; | ||
/** | ||
* Uses Bootstrap 4's 'was-validated' class and :invalid attributes to determine validity and display errors | ||
* | ||
* If you need more custom front-end validation, you should extend this object and overwrite this method | ||
* | ||
* Nothing is kicked off if this returns false. It just prevents form submission, so make sure you display errors | ||
* | ||
* @returns {boolean} | ||
*/ | ||
form.fromURL.prototype.isValid = function(form){ | ||
//add .was-validated for bootstrap to show errors | ||
form.classList.add('was-validated'); | ||
//just in case the server returned the wrong response type and it's actually JSON - ignore errors | ||
try{ data = typeof data === 'string' ? JSON.parse(data) : data; } catch(e){ } | ||
//if there are any :invalid elements, the form is not valid | ||
const is_valid = !form.querySelector(':invalid'); | ||
//if the response is a string, it's probably/hopefully the form with inline errors | ||
if( typeof data === 'string' ){ | ||
//if we are looking for an element within the response | ||
if( typeof self.getIncomingElementSelector() === 'string' ){ | ||
//parse the incoming HTML | ||
const parsed = navigation.parseHTML(data, self.getIncomingElementSelector()); | ||
//if the form was not found in it, let's assume it doesn't contain the form. If not, then maybe | ||
if( !parsed.html.length ){ | ||
return self.triggerOnError(`${self.getIncomingElementSelector()} could not be found in response from the server`, data, form); | ||
} | ||
//provide the form's HTML in an object containing other details like the route and the full response to insertForm | ||
return self.insertForm(parsed, data, form); | ||
} | ||
return self.insertForm({html:data}, data, form); | ||
} | ||
//if the response is an object, it's probably JSON | ||
else if( typeof data === 'object' ){ | ||
//if it contains the HTML, just pop it back on the page | ||
if( data.html ){ | ||
return self.insertForm({html:data.html}, data, form); | ||
} | ||
//if it's valid, clear the validation indicators | ||
if( is_valid ) form.classList.remove('was-validated'); | ||
//if it contains an error message, trigger the callback | ||
if( data.error ){ | ||
return self.triggerOnError(data.error, data, form); | ||
} | ||
return is_valid; | ||
}; | ||
//if it doesn't APPEAR to be the form again, or an error, let's call it a success | ||
return self.triggerOnSuccess(data, form) | ||
} | ||
}) | ||
.catch(function (error) { | ||
navigation.hideLoader(); | ||
throw error; | ||
}); | ||
return this; | ||
} | ||
/** | ||
* Returns an object containing all form values to be submitted | ||
* | ||
* Override/extend this if you want to manipulate the data prior to submission | ||
* | ||
* @returns FormData | ||
*/ | ||
getFormValues(form){ | ||
return new FormData(form); | ||
} | ||
} | ||
/** | ||
* Submits the form using XHR | ||
* Grabs a form from a URL and returns it to the current page | ||
* | ||
* 1) Determines the URL | ||
* 2) Determines the method (GET, POST, PATCH, etc) | ||
* 3) Determines if the form is valid | ||
* 4) Gets the form's values | ||
* 5) Submits the form | ||
* 6) Replaces the form, runs onError, or runs onSuccess based on the response (see next line) | ||
* Response Type = Action Taken | ||
* string html with form inside = replace form | ||
* string html with incomingElementSelector set, but not found = kickoff onError | ||
* string - replace form on page with entire response | ||
* object.html = replace form | ||
* object.error = kickoff onError | ||
* object in general = kickoff onSuccess | ||
* Also handles form submission using XHR and can open a modal to display the form | ||
* | ||
* @param form | ||
* @returns {form|boolean} | ||
*/ | ||
form.fromURL.prototype.submitForm = function(form) { | ||
//cache for use inside other scopes | ||
var self = this; | ||
export class FormFromURL extends XHRForm { | ||
//get the provided submit URL | ||
let url = this.getSubmitURL(form); | ||
//if the URL is null, grab from the form | ||
if( url === null ){ | ||
if( form.attributes.action ){ //check that it was set explicitly | ||
url = form.action; //grab JUST the value | ||
} | ||
/** | ||
* @param url - string | ||
* @param options - object{incomingElementSelector,insertIntoElement, onload} | ||
*/ | ||
constructor(url, options){ | ||
super(null, options); | ||
if( typeof url !== "string" ) throw `${url} is not a string`; | ||
//if options are undefined, set them | ||
options = typeof options === "undefined" ? {} : options; | ||
//make sure options is an object (empty or not) | ||
type_checks.isDataObject(options, Object.keys(FormFromURLDefaults), false, false, true); | ||
//extend defaults with provided options | ||
options = {...FormFromURLDefaults, ...options}; | ||
this.setURL(url); | ||
this.setIncomingElementSelector(options.incomingElementSelector); | ||
this.setInsertIntoElement(options.insertIntoElement); | ||
this.onload(options.onload); | ||
} | ||
//default to the URL used to grab the form if it's not provided | ||
url = !url ? this.getURL() : url; | ||
//get the provided submit method | ||
let method = this.getSubmitMethod(); | ||
//if it's null, grab it from the form | ||
if( method === null ){ | ||
if( typeof form.attributes.method !== 'undefined' ){ //check that it was set explicitly | ||
method = form.method; //grab JUST the value | ||
} | ||
/** | ||
* Override the parent because it's not required for this class | ||
* | ||
* Still keeping it functional but removing all validation | ||
* | ||
* @param form | ||
* @returns {XHRForm} | ||
*/ | ||
setForm(form){ | ||
this._form = form; | ||
return this; | ||
} | ||
//default to post if we still don't have a method and lowercase anything that was provided | ||
method = !method ? 'post' : method.toLowerCase(); | ||
//if not valid, stop here until they resubmit | ||
if (!this.isValid(form)) return false; | ||
/** | ||
* Set the URL from which the form will be retrieved | ||
* | ||
* @param url | ||
* @returns {form} | ||
*/ | ||
setURL(url){ | ||
if( typeof url !== 'string' ) throw `${url} is not a string`; | ||
this._url = url; | ||
return this; | ||
} | ||
navigation.showLoader(); | ||
/** | ||
* Get the form's URL | ||
* | ||
* @returns {*|string} | ||
*/ | ||
getURL(){ | ||
return this._url; | ||
} | ||
//get form values | ||
const form_values = Array.from( | ||
this.getFormValues(form), | ||
e => e.map(encodeURIComponent).join('=') | ||
).join('&'); | ||
/** | ||
* If the URL provided returns HTML, this selector will be used to pull the form out | ||
* | ||
* If left null, it will assume the entire response is the form's HTML | ||
* | ||
* @param selector: string|null | ||
* @returns {form} | ||
*/ | ||
setIncomingElementSelector(selector){ | ||
if( selector !== null && typeof selector !== 'string' ) throw `${selector} is not a string or null value`; | ||
this._incomingElementSelector = selector; | ||
return this; | ||
} | ||
axios({ | ||
url: url, | ||
method: method, | ||
data: form_values, | ||
}).then(function (response) { | ||
navigation.hideLoader(); | ||
/** | ||
* Returns a selector for the form or a parent of it that will be returned from the URL | ||
* | ||
* @returns {*|string} | ||
*/ | ||
getIncomingElementSelector(){ | ||
return this._incomingElementSelector; | ||
} | ||
let data = response.data; | ||
/** | ||
* Allows you to set a parent element that the form will be inserted into using the default insertForm method | ||
* Alternatively, you can leave this and override insertForm() and have more control over where it should go | ||
* | ||
* Uses dom.getElement() so you can pass a string, jQuery object, object, etc | ||
* However if more than 1 element is detected, an error will be thrown | ||
* | ||
* @param element | ||
*/ | ||
setInsertIntoElement(element){ | ||
this._insertIntoElement = element; | ||
} | ||
//just in case the server returned the wrong response type and it's actually JSON - ignore errors | ||
try{ data = typeof data === 'string' ? JSON.parse(data) : data; } catch(e){ } | ||
/** | ||
* Returns the element the form will be inserted into | ||
* | ||
* @returns {*} | ||
*/ | ||
getInsertIntoElement(){ | ||
return this._insertIntoElement; | ||
} | ||
//if the response is a string, it's probably/hopefully the form with inline errors | ||
if( typeof data === 'string' ){ | ||
//if we are looking for an element within the response | ||
if( typeof self.getIncomingElementSelector() === 'string' ){ | ||
//parse the incoming HTML | ||
const parsed = navigation.parseHTML(data, self.getIncomingElementSelector()); | ||
//if the form was not found in it, let's assume it doesn't contain the form. If not, then maybe | ||
if( !parsed.html.length ){ | ||
return self.triggerOnError(`${self.getIncomingElementSelector()} could not be found in response from the server`, data, form); | ||
/** | ||
* Get the form from the URL and pass to insertForm | ||
* | ||
* There are three main ways to provide the form from your server: | ||
* 1) Straight HTML. The entire response is the form and that's it. | ||
* 2) Straight HTML, but the form is only a part of the response so it needs to be parsed out based on a selector. | ||
* 3) A JSON object containing the key "html" like this: {"html":"<form>your form here</form>"} | ||
* | ||
*/ | ||
getForm(){ | ||
var self = this; | ||
navigation.showLoader(); | ||
axios.get(this.getURL()).then(function (response) { | ||
navigation.hideLoader(); | ||
let data = response.data; | ||
//just in case the server returned the wrong response type and it's actually JSON - ignore errors | ||
try{ data = typeof data === 'string' ? JSON.parse(data) : data; } catch(e){ } | ||
//if the response is a string (probably HTML) | ||
if( typeof data === 'string' ){ | ||
if( typeof self.getIncomingElementSelector() === 'string' ){ | ||
//parse the incoming HTML | ||
const parsed = navigation.parseHTML(data, self.getIncomingElementSelector()); | ||
//provide the form's HTML in an object containing other details like the route and the full response to insertForm | ||
return self.insertForm(parsed, data); | ||
} | ||
//provide the form's HTML in an object containing other details like the route and the full response to insertForm | ||
return self.insertForm(parsed, data, form); | ||
//otherwise the entire response is assumed to be the form | ||
return self.insertForm({html:data}); | ||
} | ||
return self.insertForm({html:data}, data, form); | ||
} | ||
//if the response is an object, it's probably JSON | ||
else if( typeof data === 'object' ){ | ||
//if it contains the HTML, just pop it back on the page | ||
if( data.html ){ | ||
return self.insertForm({html:data.html}, data, form); | ||
//if the response is an object (probably JSON) | ||
else if( typeof data === 'object' ){ | ||
//if HTML was provided in the object | ||
if( typeof data.html !== "undefined" ){ | ||
return self.insertForm({html:data.html}, data); | ||
} | ||
} | ||
//if it contains an error message, trigger the callback | ||
if( data.error ){ | ||
return self.triggerOnError(data.error, data, form); | ||
} | ||
//if it doesn't APPEAR to be the form again, or an error, let's call it a success | ||
return self.triggerOnSuccess(data, form) | ||
} | ||
}) | ||
throw `Unexpected server response ${data}`; | ||
}) | ||
.catch(function (error) { | ||
@@ -467,4 +571,84 @@ navigation.hideLoader(); | ||
}); | ||
} | ||
return this; | ||
}; | ||
/** | ||
* Allows you to insert the form wherever you want on the page | ||
* Override this method to customize where the form is inserted | ||
* (maybe you want to open a modal first and place it there?) | ||
* | ||
* parsed_content.html will always be the HTML | ||
* | ||
* parsed_content may contain other data like route and title if the form was pulled out of | ||
* a full HTML page which contains those items | ||
* | ||
* response is the full server response (html string or object from JSON - not provided if the response is only the form's HTML) | ||
* | ||
* form is provided if this is after the form was submitted and HTML was returned form the server | ||
* | ||
* @param parsed_content | ||
* @param response | ||
* @param form | ||
* @returns {*|Element|HTMLDocument} | ||
*/ | ||
insertForm(parsed_content, response, form){ | ||
//selector for where the form will go | ||
let el = this.getInsertIntoElement(); | ||
//if not provided | ||
if( el === null ) throw 'Cannot determine where to insert form. Overwrite insertForm() or provide insertIntoElement'; | ||
//get the container element - error if not found | ||
el = dom.getElement(el, true); | ||
//put the form in the container element | ||
el.innerHTML = parsed_content.html; | ||
//find the newly added form | ||
form = el.querySelector('form'); | ||
//attach an on-submit listener to send the form's values via XHR | ||
this.attachSubmitHandler(form); | ||
//run the onload callback now that the form is there | ||
this.triggerOnload(form); | ||
return el; | ||
} | ||
/** | ||
* Use this method to modify the form immediately after it's displayed | ||
* | ||
* You'll likely want to attach plugins for datepickers/dropdowns, or maybe hide a field based on the value of another | ||
* | ||
* @param callback | ||
* @returns {form} | ||
*/ | ||
onload(callback){ | ||
if( typeof callback !== 'function' ) throw `${callback} is not a function`; | ||
this._onload.push(callback); | ||
return this; | ||
} | ||
/** | ||
* Clears all onload callbacks you've set | ||
* | ||
* @returns {FormFromURL} | ||
*/ | ||
clearOnloadCallbacks(){ | ||
this._onload = []; | ||
return this; | ||
} | ||
//all onload callbacks | ||
_onload = []; | ||
/** | ||
* @param form | ||
*/ | ||
triggerOnload(form){ | ||
this._onload.forEach(function(onload){ | ||
onload(form); | ||
}); | ||
return this; | ||
} | ||
} |
{ | ||
"name": "@htmlguyllc/jpack", | ||
"version": "1.0.12", | ||
"version": "1.0.13", | ||
"description": "Core Javascript Library of Everyday Objects, Events, and Utilities", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
179
README.md
@@ -232,113 +232,114 @@ # jPack | ||
### -Form | ||
_Makes dynamic form interactions simpler - currently only supports pulling a form from another page and submitting it using an XHR request_ | ||
### -XHRForm | ||
_Submits a form using XHR_ | ||
Method/Property | Params (name:type) | Return | Notes | ||
--- | --- | --- | --- | ||
fromURL|url:string, options:object|self|instantiate a new form.fromURL('/my-form-url', options); to grab a form from another page and insert it into the current | ||
constructor|form:Element,options:object |self| | ||
setXHRSubmit|enabled:bool|self|enable/disable the XHR submission of the form | ||
setSubmitMethod|method:string|self|override the form and provide a method (GET, POST, PATCH) | ||
getSubmitMethod| |method:string| | ||
setSubmitURL|url:mixed|self|pass null to use the form's action, function to dynamically generate the URL (receives the form as a param), or string | ||
getSubmitURL| |url:string|returns whatever was set in the constructor or using setSubmitURL, not the final URL | ||
attachSubmitHandler|form:mixed|self|attaches the event listener to on submit of the passed form | ||
onSuccess|callback:function|self|adds an onSuccess callback (you can add as many as you'd like) | ||
clearOnSuccessCallbacks| |self| | ||
triggerOnSuccess|response:mixed, form:Element|self|runs all onSuccess callbacks and passes the server's response and the form element | ||
onError|callback:function|self|adds an onError callback (you can add as many as you'd like) | ||
clearOnErrorCallbacks| |self| | ||
triggerOnError|error:string, response:mixed, form:Element|self|triggers all onError callbacks and passes the error string, server response, and form Element | ||
submitForm|form:Element|self|gets URL and method, checks form validity using .validate(), gets values, submits, and kicks off callbacks | ||
getFormValues|form:Element|self|returns data from the form to be submitted - override this if you want to manipulate it first | ||
setValidateCallback|callback:function|is_valid:bool|pass a function to validate the form and return true if it's valid, false if it's not. False prevents form submission so you must display errors for the user within here. The default callback uses Bootstrap 4's "was-validated" class to show errors and HTML5's :invalid attribute to validate | ||
validate|form:Element|bool|passes the form to the validate callback and returns the response | ||
##### To use: | ||
```javascript | ||
import {form} from '@htmlguyllc/jpack/es/components'; | ||
import {XHRForm} from '@htmlguyllc/jpack/es/components'; | ||
var remote_form = new form.fromURL('/my-form', { | ||
incomingElementSelector: null, //the form element or wrapper that you want to retrieve from the URL | ||
insertIntoElement: null, //what element to put the form into | ||
onload: function(form){ return this; }, //once the form is loaded onto the page | ||
xhrSubmit: true, //submit the form using XHR instead of the default action | ||
submitURL:null, //will be grabbed from the form's action attribute, or fallback to the URL the form was retrieved from | ||
submitMethod:null, //will be grabbed from the form's method attribute, or fallback to "POST" | ||
onError: function(error, response, form){ }, //called when the form is submitted and fails | ||
onSuccess: function(response, form){ }, //called when the form is submitted successfully | ||
//shown with defaults | ||
var remote_form = new XHRForm(document.getElementById('my-form'), { | ||
xhrSubmit: true, //wouldn't make a whole lotta sent to use this if this were false lol, but it's here for extending classes and incase you want to toggle it for whatever reason | ||
submitURL:null, //when null, the form's action will be used (if explicitly defined), otherwise it falls back to the URL the form was retrieved from | ||
submitMethod:null, //when null, the form's method will be used (if explicitly defined), otherwise it falls back to POST | ||
onError: function(error, response, form){ }, //although you can add more, you can only pass 1 to start with in the constructor | ||
onSuccess: function(response, form){ }, //although you can add more, you can only pass 1 to start with in the constructor | ||
//validate the form, display any errors and return false to block submission | ||
validateForm: function(form){ | ||
//add .was-validated for bootstrap to show errors | ||
form.classList.add('was-validated'); | ||
//if there are any :invalid elements, the form is not valid | ||
const is_valid = !form.querySelector(':invalid'); | ||
//if it's valid, clear the validation indicators | ||
if( is_valid ) form.classList.remove('was-validated'); | ||
return is_valid; | ||
} | ||
}); | ||
//grab the form and insert in into the "insertIntoElement" | ||
remote_form.getForm(); | ||
//attach the submission handler | ||
remote_form.attachSubmitHandler(); | ||
``` | ||
//let's say you want to show it in a modal, here's what you would need to do: | ||
### -FormFromURL | ||
_Allows you to pull a form from a URL and insert it into the current page very easily including optional XHR form submission!_ | ||
var modal_form = new form.fromURL('/my-form-popup', { | ||
incomingElementSelector: 'form', | ||
onload: function(form){ | ||
//attach plugins or do something with the form now that it's showing in the modal | ||
return this; | ||
}, | ||
onError: function(error, response, form){ | ||
$.jAlert({ | ||
title:'Error', | ||
theme:'red', | ||
content:error | ||
}); | ||
}, | ||
onSuccess: function(response, form){ | ||
form.parents('.jAlert').closeAlert(); | ||
//do something else | ||
}, | ||
}); | ||
Method/Property | Params (name:type) | Return | Notes | ||
--- | --- | --- | --- | ||
constructor | url:string, options:object |self| | ||
setURL|url:string|self|set the URL to pull the form from | ||
getURL| |url:string| | ||
setIncomingElementSelector|selector:string|self|set a selector for the form element or it's parent that is returned by the URL | ||
getIncomingElementSelector| |selector:string| | ||
setInsertIntoElement|element:mixed|self|set the element that the form should be inserted into | ||
getInsertIntoElement| |element:mixed| | ||
getForm| |void|pulls the form from the URL and runs the insertForm method | ||
insertForm|parsed_content:object, response:mixed, form:Element/null|el:Element|inserts the form into the parent element, attaches the submit handler, triggers onload, and returns the parent element | ||
onload|callback:function|self|adds a callback function to be run when the form is loaded on the page | ||
clearOnloadCallbacks| |self|removes all onload callbacks | ||
triggerOnload|form:Element|self|runs all onload callbacks and passes the form to them | ||
//override the insertForm method to use a modal | ||
modal_form.insertForm = function(parsed_content, response, form){ | ||
var self = this; | ||
__There are several methods and properties inherited from XHRForm that are not listed here. Please see XHRForm above for those details__ | ||
##### To use: | ||
```javascript | ||
import {FormFromURL} from '@htmlguyllc/jpack/es/components'; | ||
//shown with defaults | ||
var remote_form = new FormFromURL('/my-form', { | ||
incomingElementSelector: null, //when null, it assumes the entire response is the form's HTML | ||
insertIntoElement: null, //error on null, must provide this | ||
onload: function(form){ return this; }, //although you can add more, you can only pass 1 to start with in the constructor | ||
xhrSubmit: true, | ||
submitURL:null, //when null, the form's action will be used (if explicitly defined), otherwise it falls back to the URL the form was retrieved from | ||
submitMethod:null, //when null, the form's method will be used (if explicitly defined), otherwise it falls back to POST | ||
onError: function(error, response, form){ }, //although you can add more, you can only pass 1 to start with in the constructor | ||
onSuccess: function(response, form){ }, //although you can add more, you can only pass 1 to start with in the constructor | ||
//validate the form, display any errors and return false to block submission | ||
validateForm: function(form){ | ||
//add .was-validated for bootstrap to show errors | ||
form.classList.add('was-validated'); | ||
//if form is already defined, it was submitted and the response contained HTML, so we need to just replace it ourselves | ||
if( form ){ | ||
//replace and reassign | ||
form = dom.replaceElWithHTML(form, parsed_content.html); | ||
//attach submit handler | ||
self.attachSubmitHandler(form); | ||
//trigger onload again (you can pas a param to say it's the second time if you want | ||
self.triggerOnload(form); | ||
return; | ||
} | ||
//if there are any :invalid elements, the form is not valid | ||
const is_valid = !form.querySelector(':invalid'); | ||
//using jAlert as an example (https://htmlguyllc.github.io/jAlert/) | ||
$.jAlert({ | ||
title:'My Form', | ||
theme:'blue', | ||
size: '1000px', | ||
content: parsed_content.html, | ||
onOpen: function(alert){ | ||
//find my form in there | ||
form = alert[0].querySelector(self.getIncomingElementSelector()); | ||
//attach an on-submit listener to send the form's values via XHR | ||
self.attachSubmitHandler(form); | ||
//if it's valid, clear the validation indicators | ||
if( is_valid ) form.classList.remove('was-validated'); | ||
//run the onload callback now that the form is there | ||
self.triggerOnload(form); | ||
return is_valid; | ||
} | ||
}); | ||
}; | ||
}); | ||
//show the form in a modal | ||
modal_form.getForm(); | ||
//grab the form and insert in into the "insertIntoElement" | ||
remote_form.getForm(); | ||
``` | ||
#### Prototyping: | ||
#### Extending: | ||
You can use prototypes to globally overwrite 3 methods in form.fromURL (isValid, insertForm, and submitForm). | ||
FormFromURL extends XHRForm and either can be extended as you need. | ||
```javascript | ||
import {form} from '@htmlguyllc/jpack/es/components'; | ||
See examples/FormModalFromURL for an example | ||
form.fromURL.prototype.insertForm = function(parsed_content, response, form) { | ||
//this is useful if you always want to show your form in a modal like shown in the example above | ||
}; | ||
form.fromURL.prototype.isValid = function(form){ | ||
//perform validation on the form and return a bool | ||
//false prevents form submission so make sure you display any errors for the user | ||
}; | ||
//now create a new form and both methods above will be used | ||
var my_form = new form.fromURL('/my-form'); | ||
//no matter how many you create, they all share the same logic now | ||
var my_form2 = new form.fromURL('/my-form2'); | ||
``` | ||
## - Objects - | ||
@@ -345,0 +346,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
251137
24
3453
599