From 96c8f980e96e79104b4de58f9c0dacd5846645bb Mon Sep 17 00:00:00 2001 From: Eric Woodward Date: Sat, 13 Sep 2025 19:44:55 -0400 Subject: [PATCH] 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 --- dist/demo.html | 4 + dist/fluent-dom-esm.js | 77 +++--- dist/fluent-dom-esm.types.d.ts | 8 +- jsr.json | 2 +- package.json | 2 +- public/demo.html | 4 + src/__tests__/fluent-dom-esm.append.test.ts | 219 ++++++++++++++++++ src/__tests__/fluent-dom-esm.attr.test.ts | 67 ++++++ src/__tests__/fluent-dom-esm.create.test.ts | 50 ++++ src/__tests__/fluent-dom-esm.object.test.ts | 67 ++++++ .../fluent-dom-esm.replaceChildren.test.ts | 135 +++++++++++ src/__tests__/fluent-dom-esm.test.ts | 51 ++-- src/fluent-dom-esm.ts | 101 +++++--- src/fluent-dom-esm.types.ts | 17 +- tsconfig.json | 6 +- vite.config.js | 10 +- vitest.config.js | 7 - setupTests.ts => vitest.setup.ts | 0 yarn.lock | 6 +- 19 files changed, 732 insertions(+), 101 deletions(-) create mode 100644 src/__tests__/fluent-dom-esm.append.test.ts create mode 100644 src/__tests__/fluent-dom-esm.attr.test.ts create mode 100644 src/__tests__/fluent-dom-esm.create.test.ts create mode 100644 src/__tests__/fluent-dom-esm.object.test.ts create mode 100644 src/__tests__/fluent-dom-esm.replaceChildren.test.ts delete mode 100644 vitest.config.js rename setupTests.ts => vitest.setup.ts (100%) diff --git a/dist/demo.html b/dist/demo.html index 0d1dbbd..8cdee19 100644 --- a/dist/demo.html +++ b/dist/demo.html @@ -148,6 +148,10 @@ /\`[^`]+\`/g, (val) => `${val}`, ) + .replace( + /\/\/.*/g, + (val) => `${val}`, + ) .replace(/\.\w+/g, (val) => val !== ".js" ? `${val}` diff --git a/dist/fluent-dom-esm.js b/dist/fluent-dom-esm.js index 21ba35c..2913972 100644 --- a/dist/fluent-dom-esm.js +++ b/dist/fluent-dom-esm.js @@ -4,22 +4,22 @@ const isHTMLElement = (value) => !!value.nodeType; const isNumber = (value) => typeof value === "number"; 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 * - * 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) * * 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) * @license http://sam.zoy.org/wtfpl/ */ -const APP_VERSION = "2.2.1"; +const APP_VERSION = "2.3.0"; const fluentDomEsm = (function() { const FluentDom = function(nodeOrQuerySelector) { if (typeof nodeOrQuerySelector !== "string") @@ -40,35 +40,43 @@ const fluentDomEsm = (function() { let root = node || null; this.fluentDom = APP_VERSION; 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"); } + if (typeof name === "undefined") { + throw new Error("Cannot set an attribute without a name"); + } root.setAttribute(name, value); return this; }; - this.app = this.append = function(value) { + this.app = this.append = function(...content) { if (!root || !root?.appendChild) { throw new Error("Cannot append to a non-element"); } - const type = typeof value; - if (type === "object") { - if (isFluentDomObject(value)) { - const domVal = value.toDom(); - if (domVal !== null) root.appendChild(domVal); - } else if (isHTMLElement(value)) { - root.appendChild(value); + if (!content || content.length < 1) { + throw new Error("Cannot append without a value"); + } + content.forEach((value) => { + const type = typeof value; + if (type === "object") { + if (isFluentDomObject(value)) { + const domVal = value.toDom(); + if (domVal !== null) root?.appendChild(domVal); + } else if (isHTMLElement(value)) { + root?.appendChild(value); + } else { + throw new Error( + "Invalid argument: not an HTMLElement or FluentDom object" + ); + } + } else if (isNumber(value) || isString(value)) { + root?.appendChild(document.createTextNode(`${value}`)); } else { throw new Error( - "Invalid argument: not an HTMLElement or FluentDom object" + `Invalid argument: not a valid type (${typeof value})` ); } - } else if (isNumber(value) || isString(value)) { - root.appendChild(document.createTextNode(`${value}`)); - } else { - throw new Error( - `Invalid argument: not a valid type (${typeof value})` - ); - } + }); return this; }; this.c = this.create = function(tagName) { @@ -87,9 +95,7 @@ const fluentDomEsm = (function() { }; this.html = function(content) { if (!root) { - throw new Error( - "Cannot get or set innerHTML for a non-element" - ); + throw new Error("Cannot set innerHTML for a non-element"); } root.innerHTML = content; return this; @@ -108,6 +114,25 @@ const fluentDomEsm = (function() { root = document.querySelector(selector); 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) { if (!root) { throw new Error("Cannot get or set style for a non-element"); @@ -150,7 +175,7 @@ const fluentDomEsm = (function() { return this; }; this.v = this.version = function() { - return APP_VERSION; + return this.fluentDom; }; }; return FluentDom; diff --git a/dist/fluent-dom-esm.types.d.ts b/dist/fluent-dom-esm.types.d.ts index b211d29..6d49313 100644 --- a/dist/fluent-dom-esm.types.d.ts +++ b/dist/fluent-dom-esm.types.d.ts @@ -1,9 +1,9 @@ export interface FluentDomObject { - (nodeOrQuerySelector: string | HTMLElement): FluentDomObject; + (nodeOrQuerySelector: HTMLElement | string): FluentDomObject; fluentDom: string; a: (name: string, value: string) => FluentDomObject; - app: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; - append: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; + app: (...content: (FluentDomObject | HTMLElement | string)[]) => FluentDomObject; + append: (...content: (FluentDomObject | HTMLElement | string)[]) => FluentDomObject; attr: (name: string, value: string) => FluentDomObject; c: (tagName: string) => FluentDomObject; className: (className: string) => FluentDomObject; @@ -19,6 +19,8 @@ export interface FluentDomObject { listen: (type: keyof HTMLElementEventMap, listener: () => {}, optionsOrUseCapture?: boolean | object) => FluentDomObject; q: (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); style: ((prop: CSSStyleDeclaration) => FluentDomObject) | ((prop: string, value: string) => FluentDomObject); t: (text: string) => FluentDomObject; diff --git a/jsr.json b/jsr.json index 0101b6c..57f550d 100644 --- a/jsr.json +++ b/jsr.json @@ -1,5 +1,5 @@ { "name": "@itsericwoodward/fluent-dom-esm", - "version": "2.2.1", + "version": "2.3.0", "exports": "./src/fluent-dom-esm.ts" } \ No newline at end of file diff --git a/package.json b/package.json index 6a6a5af..2b8bb90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fluent-dom-esm", - "version": "2.2.1", + "version": "2.3.0", "description": "", "license": "WTFPL", "exports": { diff --git a/public/demo.html b/public/demo.html index 0d1dbbd..8cdee19 100644 --- a/public/demo.html +++ b/public/demo.html @@ -148,6 +148,10 @@ /\`[^`]+\`/g, (val) => `${val}`, ) + .replace( + /\/\/.*/g, + (val) => `${val}`, + ) .replace(/\.\w+/g, (val) => val !== ".js" ? `${val}` diff --git a/src/__tests__/fluent-dom-esm.append.test.ts b/src/__tests__/fluent-dom-esm.append.test.ts new file mode 100644 index 0000000..39d3cc4 --- /dev/null +++ b/src/__tests__/fluent-dom-esm.append.test.ts @@ -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); + }); +}); diff --git a/src/__tests__/fluent-dom-esm.attr.test.ts b/src/__tests__/fluent-dom-esm.attr.test.ts new file mode 100644 index 0000000..81e1c26 --- /dev/null +++ b/src/__tests__/fluent-dom-esm.attr.test.ts @@ -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(""); + 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(""); + 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" }); + }); +}); diff --git a/src/__tests__/fluent-dom-esm.create.test.ts b/src/__tests__/fluent-dom-esm.create.test.ts new file mode 100644 index 0000000..7c49521 --- /dev/null +++ b/src/__tests__/fluent-dom-esm.create.test.ts @@ -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("
"); + 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(""); + 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("
"); + 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(""); + 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(""); + + $el = $el.c("ul"); + expect($el.toDom()).not.toBeNull(); + expect($el.toDom()).toBeEmptyDOMElement(); + expect($el.toDom()?.outerHTML).toEqual(""); + }); +}); diff --git a/src/__tests__/fluent-dom-esm.object.test.ts b/src/__tests__/fluent-dom-esm.object.test.ts new file mode 100644 index 0000000..dbb2193 --- /dev/null +++ b/src/__tests__/fluent-dom-esm.object.test.ts @@ -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( + '
text content
', + ); + expect(document.body?.innerHTML).toEqual( + '
text content
', + ); + }); + + 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("
"); + 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(""); + 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("
"); + 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(""); + 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); + }); +}); diff --git a/src/__tests__/fluent-dom-esm.replaceChildren.test.ts b/src/__tests__/fluent-dom-esm.replaceChildren.test.ts new file mode 100644 index 0000000..170b6a4 --- /dev/null +++ b/src/__tests__/fluent-dom-esm.replaceChildren.test.ts @@ -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"); + }); +}); diff --git a/src/__tests__/fluent-dom-esm.test.ts b/src/__tests__/fluent-dom-esm.test.ts index 75e5616..f601495 100644 --- a/src/__tests__/fluent-dom-esm.test.ts +++ b/src/__tests__/fluent-dom-esm.test.ts @@ -5,28 +5,43 @@ import { describe, expect, it } from "vitest"; import $d from "../fluent-dom-esm"; describe("fluent-dom-esm tests", () => { - 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( - '
text content
', - ); - expect(document.body?.innerHTML).toEqual( - '
text content
', + it("should update an anchor's href via .h() and .href()", () => { + // create base anchor + const $a1 = $d.c("a"); + expect($a1.toDom()).not.toBeNull(); + expect($a1.toDom()).toBeEmptyDOMElement(); + expect($a1.toDom()?.outerHTML).toEqual(""); + expect($a1.toDom()?.getAttribute("href")).toBeNull(); + + // 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(""); + 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", () => { - $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()`", () => { + it("should append text provided via `.t()` and `.text()`", () => { const testVal = "testing 123"; 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); }); }); diff --git a/src/fluent-dom-esm.ts b/src/fluent-dom-esm.ts index 73df635..19068fc 100644 --- a/src/fluent-dom-esm.ts +++ b/src/fluent-dom-esm.ts @@ -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 * - * 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) * * 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) * @license http://sam.zoy.org/wtfpl/ */ @@ -24,7 +24,7 @@ import { isString, } 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 @@ -33,7 +33,7 @@ export default (function () { /** * 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") return new (FluentDomInternal as any)(nodeOrQuerySelector); @@ -74,13 +74,12 @@ export default (function () { * Sets the named attribute on the wrapped HTMLElement */ 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"); } + if (typeof name === "undefined") { + throw new Error("Cannot set an attribute without a name"); + } root.setAttribute(name, value); return this; @@ -89,30 +88,36 @@ export default (function () { /** * Appends value to wrapped HTMLElement */ - this.app = this.append = function (value) { + this.app = this.append = function (...content) { if (!root || !root?.appendChild) { throw new Error("Cannot append to a non-element"); } - const type = typeof value; - if (type === "object") { - if (isFluentDomObject(value)) { - const domVal = value.toDom(); - if (domVal !== null) root.appendChild(domVal); - } else if (isHTMLElement(value)) { - root.appendChild(value); + if (!content || content.length < 1) { + throw new Error("Cannot append without a value"); + } + + content.forEach((value) => { + const type = typeof value; + if (type === "object") { + if (isFluentDomObject(value)) { + const domVal = value.toDom(); + if (domVal !== null) root?.appendChild(domVal); + } else if (isHTMLElement(value)) { + root?.appendChild(value); + } else { + throw new Error( + "Invalid argument: not an HTMLElement or FluentDom object", + ); + } + } else if (isNumber(value) || isString(value)) { + root?.appendChild(document.createTextNode(`${value}`)); } else { throw new Error( - "Invalid argument: not an HTMLElement or FluentDom object", + `Invalid argument: not a valid type (${typeof value})`, ); } - } else if (isNumber(value) || isString(value)) { - root.appendChild(document.createTextNode(`${value}`)); - } else { - throw new Error( - `Invalid argument: not a valid type (${typeof value})`, - ); - } + }); return this; }; @@ -150,11 +155,9 @@ export default (function () { /** * Sets the wrapped HTMLElement's "innerHTML" property */ - this.html = function (content: string) { + this.html = function (content) { if (!root) { - throw new Error( - "Cannot get or set innerHTML for a non-element", - ); + throw new Error("Cannot set innerHTML for a non-element"); } root.innerHTML = content; @@ -180,11 +183,43 @@ export default (function () { 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); 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 */ @@ -272,7 +307,7 @@ export default (function () { * Returns library version */ this.v = this.version = function () { - return APP_VERSION; + return this.fluentDom; }; }; diff --git a/src/fluent-dom-esm.types.ts b/src/fluent-dom-esm.types.ts index 628e58c..946675c 100644 --- a/src/fluent-dom-esm.types.ts +++ b/src/fluent-dom-esm.types.ts @@ -1,10 +1,14 @@ export interface FluentDomObject { - (nodeOrQuerySelector: string | HTMLElement): FluentDomObject; + (nodeOrQuerySelector: HTMLElement | string): FluentDomObject; fluentDom: string; a: (name: string, value: string) => FluentDomObject; - app: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; - append: (obj: FluentDomObject | HTMLElement | string) => FluentDomObject; + app: ( + ...content: (FluentDomObject | HTMLElement | string)[] + ) => FluentDomObject; + append: ( + ...content: (FluentDomObject | HTMLElement | string)[] + ) => FluentDomObject; attr: (name: string, value: string) => FluentDomObject; c: (tagName: string) => FluentDomObject; @@ -34,6 +38,13 @@ export interface FluentDomObject { q: (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); diff --git a/tsconfig.json b/tsconfig.json index af8313b..02b114b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "target": "ES2022", + "target": "es6", "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "es6", + "lib": ["es6", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ diff --git a/vite.config.js b/vite.config.js index 4b2611c..406bd90 100644 --- a/vite.config.js +++ b/vite.config.js @@ -6,14 +6,18 @@ import { defineConfig } from 'vite'; const __dirname = dirname(fileURLToPath(import.meta.url)); export default defineConfig({ - plugins: [dts({exclude: ["./setupTests.ts", "**/*.test.ts"]})], + plugins: [dts({exclude: ["./vitest.setup.ts", "**/*.test.ts"]})], build: { lib: { entry: resolve(__dirname, 'src/fluent-dom-esm.ts'), name: 'fluent-dom-esm', fileName: 'fluent-dom-esm', - formats: ['es'] + formats: ['es'], + outDir: './dist' }, minify: false - } + }, + test: { + setupFiles: ["./vitest.setup.ts"] + }, }); diff --git a/vitest.config.js b/vitest.config.js deleted file mode 100644 index bd2b61b..0000000 --- a/vitest.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - test: { - setupFiles: ["./setupTests.ts"], - }, -}) diff --git a/setupTests.ts b/vitest.setup.ts similarity index 100% rename from setupTests.ts rename to vitest.setup.ts diff --git a/yarn.lock b/yarn.lock index bbf7080..14c9ef1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -809,14 +809,14 @@ __metadata: linkType: hard "debug@npm:4, debug@npm:^4.3.4, debug@npm:^4.4.0, debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" + version: 4.4.3 + resolution: "debug@npm:4.4.3" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 languageName: node linkType: hard