Skip to content

Commit

Permalink
[reactive-element] Make context decorators work with standard decorat…
Browse files Browse the repository at this point in the history
…ors (#4151)
  • Loading branch information
justinfagnani authored Aug 30, 2023
1 parent 0f6878d commit 5680d48
Show file tree
Hide file tree
Showing 8 changed files with 429 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-lobsters-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/context': minor
---

Make context decorators work with standard decorators
22 changes: 21 additions & 1 deletion packages/labs/context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"scripts": {
"build": "wireit",
"build:ts": "wireit",
"build:ts:std-decorators-tests": "wireit",
"build:ts:types": "wireit",
"build:rollup": "wireit",
"test": "wireit",
Expand All @@ -46,6 +47,7 @@
"dependencies": [
"build:rollup",
"build:ts",
"build:ts:std-decorators-tests",
"build:ts:types",
"../../lit:build",
"../../reactive-element:build",
Expand All @@ -68,6 +70,22 @@
"tsconfig.tsbuildinfo"
]
},
"build:ts:std-decorators-tests": {
"#comment": "This is a separate script from build:ts because it needs a tsconfig without experimentalDecorators.",
"command": "tsc --pretty --project tsconfig.std-decorators-tests.json",
"clean": "if-file-deleted",
"dependencies": [
"build:ts"
],
"files": [
"src/test/std-decorators/**/*.ts",
"tsconfig.std-decorators-tests.json"
],
"output": [
"development/test/std-decorators",
"tsconfig.std-decorators-tests.tsbuildinfo"
]
},
"build:ts:types": {
"command": "treemirror development . \"**/*.d.ts{,.map}\"",
"dependencies": [
Expand Down Expand Up @@ -116,7 +134,8 @@
"test:dev": {
"command": "MODE=dev node ../../tests/run-web-tests.js \"development/**/*_test.js\" --config ../../tests/web-test-runner.config.js",
"dependencies": [
"build",
"build:ts",
"build:ts:std-decorators-tests",
"../../tests:build"
],
"env": {
Expand All @@ -131,6 +150,7 @@
"command": "MODE=prod node ../../tests/run-web-tests.js \"development/**/*_test.js\" --config ../../tests/web-test-runner.config.js",
"dependencies": [
"build",
"build:ts:std-decorators-tests",
"../../tests:build"
],
"env": {
Expand Down
57 changes: 45 additions & 12 deletions packages/labs/context/src/lib/decorators/consume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import {ReactiveElement} from '@lit/reactive-element';
import {decorateProperty} from '@lit/reactive-element/decorators/base.js';
import {ContextConsumer} from '../controllers/context-consumer.js';
import {Context} from '../create-context.js';

Expand Down Expand Up @@ -47,28 +46,62 @@ export function consume<ValueType>({
}: {
context: Context<unknown, ValueType>;
subscribe?: boolean;
}): ConsumerDecorator<ValueType> {
return decorateProperty({
finisher: (ctor: typeof ReactiveElement, name: PropertyKey) => {
ctor.addInitializer((element: ReactiveElement): void => {
new ContextConsumer(element, {
}): ConsumeDecorator<ValueType> {
return (<C extends ReactiveElement, V extends ValueType>(
protoOrTarget: ClassAccessorDecoratorTarget<C, V>,
nameOrContext: PropertyKey | ClassAccessorDecoratorContext<C, V>
) => {
if (typeof nameOrContext === 'object') {
// Standard decorators branch
nameOrContext.addInitializer(function (this: ReactiveElement): void {
new ContextConsumer(this, {
context,
callback: (value: ValueType) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- have to force the property on the type
(element as any)[name] = value;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any)[nameOrContext.name] = value;
},
subscribe,
});
});
},
});
} else {
// Experimental decorators branch
(protoOrTarget.constructor as typeof ReactiveElement).addInitializer(
(element: ReactiveElement): void => {
new ContextConsumer(element, {
context,
callback: (value: ValueType) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(element as any)[nameOrContext] = value;
},
subscribe,
});
}
);
}
}) as ConsumeDecorator<ValueType>;
}

type ConsumerDecorator<ValueType> = {
<K extends PropertyKey, Proto extends ReactiveElement>(
/**
* Generates a public interface type that removes private and protected fields.
* This allows accepting otherwise incompatible versions of the type (e.g. from
* multiple copies of the same package in `node_modules`).
*/
type Interface<T> = {
[K in keyof T]: T[K];
};

type ConsumeDecorator<ValueType> = {
// legacy
<K extends PropertyKey, Proto extends Interface<ReactiveElement>>(
protoOrDescriptor: Proto,
name?: K
): FieldMustMatchProvidedType<Proto, K, ValueType>;

// standard
<C extends Interface<ReactiveElement>, V extends ValueType>(
value: ClassAccessorDecoratorTarget<C, V>,
context: ClassAccessorDecoratorContext<C, V>
): void;
};

// Note TypeScript requires the return type of a decorator to be `void | any`
Expand Down
64 changes: 54 additions & 10 deletions packages/labs/context/src/lib/decorators/provide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import {ReactiveElement} from '@lit/reactive-element';
import {decorateProperty} from '@lit/reactive-element/decorators/base.js';
import {Context} from '../create-context.js';
import {ContextProvider} from '../controllers/context-provider.js';

Expand Down Expand Up @@ -48,15 +47,43 @@ export function provide<ValueType>({
}: {
context: Context<unknown, ValueType>;
}): ProvideDecorator<ValueType> {
return decorateProperty({
finisher: (ctor: typeof ReactiveElement, name: PropertyKey) => {
const controllerMap = new WeakMap();
ctor.addInitializer((element: ReactiveElement): void => {
controllerMap.set(element, new ContextProvider(element, {context}));
return (<C extends ReactiveElement, V extends ValueType>(
protoOrTarget: ClassAccessorDecoratorTarget<C, V>,
nameOrContext: PropertyKey | ClassAccessorDecoratorContext<C, V>
) => {
// Map of instances to controllers
const controllerMap = new WeakMap();
if (typeof nameOrContext === 'object') {
// Standard decorators branch
nameOrContext.addInitializer(function (this: C) {
controllerMap.set(this, new ContextProvider(this, {context}));
});
return {
get(this: C) {
return protoOrTarget.get.call(this);
},
set(this: C, value: V) {
controllerMap.get(this)?.setValue(value);
return protoOrTarget.set.call(this, value);
},
init(this: C, value: V) {
controllerMap.get(this)?.setValue(value);
return value;
},
};
} else {
// Experimental decorators branch
(protoOrTarget.constructor as typeof ReactiveElement).addInitializer(
(element: ReactiveElement): void => {
controllerMap.set(element, new ContextProvider(element, {context}));
}
);
// proxy any existing setter for this property and use it to
// notify the controller of an updated value
const descriptor = Object.getOwnPropertyDescriptor(ctor.prototype, name);
const descriptor = Object.getOwnPropertyDescriptor(
protoOrTarget,
nameOrContext
);
const oldSetter = descriptor?.set;
const newDescriptor = {
...descriptor,
Expand All @@ -67,16 +94,33 @@ export function provide<ValueType>({
}
},
};
Object.defineProperty(ctor.prototype, name, newDescriptor);
},
});
Object.defineProperty(protoOrTarget, nameOrContext, newDescriptor);
return;
}
}) as ProvideDecorator<ValueType>;
}

/**
* Generates a public interface type that removes private and protected fields.
* This allows accepting otherwise compatible versions of the type (e.g. from
* multiple copies of the same package in `node_modules`).
*/
type Interface<T> = {
[K in keyof T]: T[K];
};

type ProvideDecorator<ContextType> = {
// legacy
<K extends PropertyKey, Proto extends ReactiveElement>(
protoOrDescriptor: Proto,
name?: K
): FieldMustMatchContextType<Proto, K, ContextType>;

// standard
<C extends Interface<ReactiveElement>, V extends ContextType>(
value: ClassAccessorDecoratorTarget<C, V>,
context: ClassAccessorDecoratorContext<C, V>
): void;
};

// Note TypeScript requires the return type of a decorator to be `void | any`
Expand Down
Loading

0 comments on commit 5680d48

Please sign in to comment.