From 57bbbe865e6209619d9341c46e03a291d872a8f1 Mon Sep 17 00:00:00 2001 From: Sebastien Filion Date: Tue, 7 Jun 2022 14:40:47 -0400 Subject: [PATCH] Fix all types and update tests --- library/asserts.js | 699 -------------------------------------- library/component.d.ts | 118 ------- library/component.ts | 26 +- library/component_test.ts | 50 +-- library/testing.d.ts | 36 -- library/testing.js | 103 ------ library/testing.ts | 176 ++++++++++ library/testing_test.js | 4 +- library/utilities.d.ts | 71 ---- library/utilities.js | 142 -------- library/utilities.ts | 194 +++++++++++ library/utilities_test.ts | 14 +- mod.ts | 6 +- tsconfig.json | 6 +- 14 files changed, 415 insertions(+), 1230 deletions(-) delete mode 100644 library/asserts.js delete mode 100644 library/component.d.ts delete mode 100644 library/testing.d.ts delete mode 100644 library/testing.js create mode 100644 library/testing.ts delete mode 100644 library/utilities.d.ts delete mode 100644 library/utilities.js create mode 100644 library/utilities.ts diff --git a/library/asserts.js b/library/asserts.js deleted file mode 100644 index ef38b8d..0000000 --- a/library/asserts.js +++ /dev/null @@ -1,699 +0,0 @@ -// deno-fmt-ignore-file -// deno-lint-ignore-file -// This code was bundled using `deno bundle` and it's not recommended to edit it manually - -const { Deno } = globalThis; -const noColor = typeof Deno?.noColor === "boolean" ? Deno.noColor : true; -let enabled = !noColor; -function code(open, close) { - return { - open: `\x1b[${open.join(";")}m`, - close: `\x1b[${close}m`, - regexp: new RegExp(`\\x1b\\[${close}m`, "g") - }; -} -function run(str, code1) { - return enabled ? `${code1.open}${str.replace(code1.regexp, code1.open)}${code1.close}` : str; -} -function bold(str) { - return run(str, code([ - 1 - ], 22)); -} -function red(str) { - return run(str, code([ - 31 - ], 39)); -} -function green(str) { - return run(str, code([ - 32 - ], 39)); -} -function white(str) { - return run(str, code([ - 37 - ], 39)); -} -function gray(str) { - return brightBlack(str); -} -function brightBlack(str) { - return run(str, code([ - 90 - ], 39)); -} -function bgRed(str) { - return run(str, code([ - 41 - ], 49)); -} -function bgGreen(str) { - return run(str, code([ - 42 - ], 49)); -} -const ANSI_PATTERN = new RegExp([ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))", -].join("|"), "g"); -function stripColor(string) { - return string.replace(ANSI_PATTERN, ""); -} -var DiffType; -(function(DiffType1) { - DiffType1["removed"] = "removed"; - DiffType1["common"] = "common"; - DiffType1["added"] = "added"; -})(DiffType || (DiffType = {})); -const REMOVED = 1; -const COMMON = 2; -const ADDED = 3; -function createCommon(A, B, reverse) { - const common = []; - if (A.length === 0 || B.length === 0) return []; - for(let i = 0; i < Math.min(A.length, B.length); i += 1){ - if (A[reverse ? A.length - i - 1 : i] === B[reverse ? B.length - i - 1 : i]) { - common.push(A[reverse ? A.length - i - 1 : i]); - } else { - return common; - } - } - return common; -} -function diff(A1, B1) { - const prefixCommon = createCommon(A1, B1); - const suffixCommon = createCommon(A1.slice(prefixCommon.length), B1.slice(prefixCommon.length), true).reverse(); - A1 = suffixCommon.length ? A1.slice(prefixCommon.length, -suffixCommon.length) : A1.slice(prefixCommon.length); - B1 = suffixCommon.length ? B1.slice(prefixCommon.length, -suffixCommon.length) : B1.slice(prefixCommon.length); - const swapped1 = B1.length > A1.length; - [A1, B1] = swapped1 ? [ - B1, - A1 - ] : [ - A1, - B1 - ]; - const M1 = A1.length; - const N1 = B1.length; - if (!M1 && !N1 && !suffixCommon.length && !prefixCommon.length) return []; - if (!N1) { - return [ - ...prefixCommon.map((c)=>({ - type: DiffType.common, - value: c - }) - ), - ...A1.map((a)=>({ - type: swapped1 ? DiffType.added : DiffType.removed, - value: a - }) - ), - ...suffixCommon.map((c)=>({ - type: DiffType.common, - value: c - }) - ), - ]; - } - const offset = N1; - const delta = M1 - N1; - const size = M1 + N1 + 1; - const fp1 = new Array(size).fill({ - y: -1 - }); - const routes = new Uint32Array((M1 * N1 + size + 1) * 2); - const diffTypesPtrOffset = routes.length / 2; - let ptr = 0; - let p = -1; - function backTrace(A, B, current, swapped) { - const M = A.length; - const N = B.length; - const result = []; - let a = M - 1; - let b = N - 1; - let j = routes[current.id]; - let type = routes[current.id + diffTypesPtrOffset]; - while(true){ - if (!j && !type) break; - const prev = j; - if (type === 1) { - result.unshift({ - type: swapped ? DiffType.removed : DiffType.added, - value: B[b] - }); - b -= 1; - } else if (type === 3) { - result.unshift({ - type: swapped ? DiffType.added : DiffType.removed, - value: A[a] - }); - a -= 1; - } else { - result.unshift({ - type: DiffType.common, - value: A[a] - }); - a -= 1; - b -= 1; - } - j = routes[prev]; - type = routes[prev + diffTypesPtrOffset]; - } - return result; - } - function createFP(slide, down, k, M) { - if (slide && slide.y === -1 && down && down.y === -1) { - return { - y: 0, - id: 0 - }; - } - if (down && down.y === -1 || k === M || (slide && slide.y) > (down && down.y) + 1) { - const prev = slide.id; - ptr++; - routes[ptr] = prev; - routes[ptr + diffTypesPtrOffset] = ADDED; - return { - y: slide.y, - id: ptr - }; - } else { - const prev = down.id; - ptr++; - routes[ptr] = prev; - routes[ptr + diffTypesPtrOffset] = REMOVED; - return { - y: down.y + 1, - id: ptr - }; - } - } - function snake(k, slide, down, _offset, A, B) { - const M = A.length; - const N = B.length; - if (k < -N || M < k) return { - y: -1, - id: -1 - }; - const fp = createFP(slide, down, k, M); - while(fp.y + k < M && fp.y < N && A[fp.y + k] === B[fp.y]){ - const prev = fp.id; - ptr++; - fp.id = ptr; - fp.y += 1; - routes[ptr] = prev; - routes[ptr + diffTypesPtrOffset] = COMMON; - } - return fp; - } - while(fp1[delta + offset].y < N1){ - p = p + 1; - for(let k = -p; k < delta; ++k){ - fp1[k + offset] = snake(k, fp1[k - 1 + offset], fp1[k + 1 + offset], offset, A1, B1); - } - for(let k1 = delta + p; k1 > delta; --k1){ - fp1[k1 + offset] = snake(k1, fp1[k1 - 1 + offset], fp1[k1 + 1 + offset], offset, A1, B1); - } - fp1[delta + offset] = snake(delta, fp1[delta - 1 + offset], fp1[delta + 1 + offset], offset, A1, B1); - } - return [ - ...prefixCommon.map((c)=>({ - type: DiffType.common, - value: c - }) - ), - ...backTrace(A1, B1, fp1[delta + offset], swapped1), - ...suffixCommon.map((c)=>({ - type: DiffType.common, - value: c - }) - ), - ]; -} -function diffstr(A, B) { - function tokenize(string, { wordDiff =false } = {}) { - if (wordDiff) { - const tokens = string.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/); - const words = /^[a-zA-Z\u{C0}-\u{FF}\u{D8}-\u{F6}\u{F8}-\u{2C6}\u{2C8}-\u{2D7}\u{2DE}-\u{2FF}\u{1E00}-\u{1EFF}]+$/u; - for(let i = 0; i < tokens.length - 1; i++){ - if (!tokens[i + 1] && tokens[i + 2] && words.test(tokens[i]) && words.test(tokens[i + 2])) { - tokens[i] += tokens[i + 2]; - tokens.splice(i + 1, 2); - i--; - } - } - return tokens.filter((token)=>token - ); - } else { - const tokens = [], lines = string.split(/(\n|\r\n)/); - if (!lines[lines.length - 1]) { - lines.pop(); - } - for(let i = 0; i < lines.length; i++){ - if (i % 2) { - tokens[tokens.length - 1] += lines[i]; - } else { - tokens.push(lines[i]); - } - } - return tokens; - } - } - function createDetails(line, tokens) { - return tokens.filter(({ type })=>type === line.type || type === DiffType.common - ).map((result, i, t)=>{ - if (result.type === DiffType.common && t[i - 1] && t[i - 1]?.type === t[i + 1]?.type && /\s+/.test(result.value)) { - result.type = t[i - 1].type; - } - return result; - }); - } - const diffResult = diff(tokenize(`${A}\n`), tokenize(`${B}\n`)); - const added = [], removed = []; - for (const result1 of diffResult){ - if (result1.type === DiffType.added) { - added.push(result1); - } - if (result1.type === DiffType.removed) { - removed.push(result1); - } - } - const aLines = added.length < removed.length ? added : removed; - const bLines = aLines === removed ? added : removed; - for (const a of aLines){ - let tokens = [], b; - while(bLines.length){ - b = bLines.shift(); - tokens = diff(tokenize(a.value, { - wordDiff: true - }), tokenize(b?.value ?? "", { - wordDiff: true - })); - if (tokens.some(({ type , value })=>type === DiffType.common && value.trim().length - )) { - break; - } - } - a.details = createDetails(a, tokens); - if (b) { - b.details = createDetails(b, tokens); - } - } - return diffResult; -} -const CAN_NOT_DISPLAY = "[Cannot display]"; -class AssertionError extends Error { - constructor(message){ - super(message); - this.name = "AssertionError"; - } -} -function _format(v) { - const { Deno: Deno1 } = globalThis; - return typeof Deno1?.inspect === "function" ? Deno1.inspect(v, { - depth: Infinity, - sorted: true, - trailingComma: true, - compact: false, - iterableLimit: Infinity - }) : `"${String(v).replace(/(?=["\\])/g, "\\")}"`; -} -function createColor(diffType, { background =false } = {}) { - switch(diffType){ - case DiffType.added: - return (s)=>background ? bgGreen(white(s)) : green(bold(s)) - ; - case DiffType.removed: - return (s)=>background ? bgRed(white(s)) : red(bold(s)) - ; - default: - return white; - } -} -function createSign(diffType) { - switch(diffType){ - case DiffType.added: - return "+ "; - case DiffType.removed: - return "- "; - default: - return " "; - } -} -function buildMessage(diffResult, { stringDiff =false } = {}) { - const messages = [], diffMessages = []; - messages.push(""); - messages.push(""); - messages.push(` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${green(bold("Expected"))}`); - messages.push(""); - messages.push(""); - diffResult.forEach((result)=>{ - const c = createColor(result.type); - const line = result.details?.map((detail)=>detail.type !== DiffType.common ? createColor(detail.type, { - background: true - })(detail.value) : detail.value - ).join("") ?? result.value; - diffMessages.push(c(`${createSign(result.type)}${line}`)); - }); - messages.push(...stringDiff ? [ - diffMessages.join("") - ] : diffMessages); - messages.push(""); - return messages; -} -function isKeyedCollection(x) { - return [ - Symbol.iterator, - "size" - ].every((k)=>k in x - ); -} -function equal(c, d) { - const seen = new Map(); - return function compare(a, b) { - if (a && b && (a instanceof RegExp && b instanceof RegExp || a instanceof URL && b instanceof URL)) { - return String(a) === String(b); - } - if (a instanceof Date && b instanceof Date) { - const aTime = a.getTime(); - const bTime = b.getTime(); - if (Number.isNaN(aTime) && Number.isNaN(bTime)) { - return true; - } - return a.getTime() === b.getTime(); - } - if (Object.is(a, b)) { - return true; - } - if (a && typeof a === "object" && b && typeof b === "object") { - if (a && b && a.constructor !== b.constructor) { - return false; - } - if (a instanceof WeakMap || b instanceof WeakMap) { - if (!(a instanceof WeakMap && b instanceof WeakMap)) return false; - throw new TypeError("cannot compare WeakMap instances"); - } - if (a instanceof WeakSet || b instanceof WeakSet) { - if (!(a instanceof WeakSet && b instanceof WeakSet)) return false; - throw new TypeError("cannot compare WeakSet instances"); - } - if (seen.get(a) === b) { - return true; - } - if (Object.keys(a || {}).length !== Object.keys(b || {}).length) { - return false; - } - if (isKeyedCollection(a) && isKeyedCollection(b)) { - if (a.size !== b.size) { - return false; - } - let unmatchedEntries = a.size; - for (const [aKey, aValue] of a.entries()){ - for (const [bKey, bValue] of b.entries()){ - if (aKey === aValue && bKey === bValue && compare(aKey, bKey) || compare(aKey, bKey) && compare(aValue, bValue)) { - unmatchedEntries--; - } - } - } - return unmatchedEntries === 0; - } - const merged = { - ...a, - ...b - }; - for (const key of [ - ...Object.getOwnPropertyNames(merged), - ...Object.getOwnPropertySymbols(merged), - ]){ - if (!compare(a && a[key], b && b[key])) { - return false; - } - if (key in a && !(key in b) || key in b && !(key in a)) { - return false; - } - } - seen.set(a, b); - if (a instanceof WeakRef || b instanceof WeakRef) { - if (!(a instanceof WeakRef && b instanceof WeakRef)) return false; - return compare(a.deref(), b.deref()); - } - return true; - } - return false; - }(c, d); -} -function assert(expr, msg = "") { - if (!expr) { - throw new AssertionError(msg); - } -} -function assertEquals(actual, expected, msg) { - if (equal(actual, expected)) { - return; - } - let message = ""; - const actualString = _format(actual); - const expectedString = _format(expected); - try { - const stringDiff = typeof actual === "string" && typeof expected === "string"; - const diffResult = stringDiff ? diffstr(actual, expected) : diff(actualString.split("\n"), expectedString.split("\n")); - const diffMsg = buildMessage(diffResult, { - stringDiff - }).join("\n"); - message = `Values are not equal:\n${diffMsg}`; - } catch { - message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; - } - if (msg) { - message = msg; - } - throw new AssertionError(message); -} -function assertNotEquals(actual, expected, msg) { - if (!equal(actual, expected)) { - return; - } - let actualString; - let expectedString; - try { - actualString = String(actual); - } catch { - actualString = "[Cannot display]"; - } - try { - expectedString = String(expected); - } catch { - expectedString = "[Cannot display]"; - } - if (!msg) { - msg = `actual: ${actualString} expected: ${expectedString}`; - } - throw new AssertionError(msg); -} -function assertStrictEquals(actual, expected, msg) { - if (actual === expected) { - return; - } - let message; - if (msg) { - message = msg; - } else { - const actualString = _format(actual); - const expectedString = _format(expected); - if (actualString === expectedString) { - const withOffset = actualString.split("\n").map((l)=>` ${l}` - ).join("\n"); - message = `Values have the same structure but are not reference-equal:\n\n${red(withOffset)}\n`; - } else { - try { - const stringDiff = typeof actual === "string" && typeof expected === "string"; - const diffResult = stringDiff ? diffstr(actual, expected) : diff(actualString.split("\n"), expectedString.split("\n")); - const diffMsg = buildMessage(diffResult, { - stringDiff - }).join("\n"); - message = `Values are not strictly equal:\n${diffMsg}`; - } catch { - message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; - } - } - } - throw new AssertionError(message); -} -function assertNotStrictEquals(actual, expected, msg) { - if (actual !== expected) { - return; - } - throw new AssertionError(msg ?? `Expected "actual" to be strictly unequal to: ${_format(actual)}\n`); -} -function assertExists(actual, msg) { - if (actual === undefined || actual === null) { - if (!msg) { - msg = `actual: "${actual}" expected to not be null or undefined`; - } - throw new AssertionError(msg); - } -} -function assertStringIncludes(actual, expected, msg) { - if (!actual.includes(expected)) { - if (!msg) { - msg = `actual: "${actual}" expected to contain: "${expected}"`; - } - throw new AssertionError(msg); - } -} -function assertArrayIncludes(actual, expected, msg) { - const missing = []; - for(let i = 0; i < expected.length; i++){ - let found = false; - for(let j = 0; j < actual.length; j++){ - if (equal(expected[i], actual[j])) { - found = true; - break; - } - } - if (!found) { - missing.push(expected[i]); - } - } - if (missing.length === 0) { - return; - } - if (!msg) { - msg = `actual: "${_format(actual)}" expected to include: "${_format(expected)}"\nmissing: ${_format(missing)}`; - } - throw new AssertionError(msg); -} -function assertMatch(actual, expected, msg) { - if (!expected.test(actual)) { - if (!msg) { - msg = `actual: "${actual}" expected to match: "${expected}"`; - } - throw new AssertionError(msg); - } -} -function assertNotMatch(actual, expected, msg) { - if (expected.test(actual)) { - if (!msg) { - msg = `actual: "${actual}" expected to not match: "${expected}"`; - } - throw new AssertionError(msg); - } -} -function assertObjectMatch(actual, expected) { - const seen = new WeakMap(); - return assertEquals(function filter(a, b) { - if (seen.has(a) && seen.get(a) === b) { - return a; - } - seen.set(a, b); - const filtered = {}; - const entries = [ - ...Object.getOwnPropertyNames(a), - ...Object.getOwnPropertySymbols(a), - ].filter((key)=>key in b - ).map((key)=>[ - key, - a[key] - ] - ); - for (const [key1, value] of entries){ - if (Array.isArray(value)) { - const subset = b[key1]; - if (Array.isArray(subset)) { - filtered[key1] = value.slice(0, subset.length).map((element, index)=>{ - const subsetElement = subset[index]; - if (typeof subsetElement === "object" && subsetElement) { - return filter(element, subsetElement); - } - return element; - }); - continue; - } - } else if (typeof value === "object") { - const subset = b[key1]; - if (typeof subset === "object" && subset) { - filtered[key1] = filter(value, subset); - continue; - } - } - filtered[key1] = value; - } - return filtered; - }(actual, expected), expected); -} -function fail(msg) { - assert(false, `Failed assertion${msg ? `: ${msg}` : "."}`); -} -function assertThrows(fn, ErrorClass, msgIncludes = "", msg) { - let doesThrow = false; - try { - fn(); - } catch (e) { - if (e instanceof Error === false) { - throw new AssertionError("A non-Error object was thrown."); - } - if (ErrorClass && !(e instanceof ErrorClass)) { - msg = `Expected error to be instance of "${ErrorClass.name}", but was "${e.constructor.name}"${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } - if (msgIncludes && !stripColor(e.message).includes(stripColor(msgIncludes))) { - msg = `Expected error message to include "${msgIncludes}", but got "${e.message}"${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } - doesThrow = true; - } - if (!doesThrow) { - msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } -} -async function assertRejects(fn, ErrorClass, msgIncludes = "", msg) { - let doesThrow = false; - try { - await fn(); - } catch (e) { - if (e instanceof Error === false) { - throw new AssertionError("A non-Error object was thrown or rejected."); - } - if (ErrorClass && !(e instanceof ErrorClass)) { - msg = `Expected error to be instance of "${ErrorClass.name}", but was "${e.constructor.name}"${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } - if (msgIncludes && !stripColor(e.message).includes(stripColor(msgIncludes))) { - msg = `Expected error message to include "${msgIncludes}", but got "${e.message}"${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } - doesThrow = true; - } - if (!doesThrow) { - msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; - throw new AssertionError(msg); - } -} -export { assertRejects as assertThrowsAsync }; -function unimplemented(msg) { - throw new AssertionError(msg || "unimplemented"); -} -function unreachable() { - throw new AssertionError("unreachable"); -} -export { AssertionError as AssertionError }; -export { _format as _format }; -export { equal as equal }; -export { assert as assert }; -export { assertEquals as assertEquals }; -export { assertNotEquals as assertNotEquals }; -export { assertStrictEquals as assertStrictEquals }; -export { assertNotStrictEquals as assertNotStrictEquals }; -export { assertExists as assertExists }; -export { assertStringIncludes as assertStringIncludes }; -export { assertArrayIncludes as assertArrayIncludes }; -export { assertMatch as assertMatch }; -export { assertNotMatch as assertNotMatch }; -export { assertObjectMatch as assertObjectMatch }; -export { fail as fail }; -export { assertThrows as assertThrows }; -export { assertRejects as assertRejects }; -export { unimplemented as unimplemented }; -export { unreachable as unreachable }; diff --git a/library/component.d.ts b/library/component.d.ts deleted file mode 100644 index b1407c6..0000000 --- a/library/component.d.ts +++ /dev/null @@ -1,118 +0,0 @@ -export const StateSymbol: unique symbol; -export type State = { [k: string]: unknown }; -export type CustomElement = HTMLElement & { - adoptedCallback?(): void; - attributeChangedCallback?( - name: string, - oldValue: string, - newValue: string, - ): void; - connectedCallback?(): void; - disconnectedCallback?(): void; - elements?: { [k: string]: HTMLElement }; - state: Partial; -}; -export type Constructor = { - new (): E; -} & { observedAttributes: Array }; -export type FactorizeHOF< - S extends State = State, - E extends CustomElement = CustomElement, -> = ( - f: (Component: Constructor, render: (e: E, s: S) => void) => void, -) => void; -export type ConstructHOF = ( - f: (e: E) => void, -) => void; -export type HOF = ( - factorize: FactorizeHOF, - construct: ConstructHOF, -) => void; -export type AsyncRenderCallback< - S extends State, - E extends CustomElement = CustomElement, -> = ($e: E, s: S, e: Event) => S | undefined; -export type AsyncRender< - S extends State, - E extends CustomElement = CustomElement, -> = ( - f: AsyncRenderCallback, -) => (e: Event) => void; -export type Render = ( - element: E, - state: S, -) => void; -export type AttributeCallback< - S extends State, - E extends CustomElement = CustomElement, -> = >( - attributes: { name: keyof S; oldValue: X; value: X }, - element?: E, - state?: S, -) => Partial | boolean; -export type LifeCycleCallback< - S extends State, - E extends CustomElement = CustomElement, -> = - | (( - element: E, - render: AsyncRender, - ) => void) - | (( - element: E, - render: AsyncRender, - name: string, - oldValue: string, - value: string, - ) => S | void); - -export declare enum Callbacks { - "adoptedCallback", - "attributeChangedCallback", - "connectedCallback", - "disconnectedCallback", -} - -type ValueOf = T[keyof T]; - -export function factorizeComponent< - S extends State = State, - E extends CustomElement = CustomElement, ->( - render: Render, - state: S, - ...fs: Array> -): Constructor; - -export function useAttributes< - S extends State, - E extends CustomElement = CustomElement, ->( - validateAttribute: AttributeCallback, - map?: { [K in keyof S]: (x: string) => S[K] }, -): HOF; - -export function useCallbacks< - S extends State, - E extends CustomElement = CustomElement, ->( - callbacks: { - [K in keyof typeof Callbacks]?: LifeCycleCallback; - }, -): HOF; - -export function useShadow< - S extends State, - E extends CustomElement = CustomElement, ->( - options?: { mode: "open" | "closed" }, -): HOF; - -export function useTemplate< - S extends State, - E extends CustomElement = CustomElement, - X extends Node = Node, ->( - getTemplate: () => HTMLTemplateElement, - map?: { [k: string]: (e: E) => X | null }, -): HOF; diff --git a/library/component.ts b/library/component.ts index 85b6293..bf9f711 100644 --- a/library/component.ts +++ b/library/component.ts @@ -1,4 +1,4 @@ -import { maybeCall, parseSpineCaseToCamelCase } from "./utilities.js"; +import { maybeCall, parseSpineCaseToCamelCase } from "./utilities.ts"; export const StateSymbol = Symbol(); export const ValidateAttributeSymbol = Symbol(); @@ -42,8 +42,8 @@ export type ValidateAttributeCallback< > = ( a: { name: string; - oldValue: Partial[Extract, string>]; - value: Partial[Extract, string>]; + oldValue: S[Extract]; + value: S[Extract]; }, e: CustomElement, s: S, @@ -161,7 +161,7 @@ const asyncRender = < const factorizeFunctionalComponentClass = < S extends State, E extends CustomElement = CustomElement, ->(TargetConstructor = HTMLElement) => ( +>(TargetConstructor = globalThis.HTMLElement) => ( class FunctionalComponent extends TargetConstructor { constructor(gs: Array>) { super(); @@ -356,7 +356,7 @@ export const factorizeComponent = < configurable: true, enumerable: false, value() { - return maybeCall(_connectedCallback, this) + return maybeCall(() => _connectedCallback.call(this)) .then((s = {}) => { this[StateSymbol] = Object.assign({}, state, s); _render(this, this[StateSymbol], { @@ -497,14 +497,10 @@ export const useAttributes = < // Overwrite the type because sometime it's just a string oldValue: (Reflect.has(map, name) ? map[name](oldValue) - : oldValue) as unknown as Partial< - S - >[Extract, string>], + : oldValue) as unknown as S[Extract], value: (Reflect.has(map, name) ? map[name](value) - : value) as unknown as Partial< - S - >[Extract, string>], + : value) as unknown as S[Extract], }, this, Object.assign({}, this[StateSymbol]), @@ -552,7 +548,7 @@ export const useAttributes = < configurable: true, enumerable: true, value(this: E) { - return maybeCall(_connectedCallback, this) + return maybeCall(() => _connectedCallback.call(this)) .then(() => { for (const key of observedAttributes) { const normalizedKey = parseDatasetToState(key) as keyof S; @@ -664,7 +660,7 @@ export const useCallbacks = < configurable: true, enumerable: true, value(...xs: [string, string, string]) { - return maybeCall(g, this, ...xs) + return maybeCall(() => g.call(this, ...xs)) .then(() => f( this, @@ -796,8 +792,8 @@ export const useTemplate = < configurable: true, enumerable: true, value(this: E) { - return maybeCall(_connectedCallback, this) - .then(() => maybeCall(f)) + return maybeCall(() => _connectedCallback.call(this)) + .then(() => maybeCall(f)) .then((template) => { if (!template) return; (this.shadowRoot || this).appendChild( diff --git a/library/component_test.ts b/library/component_test.ts index 83f7008..96cd2b1 100644 --- a/library/component_test.ts +++ b/library/component_test.ts @@ -1,21 +1,21 @@ -import { assert, assertEquals } from "./asserts.js"; +import { assert, assertEquals } from "./asserts.ts"; import { AsyncRenderCallback, - CustomElement, ConstructHOF, + CustomElement, + factorizeComponent, HOF, LifeCycleCallback, State, StateSymbol, - factorizeComponent, useAttributes, useCallbacks, useShadow, useTemplate, + ValidateAttributeCallback, } from "./component.ts"; -// @deno-types="./testing.d.ts" -import { constructComponent, factorizeSpy, test, withDom } from "./testing.js"; -import { deferUntil, deferUntilNextFrame, noop } from "./utilities.js"; +import { constructComponent, factorizeSpy, test, withDom } from "./testing.ts"; +import { deferUntil, deferUntilNextFrame, noop } from "./utilities.ts"; type RenderSpyFunction< S extends State, @@ -44,7 +44,6 @@ test( }); }); }), - () => "ShadowRoot" in globalThis, ); test( @@ -91,7 +90,6 @@ test( }); }); }), - () => "ShadowRoot" in globalThis, ); test( @@ -111,7 +109,7 @@ test( const e = constructComponent(Component); assert(e.shadowRoot); }), - () => "ShadowRoot" in globalThis, + () => "DocumentFragment" in globalThis, ); test( @@ -119,7 +117,9 @@ test( withDom(() => { type ComponentState = { value: number }; const [attributeMapSpy, assertAttributeMapSpy] = factorizeSpy(Number); - const [validateAttributeSpy, assertValidateAttributeSpy] = factorizeSpy( + const [validateAttributeSpy, assertValidateAttributeSpy] = factorizeSpy< + ValidateAttributeCallback + >( ({ name: _name, oldValue, value }) => (oldValue !== value && value >= 0), ); const [renderSpy, assertRenderSpy] = factorizeSpy< @@ -159,7 +159,6 @@ test( }); }); }), - () => "ShadowRoot" in globalThis, ); test( @@ -170,16 +169,15 @@ test( active: false, count: 0, }; - const callback = - (( - _e: CustomElement, - render: AsyncRenderCallback, - ...xs: [string, string, string] - ) => { - render((_, { count }) => ({ active: true, count: ++count }))( - {} as unknown as Event, - ); - }) as LifeCycleCallback; + const callback = (( + _e: CustomElement, + render: AsyncRenderCallback, + ..._xs: [string, string, string] + ) => { + render((_, { count }) => ({ active: true, count: ++count }))( + {} as unknown as Event, + ); + }) as LifeCycleCallback; const [adoptedCallbackSpy, assertAdoptedCallbackSpy] = factorizeSpy( callback, ); @@ -210,13 +208,13 @@ test( const e = constructComponent(Component); + e.connectedCallback && e.connectedCallback(); + e.adoptedCallback && e.adoptedCallback(); e.attributeChangedCallback && e.attributeChangedCallback("value", null, "42"); - e.connectedCallback && e.connectedCallback(); - e.disconnectedCallback && e.disconnectedCallback(); return deferUntilNextFrame() @@ -234,7 +232,6 @@ test( assert(assertRenderSpy.callCount === 5); }); }), - () => "ShadowRoot" in globalThis, ); test( @@ -280,7 +277,10 @@ test( addButton: (e) => e.querySelector("button"), number: (e) => e.querySelector("span"), }, - )((f) => f(Component, renderSpy, initialState), noop as ConstructHOF); + )( + (f) => f(Component, renderSpy, initialState), + noop as ConstructHOF, + ); const e = constructComponent(Component); diff --git a/library/testing.d.ts b/library/testing.d.ts deleted file mode 100644 index 69cd97f..0000000 --- a/library/testing.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CustomElement, CustomElementConstructor, State } from "./component.ts"; - -type ArgumentTypes) => unknown> = F extends - (...args: infer A) => unknown ? A - : never; - -export function constructComponent< - S extends State, - E extends CustomElement = CustomElement, ->( - C: CustomElementConstructor, -): E; - -export function factorizeSpy) => unknown>( - f?: F, -): [ - F, - { - ( - assert: ( - xs: ArgumentTypes, - i: number, - zs: Array>, - ) => void | never, - ): void | never; - callCount: number; - called: boolean; - }, -]; - -export function test(name: string, f: () => void, g?: () => boolean): void; - -export function withDom( - f: (document: HTMLDocument) => void, - t?: string, -): () => Promise; diff --git a/library/testing.js b/library/testing.js deleted file mode 100644 index 00aaa4a..0000000 --- a/library/testing.js +++ /dev/null @@ -1,103 +0,0 @@ -import { maybeCall, randomUUID } from "./utilities.js"; - -export const TestsSymbol = Symbol.for("iy-tests"); - -export const constructComponent = (Component) => { - if ("customElements" in globalThis) { - const uuid = `iy-${randomUUID()}`; - globalThis.customElements.define(uuid, Component); - return globalThis.document.createElement(uuid); - } else return new Component(); -}; - -/** - * Factorizes a testing Spy. - * - * ```js - * const [f, assertF] = factorizeSpy(() => 66); - * const compose = (f, g, x) => f(g(x)); - * const x = compose(f, (x) => x * 2, 42); - * assert(x === 66); - * assert(assertF.called); - * assert(assertF.callCount === 1); - * assertF((x) => assert(x === 84)); - * ``` - */ -export const factorizeSpy = (f = () => undefined) => { - const xs = []; - let i = 0; - let called = false; - return [ - (...as) => { - called = true; - xs.push(as); - i++; - - return f(...as); - }, - Object.defineProperties( - (g) => { - xs.forEach((ys, i, zs) => g(ys, i, zs)); - }, - { - callCount: { - get: () => i, - }, - called: { - get: () => called, - }, - }, - ), - ]; -}; - -export const getTests = (h) => globalThis[TestsSymbol]?.get(h)?.entries(); - -export const test = (name, f, g) => { - if (g && !g()) return; - if (globalThis.Deno && f.length === 0) return globalThis.Deno.test(name, f); - if (!globalThis[TestsSymbol]) { - globalThis[TestsSymbol] = new Map(); - } - let tests = globalThis[TestsSymbol].get(globalThis.location?.href); - if (!tests) { - tests = new Map(); - globalThis[TestsSymbol].set(globalThis.location?.href, tests); - } - tests.set(f, { name }); -}; - -export const withDom = (f, template = "
") => { - if (!globalThis.requestAnimationFrame) { - globalThis.requestAnimationFrame = (f) => setTimeout(f); - } - - if ("DocumentFragment" in globalThis) { - const $t = globalThis.document.createElement("template"); - $t.innerHTML = template; - - return (e) => { - e.appendChild($t.content.cloneNode(true)); - return () => f(e); - }; - } - - return () => - Promise.all( - ["https://deno.land/x/deno_dom@v0.1.30-alpha/deno-dom-wasm.ts"].map((x) => - import(x) - ), - ) - .then(([{ DOMParser, Element }]) => { - const document = new DOMParser().parseFromString(template, "text/html"); - - globalThis.HTMLElement = class HTMLElement extends Element {}; - globalThis.document = document; - - return maybeCall(f, document) - .finally(() => { - window.HTMLElement = undefined; - window.document = undefined; - }); - }); -}; diff --git a/library/testing.ts b/library/testing.ts new file mode 100644 index 0000000..a9c493a --- /dev/null +++ b/library/testing.ts @@ -0,0 +1,176 @@ +import { maybeCall, noop, randomUUID } from "./utilities.ts"; +import { CustomElement, CustomElementConstructor, State } from "./component.ts"; + +export const TestsSymbol = Symbol(); + +export const constructComponent = < + S extends State, + E extends CustomElement = CustomElement, +>(Component: CustomElementConstructor): E => { + if ("customElements" in globalThis) { + const uuid = `iy-${randomUUID()}`; + globalThis.customElements.define(uuid, Component); + return globalThis.document.createElement(uuid) as unknown as E; + } else return new Component(); +}; + +export type AssertFunction< + // deno-lint-ignore no-explicit-any + F extends (...xs: Array) => any, +> = { + ( + assert: ( + xs: Parameters, // Arguments as an Array for the current call + i: number, // Number of the current call + zs: Array>, // Array of all the arguments of all the calls + ) => void | never, + ): F extends undefined ? void : ReturnType; + callCount: number; + called: boolean; +}; + +/** + * Factorizes a testing Spy. + * + * ```js + * const [f, assertF] = factorizeSpy(() => 66); + * const compose = (f, g, x) => f(g(x)); + * const x = compose(f, (x) => x * 2, 42); + * assert(x === 66); + * assert(assertF.called); + * assert(assertF.callCount === 1); + * assertF((x) => assert(x === 84)); + * ``` + */ +export const factorizeSpy = < + // deno-lint-ignore no-explicit-any + F extends (...xs: Array) => any, +>(f = noop as F): [ + F, + AssertFunction, +] => { + type XS = Parameters; + type T = ReturnType; + const axs: Array> = []; + let i = 0; + let called = false; + return [ + ((...xs: XS): T => { + called = true; + axs.push(xs); + i++; + + return f(...xs); + }) as F, + Object.defineProperties( + (g: (xs: XS, i: number, ys: Array) => void | never) => { + axs.forEach((xs, i, ys) => g(xs, i, ys)); + }, + { + callCount: { + get: () => i, + }, + called: { + get: () => called, + }, + }, + ) as AssertFunction, + ]; +}; + +export const getTests = (x: string) => + (Reflect.get(globalThis, TestsSymbol) as Map< + string, + Map void | never> + >)?.get(x)?.entries(); + +export const test = ( + name: string, + f: () => void | never, + g?: () => boolean, +) => { + if (g && !g()) return; + if (Reflect.has(globalThis, "Deno") && f.length === 0) { + return Reflect.get(globalThis, "Deno").test(name, f); + } + if (!Reflect.has(globalThis, TestsSymbol)) { + Object.defineProperty(globalThis, TestsSymbol, { + value: new Map(), + writable: true, + }); + } + let tests = Reflect.get(globalThis, TestsSymbol).get( + globalThis.location?.href, + ); + if (!tests) { + tests = new Map(); + Reflect.get(globalThis, TestsSymbol).set(globalThis.location?.href, tests); + } + tests.set(f, { name }); +}; + +export const withDom = ( + f: (e: E) => void | never, + template = "
", +): () => void => { + if (!globalThis.requestAnimationFrame) { + globalThis.requestAnimationFrame = (f) => setTimeout(f); + } + + if ("DocumentFragment" in globalThis) { + const $t = globalThis.document.createElement("template"); + $t.innerHTML = template; + + /* + When the test is ran within a browser, the test-runner will pass an Element to append + the testing environment. + */ + // deno-lint-ignore ban-ts-comment + // @ts-ignore + return (e: E) => { + e.appendChild($t.content.cloneNode(true)); + return () => f(e); + }; + } + + return () => + Promise.all( + [ + "https://deno.land/x/deno_dom@v0.1.30-alpha/deno-dom-wasm.ts", + "https://deno.land/x/deno_dom@v0.1.30-alpha/src/constructor-lock.ts", + ].map((x) => import(x)), + ) + .then(([{ DOMParser, Element }, { CTOR_KEY }]) => { + const document = new DOMParser().parseFromString(template, "text/html"); + + Reflect.defineProperty(globalThis, "HTMLElement", { + value: class HTMLElement extends Element { + constructor() { + super("test", null, [], CTOR_KEY); + } + adoptedCallback() {} + attributeChangedCallback(_n: string, _o: string, _v: string) {} + connectedCallback() {} + disconnectedCallback() {} + setAttribute(n: string, v: string) { + this.attributeChangedCallback(n, this.getAttribute(n), v); + Element.prototype.setAttribute.call(this, n, v); + } + }, + writable: true, + }); + globalThis.document = document; + + return maybeCall(() => f(document as E)) + .finally(() => { + Reflect.defineProperty(globalThis, "HTMLElement", { + value: undefined, + writable: true, + }); + Reflect.defineProperty(globalThis, "document", { + value: undefined, + writable: true, + }); + }); + }); +}; diff --git a/library/testing_test.js b/library/testing_test.js index d3f5574..b7122af 100644 --- a/library/testing_test.js +++ b/library/testing_test.js @@ -1,5 +1,5 @@ -import { assert } from "./asserts.js"; -import { factorizeSpy, test } from "./testing.js"; +import { assert } from "./asserts.ts"; +import { factorizeSpy, test } from "./testing.ts"; test( "factorizeSpy", diff --git a/library/utilities.d.ts b/library/utilities.d.ts deleted file mode 100644 index a724b40..0000000 --- a/library/utilities.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -export function appendElement( - x: X, - y: Y, -): Y; - -export function appendNode( - x: X, - y: Y, -): Y; - -export function prependElement( - x: X, - y: Y, -): Y; - -export function deferUntil( - e: E, - f: (x: E) => boolean, - d?: number, -): Promise | Promise; - -export function deferUntilNextFrame(): Promise; - -export function disconnectAllElements(e: E): void; - -export function factorizeTemplate( - html: string, -): (document: HTMLDocument) => HTMLTemplateElement; - -export function intersects(xs: Array, ys: Array): boolean; - -export function maybeCall>( - f: (e: E, ...xs: XS) => Y | Promise, - ...xs: XS -): Promise; - -export function noop(..._: Array): void; - -export function parsePascalCaseToSpineCase(x: string): string; - -export function parseSpineCaseToCamelCase(x: string): string; - -export function parseSelectorForElement( - e: E, -): Array; - -export function randomUUID(): string; - -export function requestIdleCallback(f: () => void, d?: number): void; - -export function renderFor( - te: T, - xs: Iterable, - f: (te: T, x: X, i: number, xs: Iterable) => E, - g?: (te: T, e: E) => E, - h?: (x: unknown) => string, -): Array; - -export function renderIf( - te: T, - k: () => boolean, - f: (te: T) => E, - g?: (te: T) => E, -): E; - -export function renderOnce( - ts: T, - f: (te: T) => E, - g?: (te: T, e: E) => E, - h?: (te: T, e: E) => E, -): E; diff --git a/library/utilities.js b/library/utilities.js deleted file mode 100644 index 6cd5ebc..0000000 --- a/library/utilities.js +++ /dev/null @@ -1,142 +0,0 @@ -export const deferUntil = (e, f, d = 1000 * 5) => - new Promise((resolve, reject) => { - if (f(e)) resolve(e); - else { - const t1 = setTimeout(() => { - reject(new Error("Timed out")); - t1 && clearTimeout(t1); - t2 && clearInterval(t2); - }, d); - const t2 = setInterval(() => { - if (f(e)) { - resolve(e); - t1 && clearTimeout(t1); - t2 && clearInterval(t2); - } - }); - } - }); - -export const deferUntilNextFrame = () => - new Promise( - (resolve) => globalThis.requestAnimationFrame(() => resolve()), - ); - -export const disconnectAllElements = (e) => { - for (const k in e.elements) { - for (const f of b.listeners) { - if (Object.prototype.hasOwnProperty.call(e.elements, k)) { - e.elements[k].removeEventListener(f); - } - } - } -}; - -export const factorizeTemplate = (html) => - (document) => { - const t = document.createElement("template"); - t.innerHTML = html; - return t; - }; - -export const intersects = (xs, ys) => - ys && ys.reduce( - (b, y) => !xs.includes(y) ? false : b, - true, - ) || false; - -export const maybeCall = (f, e, ...xs) => { - try { - const p = f && f.call(e, ...xs); - return (p instanceof Promise ? p : Promise.resolve(p)); - } catch (e) { - return Promise.reject(e); - } -}; - -export const noop = (..._) => undefined; - -export const parseSelectorForElement = (e) => { - const as = [e.localName]; - for (const { name, value } of e.attributes) { - if (name === "class") { - as.push( - ...value.split(" ").map((x) => `.${x}`), - ); - } else { - as.push( - name === "id" ? `#${value}` : `[${name}="${value}"]`, - ); - } - } - return as; -}; - -export const randomUUID = () => - window.crypto.randomUUID && - window.crypto.randomUUID() || - "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { - const r = Math.random() * 16 | 0, - v = c === "x" ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); - -export const removeAllChildren = (e) => { - while (e.firstElementChild) { - e.removeChild(e.firstElementChild); - } -}; - -export const appendElement = (x, y) => x.append(y) || y; -export const appendNode = (x, y) => x.appendChild(y) || y; -export const prependElement = (x, y) => x.prepend(y) || y; - -export const requestIdleCallback = (f, d = 1000) => - globalThis.requestIdleCallback && - globalThis.requestIdleCallback(f, { timeout: d }) || - globalThis.setTimeout(f); - -export const parsePascalCaseToSpineCase = (x) => - x.split(/(?=[A-Z0-9])/).join("-").toLowerCase(); - -export const parseSpineCaseToCamelCase = (x) => - x.replace(/(-\w)/g, (m) => m[1].toUpperCase()); - -/** - * Renders elements given an iterable. - */ -export const renderFor = (te, xs, f, g = appendElement, h = (x) => x.key) => { - const fragment = window.document.createDocumentFragment(); - if (xs.length >= te.children.length) { - const es = Array.from(xs).map((x, i, xs) => - renderOnce( - te, - (te) => { - const e = f(te, x, i, xs); - e.dataset.key = h(x) || String(i); - return e; - }, - (_, e) => g(fragment, e), - ) - ); - te.appendChild(fragment); - return es; - } - removeAllChildren(te); - return Array.from(xs).map((x) => g(te, f(te, x))); -}; - -export const renderIf = (te, k, f, g = (x) => x, ...xs) => { - removeAllChildren(te); - return (k()) ? f(te, ...xs) : g(te, ...xs); -}; - -export const renderOnce = ( - te, - f, - g = appendElement, - h = (te, e) => te.querySelector(parseSelectorForElement(e).join("")), -) => { - const e = f(te); - return h(te, e) || g(te, e); -}; diff --git a/library/utilities.ts b/library/utilities.ts new file mode 100644 index 0000000..44de42f --- /dev/null +++ b/library/utilities.ts @@ -0,0 +1,194 @@ +export const deferUntil = ( + e: E, + f: (x: E) => boolean, + d = 1000 * 5, +): Promise => + new Promise((resolve, reject) => { + if (f(e)) resolve(e); + else { + const t1 = setTimeout(() => { + reject(new Error("Timed out")); + t1 && clearTimeout(t1); + t2 && clearInterval(t2); + }, d); + const t2 = setInterval(() => { + if (f(e)) { + resolve(e); + t1 && clearTimeout(t1); + t2 && clearInterval(t2); + } + }); + } + }); + +export const deferUntilNextFrame = (): Promise => + new Promise( + (resolve) => + Reflect.apply( + Reflect.get(globalThis, "requestAnimationFrame"), + globalThis, + [() => resolve()], + ), + ); + +// export const disconnectAllElements = (e: { elemenets: Array }) => { +// for (const k in e.elements) { +// for (const f of b.listeners) { +// if (Object.prototype.hasOwnProperty.call(e.elements, k)) { +// e.elements[k].removeEventListener(f); +// } +// } +// } +// }; + +export const factorizeTemplate = (x: string) => + (document: Document) => { + const t = document.createElement("template"); + t.innerHTML = x; + return t; + }; + +export const intersects = (xs: Array, ys: Array) => + ys && ys.reduce( + (x, y) => !xs.includes(y) ? false : x, + true, + ) || false; + +export const maybeCall = (f: () => Promise | Y): Promise => { + try { + return Promise.resolve(f()) as Promise; + } catch (e) { + return Promise.reject(e) as Promise; + } +}; + +export const noop = (..._: Array): void => undefined; + +export const parseSelectorForElement = ( + e: E, +): Array => { + const as = [e.localName]; + for (const { name, value } of Array.from(e.attributes)) { + if (name === "class") { + as.push( + ...value.split(" ").map((x) => `.${x}`), + ); + } else { + as.push( + name === "id" ? `#${value}` : `[${name}="${value}"]`, + ); + } + } + return as; +}; + +export const randomUUID = (): string => + Reflect.has(globalThis?.crypto, "randomUUID") && + Reflect.apply( + Reflect.get(globalThis?.crypto, "randomUUID"), + globalThis.crypto, + [], + ) || + "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { + const r = Math.random() * 16 | 0, + v = c === "x" ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + +export const removeAllChildren = (e: E) => { + while (e.firstElementChild) { + e.removeChild(e.firstElementChild); + } +}; + +export const appendElement = ( + x: X, + ...y: Array +): Y => { + x.append(...y); + return y[0]; +}; +export const appendNode = (x: X, y: Y): Y => { + x.appendChild(y); + return y; +}; +export const prependElement = ( + x: X, + ...y: Array +): Y => { + x.prepend(...y); + return y[0]; +}; + +export const requestIdleCallback = (f: () => void, d = 1000) => + Reflect.has(globalThis, "requestIdleCallback") && + Reflect.apply(Reflect.get(globalThis, "requestIdleCallback"), null, [f, { + timeout: d, + }]) || + globalThis.setTimeout(f); + +export const parsePascalCaseToSpineCase = (x: string) => + x.split(/(?=[A-Z0-9])/).join("-").toLowerCase(); + +export const parseSpineCaseToCamelCase = (x: string) => + x.replace(/(-\w)/g, (m) => m[1].toUpperCase()); + +/** + * Renders elements given an iterable. + */ +export const renderFor = < + X, + T extends Element, + E extends HTMLElement & { key?: string }, +>( + te: T, + xs: Array, + f: (te: T, x: X, i: number, xs: Iterable) => E, + g = appendElement, + h?: (x: X) => string, +) => { + const fragment = globalThis.document.createDocumentFragment(); + if (xs.length >= te.children.length) { + const es = Array.from(xs).map((x, i, xs) => + renderOnce( + te, + (te: T) => { + const e = f(te, x, i, xs); + e.dataset.key = h ? h(x) : String(i); + return e; + }, + (_: X, e: Y) => + g(fragment as unknown as X, e), + ) + ); + te.appendChild(fragment); + return es; + } + removeAllChildren(te); + return Array.from(xs).map((x, i) => g(te, f(te, x, i, xs) as Element)); +}; + +export const renderIf = < + T extends Element, +>( + te: T, + k: () => boolean, + f: (te: T) => void, + g = (te: T) => te, +) => { + removeAllChildren(te); + return (k()) ? f(te) : g(te); +}; + +export const renderOnce = < + T extends Element, + E extends HTMLElement, +>( + te: T, + f: (te: T) => E, + g = appendElement, + h = (te: T, e: E) => te.querySelector(parseSelectorForElement(e).join("")), +) => { + const e = f(te); + return h(te, e) || g(te, e as Element); +}; diff --git a/library/utilities_test.ts b/library/utilities_test.ts index d7e9b84..26d44f9 100644 --- a/library/utilities_test.ts +++ b/library/utilities_test.ts @@ -1,14 +1,6 @@ -import { assert } from "./asserts.js"; -import { test } from "./testing.js"; -// @deno-types="./utilities.d.ts" -import { - appendElement, - deferUntil, - deferUntilNextFrame, - prependElement, - randomUUID, - renderFor, -} from "./utilities.js"; +import { assert } from "./asserts.ts"; +import { test } from "./testing.ts"; +import { deferUntil, deferUntilNextFrame, randomUUID } from "./utilities.ts"; globalThis.requestAnimationFrame = (f: FrameRequestCallback) => setTimeout(f); diff --git a/mod.ts b/mod.ts index 276cf49..15db55d 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,4 @@ export * from "./library/asserts.ts"; export * from "./library/component.ts"; -// @deno-types="./library/testing.d.ts" -export * from "./library/testing.js"; -// @deno-types="./library/utilities.d.ts" -export * from "./library/utilities.js"; +export * from "./library/testing.ts"; +export * from "./library/utilities.ts"; diff --git a/tsconfig.json b/tsconfig.json index 58de386..7ee3aac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,8 @@ "lib": [ "esnext", "dom", - "dom.iterable", - "dom.asynciterable" + "dom.iterable" ], - - "strict": true, + "strict": true } }