Last active
May 18, 2021 03:43
-
-
Save kmuenkel/ad18a3086661d52de6899c74fe9ec16f to your computer and use it in GitHub Desktop.
Apply this 'data-ajax-form' attribute to a Form element, and instantiate this class. That's it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @class AjaxSubmit | |
*/ | |
AjaxSubmit = class | |
{ | |
/** | |
* @type {string} | |
*/ | |
rootUrl = ''; | |
/** | |
* @type {{}} | |
*/ | |
data = {}; | |
/** | |
* @type {{}} | |
*/ | |
middlewares = {}; | |
/** | |
* @returns boolean|undefined | |
*/ | |
missingDataHandler = () => {}; | |
/** | |
* AjaxSubmit constructor. | |
* @param selector | |
* @param rootUrl | |
*/ | |
constructor(selector, rootUrl) | |
{ | |
this.rootUrl = rootUrl ? rootUrl : ''; | |
selector = selector ? selector : "[data-ajax-form]" | |
$(selector).each((index, element) => { | |
element = $(element); | |
if (element.data('ajax-form-set') !== true) { | |
element.data('ajax-form-set', true); | |
$(element).submit(this.submitForm.bind(this)); | |
} | |
}); | |
} | |
/** | |
* @param middleware | |
* @param name | |
* @returns AjaxSubmit | |
*/ | |
addMiddleware(middleware, name) | |
{ | |
name = !name ? Object.keys(this.middlewares).length : name; | |
this.middlewares[name] = middleware; | |
return this; | |
} | |
/** | |
* @param middleware | |
* @returns AjaxSubmit | |
*/ | |
setMiddleware(middleware) | |
{ | |
this.middlewares = middleware; | |
return this; | |
} | |
/** | |
* @param handler | |
* @returns AjaxSubmit | |
*/ | |
setMissingDataHandler(handler) | |
{ | |
this.missingDataHandler = handler; | |
return this; | |
} | |
/** | |
* @param element | |
* @returns {Promise<[unknown, _]>} | |
*/ | |
base64encodeFiles(element) | |
{ | |
let reader = new FileReader; | |
let fileArray = Array.from(element.files); | |
let encodeFile = file => new Promise((resolve, reject) => { | |
reader.readAsDataURL(file); | |
reader.onload = () => { | |
let encodedParts = reader.result.split(";"); | |
encodedParts.splice(1, 0, "df:"+file.name); | |
resolve(encodedParts.join(";")); | |
}; | |
reader.onerror = () => reject(new Error("Error parsing file")); | |
}); | |
let encodedFiles = fileArray.map(encodeFile); | |
return Promise.all(encodedFiles).then(files => { | |
return element.multiple ? files : (files.length ? files[0] : null); | |
}); | |
} | |
/** | |
* | |
* @param form | |
* @returns {Promise<{}>} | |
*/ | |
normalizeFiles(form) | |
{ | |
let fileGroups = {}; | |
let filePromises = [].reduce.call(form.elements, (data, element) => { | |
if (element.name && element.type === 'file') { | |
fileGroups[element.name] = element.multiple ? [] : null | |
let filePromise = this.base64encodeFiles(element).then(fileSet => { | |
return {[element.name]: fileSet}; | |
}); | |
data.push(filePromise); | |
} | |
return data; | |
}, []); | |
return Promise.all(filePromises).then(fileSets => { | |
fileSets.forEach(filesSet => Object.assign(fileGroups, filesSet)); | |
return fileGroups; | |
}); | |
} | |
/** | |
* @param form | |
*/ | |
parseFormData(form) | |
{ | |
let getFormValues = fileSets => { | |
return [].reduce.call(form.elements, (data, element) => { | |
if (element.name) { | |
let value = this.getElementValue(element, fileSets, data); | |
if (value !== null) { | |
data.push({name: element.name, value: value}); | |
} | |
} | |
return data; | |
}, []); | |
}; | |
return this.normalizeFiles(form).then(getFormValues); | |
} | |
/** | |
* @param element | |
* @param base64Files | |
* @param data | |
* @returns {*} | |
*/ | |
getElementValue(element, base64Files, data) | |
{ | |
let value = element.value; | |
if (element.type === 'checkbox') { | |
value = element.checked ? (element.hasAttribute('value') ? element.getAttribute('value') : element.checked) : null; | |
} else if (element.type === 'file' && base64Files) { | |
value = base64Files[element.name]; | |
} else if (element.options && element.multiple) { | |
value = [].reduce.call(element, (values, option) => { | |
return option.selected ? values.concat(option.value) : values; | |
}, []); | |
} | |
return value; | |
} | |
/** | |
* @param elements | |
* @returns {[]} | |
*/ | |
normalizeFormNames(elements) | |
{ | |
let normalized = []; | |
Object.keys(elements).forEach(index => { | |
let value = elements[index].value; | |
let keys = elements[index].name.replace(/\]$/, '').split(/\]\[|\[/g); | |
normalized.push({keys: keys, value: value}); | |
}); | |
return normalized; | |
} | |
/** | |
* @param elements | |
* @returns {{}} | |
*/ | |
serializeQueryElements(elements) | |
{ | |
let normalized = this.normalizeFormNames(elements); | |
let currentLevel = {}; | |
Object.keys(normalized).forEach(index => { | |
let data = normalized[index]; | |
let currentKey = normalized[index].keys.shift(); | |
currentLevel[currentKey] = this.serializeElement(data, currentLevel[currentKey]); | |
}); | |
return currentLevel; | |
} | |
/** | |
* @param data | |
* @param currentLevel | |
* @returns * | |
*/ | |
serializeElement(data, currentLevel) | |
{ | |
let currentKey = data.keys.shift(); | |
if (currentKey !== undefined) { | |
currentLevel = currentLevel ? currentLevel : (currentKey === "" ? [] : {}); | |
currentKey = currentKey === "" ? Object.keys(currentLevel).length : currentKey; | |
currentLevel[currentKey] = this.serializeElement(data, currentLevel[currentKey]); | |
return currentLevel; | |
} | |
return data.value; | |
} | |
/** | |
* @param data | |
* @returns * | |
*/ | |
processMiddleware(data) | |
{ | |
return Object.keys(this.middlewares).reduce((handled, name) => { | |
let middleware = this.middlewares[name]; | |
if (handled !== false) { | |
handled = middleware(handled); | |
if (!handled) { | |
handled = false; | |
console.log("Middleware '"+name+"' failed."); | |
} | |
} | |
return handled; | |
}, data); | |
} | |
/** | |
* @param data | |
* @returns boolean|* | |
*/ | |
validateData(data) | |
{ | |
if (data === false) { | |
let defaultData = this.missingDataHandler(); | |
defaultData = defaultData === true ? {} : defaultData; | |
data = ![false, undefined].includes(defaultData) ? defaultData : data; | |
} | |
return data; | |
} | |
/** | |
* @param event | |
*/ | |
submitForm(event) | |
{ | |
let callbacks = this.makeRequestCallbacks(event.target); | |
let transmitRequest = request => { | |
$.ajax(request).done(callbacks.success).fail(callbacks.failure).always(callbacks.final); | |
}; | |
event.preventDefault(); | |
$(event.target).children(":input").prop("readonly", true); | |
this.makeFormRequest(event.target).then(transmitRequest); | |
} | |
/** | |
* @param form | |
* @returns {Promise<{data: *, type: *, url: string}>} | |
*/ | |
makeFormRequest(form) | |
{ | |
let method = form.getAttribute("method"); | |
let relativePath = form.getAttribute("action"); | |
let url = (this.rootUrl ? this.rootUrl + "/" : "") + relativePath; | |
let createRequest = data => { | |
return { | |
type: method, | |
url: url, | |
data: JSON.stringify(data), | |
contentType: 'application/json; charset=uft-8', | |
dataType: 'json' | |
}; | |
}; | |
return this.makeFormData(form).then(createRequest); | |
} | |
/** | |
* @param form | |
* @returns {Promise<{}>} | |
*/ | |
makeFormData(form) | |
{ | |
let transformData = data => { | |
data = this.serializeQueryElements(data); | |
data = this.processMiddleware(data); | |
data = this.validateData(data); | |
return data; | |
}; | |
return this.parseFormData(form).then(transformData); | |
} | |
/** | |
* @param form | |
* @returns {{success: success, failure: failure, final: final}} | |
*/ | |
makeRequestCallbacks(form) | |
{ | |
let success = (response, status, error) => { | |
let handler = $(form).data("responseHandler"); | |
handler = (typeof handler === 'function') ? handler(response, status, error) : handler; | |
handler !== false && $(form).trigger("reset"); | |
}; | |
let failure = (response, status, error) => { | |
let errorHandler = $(form).data("errorHandler"); | |
errorHandler = (typeof errorHandler === 'function') ? errorHandler(response, status, error) : errorHandler; | |
if (response.status === 422 && response.responseJSON) { | |
Object.keys(response.responseJSON.errors).forEach(field => { | |
$("[name=" + field.replace(/\./g, "\\.")+"]").addClass('invalidInput') | |
.focus(event => $(event.target).removeClass('invalidInput')); | |
$("span[aria-labelledby='select2-" + field + "-container']").addClass('invalidInput') | |
.focus(event => $(event.target).removeClass('invalidInput')); | |
response.responseJSON.errors[field].forEach(error => console.log(field + ": " + error)); | |
}); | |
} else { | |
console.log(response, status, error); | |
} | |
}; | |
let final = () => { | |
$(form).children(":input").prop("readonly", false); | |
this.data = {}; | |
} | |
return {success: success, failure: failure, final: final}; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment