diff --git a/package.json b/package.json index 4e6b1c07..08ce2f78 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "scripts": { "dist": "./scripts/dist.sh", "lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/", + "lint-fix": "eslint src/htmx.js test/attributes/ test/core/ test/util/ --fix", "format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/", "test": "npm run lint && mocha-chrome test/index.html", "test-types": "tsc --project ./jsconfig.json", diff --git a/src/htmx.js b/src/htmx.js index f215f1e8..8f3ff6d6 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -2429,6 +2429,11 @@ var htmx = (function() { // Input Value Processing //= =================================================================== + /** + * @param {HTMLElement[]} processed + * @param {HTMLElement} elt + * @returns {boolean} + */ function haveSeenNode(processed, elt) { for (let i = 0; i < processed.length; i++) { const node = processed[i] @@ -2453,30 +2458,43 @@ var htmx = (function() { return true } - function addValueToValues(name, value, values) { - // This is a little ugly because both the current value of the named value in the form - // and the new value could be arrays, so we have to handle all four cases :/ + /** @param {string} name + * @param {string|Array} value + * @param {FormData} formData */ + function addValueToFormData(name, value, formData) { if (name != null && value != null) { - const current = values[name] - if (current === undefined) { - values[name] = value - } else if (Array.isArray(current)) { - if (Array.isArray(value)) { - values[name] = current.concat(value) - } else { - current.push(value) - } + if (Array.isArray(value)) { + value.forEach(function(v) { formData.append(name, v) }) } else { - if (Array.isArray(value)) { - values[name] = [current].concat(value) - } else { - values[name] = [current, value] - } + formData.append(name, value) } } } - function processInputValue(processed, values, errors, elt, validate) { + /** @param {string} name + * @param {string|Array} value + * @param {FormData} formData */ + function removeValueFromFormData(name, value, formData) { + if (name != null && value != null) { + let values = formData.getAll(name) + if (Array.isArray(value)) { + values = values.filter(v => value.indexOf(v) < 0) + } else { + values = values.filter(v => v !== value) + } + formData.delete(name) + forEach(values, v => formData.append(name, v)) + } + } + + /** + * @param {HTMLElement[]} processed + * @param {FormData} formData + * @param {HtmxElementValidationError[]} errors + * @param {HTMLElement|HTMLInputElement|HTMLFormElement} elt + * @param {boolean} validate + */ + function processInputValue(processed, formData, errors, elt, validate) { if (elt == null || haveSeenNode(processed, elt)) { return } else { @@ -2492,19 +2510,40 @@ var htmx = (function() { if (elt.files) { value = toArray(elt.files) } - addValueToValues(name, value, values) + addValueToFormData(name, value, formData) if (validate) { validateElement(elt, errors) } } if (matches(elt, 'form')) { - const inputs = elt.elements - forEach(inputs, function(input) { - processInputValue(processed, values, errors, input, validate) + forEach(elt.elements, function(input) { + if (processed.indexOf(input) >= 0) { + // The input has already been processed and added to the values, but the FormData that will be + // constructed right after on the form, will include it once again. So remove that input's value + // now to avoid duplicates + removeValueFromFormData(input.name, input.value, formData) + } else { + processed.push(input) + } + if (validate) { + validateElement(input, errors) + } + }) + new FormData(elt).forEach(function(value, name) { + addValueToFormData(name, value, formData) }) } } + /** + * @typedef {{elt: HTMLElement, message: string, validity: ValidityState}} HtmxElementValidationError + */ + + /** + * + * @param {HTMLElement|HTMLObjectElement} element + * @param {HtmxElementValidationError[]} errors + */ function validateElement(element, errors) { if (element.willValidate) { triggerEvent(element, 'htmx:validation:validate') @@ -2516,13 +2555,32 @@ var htmx = (function() { } /** - * @param {HTMLElement} elt + * Override values in the one FormData with those from another. + * @param {FormData} receiver the formdata that will be mutated + * @param {FormData} donor the formdata that will provide the overriding values + * @returns {FormData} the {@linkcode receiver} + */ + function overrideFormData(receiver, donor) { + for (const key of donor.keys()) { + receiver.delete(key) + donor.getAll(key).forEach(function(value) { + receiver.append(key, value) + }) + } + return receiver + } + + /** + * @param {HTMLElement|HTMLFormElement} elt * @param {string} verb + * @returns {{errors: HtmxElementValidationError[], formData: FormData, values: Object}} */ function getInputValues(elt, verb) { + /** @type HTMLElement[] */ const processed = [] - let values = {} - const formValues = {} + const formData = new FormData() + const priorityFormData = new FormData() + /** @type HtmxElementValidationError[] */ const errors = [] const internalData = getInternalData(elt) if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) { @@ -2538,38 +2596,44 @@ var htmx = (function() { // for a non-GET include the closest form if (verb !== 'get') { - processInputValue(processed, formValues, errors, closest(elt, 'form'), validate) + processInputValue(processed, priorityFormData, errors, closest(elt, 'form'), validate) } // include the element itself - processInputValue(processed, values, errors, elt, validate) + processInputValue(processed, formData, errors, elt, validate) // if a button or submit was clicked last, include its value if (internalData.lastButtonClicked || elt.tagName === 'BUTTON' || (elt.tagName === 'INPUT' && getRawAttribute(elt, 'type') === 'submit')) { const button = internalData.lastButtonClicked || elt const name = getRawAttribute(button, 'name') - addValueToValues(name, button.value, formValues) + addValueToFormData(name, button.value, priorityFormData) } // include any explicit includes const includes = findAttributeTargets(elt, 'hx-include') forEach(includes, function(node) { - processInputValue(processed, values, errors, node, validate) + processInputValue(processed, formData, errors, node, validate) // if a non-form is included, include any input values within it if (!matches(node, 'form')) { forEach(node.querySelectorAll(INPUT_SELECTOR), function(descendant) { - processInputValue(processed, values, errors, descendant, validate) + processInputValue(processed, formData, errors, descendant, validate) }) } }) - // form values take precedence, overriding the regular values - values = mergeObjects(values, formValues) + // values from a