Skip to content

Commit

Permalink
add queryAll function (#1156)
Browse files Browse the repository at this point in the history
* add queryAll function

* allow for either a function or patch object to be passed to both query and queryAll

* cleanup
  • Loading branch information
deebloo authored Jan 25, 2025
1 parent fd3213d commit 58e7905
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 40 deletions.
1 change: 1 addition & 0 deletions packages/element/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { attr } from "./lib/attr.js";
export { listen } from "./lib/listen.js";
export { element } from "./lib/element.js";
export { query } from "./lib/query.js";
export { queryAll } from "./lib/query-all.js";
export { ready } from "./lib/lifecycle.js";
export { attrChanged } from "./lib/attr-changed.js";
132 changes: 132 additions & 0 deletions packages/element/src/lib/query-all.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { expect } from "chai";

import { element } from "./element.js";
import { queryAll } from "./query-all.js";
import { html } from "./tags.js";

it("should work", () => {
@element({
tagName: "query-test-1",
shadowDom: [
html`
<form>
<input id="fname" name="fname" />
<input id="lname" name="lname" />
</form>
`,
],
})
class MyElement extends HTMLElement {
inputs = queryAll("input");
}

const el = new MyElement();

expect(el.inputs()[0]).to.equal(el.shadowRoot?.querySelector("#fname"));
expect(el.inputs()[1]).to.equal(el.shadowRoot?.querySelector("#lname"));
});

it("should patch items when patch is returned", () => {
@element({
tagName: "query-test-2",
shadowDom: [
html`
<form>
<input id="fname" name="fname" value="Danny" />
<input id="lname" name="lname" value="Blue" />
</form>
`,
],
})
class MyElement extends HTMLElement {
inputs = queryAll("input");
}

const el = new MyElement();

el.inputs((node) => {
if (node.id === "fname") {
return {
value: "Foo",
};
}

return null;
});

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#fname")?.value,
).to.equal("Foo");

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#lname")?.value,
).to.equal("Blue");
});

it("should patch the selected item when cached", () => {
@element({
tagName: "query-test-3",
shadowDom: [
html`
<form>
<input id="fname" name="fname" />
<input id="lname" name="lname" />
</form>
`,
],
})
class MyElement extends HTMLElement {
inputs = queryAll("input");
}

const el = new MyElement();
el.inputs();

el.inputs((node) => {
if (node.id === "fname") {
return {
value: "Foo",
};
}

return {
value: "Bar",
};
});

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#fname")?.value,
).to.equal("Foo");

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#lname")?.value,
).to.equal("Bar");
});

it("should apply the same patch to all elements", () => {
@element({
tagName: "query-test-4",
shadowDom: [
html`
<form>
<input id="fname" name="fname" />
<input id="lname" name="lname" />
</form>
`,
],
})
class MyElement extends HTMLElement {
inputs = queryAll("input");
}

const el = new MyElement();
el.inputs({ value: "TEST" });

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#fname")?.value,
).to.equal("TEST");

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#lname")?.value,
).to.equal("TEST");
});
74 changes: 74 additions & 0 deletions packages/element/src/lib/query-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
type Tags = keyof HTMLElementTagNameMap;
type SVGTags = keyof SVGElementTagNameMap;
type MathTags = keyof MathMLElementTagNameMap;

type NodeUpdate<T extends Node> = Partial<T> | ((node: T) => Partial<T> | null);

type QueryAllResult<T extends Node> = (
updates?: NodeUpdate<T>,
) => NodeListOf<T>;

export function queryAll<K extends Tags>(
selectors: K,
): QueryAllResult<HTMLElementTagNameMap[K]>;
export function queryAll<K extends SVGTags>(
selectors: K,
): QueryAllResult<SVGElementTagNameMap[K]>;
export function queryAll<K extends MathTags>(
selectors: K,
): QueryAllResult<MathMLElementTagNameMap[K]>;
export function queryAll<E extends HTMLElement = HTMLElement>(
selectors: string,
): QueryAllResult<E>;
export function queryAll<K extends Tags>(
query: K,
): QueryAllResult<HTMLElementTagNameMap[K]> {
let res: NodeListOf<HTMLElementTagNameMap[K]> | null = null;

return function (
this: HTMLElementTagNameMap[K],
update?: NodeUpdate<HTMLElementTagNameMap[K]>,
) {
if (res) {
return patchNodes(res, update);
}

if (this.shadowRoot) {
res = this.shadowRoot.querySelectorAll<K>(query);
} else {
res = this.querySelectorAll<K>(query);
}

if (!res) {
throw new Error(`could not find ${query}`);
}

return patchNodes(res, update);
};
}

function patchNodes<T extends HTMLElement>(
target: NodeListOf<T>,
update?: NodeUpdate<T>,
): NodeListOf<T> {
if (!update) {
return target;
}

for (const node of target) {
const patch = typeof update === "function" ? update(node) : update;

if (patch) {
for (const update in patch) {
const newValue = patch[update];
const oldValue = node[update];

if (newValue && newValue !== oldValue) {
node[update] = newValue;
}
}
}
}

return target;
}
92 changes: 61 additions & 31 deletions packages/element/src/lib/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,34 +57,64 @@ it("should patch the selected item", () => {
).to.equal("Bar");
});

// it("should patch the selected item when cached", () => {
// @element({
// tagName: "query-test-3",
// shadowDom: [
// html`
// <form>
// <input id="fname" name="fname" />
// <input id="lname" name="lname" />
// </form>
// `,
// ],
// })
// class MyElement extends HTMLElement {
// fname = query<HTMLInputElement>("#fname");
// lname = query<HTMLInputElement>("#lname");
// }

// const el = new MyElement();
// el.fname();
// el.lname();
// el.fname({ value: "Foo" });
// el.lname({ value: "Bar" });

// expect(
// el.shadowRoot?.querySelector<HTMLInputElement>("#fname")?.value,
// ).to.equal("Foo");

// expect(
// el.shadowRoot?.querySelector<HTMLInputElement>("#lname")?.value,
// ).to.equal("Bar");
// });
it("should patch the selected item when cached", () => {
@element({
tagName: "query-test-3",
shadowDom: [
html`
<form>
<input id="fname" name="fname" />
<input id="lname" name="lname" />
</form>
`,
],
})
class MyElement extends HTMLElement {
fname = query<HTMLInputElement>("#fname");
lname = query<HTMLInputElement>("#lname");
}

const el = new MyElement();
el.fname();
el.lname();
el.fname({ value: "Foo" });
el.lname({ value: "Bar" });

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#fname")?.value,
).to.equal("Foo");

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#lname")?.value,
).to.equal("Bar");
});

it("should use function to update", () => {
@element({
tagName: "query-test-4",
shadowDom: [
html`
<form>
<input id="fname" name="fname" />
<input id="lname" name="lname" />
</form>
`,
],
})
class MyElement extends HTMLElement {
fname = query<HTMLInputElement>("#fname");
lname = query<HTMLInputElement>("#lname");
}

const el = new MyElement();
el.fname(() => ({ value: "Foo" }));
el.lname(() => ({ value: "Bar" }));

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#fname")?.value,
).to.equal("Foo");

expect(
el.shadowRoot?.querySelector<HTMLInputElement>("#lname")?.value,
).to.equal("Bar");
});
25 changes: 16 additions & 9 deletions packages/element/src/lib/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ type Tags = keyof HTMLElementTagNameMap;
type SVGTags = keyof SVGElementTagNameMap;
type MathTags = keyof MathMLElementTagNameMap;

type QueryResult<T> = (updates?: Partial<T>) => T;
type NodeUpdate<T extends Node> = Partial<T> | ((node: T) => Partial<T>);

type QueryResult<T extends Node> = (updates?: NodeUpdate<T>) => T;

export function query<K extends Tags>(
selectors: K,
Expand All @@ -23,7 +25,7 @@ export function query<K extends Tags>(

return function (this: HTMLElementTagNameMap[K], updates) {
if (res) {
return patch(res, updates);
return patchNode(res, updates);
}

if (this.shadowRoot) {
Expand All @@ -36,21 +38,26 @@ export function query<K extends Tags>(
throw new Error(`could not find ${query}`);
}

return patch(res, updates);
return patchNode(res, updates);
};
}

function patch<T extends HTMLElement>(target: T, updates?: Partial<T>) {
if (!updates) {
function patchNode<T extends HTMLElement>(
target: T,
update?: Partial<T> | ((node: T) => Partial<T>),
): T {
if (!update) {
return target;
}

for (const update in updates) {
const newValue = updates[update];
const oldValue = target[update];
const patch = typeof update === "function" ? update(target) : update;

for (const key in patch) {
const newValue = patch[key];
const oldValue = target[key];

if (newValue && newValue !== oldValue) {
target[update] = newValue;
target[key] = newValue;
}
}

Expand Down

0 comments on commit 58e7905

Please sign in to comment.