From 55f3344e96f99b62b32df27f5f55bf95392c808a Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Wed, 4 Sep 2024 23:03:23 -0700 Subject: [PATCH] Adds `AttrAccessor.prototype.write`. This is a corollary to `AttrAccessor.prototype.read` and `ElementAccessor.prototype.write` and allows easy editing of element attributes without requiring a full signal reactivity implementation. --- src/attribute-accessor.test.ts | 156 +++++++++++++++++++++++++++++++++ src/attribute-accessor.ts | 16 ++++ 2 files changed, 172 insertions(+) diff --git a/src/attribute-accessor.test.ts b/src/attribute-accessor.test.ts index 2f5a403..a616cdc 100644 --- a/src/attribute-accessor.test.ts +++ b/src/attribute-accessor.test.ts @@ -279,5 +279,161 @@ describe('attribute-accessor', () => { }; }); }); + + describe('write', () => { + it('serializes and writes the given value to the attribute', () => { + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'foo'); + + attr.write('bar', String); + + expect(el.getAttribute('foo')).toBe('bar'); + }); + + it('serializes the attribute with the given primitive serializer', () => { + { + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'name'); + + attr.write('Devel', String); + + expect(el.getAttribute('name')).toBe('Devel'); + } + + { + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'id'); + + attr.write(12345, Number); + + expect(el.getAttribute('id')).toBe('12345'); + } + + { + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'id'); + + attr.write(12345n, BigInt); + + expect(el.getAttribute('id')).toBe('12345'); + } + }); + + it('deserializes booleans based on text value, not attribute presence', () => { + { + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'enabled'); + + attr.write(false, Boolean); + + expect(el.getAttribute('enabled')).toBe('false'); + } + + { + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'enabled'); + + attr.write(true, Boolean); + + expect(el.getAttribute('enabled')).toBe('true'); + } + }); + + 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 = document.createElement('div'); + const attr = AttrAccessor.from(el, 'hello'); + + attr.write({ foo: 'bar' }, serializer); + + expect(el.getAttribute('hello')).toBe('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 = document.createElement('div'); + const attr = AttrAccessor.from(el, 'name'); + + attr.write(new User('Devel'), User); + + expect(el.getAttribute('name')).toBe('Devel'); + }); + + it('throws an error if the deserialization process throws', () => { + const err = new Error('Failed to deserialize.'); + const serializer: AttrSerializer = { + serialize(): string { + throw err; + }, + + deserialize(): string { + return 'unused'; + } + }; + + const el = document.createElement('div'); + const attr = AttrAccessor.from(el, 'hello'); + + expect(() => attr.write('test', serializer)).toThrow(err); + }); + + it('throws a compile-time error when used with an element serializer', () => { + // Type-only test, only needs to compile, not execute. + expect().nothing(); + () => { + const attr = {} as AttrAccessor; + + const serializer = {} as ElementSerializer; + // @ts-expect-error + attr.write(1234, serializer); + + const serializable = {} as ElementSerializable; + // @ts-expect-error + attr.write(1234, serializable); + }; + }); + + it('throws a compile-time error when used with an incompatible serializer', () => { + // Type-only test, only needs to compile, not execute. + expect().nothing(); + () => { + const attr = {} as AttrAccessor; + + // @ts-expect-error + attr.write('test', Number); + + const serializer = {} as AttrSerializer; + // @ts-expect-error + attr.write('test', serializer); + + const serializable = {} as AttrSerializable; + // @ts-expect-error + attr.write('test', serializable); + }; + }); + }); }); }); diff --git a/src/attribute-accessor.ts b/src/attribute-accessor.ts index cfdbf44..fbb6b35 100644 --- a/src/attribute-accessor.ts +++ b/src/attribute-accessor.ts @@ -90,4 +90,20 @@ export class AttrAccessor { >(token); return serializer.deserialize(serialized); } + + /** + * Writes the underlying attribute by serializing the input value with the + * {@link AttrSerializer} referenced by the provided token. + * + * @param value The value to serialize and write to the attribute. + * @param token A token which resolves to an {@link AttrSerializer} to + * serialize the attribute value with. + */ + public write>( + value: Value, + token: Token, + ): void { + const serializer = resolveSerializer(token) as AttrSerializer; + this.#native.setAttribute(this.#name, serializer.serialize(value)); + } }