diff --git a/dist/htmx.d.ts b/dist/htmx.esm.d.ts similarity index 78% rename from dist/htmx.d.ts rename to dist/htmx.esm.d.ts index 2176ff4b..41252c4b 100644 --- a/dist/htmx.d.ts +++ b/dist/htmx.esm.d.ts @@ -1,3 +1,145 @@ +export default htmx; +export type HttpVerb = 'get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch'; +export type SwapOptions = { + select?: string; + selectOOB?: string; + eventInfo?: any; + anchor?: string; + contextElement?: Element; + afterSwapCallback?: swapCallback; + afterSettleCallback?: swapCallback; +}; +export type swapCallback = () => any; +export type HtmxSwapStyle = 'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string; +export type HtmxSwapSpecification = { + swapStyle: HtmxSwapStyle; + swapDelay: number; + settleDelay: number; + transition?: boolean; + ignoreTitle?: boolean; + head?: string; + scroll?: 'top' | 'bottom'; + scrollTarget?: string; + show?: string; + showTarget?: string; + focusScroll?: boolean; +}; +export type ConditionalFunction = ((this: Node, evt: Event) => boolean) & { + source: string; +}; +export type HtmxTriggerSpecification = { + trigger: string; + pollInterval?: number; + eventFilter?: ConditionalFunction; + changed?: boolean; + once?: boolean; + consume?: boolean; + delay?: number; + from?: string; + target?: string; + throttle?: number; + queue?: string; + root?: string; + threshold?: string; +}; +export type HtmxElementValidationError = { + elt: Element; + message: string; + validity: ValidityState; +}; +export type HtmxHeaderSpecification = Record; +export type HtmxAjaxHelperContext = { + source?: Element | string; + event?: Event; + handler?: HtmxAjaxHandler; + target?: Element | string; + swap?: HtmxSwapStyle; + values?: any | FormData; + headers?: Record; + select?: string; +}; +export type HtmxRequestConfig = { + boosted: boolean; + useUrlParams: boolean; + formData: FormData; + /** + * formData proxy + */ + parameters: any; + unfilteredFormData: FormData; + /** + * unfilteredFormData proxy + */ + unfilteredParameters: any; + headers: HtmxHeaderSpecification; + target: Element; + verb: HttpVerb; + errors: HtmxElementValidationError[]; + withCredentials: boolean; + timeout: number; + path: string; + triggeringEvent: Event; +}; +export type HtmxResponseInfo = { + xhr: XMLHttpRequest; + target: Element; + requestConfig: HtmxRequestConfig; + etc: HtmxAjaxEtc; + boosted: boolean; + select: string; + pathInfo: { + requestPath: string; + finalRequestPath: string; + responsePath: string | null; + anchor: string; + }; + failed?: boolean; + successful?: boolean; +}; +export type HtmxAjaxEtc = { + returnPromise?: boolean; + handler?: HtmxAjaxHandler; + select?: string; + targetOverride?: Element; + swapOverride?: HtmxSwapStyle; + headers?: Record; + values?: any | FormData; + credentials?: boolean; + timeout?: number; +}; +export type HtmxResponseHandlingConfig = { + code?: string; + swap: boolean; + error?: boolean; + ignoreTitle?: boolean; + select?: string; + target?: string; + swapOverride?: string; + event?: string; +}; +export type HtmxBeforeSwapDetails = HtmxResponseInfo & { + shouldSwap: boolean; + serverResponse: any; + isError: boolean; + ignoreTitle: boolean; + selectOverride: string; +}; +export type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any; +export type HtmxSettleTask = (() => void); +export type HtmxSettleInfo = { + tasks: HtmxSettleTask[]; + elts: Element[]; + title?: string; +}; +export type HtmxExtension = { + init: (api: any) => void; + onEvent: (name: string, event: Event | CustomEvent) => boolean; + transformResponse: (text: string, xhr: XMLHttpRequest, elt: Element) => string; + isInlineSwap: (swapStyle: HtmxSwapStyle) => boolean; + handleSwap: (swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean | Node[]; + encodeParameters: (xhr: XMLHttpRequest, parameters: FormData, elt: Node) => any | string | null; + getSelectors: () => string[] | null; +}; declare namespace htmx { const onLoad: (callback: (elt: Node) => void) => EventListener; const process: (elt: string | Element) => void; @@ -15,7 +157,7 @@ declare namespace htmx { const toggleClass: (elt: string | Element, clazz: string) => void; const takeClass: (elt: string | Node, clazz: string) => void; const swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void; - const defineExtension: (name: string, extension: any) => void; + const defineExtension: (name: string, extension: HtmxExtension) => void; const removeExtension: (name: string) => void; const logAll: () => void; const logNone: () => void; @@ -60,136 +202,3 @@ declare namespace htmx { const _: (str: string) => any; const version: string; } -type HttpVerb = 'get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch'; -type SwapOptions = { - select?: string; - selectOOB?: string; - eventInfo?: any; - anchor?: string; - contextElement?: Element; - afterSwapCallback?: swapCallback; - afterSettleCallback?: swapCallback; -}; -type swapCallback = () => any; -type HtmxSwapStyle = 'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string; -type HtmxSwapSpecification = { - swapStyle: HtmxSwapStyle; - swapDelay: number; - settleDelay: number; - transition?: boolean; - ignoreTitle?: boolean; - head?: string; - scroll?: 'top' | 'bottom'; - scrollTarget?: string; - show?: string; - showTarget?: string; - focusScroll?: boolean; -}; -type ConditionalFunction = ((this: Node, evt: Event) => boolean) & { - source: string; -}; -type HtmxTriggerSpecification = { - trigger: string; - pollInterval?: number; - eventFilter?: ConditionalFunction; - changed?: boolean; - once?: boolean; - consume?: boolean; - delay?: number; - from?: string; - target?: string; - throttle?: number; - queue?: string; - root?: string; - threshold?: string; -}; -type HtmxElementValidationError = { - elt: Element; - message: string; - validity: ValidityState; -}; -type HtmxHeaderSpecification = Record; -type HtmxAjaxHelperContext = { - source?: Element | string; - event?: Event; - handler?: HtmxAjaxHandler; - target?: Element | string; - swap?: HtmxSwapStyle; - values?: any | FormData; - headers?: Record; - select?: string; -}; -type HtmxRequestConfig = { - boosted: boolean; - useUrlParams: boolean; - formData: FormData; - /** - * formData proxy - */ - parameters: any; - unfilteredFormData: FormData; - /** - * unfilteredFormData proxy - */ - unfilteredParameters: any; - headers: HtmxHeaderSpecification; - target: Element; - verb: HttpVerb; - errors: HtmxElementValidationError[]; - withCredentials: boolean; - timeout: number; - path: string; - triggeringEvent: Event; -}; -type HtmxResponseInfo = { - xhr: XMLHttpRequest; - target: Element; - requestConfig: HtmxRequestConfig; - etc: HtmxAjaxEtc; - boosted: boolean; - select: string; - pathInfo: { - requestPath: string; - finalRequestPath: string; - responsePath: string | null; - anchor: string; - }; - failed?: boolean; - successful?: boolean; -}; -type HtmxAjaxEtc = { - returnPromise?: boolean; - handler?: HtmxAjaxHandler; - select?: string; - targetOverride?: Element; - swapOverride?: HtmxSwapStyle; - headers?: Record; - values?: any | FormData; - credentials?: boolean; - timeout?: number; -}; -type HtmxResponseHandlingConfig = { - code?: string; - swap: boolean; - error?: boolean; - ignoreTitle?: boolean; - select?: string; - target?: string; - swapOverride?: string; - event?: string; -}; -type HtmxBeforeSwapDetails = HtmxResponseInfo & { - shouldSwap: boolean; - serverResponse: any; - isError: boolean; - ignoreTitle: boolean; - selectOverride: string; -}; -type HtmxAjaxHandler = (elt: Element, responseInfo: HtmxResponseInfo) => any; -type HtmxSettleTask = (() => void); -type HtmxSettleInfo = { - tasks: HtmxSettleTask[]; - elts: Element[]; - title?: string; -}; -type HtmxExtension = any; diff --git a/editors/jetbrains/htmx.web-types.json b/editors/jetbrains/htmx.web-types.json index 58245a07..2eafead8 100644 --- a/editors/jetbrains/htmx.web-types.json +++ b/editors/jetbrains/htmx.web-types.json @@ -29,7 +29,7 @@ "description-sections": { "Not inherited": "" }, - "doc-url": "https://htmx.org/attributes/hx-confirm/" + "doc-url": "https://htmx.org/attributes/hx-delete/" }, { "name": "hx-disable", diff --git a/package.json b/package.json index 60c2a964..0a18fbfb 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ "files": [ "LICENSE", "README.md", - "dist/htmx.d.ts", + "dist/htmx.esm.d.ts", "dist/*.js", "dist/ext/*.js", "dist/*.js.gz", "editors/jetbrains/htmx.web-types.json" ], "main": "dist/htmx.esm.js", - "types": "dist/htmx.d.ts", + "types": "dist/htmx.esm.d.ts", "unpkg": "dist/htmx.min.js", "web-types": "editors/jetbrains/htmx.web-types.json", "scripts": { @@ -29,7 +29,7 @@ "lint": "eslint src/htmx.js test/attributes/ test/core/ test/util/", "format": "eslint --fix src/htmx.js test/attributes/ test/core/ test/util/", "types-check": "tsc src/htmx.js --noEmit --checkJs --target es6 --lib dom,dom.iterable", - "types-generate": "tsc src/htmx.js --declaration --emitDeclarationOnly --allowJs --outDir dist", + "types-generate": "tsc dist/htmx.esm.js --declaration --emitDeclarationOnly --allowJs --outDir dist", "test": "npm run lint && npm run types-check && mocha-chrome test/index.html", "type-declarations": "tsc --project ./jsconfig.json", "ws-tests": "cd ./test/ws-sse && node ./server.js", diff --git a/src/htmx.d.ts b/src/htmx.d.ts index 2176ff4b..8037b2c6 100644 --- a/src/htmx.d.ts +++ b/src/htmx.d.ts @@ -15,7 +15,7 @@ declare namespace htmx { const toggleClass: (elt: string | Element, clazz: string) => void; const takeClass: (elt: string | Node, clazz: string) => void; const swap: (target: string | Element, content: string, swapSpec: HtmxSwapSpecification, swapOptions?: SwapOptions) => void; - const defineExtension: (name: string, extension: any) => void; + const defineExtension: (name: string, extension: HtmxExtension) => void; const removeExtension: (name: string) => void; const logAll: () => void; const logNone: () => void; @@ -192,4 +192,12 @@ type HtmxSettleInfo = { elts: Element[]; title?: string; }; -type HtmxExtension = any; +type HtmxExtension = { + init: (api: any) => void; + onEvent: (name: string, event: Event | CustomEvent) => boolean; + transformResponse: (text: string, xhr: XMLHttpRequest, elt: Element) => string; + isInlineSwap: (swapStyle: HtmxSwapStyle) => boolean; + handleSwap: (swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean | Node[]; + encodeParameters: (xhr: XMLHttpRequest, parameters: FormData, elt: Node) => any | string | null; + getSelectors: () => string[] | null; +}; diff --git a/src/htmx.js b/src/htmx.js index b4238521..2c394fde 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1753,7 +1753,7 @@ var htmx = (function() { try { const newElements = ext.handleSwap(swapStyle, target, fragment, settleInfo) if (newElements) { - if (typeof newElements.length !== 'undefined') { + if (Array.isArray(newElements)) { // if handleSwap returns an array (like) of elements, we handle them for (let j = 0; j < newElements.length; j++) { const child = newElements[j] @@ -1952,7 +1952,10 @@ var htmx = (function() { for (const eventName in triggers) { if (triggers.hasOwnProperty(eventName)) { let detail = triggers[eventName] - if (!isRawObject(detail)) { + if (isRawObject(detail)) { + // @ts-ignore + elt = detail.target !== undefined ? detail.target : elt + } else { detail = { value: detail } } triggerEvent(elt, eventName, detail) @@ -3388,10 +3391,10 @@ var htmx = (function() { function overrideFormData(receiver, donor) { for (const key of donor.keys()) { receiver.delete(key) - donor.getAll(key).forEach(function(value) { - receiver.append(key, value) - }) } + donor.forEach(function(value, key) { + receiver.append(key, value) + }) return receiver } @@ -4014,6 +4017,8 @@ var htmx = (function() { target.delete(name) if (typeof value.forEach === 'function') { value.forEach(function(v) { target.append(name, v) }) + } else if (typeof value === 'object' && !(value instanceof Blob)) { + target.append(name, JSON.stringify(value)) } else { target.append(name, value) } @@ -5132,12 +5137,13 @@ var htmx = (function() { */ /** + * @see https://github.com/bigskysoftware/htmx-extensions/blob/main/README.md * @typedef {Object} HtmxExtension - * @see https://htmx.org/extensions/#defining * @property {(api: any) => void} init * @property {(name: string, event: Event|CustomEvent) => boolean} onEvent * @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse * @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap - * @property {(swapStyle: HtmxSwapStyle, target: Element, fragment: Node, settleInfo: HtmxSettleInfo) => boolean} handleSwap - * @property {(xhr: XMLHttpRequest, parameters: FormData, elt: Element) => *|string|null} encodeParameters + * @property {(swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean|Node[]} handleSwap + * @property {(xhr: XMLHttpRequest, parameters: FormData, elt: Node) => *|string|null} encodeParameters + * @property {() => string[]|null} getSelectors */ diff --git a/test/core/headers.js b/test/core/headers.js index 71092821..2cf21423 100644 --- a/test/core/headers.js +++ b/test/core/headers.js @@ -147,6 +147,21 @@ describe('Core htmx AJAX headers', function() { invokedEvent.should.equal(true) }) + it('should handle JSON with target array arg HX-Trigger response header properly', function() { + this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{"foo":{"target":"#testdiv"}}' }, '']) + + var div = make('
') + var testdiv = make('
') + var invokedEvent = false + testdiv.addEventListener('foo', function(evt) { + invokedEvent = true + evt.detail.elt.should.equal(testdiv) + }) + div.click() + this.server.respond() + invokedEvent.should.equal(true) + }) + it('should survive malformed JSON in HX-Trigger response header', function() { this.server.respondWith('GET', '/test', [200, { 'HX-Trigger': '{not: valid}' }, '']) diff --git a/test/core/parameters.js b/test/core/parameters.js index 1b2e2bf9..f1df2d7e 100644 --- a/test/core/parameters.js +++ b/test/core/parameters.js @@ -293,4 +293,23 @@ describe('Core htmx Parameter Handling', function() { this.server.respond() form.innerHTML.should.equal('Clicked!') }) + + it('order of parameters follows order of input elements with POST', function() { + this.server.respondWith('POST', '/test', function(xhr) { + xhr.requestBody.should.equal('foo=bar&bar=foo&foo=bar&foo2=bar2') + xhr.respond(200, {}, 'Clicked!') + }) + + var form = make('
' + + '' + + '' + + '' + + '' + + '' + + '
') + + byId('b1').click() + this.server.respond() + form.innerHTML.should.equal('Clicked!') + }) }) diff --git a/www/content/headers/hx-trigger.md b/www/content/headers/hx-trigger.md index 0682c62d..9274f030 100644 --- a/www/content/headers/hx-trigger.md +++ b/www/content/headers/hx-trigger.md @@ -60,6 +60,12 @@ document.body.addEventListener("showMessage", function(evt){ Each property of the JSON object on the right hand side will be copied onto the details object for the event. +### Targetting Other Elements + +You can trigger events on other target elements by adding a `target` argument to the JSON object. + +`HX-Trigger: {"showMessage":{"target" : "#otherElement"}}` + ### Multiple Triggers If you wish to invoke multiple events, you can simply add additional properties to the top level JSON