Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce types.ts; improve types for internal functions with focus on hx-swap #2107

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/htmx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ export interface HtmxConfig {
* https://htmx.org/extensions/#defining
*/
export interface HtmxExtension {
init(apiRef: HtmxInternalApi): void;
onEvent?: (name: string, evt: CustomEvent) => any;
transformResponse?: (text: any, xhr: XMLHttpRequest, elt: any) => any;
isInlineSwap?: (swapStyle: any) => any;
Expand Down
22 changes: 11 additions & 11 deletions src/htmx.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ return (function () {
historyEnabled:true,
historyCacheSize:10,
refreshOnHistoryMiss:false,
defaultSwapStyle:'innerHTML',
defaultSwapStyle: /** @type{import("./types").SwapStyle} */('innerHTML'),
defaultSwapDelay:0,
defaultSettleDelay:20,
includeIndicatorStyles:true,
Expand Down Expand Up @@ -92,7 +92,7 @@ return (function () {
version: "1.9.10"
};

/** @type {import("./htmx").HtmxInternalApi} */
/** @type {import("./types").HtmxInternalApi} */
var internalAPI = {
addTriggerHandler: addTriggerHandler,
bodyContains: bodyContains,
Expand Down Expand Up @@ -404,6 +404,7 @@ return (function () {
}
}

/** @type {import("./types").splitOnWhitespace} */
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
Expand Down Expand Up @@ -2546,6 +2547,7 @@ return (function () {
*/
function getInputValues(elt, verb) {
var processed = [];
/** @type {Object<string, any>} */
var values = {};
var formValues = {};
var errors = [];
Expand Down Expand Up @@ -2707,14 +2709,10 @@ return (function () {
return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf("#") >=0
}

/**
*
* @param {HTMLElement} elt
* @param {string} swapInfoOverride
* @returns {import("./htmx").HtmxSwapSpecification}
*/
/** @type {import("./types").HtmxInternalApi['getSwapSpecification']} */
function getSwapSpecification(elt, swapInfoOverride) {
var swapInfo = swapInfoOverride ? swapInfoOverride : getClosestAttributeValue(elt, "hx-swap");
/** @type {import("./types").HtmxSwapSpecification} */
var swapSpec = {
"swapStyle" : getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
"swapDelay" : htmx.config.defaultSwapDelay,
Expand All @@ -2741,20 +2739,20 @@ return (function () {
var splitSpec = scrollSpec.split(":");
var scrollVal = splitSpec.pop();
var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
swapSpec["scroll"] = scrollVal;
swapSpec["scroll"] = /** @type {"top" | "bottom"} */(scrollVal);
swapSpec["scrollTarget"] = selectorVal;
} else if (value.indexOf("show:") === 0) {
var showSpec = value.substr(5);
var splitSpec = showSpec.split(":");
var showVal = splitSpec.pop();
var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
swapSpec["show"] = showVal;
swapSpec["show"] = /** @type {"top" | "bottom"} */(showVal);
swapSpec["showTarget"] = selectorVal;
} else if (value.indexOf("focus-scroll:") === 0) {
var focusScrollVal = value.substr("focus-scroll:".length);
swapSpec["focusScroll"] = focusScrollVal == "true";
} else if (i == 0) {
swapSpec["swapStyle"] = value;
swapSpec["swapStyle"] = /** @type {any} */(value);
} else {
logError('Unknown modifier in hx-swap: ' + value);
}
Expand Down Expand Up @@ -2796,6 +2794,7 @@ return (function () {
return {tasks: [], elts: [target]};
}

/** @type {import("./types").updateScrollState} */
function updateScrollState(content, swapSpec) {
var first = content[0];
var last = content[content.length - 1];
Expand Down Expand Up @@ -3461,6 +3460,7 @@ return (function () {
if (hasHeader(xhr, /HX-Location:/i)) {
saveCurrentPageToHistory();
var redirectPath = xhr.getResponseHeader("HX-Location");
/** @type {import("./types").HtmxSwapSpecification} */
var swapSpec;
if (redirectPath.indexOf("{") === 0) {
swapSpec = parseJSON(redirectPath);
Expand Down
174 changes: 174 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* @fileoverview Internal types used inside htmx.js but are not part of
* the public API. The main benefit of having this file is that you can
* have better DX defining types in TypeScript and then just import them
* into the JsDoc comments in the htmx.js file. */

interface HtmxTriggerSpecification {
sseEvent?: string;
trigger: string;
root?: Element | Document | null;
threshold?: string;
delay?: number;
pollInterval?: number;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looked through and updated this with keys found in source:

export interface HtmxTriggerSpecification {
    trigger: string;
    sseEvent?: string;
    eventFilter?: string;
    changed?: boolean;
    once?: boolean;
    consume?: boolean;
    from?: string;
    target?: string;
    throttle?: number;
    queue?: string;
    root?: string;
    threshold?: string;
    delay?: number;
    pollInterval?: number;
};


export interface HtmxSwapSpecification {
swapStyle: SwapStyle;
swapDelay: number;
settleDelay: number;

scroll?: "top" | "bottom";
scrollTarget?: string | null;

show?: "top" | "bottom";
showTarget?: string | null;

transition?: boolean;
ignoreTitle?: boolean;
focusScroll?: boolean;
}

interface HtmxSettleInfo {
title?: string;
tasks?: (() => void)[];
elts?: Element[];
}

interface ListenerInfo {
listener: EventListener;
on: HTMLElement;
trigger: string;
}

/** the http verb used in the request (lowercase) */
type Verb = "get" | "post" | "put" | "delete" | "patch";

interface KnownInternalData {
initHash?: number | null;
listenerInfos?: ListenerInfo[];
path?: string;
streamPaused?: boolean;
streamReader?: ReadableStreamDefaultReader;
verb?: Verb;
lastButtonClicked?: Element | null;
timeout?: number;
webSocket?: WebSocket;
sseEventSource?: EventSource;
onHandlers?: { event: string; listener: EventListener }[];
xhr?: XMLHttpRequest;
requestCount?: number;
}

type InternalData = KnownInternalData & Record<PropertyKey, any>;

interface InputValues {
errors: any[];
values: Record<string, string>;
}

interface Pollable {
polling: boolean;
}

interface TriggerHandler {
(elt: Element, evt: Event): void;
(): void;
}

export type SwapStyle =
| "innerHTML"
| "outerHTML"
| "beforebegin"
| "afterbegin"
| "beforeend"
| "afterend"
| "delete"
| "none";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably add (string & NonNullable<unknown>) like this:

export type SwapStyle =
    | "innerHTML"
    | "outerHTML"
    | "afterbegin"
    | "beforebegin"
    | "afterend"
    | "beforeend"
    | "none"
    | "delete"
    | (string & NonNullable<unknown>);

because there are other swap styles (like morph:innerHTML when using idiomorph)

this way, editor autocomplete will still be there for known values, but any string can be passed.

export interface HtmxInternalApi {
addTriggerHandler(
elt: Element,
triggerSpec: HtmxTriggerSpecification,
nodeData: Pollable,
handler: TriggerHandler
): void;
bodyContains(elt: Node): boolean;
canAccessLocalStorage(): boolean;
findThisElement(elt: HTMLElement, attribute: string): HTMLElement | null;
filterValues(
inputValues: Record<string, string>,
elt: HTMLElement
): Record<string, string>;
hasAttribute(
elt: { hasAttribute: (arg0: string) => boolean },
qualifiedName: string
): boolean;
getAttributeValue(elt: HTMLElement, qualifiedName: string): string | null;
getClosestAttributeValue(
elt: HTMLElement,
attributeName: string
): string | null;
getClosestMatch(
elt: HTMLElement,
condition: (e: HTMLElement) => boolean
): HTMLElement | null;
getExpressionVars(elt: HTMLElement): Record<string, string>;
getHeaders(
elt: HTMLElement,
target: HTMLElement,
prompt: string
): Record<string, string>;
getInputValues(elt: HTMLElement, verb: string): InputValues;
getInternalData(elt: HTMLElement): InternalData;
getSwapSpecification(
elt: HTMLElement,
swapInfoOverride?: string
): HtmxSwapSpecification;
getTriggerSpecs(elt: HTMLElement): HtmxTriggerSpecification[];
getTarget(elt: HTMLElement): Element | null;
makeFragment(resp: any): Element | DocumentFragment;
mergeObjects<A extends object, B extends object>(obj1: A, obj2: B): A & B;
makeSettleInfo(target: Element): HtmxSettleInfo;
oobSwap(
oobValue: string,
oobElement: HTMLElement,
settleInfo: HtmxSettleInfo
): string;
querySelectorExt(eltOrSelector: any, selector: string): Element | null;
selectAndSwap(
swapStyle: SwapStyle,
target: Element | null,
elt: Element | null,
responseText: string,
settleInfo: HtmxSettleInfo,
selectOverride?: string | null
): void;
settleImmediately(tasks: { call: () => void }[]): void;
shouldCancel(evt: Event, elt: HTMLElement): boolean;
triggerEvent(
elt: Element | null,
eventName: string,
detail?: EventDetail
): boolean;
triggerErrorEvent(
elt: HTMLElement,
eventName: string,
detail?: EventDetail
): void;
withExtensions(
elt: HTMLElement,
toDo: (extension: import("./htmx").HtmxExtension) => void
): void;
}

type EventDetail = {
[key: PropertyKey]: unknown;
};

export declare function updateScrollState(
content,
swapSpec: HtmxSwapSpecification
): void;

export declare function splitOnWhitespace(trigger: string): string[];