diff --git a/src/element-ref.test.html b/src/element-ref.test.html deleted file mode 100644 index 1b19775..0000000 --- a/src/element-ref.test.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - `element-ref` tests - - - - diff --git a/src/element-ref.test.ts b/src/element-ref.test.ts deleted file mode 100644 index 34b1907..0000000 --- a/src/element-ref.test.ts +++ /dev/null @@ -1,634 +0,0 @@ -import { ElementRef } from './element-ref.js'; -import { type AttrSerializable, type AttrSerializer, type ElementSerializable, type ElementSerializer, toSerializer } from './serializers.js'; -import { parseHtml } from './testing/html-parser.js'; - -describe('element-ref', () => { - describe('ElementRef', () => { - describe('from', () => { - it('constructs a new `ElementRef` from the given native element', () => { - const el = ElementRef.from(document.createElement('div')); - - expect(el).toBeInstanceOf(ElementRef); - - // Verify that native element type is inferred from the input. - () => { - el.native satisfies HTMLDivElement; - }; - }); - - it('throws an error when given a non-Element type', () => { - const textNode = document.createTextNode('Hello, World!'); - expect(() => ElementRef.from(textNode as unknown as Element)) - .toThrowError(/Tried to create an `ElementRef` of `nodeType` 3/); - - expect(() => ElementRef.from(document as unknown as Element)) - .toThrowError(/Tried to create an `ElementRef` of `nodeType` 9/); - }); - - it('rejects `Node` inputs at compile-time', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - // @ts-expect-error `Node` is not assignable to `Element`. - ElementRef.from(document.createTextNode('Hello, World!')); - }; - }); - }); - - describe('native', () => { - it('returns the native element given to the `ElementRef`', () => { - const div = document.createElement('div'); - const el = ElementRef.from(div); - - expect(el.native).toBe(div); - }); - - it('should be readonly', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = ElementRef.from(document.createElement('div')); - - // @ts-expect-error `native` is `readonly`. - el.native = document.createElement('div'); - }; - }); - }); - - describe('read', () => { - it('reads the text content of the element and deserializes with the given primitive serializer', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
Hello, World!
`)); - expect(el.read(String)).toBe('Hello, World!'); - - const el2 = ElementRef.from(parseHtml(HTMLDivElement, - `
12345
`)); - expect(el2.read(Number)).toBe(12345); - - const el3 = ElementRef.from(parseHtml(HTMLDivElement, - `
true
`)); - expect(el3.read(Boolean)).toBeTrue(); - - const el4 = ElementRef.from(parseHtml(HTMLDivElement, - `
12345
`)); - expect(el4.read(BigInt)).toBe(12345n); - }); - - it('reads the text content of the element with the given custom serializer', () => { - const serializer: ElementSerializer<{ foo: string }, Element> = { - serializeTo(): void { /* no-op */ }, - - deserializeFrom(): { foo: string } { - return { foo: 'bar' }; - }, - }; - - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
Hello, World!
`)); - expect(el.read(serializer)).toEqual({ foo: 'bar' }); - }); - - it('reads the text content of the element with the given serializable', () => { - class User { - public constructor(private name: string) {} - public static [toSerializer](): ElementSerializer { - return { - serializeTo(user: User, element: Element): void { - element.textContent = user.name; - }, - - deserializeFrom(element: Element): User { - return new User(element.textContent!); - }, - }; - } - } - - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
Devel
`)); - expect(el.read(User)).toEqual(new User('Devel')); - }); - - it('throws an error if the deserialization process throws', () => { - const err = new Error('Failed to deserialize.'); - const serializer: ElementSerializer = { - serializeTo(): void { /* no-op */ }, - - deserializeFrom(): string { - throw err; - } - }; - - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
Hello, World!
`)); - expect(() => el.read(serializer)).toThrow(err); - }); - - it('resolves return type from input primitive serializer type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - const _resultStr: string = el.read(String); - const _resultNum: number = el.read(Number); - const _resultBool: boolean = el.read(Boolean); - const _resultBigInt: bigint = el.read(BigInt); - }; - }); - - it('resolves return type from input custom serializer type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - const serializer = {} as ElementSerializer; - - const _result: number = el.read(serializer); - }; - }); - - it('resolves return type from input custom serializable type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - const serializable = {} as ElementSerializable; - - const _result: number = el.read(serializable); - }; - }); - - it('resolves serializer type based on element type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - const divSerializer = {} as ElementSerializer; - el.read(divSerializer); - - const inputSerializer = - {} as ElementSerializer; - // @ts-expect-error - el.read(inputSerializer); - - const divSerializable = - {} as ElementSerializable; - el.read(divSerializable); - - const inputSerializable = - {} as ElementSerializable; - // @ts-expect-error - el.read(inputSerializable); - }; - }); - - it('throws a compile-time error for attribute serializers', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - const serializer = {} as AttrSerializer; - // @ts-expect-error - el.read(serializer); - - const serializable = {} as AttrSerializable; - // @ts-expect-error - el.read(serializable); - }; - }); - }); - - describe('attr', () => { - it('returns the attribute value for the given name', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - - expect(el.attr('foo', String)).toBe('bar'); - }); - - it('deserializes empty string when the attribute is set with no value', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - - expect(el.attr('foo', String)).toBe(''); - expect(el.attr('bar', String)).toBe(''); - }); - - it('deserializes the attribute with the given primitive serializer', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(el.attr('name', String)).toBe('Devel'); - - const el2 = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(el2.attr('id', Number)).toBe(12345); - - const el3 = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(el3.attr('id', BigInt)).toBe(12345n); - }); - - it('deserializes booleans based on text value, not attribute presence', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(el.attr('enabled', Boolean)).toBeTrue(); - - const el2 = ElementRef.from(parseHtml(HTMLDivElement, ` -
- `)); - expect(el2.attr('enabled', Boolean)).toBeFalse(); - - const el3 = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(() => el3.attr('enabled', Boolean)).toThrow(); - - const el4 = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(() => el4.attr('enabled', Boolean)).toThrow(); - - const el5 = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(() => el5.attr('enabled', Boolean)).toThrow(); - }); - - it('deserializes the attribute with the given custom serializer', () => { - const serializer: AttrSerializer<{ foo: string }> = { - serialize(value: { foo: string }): string { - return value.foo; - }, - - deserialize(): { foo: string } { - return { foo: 'bar' }; - } - }; - - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(el.attr('hello', serializer)).toEqual({ foo: 'bar' }); - }); - - it('deserializes the attribute with the given serializable', () => { - class User { - public constructor(private name: string) {} - public static [toSerializer](): AttrSerializer { - return { - serialize(user: User): string { - return user.name; - }, - - deserialize(name: string): User { - return new User(name); - }, - }; - } - } - - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(el.attr('name', User)).toEqual(new User('Devel')); - }); - - it('throws an error if the deserialization process throws', () => { - const err = new Error('Failed to deserialize.'); - const serializer: AttrSerializer = { - serialize(value: string): string { - return value; - }, - - deserialize(): string { - throw err; - } - }; - - const el = ElementRef.from(parseHtml(HTMLDivElement, - `
`)); - expect(() => el.attr('hello', serializer)).toThrow(err); - }); - - it('returns `undefined` if the attribute does not exist and is marked optional', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, `
`)); - expect(el.attr('hello', String, { optional: true })).toBeUndefined(); - }); - - it('resolves return type from input primitive serializer type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - const _result1: string = el.attr('test', String); - const _result2: string = el.attr('test', String, {}); - const _result3: string = - el.attr('test', String, { optional: false }); - - let optional = el.attr('test', String, { optional: true }); - optional = 'test' as string | undefined; - - let unknown = - el.attr('test', String, { optional: true as boolean }); - unknown = 'test' as string | undefined; - }; - }); - - it('resolves return type from input custom serializer type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - const serializer = {} as AttrSerializer; - - const _result1: number = el.attr('test', serializer); - const _result2: number = el.attr('test', serializer, {}); - const _result3: number = - el.attr('test', serializer, { optional: false }); - - let optional = el.attr('test', serializer, { optional: true }); - optional = 123 as number | undefined; - - let unknown = - el.attr('test', serializer, { optional: true as boolean }); - unknown = 123 as number | undefined; - }; - }); - - it('resolves return type from input custom serializable type', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - const serializable = {} as AttrSerializable; - - const _result1: number = el.attr('test', serializable); - const _result2: number = el.attr('test', serializable, {}); - const _result3: number = - el.attr('test', serializable, { optional: false }); - - let optional = el.attr('test', serializable, { optional: true }); - optional = 123 as number | undefined; - - let unknown = - el.attr('test', serializable, { optional: true as boolean }); - unknown = 123 as number | undefined; - }; - }); - - it('throws a compile-time error when used with an element serializer', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - const serializer = {} as ElementSerializer; - // @ts-expect-error - el.attr('test', serializer); - - const serializable = {} as ElementSerializable; - // @ts-expect-error - el.attr('test', serializable); - }; - }); - }); - - describe('query', () => { - it('returns the queried element', () => { - const el = ElementRef.from( - parseHtml(HTMLDivElement, `
Hello, World!
`)); - - expect(el.query('span').read(String)).toBe('Hello, World!'); - }); - - it('throws an error when no element is found and not marked `optional`', () => { - const el = ElementRef.from(document.createElement('div')); - - // Explicitly required. - expect(() => el.query('span', { optional: false })) - .toThrowError(/Selector "span" did not resolve to an element\./); - - // Implicitly required. - expect(() => el.query('span', {})) - .toThrowError(/Selector "span" did not resolve to an element\./); - expect(() => el.query('span')) - .toThrowError(/Selector "span" did not resolve to an element\./); - }); - - it('returns non-nullable type for required query', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - // Explicitly required. - { - let result = el.query('span', { optional: false }); - - // @ts-expect-error `null` should not be assignable to `result`. - result = null; - } - - // Implicitly required via default `optional`. - { - let result = el.query('span', {}); - - // @ts-expect-error `null` should not be assignable to `result`. - result = null; - } - - // Implicitly required via default `options`. - { - let result = el.query('span'); - - // @ts-expect-error `null` should not be assignable to `result`. - result = null; - } - }; - }); - - it('types the result based on a required query', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - let _result1: ElementRef = el.query('input#id'); - let _result2: ElementRef = el.query('input#id', {}); - let _result3: ElementRef = el.query('input#id', { - optional: false, - }); - }; - }); - - it('types the result as nullable based on an optional query', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - let _result: ElementRef | null = - el.query('input#id', { optional: true }); - }; - }); - - it('types the result as nullable based on unknown optionality', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - let _result: ElementRef | null = - el.query('input#id', { optional: true as boolean }); - }; - }); - - it('types the result as `null` for impossible queries', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - let _result: null = el.query('input::before'); - }; - }); - - it('returns `null` when no element is found and marked `optional`', () => { - const el = ElementRef.from(document.createElement('div')); - - expect(el.query('span', { optional: true })).toBeNull(); - }); - - it('returns nullable type for `optional` query', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - // `ElementRef | null` - let result = el.query('span', { optional: true }); - - // `null` should be assignable to implicit return type. - result = null; - }; - }); - - it('returns possibly nullable type for unknown optionality', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el: ElementRef = {} as any; - - // `ElementRef | null` - let result = el.query('span', { optional: true as boolean }); - - // `null` should be assignable to implicit return type. - result = null; - }; - }); - - it('scopes to the native element', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, ` -
-
- - Descendant -
- Child -
- `)); - - expect(el.query(':scope > span').read(String)).toBe('Child'); - }); - }); - - describe('queryAll', () => { - it('returns the queried elements', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, ` -
- Hello, World! - Hello again! - Hello once more! -
- `)); - - expect(el.queryAll('span').map((el) => el.read(String))).toEqual([ - 'Hello, World!', - 'Hello again!', - 'Hello once more!', - ]); - }); - - it('throws an error when no element is found and not marked `optional`', () => { - const el = ElementRef.from(document.createElement('div')); - - // Explicitly required. - expect(() => el.queryAll('span', { optional: false })) - .toThrowError(/Selector "span" did not resolve to any elements\./); - - // Implicitly required. - expect(() => el.queryAll('span')) - .toThrowError(/Selector "span" did not resolve to any elements\./); - expect(() => el.queryAll('span', {})) - .toThrowError(/Selector "span" did not resolve to any elements\./); - }); - - it('returns an empty array when no element is found and marked `optional`', () => { - const el = ElementRef.from(document.createElement('div')); - - expect(el.queryAll('span', { optional: true })).toEqual([]); - }); - - it('returns a *real* array', () => { - const el = ElementRef.from(document.createElement('div')); - - expect(el.queryAll('span', { optional: true })).toBeInstanceOf(Array); - }); - - it('scopes to the native element', () => { - const el = ElementRef.from(parseHtml(HTMLDivElement, ` -
-
- - Descendant -
- Child 1 - Child 2 -
- `)); - - expect(el.queryAll(':scope > span').map((el) => el.read(String))) - .toEqual([ - 'Child 1', - 'Child 2', - ]); - }); - - it('types the result based on query', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - const _result: ElementRef[] = el.queryAll('input'); - }; - }); - - it('type the result without `null` pollution', () => { - // Type-only test, only needs to compile, not execute. - expect().nothing(); - () => { - const el = {} as ElementRef; - - // `Array>` because pseudo-selectors always - // resolve to `null`. At runtime, this will always be an empty `[]`, - // so we type this as `Array>`. - const result = el.queryAll('input::before'); - - // @ts-expect-error `result` contains an array of `Element`, not an - // array of `HTMLInputElement`. - const _inputs: ElementRef[] = result; - }; - }); - }); - }); -}); diff --git a/src/element-ref.ts b/src/element-ref.ts deleted file mode 100644 index e46d40b..0000000 --- a/src/element-ref.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * @fileoverview Defines the {@link ElementRef} class and associated utilities. - */ - -import { type QueriedElement } from './query.js'; -import { type AttrSerializerToken, type ElementSerializerToken, type ResolveSerializer, resolveSerializer } from './serializer-tokens.js'; -import { type AttrSerializable, type AttrSerializer, type ElementSerializable, type ElementSerializer, type Serialized, bigintSerializer, booleanSerializer, numberSerializer, stringSerializer, toSerializer } from './serializers.js'; - -/** - * A wrapper class of {@link Element} which provides more ergonomic API access - * conducive to chaining. - */ -export class ElementRef { - private constructor(public readonly native: El) {} - - /** - * Creates a new {@link ElementRef} from the given {@link Element} object. - * - * @param native The native element to wrap in an {@link ElementRef}. - * @returns A new {@link ElementRef} object wrapping the input. - */ - public static from(native: El): ElementRef { - if (native.nodeType !== Node.ELEMENT_NODE) { - throw new Error(`Tried to create an \`ElementRef\` of \`nodeType\` ${ - native.nodeType} (see https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType).\n\n\`ElementRef\` must be created with an \`Element\`, not any other type of \`Node\`.`); - } - - return new ElementRef(native); - } - - /** - * Provides the value of the text content on the underlying element. - * - * @param token A "token" which identifiers an {@link ElementSerializer} to - * deserialize the read attribute string. A token is one of: - * * A primitive serializer - {@link String}, {@link Boolean}, - * {@link Number}, {@link BigInt}. - * * An {@link ElementSerializer} object. - * * A {@link ElementSerializable} object. - * @returns The value of the text content for this element deserialized based - * on the input token. - */ - public read>(token: Token): - Serialized, - ElementSerializable - >> { - const serializer = resolveSerializer< - Token, - ElementSerializer, - ElementSerializable - >(token); - - return serializer.deserializeFrom(this.native) as any; - } - - /** - * Provides the value of the attribute with the given name on the underlying - * element. - * - * Note that an attribute without a value such as `
` will - * return an empty string which is considered falsy. The correct way to check - * if an attribute exists is: `attr('foo') !== null`. - * - * @param name The name of the attribute to read. - * @param token A "token" which identifiers an {@link AttrSerializer} to - * deserialize the read attribute string. A token is one of: - * * A primitive serializer - {@link String}, {@link Boolean}, - * {@link Number}, {@link BigInt}. - * * An {@link AttrSerializer} object. - * * A {@link AttrSerializable} object. - * @returns The value of the attribute deserialized based on the input token, - * or `null` if not set. - */ - public attr>( - name: string, - token: Token, - options: { optional: true }, - ): Serialized, - AttrSerializable - >> | undefined; - public attr>( - name: string, - token: Token, - options?: { optional?: false }, - ): Serialized, - AttrSerializable - >>; - public attr>( - name: string, - token: Token, - options?: { optional?: boolean }, - ): Serialized, - AttrSerializable - >> | undefined; - public attr>( - name: string, - token: Token, - { optional }: { optional?: boolean } = {}, - ): Serialized, - AttrSerializable - >> | undefined { - const serialized = this.native.getAttribute(name); - if (serialized === null) { - if (optional) { - return undefined; - } else { - throw new Error(`Attribute "${name}" did not exist on element. Is the name wrong, or does the attribute not exist? If it is expected that the attribute may not exist, consider calling \`attr\` with \`{ optional: true }\` to ignore this error.`); - } - } - - const serializer = resolveSerializer< - Token, - AttrSerializer, - AttrSerializable - >(token); - return serializer.deserialize(serialized) as any; - } - - /** - * Queries light DOM descendants for the provided selector and returns the - * first matching element wrapped in an {@link ElementRef}. Returns - * `undefined` if no element is found. - * - * @param selector The selector to query for. - * @returns An {@link ElementRef} which wraps the query result, or `null` if - * no element is found. - */ - public query( - selector: Query, - options?: { readonly optional?: false }, - ): QueryResult; - public query( - selector: Query, - options: { readonly optional: boolean }, - ): QueryResult | null; - public query(selector: Query, { optional = false }: { - readonly optional?: boolean, - } = {}): QueryResult | null { - const child = this.native.querySelector(selector) as - QueriedElement | null; - if (!child) { - if (optional) { - return null; - } else { - throw new Error(`Selector "${selector}" did not resolve to an element. Is the selector wrong, or does the element not exist? If it is expected that the element may not exist, consider calling \`.query('${selector}', { optional: true })\` to ignore this error.`); - } - } - - return ElementRef.from(child) as QueryResult; - } - - /** - * Queries light DOM descendants for the provided selector and returns all - * matching elements, each wrapped in an {@link ElementRef}. Always returns a - * real {@link Array}, not a {@link NodeListOf} like - * {@link Element.prototype.querySelectorAll}. Returns an empty array when no - * elements match the given query. - * - * @param selector The selector to query for. - * @returns An {@link Array} of the queried elements, each wrapped in an - * {@link ElementRef}. - */ - public queryAll( - selector: Selector, - { optional = false }: { optional?: boolean } = {}, - ): Array>> { - const elements = this.native.querySelectorAll(selector) as - NodeListOf>; - if (!optional && elements.length === 0) { - throw new Error(`Selector "${selector}" did not resolve to any elements. Is the selector wrong, or do the elements not exist? If it is expected that the elements may not exist, consider calling \`.queryAll('${selector}', { optional: true })\` to ignore this error.`); - } - - return Array.from(elements).map((el) => ElementRef.from(el)); - } -} - -// `QueriedElement` returns `null` when given a pseudo-element selector. Need to -// avoid boxing this `null` into `ElementRef`. -type QueryResult = - QueriedElement extends null - ? null - : ElementRef> -; - -// `QueriedElement` returns `null` when given a pseudo-element selector. Need to -// avoid boxing this `null` into `null[]`, when any such values would be -// filtered out of the result. -type QueryAllResult = - QueriedElement extends null - ? Element - : QueriedElement -; diff --git a/src/index.ts b/src/index.ts index 4032e04..3443d49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,6 @@ export { type HydrateLifecycle, defineComponent } from './component.js'; export { ComponentRef, type OnDisconnect, type OnConnect } from './component-ref.js'; export { Dehydrated } from './dehydrated.js'; export { ElementAccessor } from './element-accessor.js'; -export { ElementRef } from './element-ref.js'; export { type Queryable } from './queryable.js'; export { QueryRoot } from './query-root.js'; export { hydrate, isHydrated } from './hydration.js';