Skip to content

Commit

Permalink
WIP: Add element class implicit serializers.
Browse files Browse the repository at this point in the history
  • Loading branch information
dgp1130 committed Dec 21, 2023
1 parent 2903ee3 commit 3b5ca42
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 37 deletions.
4 changes: 2 additions & 2 deletions src/component-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@ export class ComponentRef {
: this.host.query(elementOrSelector);

// Read the initial value from the DOM.
const initial = element.read(token as any);
const initial = element.read(token);

// 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);
this.bind(element, value, token as any);

// Return a writeable version of the signal.
return value as any;
Expand Down
22 changes: 13 additions & 9 deletions src/demo/auto-counter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ import { defineComponent } from 'hydroactive';

/** Automatically increments the count over time. */
export const AutoCounter = defineComponent('auto-counter', (comp) => {
const count = comp.live('span', Number);
const span1 = comp.host.query('span').read(HTMLSpanElement);
// @ts-expect-error
const span2 = comp.host.query('.foo').read(HTMLInputElement);
// @ts-expect-error
const span3 = comp.host.query('input').read(HTMLSelectElement);
const span4 = comp.host.query('span').read(Element);
const span5 = comp.host.query('span').read({
serializeTo(input: HTMLInputElement, span: HTMLSpanElement): void {
span.querySelector('input')!.replaceWith(input);
},

comp.connected(() => {
const id = setInterval(() => {
count.set(count() + 1);
}, 1_000);

return () => {
clearInterval(id);
};
deserializeFrom(span: HTMLSpanElement): HTMLInputElement {
return span.querySelector('input') as HTMLInputElement;
}
});
});
16 changes: 15 additions & 1 deletion src/element-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,21 @@ export class ElementRef<El extends Element> {
ElementSerializable<unknown, El>
>(token);

return serializer.deserializeFrom(this.native) as any;
// Should work.
type Test = ResolveSerializer<
typeof Element,
ElementSerializer<unknown, HTMLInputElement>,
ElementSerializable<unknown, HTMLInputElement>
>;

// Should not work.
type TestAnother = ResolveSerializer<
typeof HTMLInputElement,
ElementSerializer<unknown, Element>,
ElementSerializable<unknown, Element>
>;

return serializer.deserializeFrom(this.native as any) as any;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/serializer-tokens.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type ResolveSerializer, resolveSerializer } from './serializer-tokens.js';
import { type AttrSerializer, type ElementSerializer, bigintSerializer, booleanSerializer, numberSerializer, stringSerializer, toSerializer, ElementSerializable, AttrSerializable } from './serializers.js';
import { elementSerializer } from './serializers/primitive-serializers.js';

describe('serializer-tokens', () => {
describe('resolveSerializer', () => {
Expand Down Expand Up @@ -274,6 +275,36 @@ describe('serializer-tokens', () => {
};
});

it('resolves `Element` types', () => {
expect().nothing();
() => {
const _elSerializerResult: ResolveSerializer<
typeof Element,
ElementSerializer<unknown, any>,
ElementSerializable<unknown, any>
> = elementSerializer;
const _elSerializerResult2: typeof elementSerializer =
{} as ResolveSerializer<
typeof Element,
ElementSerializer<unknown, any>,
ElementSerializable<unknown, any>
>;

const _spanSerializerResult: ResolveSerializer<
typeof HTMLSpanElement,
ElementSerializer<unknown, any>,
ElementSerializable<unknown, any>
> = {} as ElementSerializer<HTMLSpanElement, HTMLSpanElement>;
const _spanSerializerResult2:
ElementSerializer<HTMLSpanElement, HTMLSpanElement> =
{} as ResolveSerializer<
typeof HTMLSpanElement,
ElementSerializer<unknown, any>,
ElementSerializable<unknown, any>
>;
};
});

it('throws a compile-time error for serializer types of the wrong form (element vs attr)', () => {
expect().nothing();
() => {
Expand Down
89 changes: 66 additions & 23 deletions src/serializer-tokens.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ElementSerializer, ElementSerializable, AttrSerializer, AttrSerializable, stringSerializer, numberSerializer, booleanSerializer, bigintSerializer, toSerializer } from './serializers.js';
import { ElementSerializer, ElementSerializable, AttrSerializer, AttrSerializable, stringSerializer, numberSerializer, booleanSerializer, bigintSerializer, toSerializer, Serialized } from './serializers.js';
import { elementSerializer } from './serializers/primitive-serializers.js';

// Tokens which reference `Serializer` objects for primitive types, filtered
// down only to those which extend the given input type.
Expand All @@ -18,6 +19,7 @@ export type ElementSerializerToken<Value, El extends Element> =
| PrimitiveSerializerToken<Value>
| ElementSerializer<Value, El>
| ElementSerializable<Value, El>
| Value extends Element ? Value : never
;

/**
Expand Down Expand Up @@ -56,6 +58,7 @@ export function resolveSerializer<
SerializerKind,
SerializableKind
> {
// Resolve primitive tokens.
switch (token) {
case String: {
return stringSerializer as
Expand All @@ -69,21 +72,28 @@ export function resolveSerializer<
} case BigInt: {
return bigintSerializer as
ResolveSerializer<Token, SerializerKind, SerializableKind>;
} default: {
if (toSerializer in token) {
type Serializable =
| ElementSerializable<unknown, any>
| AttrSerializable<unknown>
;
return (token as Serializable)[toSerializer]() as
ResolveSerializer<Token, SerializerKind, SerializableKind>;
} else {
// Already a serializer.
return token as
ResolveSerializer<Token, SerializerKind, SerializableKind>;
}
}
}

// Resolve serializable tokens.
if (toSerializer in (token as Record<string, unknown>)) {
type Serializable =
| ElementSerializable<unknown, any>
| AttrSerializable<unknown>
;
return (token as Serializable)[toSerializer]() as
ResolveSerializer<Token, SerializerKind, SerializableKind>;
}

// Resolve `Element` tokens.
if (staticExtends(token, Element)) {
return elementSerializer as
ResolveSerializer<Token, SerializerKind, SerializableKind>;
}

// Resolve `Serializer` tokens, they already are a `Serializer`!
return token as
ResolveSerializer<Token, SerializerKind, SerializableKind>;
}

/**
Expand Down Expand Up @@ -118,13 +128,46 @@ export type ResolveSerializer<
? ReturnType<Token[typeof toSerializer]>
: Token extends SerializerKind
? Token
: Token extends typeof String
? typeof stringSerializer
: Token extends typeof Number
? typeof numberSerializer
: Token extends typeof Boolean
? typeof booleanSerializer
: Token extends typeof BigInt
? typeof bigintSerializer
: never
: Token extends typeof Element
? InstanceType<Token> extends ElementOf<SerializerKind>
? ElementSerializer<InstanceType<Token>, InstanceType<Token>>
: never
: Token extends typeof String
? typeof stringSerializer
: Token extends typeof Number
? typeof numberSerializer
: Token extends typeof Boolean
? typeof booleanSerializer
: Token extends typeof BigInt
? typeof bigintSerializer
: never
;

type ElementOr<Token, Alternative> = Token extends typeof Element
? InstanceType<Token>
: Alternative
;

type ElementOf<SerializerKind extends
| ElementSerializer<unknown, any>
| AttrSerializer<unknown>
> = SerializerKind extends ElementSerializer<unknown, infer El> ? El : never;

/** TODO: `instanceof` but for classes instead of instances. */
function staticExtends(maybeChild: unknown, parent: unknown): boolean {
if (maybeChild === null || maybeChild === undefined) return false;

for (const proto of prototypeChain(maybeChild)) {
if (proto === parent) return true;
}

return false;
}

function* prototypeChain(obj: unknown): Generator<unknown, void, void> {
const proto = Object.getPrototypeOf(obj);
if (proto === null) return;

yield proto;
yield* prototypeChain(proto);
}
11 changes: 11 additions & 0 deletions src/serializers/primitive-serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,14 @@ export const stringSerializer =
return serial;
}
}();

/** TODO */
export const elementSerializer: ElementSerializer<Element, Element> = {
serializeTo(newElement: Element, oldElement: Element): void {
oldElement.replaceWith(newElement);
},

deserializeFrom(element: Element): Element {
return element;
},
};
4 changes: 2 additions & 2 deletions src/serializers/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ export type Serialized<
| AttrSerializable<unknown>,
> = Serial extends AttrSerializable<infer Value>
? Value
: Serial extends ElementSerializable<infer Value, Element>
: Serial extends ElementSerializable<infer Value, any>
? Value
: Serial extends ElementSerializer<infer Value, Element>
: Serial extends ElementSerializer<infer Value, any>
? Value
: Serial extends AttrSerializer<infer Value>
? Value
Expand Down

0 comments on commit 3b5ca42

Please sign in to comment.