update to v2.3.0

add `.rep()` & `.replaceChildren()` function
update `.app()` & `.append()` to support rest params
update build target to ES6
fix comment color on demo page
add (more) tests
This commit is contained in:
2025-09-13 19:44:55 -04:00
parent af2e0d2eec
commit 96c8f980e9
19 changed files with 732 additions and 101 deletions

4
dist/demo.html vendored
View File

@@ -148,6 +148,10 @@
/\`[^`]+\`/g, /\`[^`]+\`/g,
(val) => `<span class="code-str">${val}</span>`, (val) => `<span class="code-str">${val}</span>`,
) )
.replace(
/\/\/.*/g,
(val) => `<span class="code-cmnt">${val}</span>`,
)
.replace(/\.\w+/g, (val) => .replace(/\.\w+/g, (val) =>
val !== ".js" val !== ".js"
? `<span class="code-func">${val}</span>` ? `<span class="code-func">${val}</span>`

View File

@@ -4,22 +4,22 @@ const isHTMLElement = (value) => !!value.nodeType;
const isNumber = (value) => typeof value === "number"; const isNumber = (value) => typeof value === "number";
const isString = (value) => typeof value === "string"; const isString = (value) => typeof value === "string";
/** /**
* fluent-dom-esm v2.2.1 * fluent-dom-esm v2.3.0
* *
* Fluent DOM Manipulation, adapted to ESM and cranked up to v2.2(.1). * Fluent DOM Manipulation, adapted to ESM and cranked up to v2.3(.0).
* *
* https://git.itsericwoodward.com/eric/fluent-dom-esm * https://git.itsericwoodward.com/eric/fluent-dom-esm
* *
* v2.2.1 Copyright (c) 2025 Eric Woodward * v2.3.0 Copyright (c) 2025 Eric Woodward
* Original copyright (c) 2009 Tommy Montgomery (https://glacius.tmont.com/articles/fluent-dom-manipulation-in-javascript) * Original copyright (c) 2009 Tommy Montgomery (https://glacius.tmont.com/articles/fluent-dom-manipulation-in-javascript)
* *
* Released under the WTFPL (Do What the Fuck You Want to Public License) * Released under the WTFPL (Do What the Fuck You Want to Public License)
* *
* @author Eric Woodward (v2 update) * @author Eric Woodward (v2+)
* @author Tommy Montgomery (original) * @author Tommy Montgomery (original)
* @license http://sam.zoy.org/wtfpl/ * @license http://sam.zoy.org/wtfpl/
*/ */
const APP_VERSION = "2.2.1"; const APP_VERSION = "2.3.0";
const fluentDomEsm = (function() { const fluentDomEsm = (function() {
const FluentDom = function(nodeOrQuerySelector) { const FluentDom = function(nodeOrQuerySelector) {
if (typeof nodeOrQuerySelector !== "string") if (typeof nodeOrQuerySelector !== "string")
@@ -40,35 +40,43 @@ const fluentDomEsm = (function() {
let root = node || null; let root = node || null;
this.fluentDom = APP_VERSION; this.fluentDom = APP_VERSION;
this.a = this.attr = function(name, value) { this.a = this.attr = function(name, value) {
if (!root || typeof name === "undefined" || typeof value === "undefined") { if (!root) {
throw new Error("Cannot set an attribute on a non-element"); throw new Error("Cannot set an attribute on a non-element");
} }
if (typeof name === "undefined") {
throw new Error("Cannot set an attribute without a name");
}
root.setAttribute(name, value); root.setAttribute(name, value);
return this; return this;
}; };
this.app = this.append = function(value) { this.app = this.append = function(...content) {
if (!root || !root?.appendChild) { if (!root || !root?.appendChild) {
throw new Error("Cannot append to a non-element"); throw new Error("Cannot append to a non-element");
} }
if (!content || content.length < 1) {
throw new Error("Cannot append without a value");
}
content.forEach((value) => {
const type = typeof value; const type = typeof value;
if (type === "object") { if (type === "object") {
if (isFluentDomObject(value)) { if (isFluentDomObject(value)) {
const domVal = value.toDom(); const domVal = value.toDom();
if (domVal !== null) root.appendChild(domVal); if (domVal !== null) root?.appendChild(domVal);
} else if (isHTMLElement(value)) { } else if (isHTMLElement(value)) {
root.appendChild(value); root?.appendChild(value);
} else { } else {
throw new Error( throw new Error(
"Invalid argument: not an HTMLElement or FluentDom object" "Invalid argument: not an HTMLElement or FluentDom object"
); );
} }
} else if (isNumber(value) || isString(value)) { } else if (isNumber(value) || isString(value)) {
root.appendChild(document.createTextNode(`${value}`)); root?.appendChild(document.createTextNode(`${value}`));
} else { } else {
throw new Error( throw new Error(
`Invalid argument: not a valid type (${typeof value})` `Invalid argument: not a valid type (${typeof value})`
); );
} }
});
return this; return this;
}; };
this.c = this.create = function(tagName) { this.c = this.create = function(tagName) {
@@ -87,9 +95,7 @@ const fluentDomEsm = (function() {
}; };
this.html = function(content) { this.html = function(content) {
if (!root) { if (!root) {
throw new Error( throw new Error("Cannot set innerHTML for a non-element");
"Cannot get or set innerHTML for a non-element"
);
} }
root.innerHTML = content; root.innerHTML = content;
return this; return this;
@@ -108,6 +114,25 @@ const fluentDomEsm = (function() {
root = document.querySelector(selector); root = document.querySelector(selector);
return this; return this;
}; };
this.rep = this.replaceChildren = function(...content) {
if (!root) {
throw new Error("Cannot replaceChildren on a non-element");
}
if (!root.replaceChildren) {
if (!content) return this.html("");
return this.html("").append(...content);
}
if (content.length && content[0] && isFluentDomObject(content[0])) {
const domValues = content.map(
(val) => val.toDom()
);
root.replaceChildren(
...domValues.filter((val) => val !== null)
);
}
root.replaceChildren(...content);
return this;
};
function styleFunction(prop, value) { function styleFunction(prop, value) {
if (!root) { if (!root) {
throw new Error("Cannot get or set style for a non-element"); throw new Error("Cannot get or set style for a non-element");
@@ -150,7 +175,7 @@ const fluentDomEsm = (function() {
return this; return this;
}; };
this.v = this.version = function() { this.v = this.version = function() {
return APP_VERSION; return this.fluentDom;
}; };
}; };
return FluentDom; return FluentDom;

View File

@@ -1,9 +1,9 @@
export interface FluentDomObject { export interface FluentDomObject {
(nodeOrQuerySelector: string | HTMLElement): FluentDomObject; (nodeOrQuerySelector: HTMLElement | string): FluentDomObject;
fluentDom: string; fluentDom: string;
a: (name: string, value: string) => FluentDomObject; a: (name: string, value: string) => FluentDomObject;
app: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; app: (...content: (FluentDomObject | HTMLElement | string)[]) => FluentDomObject;
append: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; append: (...content: (FluentDomObject | HTMLElement | string)[]) => FluentDomObject;
attr: (name: string, value: string) => FluentDomObject; attr: (name: string, value: string) => FluentDomObject;
c: (tagName: string) => FluentDomObject; c: (tagName: string) => FluentDomObject;
className: (className: string) => FluentDomObject; className: (className: string) => FluentDomObject;
@@ -19,6 +19,8 @@ export interface FluentDomObject {
listen: (type: keyof HTMLElementEventMap, listener: () => {}, optionsOrUseCapture?: boolean | object) => FluentDomObject; listen: (type: keyof HTMLElementEventMap, listener: () => {}, optionsOrUseCapture?: boolean | object) => FluentDomObject;
q: (selector: string) => FluentDomObject; q: (selector: string) => FluentDomObject;
querySelector: (selector: string) => FluentDomObject; querySelector: (selector: string) => FluentDomObject;
rep: (...content: (FluentDomObject | HTMLElement | string)[]) => FluentDomObject;
replaceChildren: (...content: (FluentDomObject | HTMLElement | string)[]) => FluentDomObject;
s: ((prop: CSSStyleDeclaration) => FluentDomObject) | ((prop: string, value: string) => FluentDomObject); s: ((prop: CSSStyleDeclaration) => FluentDomObject) | ((prop: string, value: string) => FluentDomObject);
style: ((prop: CSSStyleDeclaration) => FluentDomObject) | ((prop: string, value: string) => FluentDomObject); style: ((prop: CSSStyleDeclaration) => FluentDomObject) | ((prop: string, value: string) => FluentDomObject);
t: (text: string) => FluentDomObject; t: (text: string) => FluentDomObject;

View File

@@ -1,5 +1,5 @@
{ {
"name": "@itsericwoodward/fluent-dom-esm", "name": "@itsericwoodward/fluent-dom-esm",
"version": "2.2.1", "version": "2.3.0",
"exports": "./src/fluent-dom-esm.ts" "exports": "./src/fluent-dom-esm.ts"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "fluent-dom-esm", "name": "fluent-dom-esm",
"version": "2.2.1", "version": "2.3.0",
"description": "", "description": "",
"license": "WTFPL", "license": "WTFPL",
"exports": { "exports": {

View File

@@ -148,6 +148,10 @@
/\`[^`]+\`/g, /\`[^`]+\`/g,
(val) => `<span class="code-str">${val}</span>`, (val) => `<span class="code-str">${val}</span>`,
) )
.replace(
/\/\/.*/g,
(val) => `<span class="code-cmnt">${val}</span>`,
)
.replace(/\.\w+/g, (val) => .replace(/\.\w+/g, (val) =>
val !== ".js" val !== ".js"
? `<span class="code-func">${val}</span>` ? `<span class="code-func">${val}</span>`

View File

@@ -0,0 +1,219 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it } from "vitest";
import $d from "../fluent-dom-esm";
describe(".app() and .append()", () => {
beforeEach(() => {
document.body.replaceChildren();
});
it("should support single FluentDomObject argument", () => {
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add child
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "item1");
// add child
$d(document.body).app($d.c("div").id("item2").t("Item 2"));
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[1]).toHaveAttribute("id", "item2");
// reset
body?.replaceChildren();
// add child
$d(document.body).append($d.c("div").id("item1").t("Item 1"));
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "item1");
// add child
$d(document.body).append($d.c("div").id("item2").t("Item 2"));
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[1]).toHaveAttribute("id", "item2");
});
it("should support multiple FluentDomObject arguments for `.app()` and `.append()`", () => {
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add children
$d(document.body).app(
$d.c("div").id("item1").t("Item 1"),
$d.c("div").id("item2").t("Item 2"),
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[0]).toHaveAttribute("id", "item1");
expect(body?.children[1]).toHaveAttribute("id", "item2");
// reset
body?.replaceChildren();
// add children
$d(document.body).append(
$d.c("div").id("item1").t("Item 1"),
$d.c("div").id("item2").t("Item 2"),
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[0]).toHaveAttribute("id", "item1");
expect(body?.children[1]).toHaveAttribute("id", "item2");
});
it("should support single HTMLElement argument for `.app()` and `.append()`", () => {
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add child
$d(document.body).app(
$d.c("div").id("item1").t("Item 1").toDom() ?? "",
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "item1");
// add child
$d(document.body).app(
$d.c("div").id("item2").t("Item 2").toDom() ?? "",
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[1]).toHaveAttribute("id", "item2");
// reset
body?.replaceChildren();
// add child
$d(document.body).append(
$d.c("div").id("item1").t("Item 1").toDom() ?? "",
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "item1");
// add child
$d(document.body).append(
$d.c("div").id("item2").t("Item 2").toDom() ?? "",
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[1]).toHaveAttribute("id", "item2");
});
it("should support multiple HTMLElement arguments for `.app()` and `.append()`", () => {
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add children
$d(document.body).app(
$d.c("div").id("item1").t("Item 1").toDom() ?? "",
$d.c("div").id("item2").t("Item 2").toDom() ?? "",
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[0]).toHaveAttribute("id", "item1");
expect(body?.children[1]).toHaveAttribute("id", "item2");
// reset
body?.replaceChildren();
// add child
$d(document.body).append(
$d.c("div").id("item1").t("Item 1").toDom() ?? "",
$d.c("div").id("item2").t("Item 2").toDom() ?? "",
);
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(2);
expect(body?.children[0]).toHaveAttribute("id", "item1");
expect(body?.children[1]).toHaveAttribute("id", "item2");
});
it("should support single string argument for `.app()` and `.append()`", () => {
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add child
$d(document.body).append("Item 1");
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(0);
// add child
$d(document.body).app("Item 2");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(0);
// reset
body?.replaceChildren();
// add child
$d(document.body).append("Item 1");
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(0);
// add child
$d(document.body).app("Item 2");
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(0);
});
it("should support multiple string arguments for `.app()` and `.append()`", () => {
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add children
$d(document.body).append("Item 1", "Item 2");
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(0);
// reset
body?.replaceChildren();
// add child
$d(document.body).append("Item 1", "Item 2");
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body).toHaveTextContent("Item 2");
expect(body?.children.length).toEqual(0);
});
});

View File

@@ -0,0 +1,67 @@
// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import $d from "../fluent-dom-esm";
describe(".a() and .attr()", () => {
it("adds an attribute to the wrapped HTMLElement", () => {
// create base anchor
const $a1 = $d.c("a");
expect($a1.toDom()).not.toBeNull();
expect($a1.toDom()).toBeEmptyDOMElement();
expect($a1.toDom()?.outerHTML).toEqual("<a></a>");
expect($a1.toDom()?.getAttribute("href")).toBeNull();
// add href attribute
$a1.a("href", "http://example.com");
expect($a1.toDom()?.getAttribute("href")).not.toBeNull();
expect($a1.toDom()?.getAttribute("href")).toEqual("http://example.com");
// create base anchor
const $a2 = $d.c("a");
expect($a2.toDom()).not.toBeNull();
expect($a2.toDom()).toBeEmptyDOMElement();
expect($a2.toDom()?.outerHTML).toEqual("<a></a>");
expect($a2.toDom()?.getAttribute("href")).toBeNull();
// add href attribute
$a2.attr("href", "mailto:someone@somewhere.com");
expect($a2.toDom()?.getAttribute("href")).not.toBeNull();
expect($a2.toDom()?.getAttribute("href")).toEqual(
"mailto:someone@somewhere.com",
);
});
it("disables a form element when the 'disabled' attribute is set", () => {
const $button1 = $d.c("button").t("click me");
expect($button1.toDom()).not.toBeNull();
expect($button1.toDom()).not.toBeDisabled();
$button1.a("disabled", "");
expect($button1.toDom()).toBeDisabled();
$button1.a("name", "button1");
expect($button1.toDom()?.getAttribute("name")).toEqual("button1");
const $button2 = $d.c("button").t("click me");
expect($button2.toDom()).not.toBeNull();
expect($button2.toDom()).not.toBeDisabled();
$button2.attr("disabled", "");
expect($button2.toDom()).toBeDisabled();
$button1.attr("name", "button2");
expect($button1.toDom()?.getAttribute("name")).toEqual("button2");
});
it("updates the HTMLElements dataset when a data attribute is set", () => {
const $div1 = $d.c("div");
expect($div1.toDom()).not.toBeNull();
expect($div1.toDom()?.dataset).toEqual({});
$div1.a("data-test", "some data");
expect($div1.toDom()?.dataset).toEqual({ test: "some data" });
const $div2 = $d.c("div");
expect($div2.toDom()).not.toBeNull();
expect($div2.toDom()?.dataset).toEqual({});
$div2.a("data-test", "some more data");
expect($div2.toDom()?.dataset).toEqual({ test: "some more data" });
});
});

View File

@@ -0,0 +1,50 @@
// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import $d from "../fluent-dom-esm";
describe(".c() and .create()", () => {
it("should create an FluentDomObject-wrapped HTMLElement", () => {
const $div1 = $d(document.body).c("div");
expect($div1.toDom()).not.toBeNull();
expect($div1.toDom()).toBeEmptyDOMElement();
expect($div1.toDom()?.outerHTML).toEqual("<div></div>");
expect($div1.fluentDom).toBeDefined();
expect($div1.fluentDom).toEqual($div1.version());
const $span1 = $d(document.body).c("span");
expect($span1.toDom()).not.toBeNull();
expect($span1.toDom()).toBeEmptyDOMElement();
expect($span1.toDom()?.outerHTML).toEqual("<span></span>");
expect($span1.fluentDom).toBeDefined();
expect($span1.fluentDom).toEqual($span1.version());
const $div2 = $d(document.body).create("div");
expect($div2.toDom()).not.toBeNull();
expect($div2.toDom()).toBeEmptyDOMElement();
expect($div2.toDom()?.outerHTML).toEqual("<div></div>");
expect($div2.fluentDom).toBeDefined();
expect($div2.fluentDom).toEqual($div2.version());
const $span2 = $d(document.body).create("span");
expect($span2.toDom()).not.toBeNull();
expect($span2.toDom()).toBeEmptyDOMElement();
expect($span2.toDom()?.outerHTML).toEqual("<span></span>");
expect($span2.fluentDom).toBeDefined();
expect($span2.fluentDom).toEqual($span2.version());
});
it("should replace the current object with a new object", () => {
// default state
let $el = $d(document.body);
expect($el.toDom()).not.toBeNull();
expect($el.toDom()).toBeEmptyDOMElement();
expect($el.toDom()?.outerHTML).toEqual("<body></body>");
$el = $el.c("ul");
expect($el.toDom()).not.toBeNull();
expect($el.toDom()).toBeEmptyDOMElement();
expect($el.toDom()?.outerHTML).toEqual("<ul></ul>");
});
});

View File

@@ -0,0 +1,67 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it } from "vitest";
import $d from "../fluent-dom-esm";
describe("base object", () => {
beforeEach(() => {
document.body.replaceChildren();
});
it("should allow for wrapping an existing node", () => {
const $body = $d(document.body);
$d(document.body).app($d.c("div").cls("test-class").t("text content"));
expect($body.toDom()?.innerHTML).toEqual(
'<div class="test-class">text content</div>',
);
expect(document.body?.innerHTML).toEqual(
'<div class="test-class">text content</div>',
);
});
it("should allow for wrapping an existing node via a querySelector for an ID", () => {
$d(document.body)
.app($d.c("div").id("item1").t("Item 1"))
.app($d.c("div").id("item2").t("Item 2"))
.app($d.c("div").id("item3").cls("item-num3").t("Item 3"));
expect($d("#item2").toDom()).toHaveTextContent("Item 2");
expect($d("#item3").toDom()).toHaveClass("item-num3");
});
it("should create an FluentDomObject-wrapped HTMLElement via .c() and .create()", () => {
const $div1 = $d.c("div");
expect($div1.toDom()).not.toBeNull();
expect($div1.toDom()).toBeEmptyDOMElement();
expect($div1.toDom()?.outerHTML).toEqual("<div></div>");
expect($div1.fluentDom).toBeDefined();
expect($div1.fluentDom).toEqual($div1.version());
const $span1 = $d.c("span");
expect($span1.toDom()).not.toBeNull();
expect($span1.toDom()).toBeEmptyDOMElement();
expect($span1.toDom()?.outerHTML).toEqual("<span></span>");
expect($span1.fluentDom).toBeDefined();
expect($span1.fluentDom).toEqual($span1.version());
const $div2 = $d.create("div");
expect($div2.toDom()).not.toBeNull();
expect($div2.toDom()).toBeEmptyDOMElement();
expect($div2.toDom()?.outerHTML).toEqual("<div></div>");
expect($div2.fluentDom).toBeDefined();
expect($div2.fluentDom).toEqual($div2.version());
const $span2 = $d.create("span");
expect($span2.toDom()).not.toBeNull();
expect($span2.toDom()).toBeEmptyDOMElement();
expect($span2.toDom()?.outerHTML).toEqual("<span></span>");
expect($span2.fluentDom).toBeDefined();
expect($span2.fluentDom).toEqual($span2.version());
});
it("should return the FluentDomObject version via .v() and .version()", () => {
const $div = $d.c("div");
expect($d.v()).toEqual($div.fluentDom);
expect($d.version()).toEqual($div.fluentDom);
});
});

View File

@@ -0,0 +1,135 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it } from "vitest";
import $d from "../fluent-dom-esm";
describe("fluent-dom-esm tests", () => {
beforeEach(() => {
document.body.replaceChildren();
});
it("should support no arguments for `.rep()` and `.replaceChildren()`", () => {
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect($d(document.body).rep().toDom()).toBeEmptyDOMElement();
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect(
$d(document.body).replaceChildren().toDom(),
).toBeEmptyDOMElement();
});
it("should support an empty array for `.rep()` and `.replaceChildren()`", () => {
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect(
$d(document.body)
.rep(...[])
.toDom(),
).toBeEmptyDOMElement();
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect(
$d(document.body)
.replaceChildren(...[])
.toDom(),
).toBeEmptyDOMElement();
});
it("should support HTMLElement arguments for `.rep()` and `.replaceChildren()`", () => {
const newChild1 =
$d.c("div").id("new-child1").t("New Child 1").toDom() ?? "";
const newChild2 =
$d.c("div").id("new-child2").t("New Child 2").toDom() ?? "";
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add child
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "item1");
// replace with one child
$d(document.body).rep(newChild1);
expect(body).not.toBeEmptyDOMElement();
expect(body).not.toHaveTextContent("Item 1");
expect(body).toHaveTextContent("New Child 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "new-child1");
// replace with two children
$d(document.body).rep(newChild1, newChild2);
expect(body).not.toBeEmptyDOMElement();
expect(body?.children.length).toEqual(2);
expect(body?.children[0]).toHaveAttribute("id", "new-child1");
expect(body?.children[0]).toHaveAttribute("id", "new-child1");
expect(body).toHaveTextContent("New Child 1");
expect(body).toHaveTextContent("New Child 2");
// replace with one child
$d(document.body).replaceChildren(newChild1);
expect(body).not.toBeEmptyDOMElement();
expect(body).not.toHaveTextContent("Item 1");
expect(body).toHaveTextContent("New Child 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "new-child1");
// replace with two children
$d(document.body).replaceChildren(newChild1, newChild2);
expect(body).not.toBeEmptyDOMElement();
expect(body?.children.length).toEqual(2);
expect(body?.children[0]).toHaveAttribute("id", "new-child1");
expect(body?.children[0]).toHaveAttribute("id", "new-child1");
expect(body).toHaveTextContent("New Child 1");
expect(body).toHaveTextContent("New Child 2");
});
it("should support string arguments for `.rep()` and `.replaceChildren()`", () => {
const newChild1 = "New Child 1";
const newChild2 = "New Child 2";
// default state
const body = $d(document.body).toDom();
expect(body).not.toBeNull();
expect(body).toBeEmptyDOMElement();
// add child
$d(document.body).app($d.c("div").id("item1").t("Item 1"));
expect(body).not.toBeEmptyDOMElement();
expect(body).toHaveTextContent("Item 1");
expect(body?.children.length).toEqual(1);
expect(body?.children[0]).toHaveAttribute("id", "item1");
// replace with one child
$d(document.body).rep(newChild1);
expect(body).not.toBeEmptyDOMElement();
expect(body).not.toHaveTextContent("Item 1");
expect(body).toHaveTextContent("New Child 1");
expect(body?.children.length).toEqual(0);
// replace with two children
$d(document.body).rep(newChild1, newChild2);
expect(body).not.toBeEmptyDOMElement();
expect(body?.children.length).toEqual(0);
expect(body).toHaveTextContent("New Child 1");
expect(body).toHaveTextContent("New Child 2");
// replace with one child
$d(document.body).replaceChildren(newChild1);
expect(body).not.toBeEmptyDOMElement();
expect(body).not.toHaveTextContent("Item 1");
expect(body).toHaveTextContent("New Child 1");
expect(body?.children.length).toEqual(0);
// replace with two children
$d(document.body).replaceChildren(newChild1, newChild2);
expect(body).not.toBeEmptyDOMElement();
expect(body?.children.length).toEqual(0);
expect(body).toHaveTextContent("New Child 1");
expect(body).toHaveTextContent("New Child 2");
});
});

View File

@@ -5,28 +5,43 @@ import { describe, expect, it } from "vitest";
import $d from "../fluent-dom-esm"; import $d from "../fluent-dom-esm";
describe("fluent-dom-esm tests", () => { describe("fluent-dom-esm tests", () => {
it("should allow for wrapping an existing node", () => { it("should update an anchor's href via .h() and .href()", () => {
const $body = $d(document.body); // create base anchor
$d(document.body).app($d.c("div").cls("test-class").t("text content")); const $a1 = $d.c("a");
expect($body.toDom()?.innerHTML).toEqual( expect($a1.toDom()).not.toBeNull();
'<div class="test-class">text content</div>', expect($a1.toDom()).toBeEmptyDOMElement();
); expect($a1.toDom()?.outerHTML).toEqual("<a></a>");
expect(document.body?.innerHTML).toEqual( expect($a1.toDom()?.getAttribute("href")).toBeNull();
'<div class="test-class">text content</div>',
// add href attribute
$a1.h("http://example.com");
expect($a1.toDom()?.getAttribute("href")).not.toBeNull();
expect($a1.toDom()?.getAttribute("href")).toEqual("http://example.com");
// create base anchor
const $a2 = $d.c("a");
expect($a2.toDom()).not.toBeNull();
expect($a2.toDom()).toBeEmptyDOMElement();
expect($a2.toDom()?.outerHTML).toEqual("<a></a>");
expect($a2.toDom()?.getAttribute("href")).toBeNull();
// add href attribute
$a2.href("mailto:someone@somewhere.com");
expect($a2.toDom()?.getAttribute("href")).not.toBeNull();
expect($a2.toDom()?.getAttribute("href")).toEqual(
"mailto:someone@somewhere.com",
); );
}); });
it("should allow for wrapping an existing node via a querySelector for an ID", () => { it("should append text provided via `.t()` and `.text()`", () => {
$d(document.body)
.app($d.c("div").id("item1").t("Item 1"))
.app($d.c("div").id("item2").t("Item 2"))
.app($d.c("div").id("item3").cls("item-num3").t("Item 3"));
expect($d("#item2").toDom()).toHaveTextContent("Item 2");
expect($d("#item3").toDom()).toHaveClass("item-num3");
});
it("should append text provided via `.text()`", () => {
const testVal = "testing 123"; const testVal = "testing 123";
expect($d.c("div").t(testVal).toDom()).toHaveTextContent(testVal); expect($d.c("div").t(testVal).toDom()).toHaveTextContent(testVal);
expect($d.c("div").text(testVal).toDom()).toHaveTextContent(testVal);
});
it("should return the FluentDomObject version via .v() and .version()", () => {
const $div = $d.c("div");
expect($div.v()).toEqual($div.fluentDom);
expect($div.version()).toEqual($div.fluentDom);
}); });
}); });

View File

@@ -1,16 +1,16 @@
/** /**
* fluent-dom-esm v2.2.1 * fluent-dom-esm v2.3.0
* *
* Fluent DOM Manipulation, adapted to ESM and cranked up to v2.2(.1). * Fluent DOM Manipulation, adapted to ESM and cranked up to v2.3(.0).
* *
* https://git.itsericwoodward.com/eric/fluent-dom-esm * https://git.itsericwoodward.com/eric/fluent-dom-esm
* *
* v2.2.1 Copyright (c) 2025 Eric Woodward * v2.3.0 Copyright (c) 2025 Eric Woodward
* Original copyright (c) 2009 Tommy Montgomery (https://glacius.tmont.com/articles/fluent-dom-manipulation-in-javascript) * Original copyright (c) 2009 Tommy Montgomery (https://glacius.tmont.com/articles/fluent-dom-manipulation-in-javascript)
* *
* Released under the WTFPL (Do What the Fuck You Want to Public License) * Released under the WTFPL (Do What the Fuck You Want to Public License)
* *
* @author Eric Woodward (v2 update) * @author Eric Woodward (v2+)
* @author Tommy Montgomery (original) * @author Tommy Montgomery (original)
* @license http://sam.zoy.org/wtfpl/ * @license http://sam.zoy.org/wtfpl/
*/ */
@@ -24,7 +24,7 @@ import {
isString, isString,
} from "./fluent-dom-esm.type-guards"; } from "./fluent-dom-esm.type-guards";
const APP_VERSION = "2.2.1"; const APP_VERSION = "2.3.0";
/** /**
* IIFE that creates the FluentDomObject as default export * IIFE that creates the FluentDomObject as default export
@@ -33,7 +33,7 @@ export default (function () {
/** /**
* Wraps provided element in a FluentDomObject which is then returned * Wraps provided element in a FluentDomObject which is then returned
*/ */
const FluentDom = function (nodeOrQuerySelector: string | HTMLElement) { const FluentDom = function (nodeOrQuerySelector: HTMLElement | string) {
if (typeof nodeOrQuerySelector !== "string") if (typeof nodeOrQuerySelector !== "string")
return new (FluentDomInternal as any)(nodeOrQuerySelector); return new (FluentDomInternal as any)(nodeOrQuerySelector);
@@ -74,13 +74,12 @@ export default (function () {
* Sets the named attribute on the wrapped HTMLElement * Sets the named attribute on the wrapped HTMLElement
*/ */
this.a = this.attr = function (name, value) { this.a = this.attr = function (name, value) {
if ( if (!root) {
!root ||
typeof name === "undefined" ||
typeof value === "undefined"
) {
throw new Error("Cannot set an attribute on a non-element"); throw new Error("Cannot set an attribute on a non-element");
} }
if (typeof name === "undefined") {
throw new Error("Cannot set an attribute without a name");
}
root.setAttribute(name, value); root.setAttribute(name, value);
return this; return this;
@@ -89,30 +88,36 @@ export default (function () {
/** /**
* Appends value to wrapped HTMLElement * Appends value to wrapped HTMLElement
*/ */
this.app = this.append = function (value) { this.app = this.append = function (...content) {
if (!root || !root?.appendChild) { if (!root || !root?.appendChild) {
throw new Error("Cannot append to a non-element"); throw new Error("Cannot append to a non-element");
} }
if (!content || content.length < 1) {
throw new Error("Cannot append without a value");
}
content.forEach((value) => {
const type = typeof value; const type = typeof value;
if (type === "object") { if (type === "object") {
if (isFluentDomObject(value)) { if (isFluentDomObject(value)) {
const domVal = value.toDom(); const domVal = value.toDom();
if (domVal !== null) root.appendChild(domVal); if (domVal !== null) root?.appendChild(domVal);
} else if (isHTMLElement(value)) { } else if (isHTMLElement(value)) {
root.appendChild(value); root?.appendChild(value);
} else { } else {
throw new Error( throw new Error(
"Invalid argument: not an HTMLElement or FluentDom object", "Invalid argument: not an HTMLElement or FluentDom object",
); );
} }
} else if (isNumber(value) || isString(value)) { } else if (isNumber(value) || isString(value)) {
root.appendChild(document.createTextNode(`${value}`)); root?.appendChild(document.createTextNode(`${value}`));
} else { } else {
throw new Error( throw new Error(
`Invalid argument: not a valid type (${typeof value})`, `Invalid argument: not a valid type (${typeof value})`,
); );
} }
});
return this; return this;
}; };
@@ -150,11 +155,9 @@ export default (function () {
/** /**
* Sets the wrapped HTMLElement's "innerHTML" property * Sets the wrapped HTMLElement's "innerHTML" property
*/ */
this.html = function (content: string) { this.html = function (content) {
if (!root) { if (!root) {
throw new Error( throw new Error("Cannot set innerHTML for a non-element");
"Cannot get or set innerHTML for a non-element",
);
} }
root.innerHTML = content; root.innerHTML = content;
@@ -180,11 +183,43 @@ export default (function () {
return this; return this;
}; };
this.q = this.querySelector = function (selector: string) { /**
* Sets wrapped HTMLElement to matching querySelector
*/
this.q = this.querySelector = function (selector) {
root = document.querySelector(selector); root = document.querySelector(selector);
return this; return this;
}; };
/**
* Replaces the children of the wrapped HTMLElement
*/
this.rep = this.replaceChildren = function (...content) {
if (!root) {
throw new Error("Cannot replaceChildren on a non-element");
}
// fallback to using `this.html()` and `this.append()`
if (!root.replaceChildren) {
if (!content) return this.html("");
return this.html("").append(...content);
}
if (content.length && content[0] && isFluentDomObject(content[0])) {
const domValues = content.map((val) =>
(val as FluentDomObject).toDom(),
);
root.replaceChildren(
...domValues.filter((val) => val !== null),
);
}
root.replaceChildren(...(content as (HTMLElement | string)[]));
return this;
};
/** /**
* Sets one or more style attributes on the wrapped HTMLElement * Sets one or more style attributes on the wrapped HTMLElement
*/ */
@@ -272,7 +307,7 @@ export default (function () {
* Returns library version * Returns library version
*/ */
this.v = this.version = function () { this.v = this.version = function () {
return APP_VERSION; return this.fluentDom;
}; };
}; };

View File

@@ -1,10 +1,14 @@
export interface FluentDomObject { export interface FluentDomObject {
(nodeOrQuerySelector: string | HTMLElement): FluentDomObject; (nodeOrQuerySelector: HTMLElement | string): FluentDomObject;
fluentDom: string; fluentDom: string;
a: (name: string, value: string) => FluentDomObject; a: (name: string, value: string) => FluentDomObject;
app: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; app: (
append: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; ...content: (FluentDomObject | HTMLElement | string)[]
) => FluentDomObject;
append: (
...content: (FluentDomObject | HTMLElement | string)[]
) => FluentDomObject;
attr: (name: string, value: string) => FluentDomObject; attr: (name: string, value: string) => FluentDomObject;
c: (tagName: string) => FluentDomObject; c: (tagName: string) => FluentDomObject;
@@ -34,6 +38,13 @@ export interface FluentDomObject {
q: (selector: string) => FluentDomObject; q: (selector: string) => FluentDomObject;
querySelector: (selector: string) => FluentDomObject; querySelector: (selector: string) => FluentDomObject;
rep: (
...content: (FluentDomObject | HTMLElement | string)[]
) => FluentDomObject;
replaceChildren: (
...content: (FluentDomObject | HTMLElement | string)[]
) => FluentDomObject;
s: s:
| ((prop: CSSStyleDeclaration) => FluentDomObject) | ((prop: CSSStyleDeclaration) => FluentDomObject)
| ((prop: string, value: string) => FluentDomObject); | ((prop: string, value: string) => FluentDomObject);

View File

@@ -1,9 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2022", "target": "es6",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "es6",
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["es6", "DOM", "DOM.Iterable"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */

View File

@@ -6,14 +6,18 @@ import { defineConfig } from 'vite';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({ export default defineConfig({
plugins: [dts({exclude: ["./setupTests.ts", "**/*.test.ts"]})], plugins: [dts({exclude: ["./vitest.setup.ts", "**/*.test.ts"]})],
build: { build: {
lib: { lib: {
entry: resolve(__dirname, 'src/fluent-dom-esm.ts'), entry: resolve(__dirname, 'src/fluent-dom-esm.ts'),
name: 'fluent-dom-esm', name: 'fluent-dom-esm',
fileName: 'fluent-dom-esm', fileName: 'fluent-dom-esm',
formats: ['es'] formats: ['es'],
outDir: './dist'
}, },
minify: false minify: false
} },
test: {
setupFiles: ["./vitest.setup.ts"]
},
}); });

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'vite';
export default defineConfig({
test: {
setupFiles: ["./setupTests.ts"],
},
})

View File

@@ -809,14 +809,14 @@ __metadata:
linkType: hard linkType: hard
"debug@npm:4, debug@npm:^4.3.4, debug@npm:^4.4.0, debug@npm:^4.4.1": "debug@npm:4, debug@npm:^4.3.4, debug@npm:^4.4.0, debug@npm:^4.4.1":
version: 4.4.1 version: 4.4.3
resolution: "debug@npm:4.4.1" resolution: "debug@npm:4.4.3"
dependencies: dependencies:
ms: "npm:^2.1.3" ms: "npm:^2.1.3"
peerDependenciesMeta: peerDependenciesMeta:
supports-color: supports-color:
optional: true optional: true
checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6
languageName: node languageName: node
linkType: hard linkType: hard