Skip to content

Commit 4a504e8

Browse files
iOvergaardCopilot
andauthored
Extensions: Adds @provideContext and @consumeContext decorators for a better developer experience (#20510)
* feat: adds first draft of a context consume decorator * feat: uses an options pattern * feat: changes approach to use `addInitializer` and `queueMicroTask` instead * feat: adds extra warning if context is consumed on disconnected controllers * feat: example implementation of consume decorator * feat: adds support for 'subscribe' * feat: initial work on provide decorator * docs: adds license to consume decorator * feat: adds support for umbraco controllers with `hostConnected` * feat: uses asPromise to handle one-time subscription instead * test: adds unit tests for consume decorator * feat: adds support for controllers through hostConnected injection * feat: adds support for controllers through hostConnected injection * test: adds unit tests for provide decorator * docs: adds more documentation around usage and adds a few warnings in console when it detects wrong usage * feat: removes unused controllerMap * docs: adds wording on standard vs legacy decorators * docs: clarifies usage around internal state * feat: adds proper return types for decorators * docs: adds more types * feat: makes element optional * feat: makes element optional * feat: uses @consume in the log viewer to showcase * chore: cleans up debug info * feat: renames to `consumeContext` and `provideContext` to stay inline with our own methods * chore: removes unneeded typings * chore: removes not needed check * chore: removes not needed check * test: adds test for rendered value * feat: splits up code into several smaller functions * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * docs: augments code example for creating a context * Update src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent 271edb5 commit 4a504e8

19 files changed

+959
-156
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { UmbContextToken } from '../token/context-token.js';
2+
import type { UmbContextMinimal } from '../types.js';
3+
import { UmbContextProvider } from '../provide/context-provider.js';
4+
import { consumeContext } from './context-consume.decorator.js';
5+
import { aTimeout, elementUpdated, expect, fixture } from '@open-wc/testing';
6+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
7+
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
8+
import { html, state } from '@umbraco-cms/backoffice/external/lit';
9+
10+
class UmbTestContextConsumerClass implements UmbContextMinimal {
11+
public prop: string = 'value from provider';
12+
getHostElement() {
13+
return undefined as any;
14+
}
15+
}
16+
17+
const testToken = new UmbContextToken<UmbTestContextConsumerClass>('my-test-context');
18+
19+
class MyTestElement extends UmbLitElement {
20+
@consumeContext({
21+
context: testToken,
22+
})
23+
@state()
24+
contextValue?: UmbTestContextConsumerClass;
25+
26+
override render() {
27+
return html`<div>${this.contextValue?.prop ?? 'no context'}</div>`;
28+
}
29+
}
30+
31+
customElements.define('my-consume-test-element', MyTestElement);
32+
33+
describe('@consume decorator', () => {
34+
let provider: UmbContextProvider;
35+
let element: MyTestElement;
36+
37+
beforeEach(async () => {
38+
provider = new UmbContextProvider(document.body, testToken, new UmbTestContextConsumerClass());
39+
provider.hostConnected();
40+
41+
element = await fixture<MyTestElement>(`<my-consume-test-element></my-consume-test-element>`);
42+
});
43+
44+
afterEach(() => {
45+
provider.destroy();
46+
(provider as any) = undefined;
47+
});
48+
49+
it('should receive a context value when provided on the host', () => {
50+
expect(element.contextValue).to.equal(provider.providerInstance());
51+
expect(element.contextValue?.prop).to.equal('value from provider');
52+
});
53+
54+
it('should render the value from the context', async () => {
55+
expect(element).shadowDom.to.equal('<div>value from provider</div>');
56+
});
57+
58+
it('should work when the decorator is used in a controller', async () => {
59+
class MyController extends UmbControllerBase {
60+
@consumeContext({ context: testToken })
61+
contextValue?: UmbTestContextConsumerClass;
62+
}
63+
64+
const controller = new MyController(element);
65+
66+
await elementUpdated(element);
67+
68+
expect(element.contextValue).to.equal(provider.providerInstance());
69+
expect(controller.contextValue).to.equal(provider.providerInstance());
70+
});
71+
72+
it('should have called the callback first', async () => {
73+
let callbackCalled = false;
74+
75+
class MyCallbackTestElement extends UmbLitElement {
76+
@consumeContext({
77+
context: testToken,
78+
callback: () => {
79+
callbackCalled = true;
80+
},
81+
})
82+
contextValue?: UmbTestContextConsumerClass;
83+
}
84+
85+
customElements.define('my-callback-consume-test-element', MyCallbackTestElement);
86+
87+
const callbackElement = await fixture<MyCallbackTestElement>(
88+
`<my-callback-consume-test-element></my-callback-consume-test-element>`,
89+
);
90+
91+
await elementUpdated(callbackElement);
92+
93+
expect(callbackCalled).to.be.true;
94+
expect(callbackElement.contextValue).to.equal(provider.providerInstance());
95+
});
96+
97+
it('should update the context value when the provider instance changes', async () => {
98+
const newProviderInstance = new UmbTestContextConsumerClass();
99+
newProviderInstance.prop = 'new value from provider';
100+
101+
const newProvider = new UmbContextProvider(element, testToken, newProviderInstance);
102+
newProvider.hostConnected();
103+
104+
await elementUpdated(element);
105+
106+
expect(element.contextValue).to.equal(newProvider.providerInstance());
107+
expect(element.contextValue?.prop).to.equal(newProviderInstance.prop);
108+
});
109+
110+
it('should be able to consume without subscribing', async () => {
111+
class MyNoSubscribeTestController extends UmbControllerBase {
112+
@consumeContext({ context: testToken, subscribe: false })
113+
contextValue?: UmbTestContextConsumerClass;
114+
}
115+
116+
const controller = new MyNoSubscribeTestController(element);
117+
await aTimeout(0); // Wait a tick for promise to resolve
118+
119+
expect(controller.contextValue).to.equal(provider.providerInstance());
120+
121+
const newProviderInstance = new UmbTestContextConsumerClass();
122+
newProviderInstance.prop = 'new value from provider';
123+
124+
const newProvider = new UmbContextProvider(element, testToken, newProviderInstance);
125+
newProvider.hostConnected();
126+
127+
await aTimeout(0); // Wait a tick for promise to resolve
128+
129+
// Should still be the old value
130+
expect(controller.contextValue).to.not.equal(newProvider.providerInstance());
131+
expect(controller.contextValue?.prop).to.equal('value from provider');
132+
});
133+
});

0 commit comments

Comments
 (0)