diff --git a/src/component-ref.test.ts b/src/component-ref.test.ts
index f13ab75..694867c 100644
--- a/src/component-ref.test.ts
+++ b/src/component-ref.test.ts
@@ -276,1287 +276,6 @@ describe('component-ref', () => {
});
});
- describe('live', () => {
- it('returns an initialized signal', () => {
- const el = parseHtml(NoopComponent, `
- Hello!
- `);
- const ref = el.getComponentRef();
-
- const text = ref.live(ref.host, String);
- expect(text()).toBe('Hello!');
- });
-
- it('binds to the provided DOM element', async () => {
- const el = parseHtml(NoopComponent, `
-
- test
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const text = ref.live(ref.host.query('span'), String);
- expect(text()).toBe('test');
-
- text.set('test2');
- await el.stable();
- expect(el.querySelector('span')!.textContent!).toBe('test2');
- });
-
- it('binds to the element returned by the provided selector query', async () => {
- const el = parseHtml(NoopComponent, `
-
- test
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const text = ref.live('span', String);
- expect(text()).toBe('test');
-
- text.set('test2');
- await el.stable();
- expect(el.querySelector('span')!.textContent!).toBe('test2');
- });
-
- it('processes the DOM element based on the provided primitive serializer token', async () => {
- {
- const el = parseHtml(NoopComponent, `
- test1
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.live(ref.host, String);
- expect(value()).toBe('test1');
-
- value.set('test2');
- await el.stable();
- expect(el.textContent!).toBe('test2');
- }
-
- {
- const el = parseHtml(NoopComponent, `
- 1234
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.live(ref.host, Number);
- expect(value()).toBe(1234);
-
- value.set(4321);
- await el.stable();
- expect(el.textContent!).toBe('4321');
- }
-
- {
- const el = parseHtml(NoopComponent, `
- true
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.live(ref.host, Boolean);
- expect(value()).toBe(true);
-
- value.set(false);
- await el.stable();
- expect(el.textContent!).toBe('false');
- }
-
- {
- const el = parseHtml(NoopComponent, `
- 1234
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.live(ref.host, BigInt);
- expect(value()).toBe(1234n);
-
- value.set(4321n);
- await el.stable();
- expect(el.textContent!).toBe('4321');
- }
- });
-
- it('processes the DOM element based on the provided custom serializer', async () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const serializer: ElementSerializer = {
- serializeTo(value: string, element: Element): void {
- element.textContent = `serialized: ${value}`;
- },
-
- deserializeFrom(): string {
- return 'deserialized';
- },
- };
-
- const value = ref.live(ref.host, serializer);
- expect(value()).toBe('deserialized');
-
- value.set('test');
- await el.stable();
- expect(el.textContent!).toBe('serialized: test');
- });
-
- it('processes the DOM element based on the provided serializable', async () => {
- 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 = parseHtml(NoopComponent, `
- Devel
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.live(ref.host, User);
- expect(value()).toEqual(new User('Devel'));
-
- value.set(new User('Devel without a Cause'));
- await el.stable();
- expect(el.textContent!).toBe('Devel without a Cause');
- });
-
- it('throws an error when binding to the same element multiple times', () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- const ref = el.getComponentRef();
-
- ref.live('span', String);
- expect(() => ref.live('#my-span', String))
- .toThrowError(/cannot bind it again/);
- });
-
- it('throws an error when binding to the same element multiple times from different components', () => {
- const outerEl = parseHtml(NoopComponent, `
-
-
-
- `);
- const outerRef = outerEl.getComponentRef();
-
- const innerEl = outerRef.host.query('noop-component').native;
- const innerRef = innerEl.getComponentRef();
-
- outerRef.live('noop-component', String);
- expect(() => innerRef.live(innerRef.host, String))
- .toThrowError(/cannot bind it again/);
- });
-
- it('infers the return type based on the serializer', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- // Primitive serializer tokens.
- const _signal1: WriteableSignal = ref.live(ref.host, String);
- const _signal2: WriteableSignal = ref.live(ref.host, Number);
- const _signal3: WriteableSignal =
- ref.live(ref.host, Boolean);
- const _signal4: WriteableSignal = ref.live(ref.host, BigInt);
-
- // Custom `ElementSerializer` type.
- const serializer = {} as ElementSerializer;
- const _signal5: WriteableSignal =
- ref.live(ref.host, serializer);
-
- // Custom `ElementSerializable` type.
- const serializable = {} as ElementSerializable;
- const _signal6: WriteableSignal =
- ref.live(ref.host, serializable);
- };
- });
-
- it('resolves serializer type based on element type', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
- const div = {} as ElementRef;
-
- const divSerializer = {} as ElementSerializer;
- ref.live(div, divSerializer);
- ref.live('div', divSerializer);
-
- const inputSerializer =
- {} as ElementSerializable;
- // @ts-expect-error
- ref.live(div, inputSerializer);
- // @ts-expect-error
- ref.live('div', inputSerializer);
-
- const elSerializer = {} as ElementSerializer;
- ref.live('.foo', elSerializer);
- // @ts-expect-error
- ref.live('.foo', divSerializer);
- };
- });
-
- it('throws a compile time error when given an attribute serializer', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- const serializer = {} as AttrSerializer;
- // @ts-expect-error
- ref.live(ref.host, serializer);
-
- const serializable = {} as AttrSerializable;
- // @ts-expect-error
- ref.live(ref.host, serializable);
- };
- });
- });
-
- describe('liveAttr', () => {
- it('returns an initialized signal', () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- const ref = el.getComponentRef();
-
- const text = ref.liveAttr(ref.host, 'value', String);
- expect(text()).toBe('Hello!');
- });
-
- it('binds to the provided DOM element', async () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const text = ref.liveAttr(ref.host.query('span'), 'value', String);
- expect(text()).toBe('test');
-
- text.set('test2');
- await el.stable();
- expect(el.querySelector('span')!.getAttribute('value')).toBe('test2');
- });
-
- it('binds to the element returned by the provided selector query', async () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const text = ref.liveAttr('span', 'value', String);
- expect(text()).toBe('test');
-
- text.set('test2');
- await el.stable();
- expect(el.querySelector('span')!.getAttribute('value')).toBe('test2');
- });
-
- it('processes the DOM element based on the provided primitive serializer token', async () => {
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.liveAttr(ref.host, 'value', String);
- expect(value()).toBe('test1');
-
- value.set('test2');
- await el.stable();
- expect(el.getAttribute('value')).toBe('test2');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.liveAttr(ref.host, 'value', Number);
- expect(value()).toBe(1234);
-
- value.set(4321);
- await el.stable();
- expect(el.getAttribute('value')).toBe('4321');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.liveAttr(ref.host, 'value', Boolean);
- expect(value()).toBe(true);
-
- value.set(false);
- await el.stable();
- expect(el.getAttribute('value')).toBe('false');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.liveAttr(ref.host, 'value', BigInt);
- expect(value()).toBe(1234n);
-
- value.set(4321n);
- await el.stable();
- expect(el.getAttribute('value')).toBe('4321');
- }
- });
-
- it('processes the DOM element based on the provided custom serializer', async () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const serializer: AttrSerializer = {
- serialize(value: string): string {
- return `serialized: ${value}`;
- },
-
- deserialize(): string {
- return 'deserialized';
- },
- };
-
- const value = ref.liveAttr(ref.host, 'value', serializer);
- expect(value()).toBe('deserialized');
-
- value.set('test');
- await el.stable();
- expect(el.getAttribute('value')).toBe('serialized: test');
- });
-
- it('processes the DOM element based on the provided serializable', async () => {
- 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 = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = ref.liveAttr(ref.host, 'user', User);
- expect(value()).toEqual(new User('Devel'));
-
- value.set(new User('Devel without a Cause'));
- await el.stable();
- expect(el.getAttribute('user')).toBe('Devel without a Cause');
- });
-
- it('throws an error when binding to the same element attribute multiple times', () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- const ref = el.getComponentRef();
-
- ref.liveAttr('span', 'value', String);
- expect(() => ref.liveAttr('#my-span', 'value', String))
- .toThrowError(/cannot bind it again/);
- });
-
- it('throws an error when binding to the same element attribute multiple times from different components', () => {
- const outerEl = parseHtml(NoopComponent, `
-
-
-
- `);
- const outerRef = outerEl.getComponentRef();
-
- const innerEl = outerRef.host.query('noop-component').native;
- const innerRef = innerEl.getComponentRef();
-
- outerRef.liveAttr('noop-component', 'value', String);
- expect(() => innerRef.liveAttr(innerRef.host, 'value', String))
- .toThrowError(/cannot bind it again/);
- });
-
- it('infers the return type based on the serializer', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- // Primitive serializer tokens.
- const _signal1: WriteableSignal =
- ref.liveAttr(ref.host, 'value', String);
- const _signal2: WriteableSignal =
- ref.liveAttr(ref.host, 'value', Number);
- const _signal3: WriteableSignal =
- ref.liveAttr(ref.host, 'value', Boolean);
- const _signal4: WriteableSignal =
- ref.liveAttr(ref.host, 'value', BigInt);
-
- // Custom `AttrSerializer` type.
- const serializer = {} as AttrSerializer;
- const _signal5: WriteableSignal =
- ref.liveAttr(ref.host, 'value', serializer);
-
- // Custom `AttrSerializable` type.
- const serializable = {} as AttrSerializable;
- const _signal6: WriteableSignal =
- ref.liveAttr(ref.host, 'value', serializable);
- };
- });
-
- it('throws a compile time error when given an element serializer', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- const serializer = {} as ElementSerializer;
- // @ts-expect-error
- ref.liveAttr(ref.host, 'test', serializer);
-
- const serializable = {} as ElementSerializable;
- // @ts-expect-error
- ref.liveAttr(ref.host, 'test', serializable);
- };
- });
- });
-
- describe('bind', () => {
- it('updates the provided element\'s text content reactively', async () => {
- const el = document.createElement('noop-component');
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- expect(el.textContent).toBe('');
-
- const value = signal('1');
- ref.bind(ref.host, () => value(), String);
- await el.stable();
- expect(el.textContent).toBe('1');
-
- value.set('2');
- await el.stable();
- expect(el.textContent).toBe('2');
- });
-
- it('does not invoke the signal until connected', async () => {
- const el = document.createElement('noop-component');
- const ref = el.getComponentRef();
- const sig = jasmine.createSpy<() => string>('sig')
- .and.returnValue('test');
-
- ref.bind(ref.host, sig, String);
- await el.stable();
- expect(sig).not.toHaveBeenCalled();
-
- document.body.appendChild(el);
-
- await el.stable();
- expect(sig).toHaveBeenCalledOnceWith();
- });
-
- it('pauses updates while disconnected', async () => {
- const el = document.createElement('noop-component');
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = signal('1');
- const sig = jasmine.createSpy<() => string>('sig')
- .and.callFake(() => value());
-
- ref.bind(ref.host, sig, String);
- await el.stable();
- expect(sig).toHaveBeenCalledOnceWith();
- expect(el.textContent).toBe('1');
- sig.calls.reset();
-
- el.remove();
-
- value.set('2');
- await el.stable();
- expect(sig).not.toHaveBeenCalled();
- expect(el.textContent).toBe('1'); // Does not update.
- });
-
- it('updates the explicitly provided element', async () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host.query('span'), () => 'test');
- await el.stable();
- expect(el.querySelector('span')!.textContent!).toBe('test');
- });
-
- it('queries for the given selector and updates that element', async () => {
- const el = parseHtml(NoopComponent, `
-
-
- `
- );
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind('span', () => 'test', String);
- await el.stable();
- expect(el.querySelector('span')!.textContent!).toBe('test');
- });
-
- it('throws when the given selector is not found', () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- expect(() => ref.bind('span', () => 'test', String)).toThrow();
- });
-
- it('serializes with an implicit primitive serializer', async () => {
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => 'test');
- await el.stable();
- expect(ref.host.native.textContent!).toBe('test');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => 1234);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('1234');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => true);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('true');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => 1234n);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('1234');
- }
- });
-
- it('serializes with an explicit primitive serializer', async () => {
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => 'test', String);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('test');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => 1234, Number);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('1234');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => true, Boolean);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('true');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => 1234n, BigInt);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('1234');
- }
- });
-
- it('serializes with a custom `Serializer`', async () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const serializer: ElementSerializer = {
- serializeTo(_value: undefined, element: Element): void {
- element.textContent = 'undefined';
- },
-
- deserializeFrom(): undefined {
- return undefined;
- },
- };
-
- ref.bind(ref.host, () => undefined, serializer);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('undefined');
- });
-
- it('serializes with a custom `Serializable`', async () => {
- 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 = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bind(ref.host, () => new User('Devel'), User);
- await el.stable();
- expect(ref.host.native.textContent!).toBe('Devel');
- });
-
- it('throws an error when binding to the same element multiple times', () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- const ref = el.getComponentRef();
-
- ref.bind('span', () => 'test1');
- expect(() => ref.bind('#my-span', () => 'test2'))
- .toThrowError(/cannot bind it again/);
- });
-
- it('throws an error when binding to the same element multiple times from different components', () => {
- const outerEl = parseHtml(NoopComponent, `
-
-
-
- `);
- const outerRef = outerEl.getComponentRef();
-
- const innerEl = outerRef.host.query('noop-component').native;
- const innerRef = innerEl.getComponentRef();
-
- outerRef.bind('noop-component', () => 'test1');
- expect(() => innerRef.bind(innerRef.host, () => 'test2'))
- .toThrowError(/cannot bind it again/);
- });
-
- it('restricts the signal result and serializer to be the same type', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- // Correct implicit primitive types.
- ref.bind(ref.host, () => 'test');
- ref.bind(ref.host, () => 1234);
- ref.bind(ref.host, () => true);
- ref.bind(ref.host, () => 1234n);
-
- // Incorrect implicit types.
- // @ts-expect-error
- ref.bind(ref.host, () => ({}));
- // @ts-expect-error
- ref.bind(ref.host, () => []);
- // @ts-expect-error
- ref.bind(ref.host, () => undefined);
- // @ts-expect-error
- ref.bind(ref.host, () => null);
-
- // Incorrect types with explicitly `undefined` serializer.
- // @ts-expect-error
- ref.bind(ref.host, () => ({}), undefined);
- // @ts-expect-error
- ref.bind(ref.host, () => [], undefined);
- // @ts-expect-error
- ref.bind(ref.host, () => undefined, undefined);
- // @ts-expect-error
- ref.bind(ref.host, () => null, undefined);
-
- // Incorrect types with possibly `undefined` serializer.
- const maybeSerializer = {} as AttrSerializer<{}> | undefined;
- // @ts-expect-error
- ref.bind(ref.host, () => ({}), maybeSerializer);
- // @ts-expect-error
- ref.bind(ref.host, () => [], maybeSerializer);
- // @ts-expect-error
- ref.bind(ref.host, () => undefined, maybeSerializer);
- // @ts-expect-error
- ref.bind(ref.host, () => null, maybeSerializer);
-
- // Correct explicit primitive types.
- ref.bind(ref.host, () => 'test', String);
- ref.bind(ref.host, () => 1234, Number);
- ref.bind(ref.host, () => true, Boolean);
- ref.bind(ref.host, () => 1234n, BigInt);
-
- // Incorrect explicit primitive types.
- // @ts-expect-error
- ref.bind(ref.host, () => 'test', Number);
- // @ts-expect-error
- ref.bind(ref.host, () => 1234, String);
- // @ts-expect-error
- ref.bind(ref.host, () => true, String);
- // @ts-expect-error
- ref.bind(ref.host, () => 1234n, String);
-
- // Correct explicit serializer types.
- const serializer = {} as ElementSerializer;
- ref.bind(ref.host, () => 'test', serializer);
-
- // Incorrect explicit serializer types.
- // @ts-expect-error
- ref.bind(ref.host, () => 1234, serializer);
-
- // Correct explicit serializable types.
- const serializable = {} as ElementSerializable;
- ref.bind(ref.host, () => 'test', serializable);
-
- // Incorrect explicit serializable types.
- // @ts-expect-error
- ref.bind(ref.host, () => 1234, serializable);
- };
- });
-
- it('resolves serializer type based on element type', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
- const div = {} as ElementRef;
-
- const divSerializer = {} as ElementSerializer;
- ref.bind(div, () => 1234, divSerializer);
- ref.bind('div', () => 1234, divSerializer);
-
- const inputSerializer =
- {} as ElementSerializable;
- // @ts-expect-error
- ref.bind(div, () => 1234, inputSerializer);
- // @ts-expect-error
- ref.bind('div', () => 1234, inputSerializer);
-
- const elSerializer = {} as ElementSerializer;
- ref.bind('.foo', () => 1234, elSerializer);
- // @ts-expect-error
- ref.bind('.foo', () => 1234, divSerializer);
- };
- });
-
- it('throws a compile time error when given an attribute serializer', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- const serializer = {} as AttrSerializer;
- // @ts-expect-error
- ref.bind(ref.host, () => 'test', serializer);
-
- const serializable = {} as AttrSerializable;
- // @ts-expect-error
- ref.bind(ref.host, () => 'test', serializable);
- };
- });
- });
-
- describe('bindAttr', () => {
- it('updates the provided element\'s named attribute reactively', async () => {
- const el = document.createElement('noop-component');
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- expect(el.textContent).toBe('');
-
- const value = signal('1');
- ref.bindAttr(ref.host, 'count', () => value(), String);
- await el.stable();
- expect(el.getAttribute('count')).toBe('1');
-
- value.set('2');
- await el.stable();
- expect(el.getAttribute('count')).toBe('2');
- });
-
- it('does not invoke the signal until connected', async () => {
- const el = document.createElement('noop-component');
- const ref = el.getComponentRef();
- const sig = jasmine.createSpy<() => string>('sig')
- .and.returnValue('test');
-
- ref.bindAttr(ref.host, 'hello', sig, String);
- await el.stable();
- expect(sig).not.toHaveBeenCalled();
-
- document.body.appendChild(el);
-
- await el.stable();
- expect(sig).toHaveBeenCalledOnceWith();
- });
-
- it('pauses updates while disconnected', async () => {
- const el = document.createElement('noop-component');
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const value = signal('1');
- const sig = jasmine.createSpy<() => string>('sig')
- .and.callFake(() => value());
-
- ref.bindAttr(ref.host, 'count', sig, String);
- await el.stable();
- expect(sig).toHaveBeenCalledOnceWith();
- expect(el.getAttribute('count')).toBe('1');
- sig.calls.reset();
-
- el.remove();
-
- value.set('2');
- await el.stable();
- expect(sig).not.toHaveBeenCalled();
- expect(el.getAttribute('count')).toBe('1'); // Does not update.
- });
-
- it('updates the explicitly provided element', async () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host.query('span'), 'name', () => 'test');
- await el.stable();
- expect(el.querySelector('span')!.getAttribute('name')).toBe('test');
- });
-
- it('queries for the given selector and updates that element', async () => {
- const el = parseHtml(NoopComponent, `
-
-
- `
- );
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr('span', 'name', () => 'test', String);
- await el.stable();
- expect(el.querySelector('span')!.getAttribute('name')).toBe('test');
- });
-
- it('throws when the given selector is not found', () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- expect(() => ref.bindAttr('span', 'name', () => 'test', String))
- .toThrow();
- });
-
- it('serializes with an implicit primitive serializer', async () => {
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => 'test');
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('test');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => 1234);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('1234');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => true);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('true');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => 1234n);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('1234');
- }
- });
-
- it('serializes with an explicit primitive serializer', async () => {
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => 'test', String);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('test');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => 1234, Number);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('1234');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => true, Boolean);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('true');
- }
-
- {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name', () => 1234n, BigInt);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('1234');
- }
- });
-
- it('serializes with a custom `Serializer`', async () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- const serializer: AttrSerializer = {
- serialize(): string {
- return 'undefined';
- },
-
- deserialize(): undefined {
- return undefined;
- },
- };
-
- ref.bindAttr(ref.host, 'name', () => undefined, serializer);
- await el.stable();
- expect(ref.host.native.getAttribute('name')).toBe('undefined');
- });
-
- it('serializes with a custom `Serializable`', async () => {
- 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 = parseHtml(NoopComponent, `
-
- `);
- document.body.appendChild(el);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'user', () => new User('Devel'), User);
- await el.stable();
- expect(ref.host.native.getAttribute('user')).toBe('Devel');
- });
-
- it('throws an error when binding to the same element attribute multiple times', () => {
- const el = parseHtml(NoopComponent, `
-
-
-
- `);
- const ref = el.getComponentRef();
-
- ref.bindAttr('span', 'name', () => 'test1');
- expect(() => ref.bindAttr('#my-span', 'name', () => 'test2'))
- .toThrowError(/cannot bind it again/);
- });
-
- it('throws an error when binding to the same element attribute multiple times from different components', () => {
- const outerEl = parseHtml(NoopComponent, `
-
-
-
- `);
- const outerRef = outerEl.getComponentRef();
-
- const innerEl = outerRef.host.query('noop-component').native;
- const innerRef = innerEl.getComponentRef();
-
- outerRef.bindAttr('noop-component', 'name', () => 'test1');
- expect(() => innerRef.bindAttr(innerRef.host, 'name', () => 'test2'))
- .toThrowError(/cannot bind it again/);
- });
-
- it('allows binding to different attributes of the same element', () => {
- const el = parseHtml(NoopComponent, `
-
- `);
- const ref = el.getComponentRef();
-
- ref.bindAttr(ref.host, 'name1', () => 'test1');
- expect(() => ref.bindAttr(ref.host, 'name2', () => 'test2'))
- .not.toThrow();
- });
-
- it('allows binding to the same attribute of different elements', () => {
- const el = parseHtml(NoopComponent, `
-
-
-
-
- `);
- const ref = el.getComponentRef();
-
- ref.bindAttr('#first', 'name', () => 'test1');
- expect(() => ref.bindAttr('#second', 'name', () => 'test2'))
- .not.toThrow();
- });
-
- it('restricts the signal result and serializer to be the same type', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- // Correct implicit primitive types.
- ref.bindAttr(ref.host, 'data', () => 'test');
- ref.bindAttr(ref.host, 'data', () => 1234);
- ref.bindAttr(ref.host, 'data', () => true);
- ref.bindAttr(ref.host, 'data', () => 1234n);
-
- // Incorrect implicit types.
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => ({}));
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => []);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => undefined);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => null);
-
- // Incorrect types with explicitly `undefined` serializer.
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => ({}), undefined);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => [], undefined);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => undefined, undefined);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => null, undefined);
-
- // Incorrect types with possibly `undefined` serializer.
- const maybeSerializer = {} as AttrSerializer<{}> | undefined;
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => ({}), maybeSerializer);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => [], maybeSerializer);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => undefined, maybeSerializer);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => null, maybeSerializer);
-
- // Correct explicit primitive types.
- ref.bindAttr(ref.host, 'data', () => 'test', String);
- ref.bindAttr(ref.host, 'data', () => 1234, Number);
- ref.bindAttr(ref.host, 'data', () => true, Boolean);
- ref.bindAttr(ref.host, 'data', () => 1234n, BigInt);
-
- // Incorrect explicit primitive types.
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 'test', Number);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 1234, String);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => true, String);
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 1234n, String);
-
- // Correct explicit serializer types.
- const serializer = {} as AttrSerializer;
- ref.bindAttr(ref.host, 'data', () => 'test', serializer);
-
- // Incorrect explicit serializer types.
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 1234, serializer);
-
- // Correct explicit serializable types.
- const serializable = {} as AttrSerializer;
- ref.bindAttr(ref.host, 'data', () => 'test', serializable);
-
- // Incorrect explicit serializable types.
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 1234, serializable);
- };
- });
-
- it('throws a compile time error when given an element serializer', () => {
- // Type-only test, only needs to compile, not execute.
- expect().nothing();
- () => {
- const ref = {} as ComponentRef;
-
- const serializer = {} as ElementSerializer;
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 'test', serializer);
-
- const serializable = {} as ElementSerializable;
- // @ts-expect-error
- ref.bindAttr(ref.host, 'data', () => 'test', serializable);
- };
- });
- });
-
describe('listen', () => {
it('listens invokes the given callback when the specified event is triggered', () => {
const el = document.createElement('noop-component');
diff --git a/src/component-ref.ts b/src/component-ref.ts
index f288ea7..2ef361c 100644
--- a/src/component-ref.ts
+++ b/src/component-ref.ts
@@ -1,9 +1,6 @@
import { ElementRef } from './element-ref.js';
import { HydroActiveComponent } from './hydroactive-component.js';
-import { QueriedElement } from './query.js';
-import { type AttrSerializerToken, type ElementSerializerToken, type ResolveSerializer, type SerializerToken, resolveSerializer } from './serializer-tokens.js';
-import { type AttrSerializable, type AttrSerializer, type ElementSerializable, type ElementSerializer, type Serialized, bigintSerializer, booleanSerializer, numberSerializer, stringSerializer } from './serializers.js';
-import { type Signal, type WriteableSignal, effect, signal } from './signals.js';
+import { effect } from './signals.js';
import { UiScheduler } from './signals/schedulers/ui-scheduler.js';
/**
@@ -15,12 +12,6 @@ export type OnConnect = () => OnDisconnect | void;
/** The type of the function invoked on disconnect. */
export type OnDisconnect = () => void;
-/** Elements whose text content is currently bound to a reactive signal. */
-const boundElements = new WeakSet();
-
-/** Element attributes whose content is currently bound to a reactive signal. */
-const boundElementAttrs = new WeakMap>();
-
/**
* Provides an ergonomic API for accessing the internal content and lifecycle
* of a HydroActive component. {@link ComponentRef} should be kept internal to
@@ -129,284 +120,6 @@ export class ComponentRef {
});
}
- /**
- * Creates a live binding to an element's text content. Returns a
- * {@link WriteableSignal} initialized to the current state of the specified
- * element in the DOM and bound such that all future writes to the signal are
- * automatically propagated back to the DOM.
- *
- * * Uses the provided element directly, or queries the component for the
- * given selector.
- * * Reads and binds to the text content of the provided element.
- * * Uses the provided serializer token to serialize to and deserialize from
- * the DOM.
- *
- * MISCONCEPTION: This is *not* a two-way binding. It is a one-way binding
- * with automatic initialization. If the bound element's text content is
- * modified outside of this binding, that change will *not* be reflected
- * automatically in the returned signal.
- *
- * @param elementOrSelector An {@link ElementRef} or a selector to look up in
- * the component to get an element. Used to read and bind the return
- * signal to.
- * @param token A "token" which identifiers an {@link ElementSerializer} to
- * serialize the `signal` result to an element. 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 A {@link WriteableSignal} initialized to the current text content
- * of the specified element. When the signal is mutated, the value is
- * automatically propagated back to the DOM.
- */
- public live<
- ElementOrSelector extends ElementRef | string,
- Token extends ElementSerializerToken>,
- >(
- elementOrSelector: ElementOrSelector,
- token: Token,
- ): WriteableSignal>,
- ElementSerializable>
- >>> {
- // Query for a selector if provided.
- const element = elementOrSelector instanceof ElementRef
- ? elementOrSelector as ElementRef>
- : this.host.query(elementOrSelector);
-
- // Read the initial value from the DOM.
- const initial = element.read(token as any);
-
- // Wrap the value in a reactive signal.
- const value = signal(initial);
-
- // Bind the signal back to the DOM to reflect future changes.
- this.bind(element as any, value, token);
-
- // Return a writeable version of the signal.
- return value as any;
- }
-
-
- /**
- * Creates a live binding to an element's attribute. Returns a
- * {@link WriteableSignal} initialized to the current state of the specified
- * element in the DOM and bound such that all future writes to the signal are
- * automatically propagated back to the DOM.
- *
- * * Uses the provided element directly, or queries the component for the
- * given selector.
- * * Reads and binds to the named attribute of the provided element.
- * * Uses the provided serializer token to serialize to and deserialize from
- * the DOM.
- *
- * MISCONCEPTION: This is *not* a two-way binding. It is a one-way binding
- * with automatic initialization. If the bound element's attribute is modified
- * outside of this binding, that change will *not* be reflected automatically
- * in the returned signal.
- *
- * Note that an attribute without a value such as `` will
- * return an empty string which is considered falsy.
- *
- * @param elementOrSelector An {@link ElementRef} or a selector to look up in
- * the component to get an element. Used to read and bind the return
- * signal to.
- * @param name The name of the attribute to bind to.
- * @param token A "token" which identifiers an {@link AttrSerializer} to
- * serialize the `signal` result to a 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 A {@link WriteableSignal} initialized to the current value of the
- * named attribute for the specified element. When the signal is mutated,
- * the value is automatically propagated back to the DOM.
- */
- public liveAttr>(
- elementOrSelector: ElementRef | string,
- name: string,
- token: Token,
- ): WriteableSignal,
- AttrSerializable
- >>> {
- // Query for a selector if provided.
- const element = elementOrSelector instanceof ElementRef
- ? elementOrSelector
- : this.host.query(elementOrSelector);
-
- // Read the initial value from the DOM.
- const initial = element.attr(name, token);
-
- // Wrap the value in a reactive signal.
- const value = signal(initial);
-
- // Bind the signal back to the DOM to reflect future changes.
- this.bindAttr(element, name, value, token);
-
- // Return a writeable version of the signal.
- return value;
- }
-
- /**
- * Invokes the given signal in a reactive context, serializes the result, and
- * renders it to the provided element's text content. Automatically re-renders
- * whenever a dependency of `signal` is modified.
- *
- * A default {@link ElementSerializer} is inferred from the return value of
- * `signal` if no token is provided.
- *
- * @param elementOrSelector The element to render to or a selector of the
- * element to render to.
- * @param signal The signal to invoke in a reactive context.
- * @param token A "token" which identifiers an {@link ElementSerializer} to
- * serialize the `signal` result to an element. A token is one of:
- * * A primitive serializer - {@link String}, {@link Boolean},
- * {@link Number}, {@link BigInt}.
- * * An {@link ElementSerializer} object.
- * * A {@link ElementSerializable} object.
- */
- public bind<
- Primitive extends string | number | boolean | bigint,
- ElementOrSelector extends ElementRef | string,
- >(
- elementOrSelector: ElementOrSelector,
- signal: Signal,
- token?: ElementSerializerToken>,
- ): void;
- public bind | string>(
- elementOrSelector: ElementOrSelector,
- signal: Signal,
- token: ElementSerializerToken>,
- ): void;
- public bind | string>(
- elementOrSelector: ElementOrSelector,
- signal: Signal,
- token?: ElementSerializerToken>,
- ): void {
- this.#bindToDom(
- elementOrSelector,
- signal,
- token,
- /* boundCheck */ (element) => {
- if (boundElements.has(element)) {
- throw new Error(`Element is already bound to another signal, cannot bind it again.`);
- }
- boundElements.add(element);
- },
- /* updateDom */ (element, serializer, value) => {
- (serializer as ElementSerializer).serializeTo(value, element);
- },
- );
- }
-
- /**
- * Invokes the given signal in a reactive context, serializes the result, and
- * renders it to the named attribute of the provided element. Automatically
- * re-renders whenever a dependency of `signal` is modified.
- *
- * A default {@link AttrSerializer} is inferred from the return value of
- * `signal` if no token is provided.
- *
- * @param elementOrSelector The element to render to or a selector of the
- * element to render to.
- * @param name The name of the attribute to bind to.
- * @param signal The signal to invoke in a reactive context.
- * @param token A "token" which identifiers an {@link AttrSerializer} to
- * serialize the `signal` result to a 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.
- */
- public bindAttr(
- elementOrSelector: ElementRef | string,
- name: string,
- signal: Signal,
- token?: AttrSerializerToken,
- ): void;
- public bindAttr(
- elementOrSelector: ElementRef | string,
- name: string,
- signal: Signal,
- token: AttrSerializerToken,
- ): void;
- public bindAttr(
- elementOrSelector: ElementRef | string,
- name: string,
- signal: Signal,
- token?: AttrSerializerToken,
- ): void {
- this.#bindToDom(
- elementOrSelector,
- signal,
- token,
- /* boundCheck */ (element) => {
- const boundAttrs = boundElementAttrs.get(element) ?? new Set();
- if (boundAttrs.has(name)) {
- throw new Error(`Element attribute (${name}) is already bound to another signal, cannot bind it again.`);
- }
- boundAttrs.add(name);
- boundElementAttrs.set(element, boundAttrs);
- },
- /* updateDom */ (element, serializer, value) => {
- const serial = (serializer as AttrSerializer).serialize(value);
- element.setAttribute(name, serial);
- },
- );
- }
-
- /**
- * Creates an effect which invokes `updateDom` with the associated element and
- * serialized value whenever the signal changes.
- *
- * Also calls `boundCheck` with the element immediately so the caller can
- * determine whether or not a binding already exists.
- */
- #bindToDom(
- elementOrSelector: ElementRef | string,
- signal: Signal,
- token: SerializerToken | undefined,
- boundCheck: (el: Element) => void,
- updateDom: (
- el: Element,
- serializer: ElementSerializer | AttrSerializer,
- value: Value,
- ) => void,
- ): void {
- // Query for a selector if provided.
- const element = elementOrSelector instanceof ElementRef
- ? elementOrSelector
- : this.host.query(elementOrSelector);
-
- // Assert that the element is not already bound to another signal.
- boundCheck(element.native);
-
- // Resolve an explicit serializer immediately, since that isn't dependent on
- // the value and we don't want to do this for every invocation of effect.
- const explicitSerializer = token
- ? resolveSerializer(token) as
- ElementSerializer | AttrSerializer
- : undefined;
-
- this.effect(() => {
- // Invoke the user-defined callback in a reactive context.
- const value = signal();
-
- // Infer a default serializer if necessary.
- const serializer = explicitSerializer ?? inferSerializer(value);
- if (!serializer) {
- throw new Error(`No default serializer for type "${
- typeof value}". Either provide a primitive type (string, number, boolean, bigint) or provide an explicit serializer.`);
- }
-
- // Update the DOM with the new value.
- updateDom(element.native, serializer, value);
- });
- }
-
/**
* Creates an event listener for the given event which invokes the provided
* handler callback function. This listener is automatically created and
@@ -469,36 +182,6 @@ export class ComponentRef {
}
}
-type PrimitiveSerializer =
- ElementSerializer & AttrSerializer;
-
-/**
- * Given the type of the provided value, returns a serializer which can
- * serialize it or `undefined` if no serializer can.
- */
-function inferSerializer(value: Value):
- PrimitiveSerializer | undefined {
- switch (typeof value) {
- case 'string': return stringSerializer as any;
- case 'number': return numberSerializer as any;
- case 'boolean': return booleanSerializer as any;
- case 'bigint': return bigintSerializer as any;
- default: return undefined;
- }
-}
-
-/**
- * Resolves an `ElementRef` or a selector string literal to the type of the
- * referenced element.
- */
-type ElementOf | string> =
- ElementOrSelector extends ElementRef
- ? El
- : ElementOrSelector extends string
- ? QueriedElement
- : Element
-;
-
// An attempt to capture all the event maps a user might reasonably encounter
// in an element discovered by an element query inside a component. Almost
// certainly not exhaustive.