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("