Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Last active May 18, 2021 03:43
Show Gist options
  • Save kmuenkel/ad18a3086661d52de6899c74fe9ec16f to your computer and use it in GitHub Desktop.
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.
/**
* @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