Rename static attributes map to attributeFor and reserve the name
This commit is contained in:
parent
c961d01fc4
commit
6efcdd210e
17
src/types.ts
17
src/types.ts
@ -181,8 +181,8 @@ export type WebComponentConstructor<S> = Omit<BaseOf<S>, 'prototype'> & {
|
|||||||
new (): WebComponentInstance<S>;
|
new (): WebComponentInstance<S>;
|
||||||
readonly prototype: WebComponentInstance<S>;
|
readonly prototype: WebComponentInstance<S>;
|
||||||
readonly observedAttributes: readonly ObservedAttributes<S>[];
|
readonly observedAttributes: readonly ObservedAttributes<S>[];
|
||||||
/** Prop key → attribute name. */
|
/** Prop key → attribute name: `El.attributeFor.fetch` is `'disable-fetch'`. */
|
||||||
readonly attributes: AttributeMap<S>;
|
readonly attributeFor: AttributeMap<S>;
|
||||||
readonly [spec]?: S;
|
readonly [spec]?: S;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -202,19 +202,26 @@ export type AttributesOf<T> = ObservedAttributes<SpecOf<T>>;
|
|||||||
/** Mixin members a prop may not shadow. */
|
/** Mixin members a prop may not shadow. */
|
||||||
type MixinMember = keyof WebComponentMembers<unknown>;
|
type MixinMember = keyof WebComponentMembers<unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every name a prop may not use. `attributeFor` is static, so it wouldn't
|
||||||
|
* collide at runtime; it's reserved so `El.attributeFor` and a prop can't be
|
||||||
|
* confused.
|
||||||
|
*/
|
||||||
|
type ReservedName = MixinMember | 'attributeFor';
|
||||||
|
|
||||||
/** Branded error type: shows up in the message when a prop name collides. */
|
/** Branded error type: shows up in the message when a prop name collides. */
|
||||||
export type PropNameConflict<K extends PropertyKey> =
|
export type PropNameConflict<K extends PropertyKey> =
|
||||||
`Error: prop "${K & string}" conflicts with an existing element member`;
|
`Error: prop "${K & string}" conflicts with an existing element member`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intersected with the spec parameter. Any prop whose name already exists on
|
* Intersected with the spec parameter. Any prop whose name already exists on
|
||||||
* the base instance (`name`, `title`, `hidden` on `HTMLElement`; `value` on a
|
* the base instance (`title`, `hidden`, `id` on `HTMLElement`; `value` on a
|
||||||
* form-ish base) or on the mixin itself is retyped to {@link PropNameConflict},
|
* form-ish base), on the mixin, or is otherwise reserved, is retyped to {@link PropNameConflict},
|
||||||
* so the error lands on that key.
|
* so the error lands on that key.
|
||||||
*/
|
*/
|
||||||
export type ValidateSpec<S> = {
|
export type ValidateSpec<S> = {
|
||||||
readonly props?: {
|
readonly props?: {
|
||||||
readonly [K in keyof PropsOf<S>]: K extends keyof InstanceType<BaseOf<S>> | MixinMember
|
readonly [K in keyof PropsOf<S>]: K extends keyof InstanceType<BaseOf<S>> | ReservedName
|
||||||
? PropNameConflict<K>
|
? PropNameConflict<K>
|
||||||
: PropsOf<S>[K];
|
: PropsOf<S>[K];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export function WebComponent<const S extends Spec>(
|
|||||||
|
|
||||||
class WebComponentElement extends Base {
|
class WebComponentElement extends Base {
|
||||||
static readonly observedAttributes = resolved.map((prop) => prop.attribute);
|
static readonly observedAttributes = resolved.map((prop) => prop.attribute);
|
||||||
static readonly attributes = Object.fromEntries(
|
static readonly attributeFor = Object.fromEntries(
|
||||||
resolved.map((prop) => [prop.key, prop.attribute]),
|
resolved.map((prop) => [prop.key, prop.attribute]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -56,9 +56,9 @@ describe('attributes', () => {
|
|||||||
expectTypeOf<AttributesOf<typeof SPEC>>().toEqualTypeOf<Expected>();
|
expectTypeOf<AttributesOf<typeof SPEC>>().toEqualTypeOf<Expected>();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('attributes maps each key to its attribute', () => {
|
test('attributeFor maps each key to its attribute', () => {
|
||||||
expectTypeOf(Example.attributes.fetch).toEqualTypeOf<'disable-fetch'>();
|
expectTypeOf(Example.attributeFor.fetch).toEqualTypeOf<'disable-fetch'>();
|
||||||
expectTypeOf(Example.attributes.ready).toEqualTypeOf<'is-ready'>();
|
expectTypeOf(Example.attributeFor.ready).toEqualTypeOf<'is-ready'>();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dataPrefix applies only to derived names', () => {
|
test('dataPrefix applies only to derived names', () => {
|
||||||
@ -157,6 +157,8 @@ describe('conflicts', () => {
|
|||||||
test('prop names colliding with mixin members are rejected', () => {
|
test('prop names colliding with mixin members are rejected', () => {
|
||||||
// @ts-expect-error -- `signal` is a mixin member
|
// @ts-expect-error -- `signal` is a mixin member
|
||||||
WebComponent({ props: { signal: string() } });
|
WebComponent({ props: { signal: string() } });
|
||||||
|
// @ts-expect-error -- `attributeFor` is reserved for the static lookup
|
||||||
|
WebComponent({ props: { attributeFor: string() } });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('conflicts are checked against the base class', () => {
|
test('conflicts are checked against the base class', () => {
|
||||||
|
|||||||
@ -109,7 +109,7 @@ describe('props', () => {
|
|||||||
props: { userId: number(), fetch: disable(), ready: enable({ attribute: 'is-ready' }) },
|
props: { userId: number(), fetch: disable(), ready: enable({ attribute: 'is-ready' }) },
|
||||||
}) {}
|
}) {}
|
||||||
expect(El.observedAttributes).toEqual(['data-user-id', 'data-disable-fetch', 'is-ready']);
|
expect(El.observedAttributes).toEqual(['data-user-id', 'data-disable-fetch', 'is-ready']);
|
||||||
expect(El.attributes).toEqual({
|
expect(El.attributeFor).toEqual({
|
||||||
userId: 'data-user-id',
|
userId: 'data-user-id',
|
||||||
fetch: 'data-disable-fetch',
|
fetch: 'data-disable-fetch',
|
||||||
ready: 'is-ready',
|
ready: 'is-ready',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user