Skip to content

Commit

Permalink
Support @provide with experimental decorators on a property (#4200)
Browse files Browse the repository at this point in the history
* Support @provide with experimental decorators on a property

It might feel like this is working at cross purposes to our standard decorators work, because those all require accessors, but I don't think so. With standard decorators we can use the type system to error out if they're used on a property instead of an accessor. With experimental decorators, they sorta work with properties, in that they do provide values and update them, but reading the value back off the field would always give undefined because we were defining it without a getter.

* Address review comments

* Fix import.
  • Loading branch information
rictic authored Sep 19, 2023
1 parent 2fa955d commit 8e737fb
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-rats-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/context': patch
---

Support @provide with experimental decorators on a property. Previously we only supported it on an accessor, which happened automatically if used with @state or @property.
32 changes: 22 additions & 10 deletions packages/labs/context/src/lib/decorators/provide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,28 @@ export function provide<ValueType>({
protoOrTarget,
nameOrContext
);
const oldSetter = descriptor?.set;
const newDescriptor = {
...descriptor,
set: function (this: ReactiveElement, value: ValueType) {
controllerMap.get(this)?.setValue(value);
if (oldSetter) {
oldSetter.call(this, value);
}
},
};
let newDescriptor;
if (descriptor === undefined) {
const valueMap = new WeakMap<ReactiveElement, ValueType>();
newDescriptor = {
get: function (this: ReactiveElement) {
return valueMap.get(this);
},
set: function (this: ReactiveElement, value: ValueType) {
controllerMap.get(this)!.setValue(value);
valueMap.set(this, value);
},
};
} else {
const oldSetter = descriptor?.set;
newDescriptor = {
...descriptor,
set: function (this: ReactiveElement, value: ValueType) {
controllerMap.get(this)?.setValue(value);
oldSetter?.call(this, value);
},
};
}
Object.defineProperty(protoOrTarget, nameOrContext, newDescriptor);
return;
}
Expand Down
56 changes: 56 additions & 0 deletions packages/labs/context/src/test/decorators/provide_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {Context, consume, provide} from '@lit-labs/context';
import {assert} from '@esm-bundle/chai';
import {LitElement, TemplateResult, html} from 'lit';

const simpleContext = 'simple-context' as Context<'simple-context', number>;

test(`@provide on a property, not an accessor`, async () => {
class ProviderWithoutAccessorElement extends LitElement {
// Note that value doesn't use `accessor`, or `@property()`, or `@state`
@provide({context: simpleContext})
value = 0;

override render(): TemplateResult {
return html`<simple-consumer></simple-consumer>`;
}
}
customElements.define(
'provider-without-accessor',
ProviderWithoutAccessorElement
);
class SimpleConsumer extends LitElement {
@consume({context: simpleContext, subscribe: true})
value = -1;

override render(): TemplateResult {
return html``;
}
}
customElements.define('simple-consumer', SimpleConsumer);

const provider = document.createElement(
'provider-without-accessor'
) as ProviderWithoutAccessorElement;
document.body.appendChild(provider);
// The field's value is written with its initial value.
assert.equal(provider.value, 0);
await provider.updateComplete;
const consumer = provider.shadowRoot?.querySelector(
'simple-consumer'
) as SimpleConsumer;
// The consumer's value is written with the provider's initial value.
assert.equal(provider.value, 0);
assert.equal(consumer.value, 0);

// Updating the provider also updates the subscribing consumer.
provider.value = 1;
await provider.updateComplete;
assert.equal(provider.value, 1);
assert.equal(consumer.value, 1);
});

0 comments on commit 8e737fb

Please sign in to comment.