diff --git a/.bmp.yml b/.bmp.yml index 28d2929..ad7a902 100644 --- a/.bmp.yml +++ b/.bmp.yml @@ -1,4 +1,4 @@ -version: 0.1.4 +version: 0.1.5 commit: 'chore: bump to v%.%.%' files: README.md: Cell v%.%.% diff --git a/README.md b/README.md index 23c7532..73d5db5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -capsule +cell -# Cell v0.1.4 +# Cell v0.1.5 > Event-driven DOM programming in a new style @@ -17,11 +17,11 @@ ## TodoMVC TodoMVC implementation is also available -[here](https://github.com/kt3k/cell-todomvc). +[here](https://github.com/kt3k/cell-todomvc) (WIP). ## Live examples -See [the live demos](https://celljs.deno.dev/). +See [the live demos](https://kt3k.github.io/cell). # Install @@ -59,11 +59,11 @@ Vanilla js (ES Module): ```html diff --git a/style.css b/docs/style.css similarity index 100% rename from style.css rename to docs/style.css diff --git a/loader.js b/loader.js deleted file mode 100644 index 4899dd5..0000000 --- a/loader.js +++ /dev/null @@ -1 +0,0 @@ -globalThis.capsuleLoader = import("./dist.min.js"); diff --git a/mod.ts b/mod.ts index 15ff480..cff21ac 100644 --- a/mod.ts +++ b/mod.ts @@ -1,4 +1,4 @@ -/*! Cell v0.1.4 | Copyright 2024 Yoshiya Hinosawa and Capsule contributors | MIT license */ +/*! Cell v0.1.5 | Copyright 2024 Yoshiya Hinosawa and Capsule contributors | MIT license */ import { documentReady, logEvent } from "./util.ts"; interface Initializer { @@ -104,7 +104,7 @@ export function register(component: Component, name: string) { // when deno_dom fixes add class. el.classList.add(name); el.classList.add(initClass); - el.addEventListener(`__ummount__:${name}`, () => { + el.addEventListener(`__unmount__:${name}`, () => { el.classList.remove(initClass); }, { once: true }); @@ -200,9 +200,13 @@ export function register(component: Component, name: string) { registry[name] = initializer; - documentReady().then(() => { - mount(name); - }); + if (document.readyState === "complete") { + mount(); + } else { + documentReady().then(() => { + mount(name); + }); + } } function addEventListener( diff --git a/test.ts b/test.ts index 4e46cb2..c86e016 100644 --- a/test.ts +++ b/test.ts @@ -1,8 +1,8 @@ // Copyright 2022 Yoshiya Hinosawa. All rights reserved. MIT license. -import { assert, assertExists, assertFalse } from "@std/assert"; +import { assert, assertEquals, assertExists, assertThrows } from "@std/assert"; import "./dom_polyfill.ts"; -import { type Context, mount, register } from "./mod.ts"; +import { type Context, mount, register, unmount } from "./mod.ts"; // disable debug logs because it's too verbose for unit testing // deno-lint-ignore no-explicit-any @@ -20,11 +20,6 @@ Deno.test("Component body is called when the component is mounted", () => { } register(Component, name); - - assertFalse(called); - - mount(); - assert(called); }); @@ -32,6 +27,7 @@ Deno.test("sub() add sub:type class to the dom", () => { const name = randomName(); document.body.innerHTML = `
`; + const div = queryByClass(name); function Component({ el, sub }: Context) { el.classList.add("foo"); @@ -41,11 +37,6 @@ Deno.test("sub() add sub:type class to the dom", () => { register(Component, name); - mount(); - - const div = document.body.querySelector(`.${name}`); - - assertExists(div); assert(div.classList.contains("foo")); assert(div.classList.contains("sub:bar")); assert(div.innerHTML === "

hello

"); @@ -69,50 +60,55 @@ Deno.test("on.__unmount__ is called when the componet is unmounted", () => { unmount(name, query(`.${name}`)!); assert(called); }); +*/ Deno.test("unmount removes the event listeners", () => { const name = randomName(); - const { on } = component(name); document.body.innerHTML = `
`; - const el = queryByClass(name); + const div = queryByClass(name); let count = 0; - on["my-event"] = () => { - count++; - }; - mount(); + function Component({ on }: Context) { + on["my-event"] = () => { + count++; + }; + } + register(Component, name); + assertEquals(count, 0); - el?.dispatchEvent(new CustomEvent("my-event")); + div.dispatchEvent(new CustomEvent("my-event")); assertEquals(count, 1); - el?.dispatchEvent(new CustomEvent("my-event")); + div.dispatchEvent(new CustomEvent("my-event")); assertEquals(count, 2); - unmount(name, el!); - el?.dispatchEvent(new CustomEvent("my-event")); + unmount(name, div!); + div.dispatchEvent(new CustomEvent("my-event")); assertEquals(count, 2); }); Deno.test("on[event] is called when the event is dispatched", () => { const name = randomName(); - const { on } = component(name); document.body.innerHTML = `
`; + const div = queryByClass(name); let called = false; + function Component({ on }: Context) { + on.click = () => { + called = true; + }; + on.click = () => { + called = true; + }; + } + register(Component, name); - on.click = () => { - called = true; - }; - - mount(); - - query("div")?.dispatchEvent(new Event("click")); + div.dispatchEvent(new Event("click")); assert(called); }); Deno.test("on(selector)[event] is called when the event is dispatched only under the selector", async () => { const name = randomName(); - const { on } = component(name); document.body.innerHTML = `
`; @@ -120,21 +116,22 @@ Deno.test("on(selector)[event] is called when the event is dispatched only under let onBtn1ClickCalled = false; let onBtn2ClickCalled = false; - on(".btn1").click = () => { - onBtn1ClickCalled = true; - }; - - on(".btn2").click = () => { - onBtn2ClickCalled = true; - }; + function Component({ on }: Context) { + on(".btn1").click = () => { + onBtn1ClickCalled = true; + }; - mount(); + on(".btn2").click = () => { + onBtn2ClickCalled = true; + }; + } + register(Component, name); const btn = queryByClass("btn1"); // FIXME(kt3k): workaround for deno_dom & deno issue // deno_dom doesn't bubble event when the direct target dom doesn't have event handler - btn?.addEventListener("click", () => {}); - btn?.dispatchEvent(new Event("click", { bubbles: true })); + btn.addEventListener("click", () => {}); + btn.dispatchEvent(new Event("click", { bubbles: true })); await new Promise((r) => setTimeout(r, 100)); assert(onBtn1ClickCalled); @@ -143,18 +140,18 @@ Deno.test("on(selector)[event] is called when the event is dispatched only under Deno.test("on.outside.event works", () => { const name = randomName(); - const { on } = component(name); document.body.innerHTML = `
`; let calledCount = 0; + function Component({ on }: Context) { + on.outside.click = () => { + calledCount++; + }; + } + register(Component, name); - on.outside.click = () => { - calledCount++; - }; - - mount(); assertEquals(calledCount, 0); const sibling = queryByClass("sibling")!; @@ -169,24 +166,14 @@ Deno.test("on.outside.event works", () => { root.addEventListener("click", () => {}); root.dispatchEvent(new Event("click", { bubbles: true })); assertEquals(calledCount, 2); -}); -Deno.test("`is` works", () => { - const name = randomName(); - const { is } = component(name); - document.body.innerHTML = `
`; - is("foo"); - mount(); - assert(queryByClass(name)?.classList.contains("foo")); -}); -Deno.test("innerHTML works", () => { - const name = randomName(); - const { innerHTML } = component(name); - document.body.innerHTML = `
`; - innerHTML("

hello

"); - mount(); - assertEquals(queryByClass(name)?.innerHTML, "

hello

"); + // checks if the event listener is removed after unmount + unmount(name, queryByClass(name)); + sibling.dispatchEvent(new Event("click", { bubbles: true })); + root.dispatchEvent(new Event("click", { bubbles: true })); + assertEquals(calledCount, 2); }); + Deno.test("pub, sub works", () => { const EVENT = "my-event"; const name1 = randomName(); @@ -196,21 +183,18 @@ Deno.test("pub, sub works", () => {
`; - { - const { on, sub } = component(name1); + function SubComponent({ on, sub }: Context) { sub(EVENT); on[EVENT] = () => { subCalled = true; }; } - { - const { on } = component(name2); - on.__mount__ = ({ pub }) => { - pub(EVENT); - }; + function PubComponent({ pub }: Context) { + pub(EVENT); } + register(SubComponent, name1); assert(!subCalled); - mount(); + register(PubComponent, name2); assert(subCalled); }); @@ -223,132 +207,142 @@ Deno.test("query, queryAll works", () => {

baz

`; - const { on } = component(name); - on.__mount__ = ({ query, queryAll }) => { + function Component({ query, queryAll }: Context) { assert(query("p") !== null); assertEquals(query("p")?.textContent, "foo"); assertEquals(queryAll("p")[0].textContent, "foo"); assertEquals(queryAll("p")[1].textContent, "bar"); assertEquals(queryAll("p")[2].textContent, "baz"); - }; + } + register(Component, name); }); + Deno.test("assign wrong type to on.event, on.outside.event, on(selector).event", () => { - const { on } = component(randomName()); - assertThrows(() => { - on.click = ""; - }); - assertThrows(() => { - on.click = 1; - }); - assertThrows(() => { - on.click = Symbol(); - }); - assertThrows(() => { - on.click = {}; - }); - assertThrows(() => { - on.click = []; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(".btn").click = "" as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(".btn").click = 1 as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(".btn").click = Symbol() as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(".btn").click = {} as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(".btn").click = [] as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on.outside.click = "" as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on.outside.click = 1 as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on.outside.click = Symbol() as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on.outside.click = {} as any; - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on.outside.click = [] as any; - }); + function Component({ on }: Context) { + assertThrows(() => { + on.click = ""; + }); + assertThrows(() => { + on.click = 1; + }); + assertThrows(() => { + on.click = Symbol(); + }); + assertThrows(() => { + on.click = {}; + }); + assertThrows(() => { + on.click = []; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(".btn").click = "" as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(".btn").click = 1 as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(".btn").click = Symbol() as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(".btn").click = {} as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(".btn").click = [] as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on.outside.click = "" as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on.outside.click = 1 as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on.outside.click = Symbol() as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on.outside.click = {} as any; + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on.outside.click = [] as any; + }); + } + register(Component, randomName()); }); + Deno.test("wrong type selector throws with on(selector).event", () => { - const { on } = component(randomName()); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(1 as any); - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on(1n as any); - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on({} as any); - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on([] as any); - }); - assertThrows(() => { - // deno-lint-ignore no-explicit-any - on((() => {}) as any); - }); + function Component({ on }: Context) { + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(1 as any); + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on(1n as any); + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on({} as any); + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on([] as any); + }); + assertThrows(() => { + // deno-lint-ignore no-explicit-any + on((() => {}) as any); + }); + } + register(Component, randomName()); }); -Deno.test("component throws with non string input", () => { + +Deno.test("register throws with non string input", () => { + function Component() {} assertThrows(() => { // deno-lint-ignore no-explicit-any - component(1 as any); + register(Component, 1 as any); }); assertThrows(() => { // deno-lint-ignore no-explicit-any - component(1n as any); + register(Component, 1n as any); }); assertThrows(() => { // deno-lint-ignore no-explicit-any - component(Symbol() as any); + register(Component, Symbol() as any); }); assertThrows(() => { // empty name throws - component(""); + register(Component, ""); }); assertThrows(() => { // deno-lint-ignore no-explicit-any - component((() => {}) as any); + register(Component, (() => {}) as any); }); assertThrows(() => { // deno-lint-ignore no-explicit-any - component({} as any); + register(Component, {} as any); }); assertThrows(() => { // deno-lint-ignore no-explicit-any - component([] as any); + register(Component, [] as any); }); }); -Deno.test("component throws with already registered name", () => { + +Deno.test("register throws with already registered name", () => { const name = randomName(); - component(name); + function Component() {} + register(Component, name); assertThrows(() => { - component(name); + register(Component, name); }); }); @@ -358,11 +352,26 @@ Deno.test("unmount with non registered name throws", () => { }); }); +Deno.test("mount() throws with unregistered name", () => { + assertThrows(() => { + mount(randomName()); + }); +}); + +Deno.test("on.foo returns null", () => { + const name = randomName(); + document.body.innerHTML = `
`; -const query = (s: string) => document.querySelector(s); -const queryByClass = (name: string) => - document.querySelector(`.${name}`); + function Component({ on }: Context) { + assertEquals(on.foo, null); + } + register(Component, name); +}); -*/ // test utils const randomName = () => "c-" + Math.random().toString(36).slice(2); +const queryByClass = (name: string) => { + const el = document.querySelector(`.${name}`); + assertExists(el); + return el; +}; diff --git a/util.ts b/util.ts index aac4a92..ebed71c 100644 --- a/util.ts +++ b/util.ts @@ -3,9 +3,8 @@ const READY_STATE_CHANGE = "readystatechange"; let p: Promise; -export function documentReady() { - return p = p || new Promise((resolve) => { - const doc = document; +export function documentReady(doc = document) { + p ??= new Promise((resolve) => { const checkReady = () => { if (doc.readyState === "complete") { resolve(); @@ -17,6 +16,7 @@ export function documentReady() { checkReady(); }); + return p; } interface LogEventMessage {