From fd0367923e9564fd2c09582eeb952f8410256a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 24 May 2022 16:48:53 +0200 Subject: [PATCH 1/4] rename context api classes --- src/Umbraco.Web.UI.Client/src/app.ts | 8 +- .../src/auth/login/login.element.ts | 19 ++-- .../backoffice/backoffice-header.element.ts | 22 ++--- .../core/context/context-consumer.mixin.ts | 90 +++++++++++++++++++ ...ester.test.ts => context-consumer.test.ts} | 24 ++--- .../src/core/context/context-consumer.ts | 49 ++++++++++ .../src/core/context/context-inject.mixin.ts | 73 --------------- .../context/context-provide.event.test.ts | 23 +++++ .../src/core/context/context-provide.event.ts | 22 ++--- .../src/core/context/context-provide.mixin.ts | 36 -------- ...test.ts => context-provider.mixin.test.ts} | 14 +-- .../core/context/context-provider.mixin.ts | 52 +++++++++++ .../src/core/context/context-provider.test.ts | 10 +-- .../src/core/context/context-provider.ts | 17 ++-- .../context/context-request.event.test.ts | 14 +-- .../src/core/context/context-request.event.ts | 22 ++--- .../src/core/context/context-requester.ts | 31 ------- .../src/core/context/context.stories.mdx | 12 +-- .../src/core/context/index.ts | 6 +- .../src/core/extension/extension.registry.ts | 6 +- 20 files changed, 305 insertions(+), 245 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts rename src/Umbraco.Web.UI.Client/src/core/context/{context-requester.test.ts => context-consumer.test.ts} (57%) create mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-consumer.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-inject.mixin.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.test.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.ts rename src/Umbraco.Web.UI.Client/src/core/context/{context-provide.mixin.test.ts => context-provider.mixin.test.ts} (64%) create mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/core/context/context-requester.ts diff --git a/src/Umbraco.Web.UI.Client/src/app.ts b/src/Umbraco.Web.UI.Client/src/app.ts index 56ce7d5526..0b47017894 100644 --- a/src/Umbraco.Web.UI.Client/src/app.ts +++ b/src/Umbraco.Web.UI.Client/src/app.ts @@ -10,7 +10,7 @@ import { customElement, state } from 'lit/decorators.js'; import { getInitStatus } from './api/fetcher'; import { UmbRoute, UmbRouter } from './core/router'; -import { UmbContextProvideMixin } from './core/context'; +import { UmbContextProviderMixin } from './core/context'; const routes: Array = [ { @@ -29,7 +29,7 @@ const routes: Array = [ // Import somewhere else? @customElement('umb-app') -export class UmbApp extends UmbContextProvideMixin(LitElement) { +export class UmbApp extends UmbContextProviderMixin(LitElement) { static styles = css` :host, #outlet { @@ -51,7 +51,7 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { connectedCallback(): void { super.connectedCallback(); - this.provide('umbExtensionRegistry', window.Umbraco.extensionRegistry); + this.provideContext('umbExtensionRegistry', window.Umbraco.extensionRegistry); } protected async firstUpdated(): Promise { @@ -62,7 +62,7 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) { this._router.setRoutes(routes); // TODO: find a solution for magic strings - this.provide('umbRouter', this._router); + this.provideContext('umbRouter', this._router); try { const { data } = await getInitStatus({}); diff --git a/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts b/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts index 115e9c718c..fd0c1f9230 100644 --- a/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts +++ b/src/Umbraco.Web.UI.Client/src/auth/login/login.element.ts @@ -4,14 +4,13 @@ import { customElement, state } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { postUserLogin } from '../../api/fetcher'; -import { UmbContextInjectMixin } from '../../core/context'; +import { UmbContextConsumerMixin } from '../../core/context'; import { UmbRouter } from '../../core/router'; // create custom element with lit-element named 'umb-login' @customElement('umb-login') -// TODO: maybe rename the mixin to UmbContextRequestMixin? -export class UmbLogin extends UmbContextInjectMixin(LitElement) { +export class UmbLogin extends UmbContextConsumerMixin(LitElement) { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -32,17 +31,9 @@ export class UmbLogin extends UmbContextInjectMixin(LitElement) { // TODO: find solution for magic string // TODO: can we use a property decorator and a callback? - this.requestContext('umbRouter'); - } - - // TODO: maybe rename this callback to contextReceived? - /* TODO: this callback is called every time a new context is received. - Maybe is would make sense to return the updated contexts (like updatedProperties) so unnecessary code is not run because a new context is added to the map. - */ - contextInjected(contexts: Map): void { - if (contexts.has('umbRouter')) { - this._router = contexts.get('umbRouter'); - } + this.consumeContext('umbRouter', (api: unknown) => { + this._router = api as UmbRouter; + }); } private _handleSubmit = (e: SubmitEvent) => { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts index 43329b3cae..9b11152c3e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice-header.element.ts @@ -5,14 +5,14 @@ import { css, CSSResultGroup, html, LitElement } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; import { getUserSections } from '../api/fetcher'; -import { UmbContextInjectMixin } from '../core/context'; +import { UmbContextConsumerMixin } from '../core/context'; import { UmbExtensionManifest, UmbExtensionRegistry, UmbManifestSectionMeta } from '../core/extension'; import { UmbRouter } from '../core/router'; // TODO: umb or not umb in file name? @customElement('umb-backoffice-header') -export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { +export class UmbBackofficeHeader extends UmbContextConsumerMixin(LitElement) { static styles: CSSResultGroup = [ UUITextStyles, css` @@ -134,19 +134,13 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) { connectedCallback() { super.connectedCallback(); - this.requestContext('umbRouter'); - this.requestContext('umbExtensionRegistry'); - } - - contextInjected(contexts: Map): void { - if (contexts.has('umbExtensionRegistry')) { - this._extensionRegistry = contexts.get('umbExtensionRegistry'); + this.consumeContext('umbExtensionRegistry', (api: unknown) => { + this._extensionRegistry = api as UmbExtensionRegistry; this._useSections(); - } - - if (contexts.has('umbRouter')) { - this._router = contexts.get('umbRouter'); - } + }); + this.consumeContext('UmbRouter', (api: unknown) => { + this._router = api as UmbRouter; + }); } private async _useSections() { diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts new file mode 100644 index 0000000000..311f0be711 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts @@ -0,0 +1,90 @@ +import { UmbContextConsumer } from './context-consumer'; + +type Constructor = new (...args: any[]) => T; + +export declare class UmbContextConsumerInterface { + consumeContext(alias: string, callback?: (_instance: unknown) => void):void; + whenAvailableOrChanged(contextAliases: string[], callback?: () => void):void; +} + +/** + * This mixin enables the component to consume contexts. + * This is done by calling the `consumeContext` method. + * + * @param {Object} superClass - superclass to be extended. + * @mixin + */ +export const UmbContextConsumerMixin = >(superClass: T) => { + class UmbContextConsumerClass extends superClass { + + // all context requesters in the element + _consumers: Map = new Map(); + // all successfully resolved context requests + _resolved: Map = new Map(); + + _attached = false; + + /** + * Setup a subscription for a request on a given context of this component. + * @param {string} alias + * @param {method} callback optional callback method called when context is received or when context is detached. + */ + consumeContext(alias: string, callback?: (_instance: unknown) => void):void { + if (this._consumers.has(alias)) return; + + const consumer = new UmbContextConsumer(this, alias, (_instance: any) => { + + // Do we still have this consumer? + + callback?.(_instance); + + // don't to anything if the context is already resolved + if (this._resolved.has(alias) && this._resolved.get(alias) === _instance) return; + + this._resolved.set(alias, _instance); + this._consumeContextCallback(alias, _instance); + }); + + this._consumers.set(alias, consumer); + + if(this._attached ) { + consumer.attach(); + } + } + + // TODO: remove requester.. + + connectedCallback() { + super.connectedCallback?.(); + this._attached = true; + this._consumers.forEach(requester => requester.attach()); + + } + + disconnectedCallback() { + super.disconnectedCallback?.(); + this._attached = false; + this._consumers.forEach(requester => requester.detach()); + this._resolved.clear(); + } + + + _consumeContextCallback(newAlias, newInstance) { + // TODO: do be done. + } + + // might return a object, so you can unsubscribe. + whenAvailableOrChanged(contextAliases: string[]) { + // TODO: To be done. + } + }; + + return UmbContextConsumerClass as unknown as Constructor & T; +} + +declare global { + interface HTMLElement { + connectedCallback(): void; + disconnectedCallback(): void; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-requester.test.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts similarity index 57% rename from src/Umbraco.Web.UI.Client/src/core/context/context-requester.test.ts rename to src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts index 7835c6c78c..0aa28485c9 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-requester.test.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.test.ts @@ -1,7 +1,7 @@ import { expect, oneEvent } from '@open-wc/testing'; import { UmbContextProvider } from './context-provider'; -import { UmbContextRequester } from './context-requester'; -import { UmbContextRequestEvent, umbContextRequestType } from './context-request.event'; +import { UmbContextConsumer } from './context-consumer'; +import { UmbContextRequestEventImplementation, umbContextRequestEventType } from './context-request.event'; const testContextKey = 'my-test-context'; @@ -9,30 +9,30 @@ class MyClass { prop = 'value from provider'; } -describe('UmbContextRequester', () => { - let requestor: UmbContextRequester; +describe('UmbContextConsumer', () => { + let consumer: UmbContextConsumer; beforeEach(() => { // eslint-disable-next-line @typescript-eslint/no-empty-function - requestor = new UmbContextRequester(document.body, testContextKey, () => {}); + consumer = new UmbContextConsumer(document.body, testContextKey, () => {}); }); describe('Public API', () => { describe('methods', () => { it('has a dispatchRequest method', () => { - expect(requestor).to.have.property('dispatchRequest').that.is.a('function'); + expect(consumer).to.have.property('dispatchRequest').that.is.a('function'); }); }); describe('events', () => { it('dispatches request context event when constructed', async () => { - const listener = oneEvent(window, umbContextRequestType); + const listener = oneEvent(window, umbContextRequestEventType); // eslint-disable-next-line @typescript-eslint/no-empty-function - new UmbContextRequester(document.body, testContextKey, () => {}); - const event = await listener as unknown as UmbContextRequestEvent; + new UmbContextConsumer(document.body, testContextKey, () => {}); + const event = await listener as unknown as UmbContextRequestEventImplementation; expect(event).to.exist; - expect(event.type).to.eq(umbContextRequestType); - expect(event.contextKey).to.eq(testContextKey); + expect(event.type).to.eq(umbContextRequestEventType); + expect(event.contextAlias).to.eq(testContextKey); }); }); }); @@ -43,7 +43,7 @@ describe('UmbContextRequester', () => { const element = document.createElement('div'); document.body.appendChild(element); - new UmbContextRequester(element, testContextKey, (_instance) => { + new UmbContextConsumer(element, testContextKey, (_instance) => { expect(_instance.prop).to.eq('value from provider'); done(); }); diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.ts new file mode 100644 index 0000000000..877af11028 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.ts @@ -0,0 +1,49 @@ +import { UmbContextRequestEventImplementation, UmbContextCallback } from './context-request.event'; +import { isUmbContextProvideEvent, umbContextProvideEventType } from './context-provide.event'; + +/** + * @export + * @class UmbContextConsumer + */ +export class UmbContextConsumer { + + /** + * Creates an instance of UmbContextConsumer. + * @param {HTMLElement} element + * @param {string} _contextKey + * @param {UmbContextCallback} _callback + * @memberof UmbContextConsumer + */ + constructor ( + protected element: HTMLElement, + private _contextKey: string, + private _callback: UmbContextCallback + ) { + + } + + /** + * @memberof UmbContextConsumer + */ + public request() { + const event = new UmbContextRequestEventImplementation(this._contextKey, this._callback); + this.element.dispatchEvent(event); + } + + public attach() { + window.addEventListener(umbContextProvideEventType, this._handleNewProvider); + this.request(); + } + + public detach() { + window.removeEventListener(umbContextProvideEventType, this._handleNewProvider); + } + + private _handleNewProvider = (event: Event) => { + if (!isUmbContextProvideEvent(event)) return; + + if (this._contextKey === event.contextAlias) { + this.request(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-inject.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-inject.mixin.ts deleted file mode 100644 index 0ffdbc030d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-inject.mixin.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { UmbContextRequester } from './context-requester'; -import { umbContextProvideType, isUmbContextProvideEvent } from './context-provide.event'; - -type Constructor = new (...args: any[]) => HTMLElement; - -export function UmbContextInjectMixin(Base: TBase) { - return class Providing extends Base { - // all context requesters in the element - _requesters: Map = new Map(); - // all successfully resolved context requests - _resolved: Map = new Map(); - - /** - * Requests a context from any parent provider - * @param {string} contextKey - */ - requestContext(contextKey: string) { - if (this._requesters.has(contextKey)) return; - - const requester = new UmbContextRequester(this, contextKey, (_instance: any) => { - // don't to anything if the context is already resolved - if (this._resolved.has(contextKey) && this._resolved.get(contextKey) === _instance) return; - - this._resolved.set(contextKey, _instance); - this.contextInjected(this._resolved); - }); - - this._requesters.set(contextKey, requester); - } - - /** - * Sends a new context request for when a new provider is added. - * It only sends requests that matches the provider context key. - * @private - * @param {UmbContextProvideEvent} event - */ - handleNewProvider = (event: Event) => { - if (!isUmbContextProvideEvent(event)) return; - - if (this._requesters.has(event.contextKey)) { - const requester = this._requesters.get(event.contextKey); - requester?.dispatchRequest(); - } - } - - connectedCallback() { - super.connectedCallback?.(); - this._requesters.forEach(requester => requester.dispatchRequest()); - window.addEventListener(umbContextProvideType, this.handleNewProvider); - } - - disconnectedCallback() { - super.disconnectedCallback?.(); - window.removeEventListener(umbContextProvideType, this.handleNewProvider); - } - - /** - * This is called once a context was successfully requested. - * Run logic here when the dependecy is ready. - * @param contexts Map of all resolved contexts - */ - contextInjected(contexts: Map) { - // This is a stub - } - }; -} - -declare global { - interface HTMLElement { - connectedCallback(): void; - disconnectedCallback(): void; - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.test.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.test.ts new file mode 100644 index 0000000000..402a06670b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.test.ts @@ -0,0 +1,23 @@ +import { expect } from '@open-wc/testing'; +import { UmbContextProvideEventImplementation, UmbContextProvideEvent } from './context-provide.event'; + +describe('UmbContextProvideEvent', () => { + + const event: UmbContextProvideEvent = new UmbContextProvideEventImplementation('my-test-context-alias'); + + it('has context', () => { + expect(event.contextAlias).to.eq('my-test-context-alias'); + }); + + it('bubbles', () => { + expect(event.bubbles).to.be.true; + }); + + it('is composed', () => { + expect(event.composed).to.be.true; + }); + + it('is cancelable', () => { + expect(event.composed).to.be.false; + }); +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.ts index f31035dadc..f9185fe0e6 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provide.event.ts @@ -1,27 +1,27 @@ -export const umbContextProvideType = 'umb:context-provide'; +export const umbContextProvideEventType = 'umb:context-provide'; /** * @export - * @interface UmbContextProvide + * @interface UmbContextProvideEvent */ -export interface UmbContextProvide { - readonly contextKey: string; +export interface UmbContextProvideEvent extends Event { + readonly contextAlias: string; } /** * @export - * @class UmbContextProvideEvent + * @class UmbContextProvideEventImplementation * @extends {Event} - * @implements {UmbContextProvide} + * @implements {UmbContextProvideEvent} */ -export class UmbContextProvideEvent extends Event implements UmbContextProvide { +export class UmbContextProvideEventImplementation extends Event implements UmbContextProvideEvent { public constructor( - public readonly contextKey: string, + public readonly contextAlias: string, ) { - super(umbContextProvideType, {bubbles: true, composed: true }); + super(umbContextProvideEventType, {bubbles: true, composed: true }); } } -export const isUmbContextProvideEvent = (event: Event): event is UmbContextProvideEvent => { - return event.type === umbContextProvideType; +export const isUmbContextProvideEvent = (event: Event): event is UmbContextProvideEventImplementation => { + return event.type === umbContextProvideEventType; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.ts deleted file mode 100644 index ee4994ce01..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { UmbContextProvider } from './context-provider'; - -type Constructor = new (...args: any[]) => HTMLElement; - -export function UmbContextProvideMixin(Base: TBase) { - return class Providing extends Base { - _providers: Map = new Map(); - - constructor(...args: any[]) { - super(); - } - - provide(contextKey: string, instance: any) { - if (this._providers.has(contextKey)) return; - this._providers.set(contextKey, new UmbContextProvider(this, contextKey, instance)); - // TODO: if already connected then attach the new one. - } - - connectedCallback() { - super.connectedCallback?.(); - this._providers.forEach(provider => provider.attach()); - } - - disconnectedCallback() { - super.disconnectedCallback?.(); - this._providers.forEach(provider => provider.detach()); - } - }; -} - -declare global { - interface HTMLElement { - connectedCallback(): void; - disconnectedCallback(): void; - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.test.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.test.ts similarity index 64% rename from src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.test.ts rename to src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.test.ts index d9247b9c78..acd55e82bd 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-provide.mixin.test.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.test.ts @@ -1,5 +1,5 @@ import { expect, fixture, html } from '@open-wc/testing'; -import { UmbContextProvideMixin } from './context-provide.mixin'; +import { UmbContextProviderMixin } from './context-provider.mixin'; class MyClass { prop: string; @@ -9,19 +9,19 @@ class MyClass { } } -class MyTestProviderElement extends UmbContextProvideMixin(HTMLElement) { +class MyTestProviderElement extends UmbContextProviderMixin(HTMLElement) { constructor() { super(); - this.provide('my-test-context-1', new MyClass('context value 1')); - this.provide('my-test-context-2', new MyClass('context value 2')); + this.provideContext('my-test-context-1', new MyClass('context value 1')); + this.provideContext('my-test-context-2', new MyClass('context value 2')); } } customElements.define('my-test-provider-element', MyTestProviderElement); -describe('UmbContextProvideMixin', async () => { +describe('UmbContextProviderMixin', async () => { const element: MyTestProviderElement = await fixture(html``); - const _providers = element['_providers']; + const _providers = (element as any)['_providers']; it('sets all providers to element', () => { expect(_providers.has('my-test-context-1')).to.be.true; @@ -33,7 +33,7 @@ describe('UmbContextProvideMixin', async () => { expect(provider).to.not.be.undefined; if (!provider) return; expect(provider['_instance'].prop).to.eq('context value 1'); - element.provide('my-test-context-1', new MyClass('new context value 1')); + element.provideContext('my-test-context-1', new MyClass('new context value 1')); expect(provider['_instance'].prop).to.eq('context value 1'); }); }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts new file mode 100644 index 0000000000..78378f9870 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.mixin.ts @@ -0,0 +1,52 @@ +import { UmbContextProvider } from './context-provider'; + +type Constructor = new (...args: any[]) => T; + +export declare class UmbContextProviderMixinInterface { + provideContext(alias: string, instance: unknown):void; +} + + +export const UmbContextProviderMixin = (superClass: T) => { + class UmbContextProviderClass extends superClass { + _providers: Map = new Map(); + + _attached = false; + + provideContext(alias: string, instance: unknown) { + + // TODO: Consider if key matches wether we should replace and re-publish the context? + if (this._providers.has(alias)) return; + + const provider = new UmbContextProvider(this, alias, instance); + this._providers.set(alias, provider); + // TODO: if already connected then attach the new one. + if(this._attached) { + provider.attach(); + } + } + + // TODO: unprovide method to enforce a detach? + + connectedCallback() { + super.connectedCallback?.(); + this._attached = true; + this._providers.forEach(provider => provider.attach()); + } + + disconnectedCallback() { + super.disconnectedCallback?.(); + this._attached = false; + this._providers.forEach(provider => provider.detach()); + } + }; + + return UmbContextProviderClass as unknown as Constructor & T; +} + +declare global { + interface HTMLElement { + connectedCallback(): void; + disconnectedCallback(): void; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.test.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.test.ts index 949e2917e0..24c25cb050 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.test.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.test.ts @@ -1,7 +1,7 @@ import { expect } from '@open-wc/testing'; import { UmbContextProvider } from './context-provider'; -import { UmbContextRequester } from './context-requester'; -import { UmbContextRequestEvent } from './context-request.event'; +import { UmbContextConsumer } from './context-consumer'; +import { UmbContextRequestEventImplementation } from './context-request.event'; class MyClass { prop = 'value from provider'; @@ -38,7 +38,7 @@ describe('UmbContextProvider', () => { }); it('handles context request events', (done) => { - const event = new UmbContextRequestEvent('my-test-context', (_instance) => { + const event = new UmbContextRequestEventImplementation('my-test-context', (_instance) => { expect(_instance.prop).to.eq('value from provider'); done(); }); @@ -46,11 +46,11 @@ describe('UmbContextProvider', () => { document.body.dispatchEvent(event); }); - it('works with UmbContextRequester', (done) => { + it('works with UmbContextConsumer', (done) => { const element = document.createElement('div'); document.body.appendChild(element); - new UmbContextRequester(element, 'my-test-context', (_instance) => { + new UmbContextConsumer(element, 'my-test-context', (_instance) => { expect(_instance.prop).to.eq('value from provider'); done(); }); diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.ts index a69896c001..a2118711c1 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-provider.ts @@ -1,5 +1,5 @@ -import { umbContextRequestType, isUmbContextRequestEvent } from './context-request.event'; -import { UmbContextProvideEvent } from './context-provide.event'; +import { umbContextRequestEventType, isUmbContextRequestEvent } from './context-request.event'; +import { UmbContextProvideEventImplementation } from './context-provide.event'; /** * @export @@ -17,26 +17,26 @@ export class UmbContextProvider { * @param {*} instance * @memberof UmbContextProvider */ - constructor (host: HTMLElement, contextKey: string, instance: any) { + constructor (host: HTMLElement, contextKey: string, instance: unknown) { this.host = host; this._contextKey = contextKey; this._instance = instance; - this.attach(); } /** * @memberof UmbContextProvider */ public attach () { - this.host.addEventListener(umbContextRequestType, this._handleContextRequest); - this.host.dispatchEvent(new UmbContextProvideEvent(this._contextKey)); + this.host.addEventListener(umbContextRequestEventType, this._handleContextRequest); + this.host.dispatchEvent(new UmbContextProvideEventImplementation(this._contextKey)); } /** * @memberof UmbContextProvider */ public detach () { - this.host.removeEventListener(umbContextRequestType, this._handleContextRequest); + this.host.removeEventListener(umbContextRequestEventType, this._handleContextRequest); + // TODO: fire unprovide event. } /** @@ -45,8 +45,9 @@ export class UmbContextProvider { * @memberof UmbContextProvider */ private _handleContextRequest = (event: Event) => { + if (!isUmbContextRequestEvent(event)) return; - if (event.contextKey !== this._contextKey) return; + if (event.contextAlias !== this._contextKey) return; event.stopPropagation(); event.callback(this._instance); diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.test.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.test.ts index 3cda432dee..68eb9780bc 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.test.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.test.ts @@ -1,30 +1,30 @@ import { expect } from '@open-wc/testing'; -import { UmbContextRequestEvent } from './context-request.event'; +import { UmbContextRequestEventImplementation, UmbContextRequestEvent } from './context-request.event'; describe('UmbContextRequestEvent', () => { const contextRequestCallback = () => { console.log('hello from callback'); }; - const contextRequestEvent: UmbContextRequestEvent = new UmbContextRequestEvent('my-test-context', contextRequestCallback); + const event: UmbContextRequestEvent = new UmbContextRequestEventImplementation('my-test-context-alias', contextRequestCallback); it('has context', () => { - expect(contextRequestEvent.contextKey).to.eq('my-test-context'); + expect(event.contextAlias).to.eq('my-test-context-alias'); }); it('has a callback', () => { - expect(contextRequestEvent.callback).to.eq(contextRequestCallback); + expect(event.callback).to.eq(contextRequestCallback); }); it('bubbles', () => { - expect(contextRequestEvent.bubbles).to.be.true; + expect(event.bubbles).to.be.true; }); it('is composed', () => { - expect(contextRequestEvent.composed).to.be.true; + expect(event.composed).to.be.true; }); it('is cancelable', () => { - expect(contextRequestEvent.composed).to.be.true; + expect(event.composed).to.be.true; }); }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.ts index 7319f1fd07..98d340f27f 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-request.event.ts @@ -1,31 +1,31 @@ -export const umbContextRequestType = 'umb:context-request'; +export const umbContextRequestEventType = 'umb:context-request'; export type UmbContextCallback = (instance: any) => void; /** * @export - * @interface UmbContextRequest + * @interface UmbContextRequestEvent */ -export interface UmbContextRequest { - readonly contextKey: string; +export interface UmbContextRequestEvent extends Event { + readonly contextAlias: string; readonly callback: UmbContextCallback; } /** * @export - * @class UmbContextRequestEvent + * @class UmbContextRequestEventImplementation * @extends {Event} - * @implements {UmbContextRequest} + * @implements {UmbContextRequestEvent} */ -export class UmbContextRequestEvent extends Event implements UmbContextRequest { +export class UmbContextRequestEventImplementation extends Event implements UmbContextRequestEvent { public constructor( - public readonly contextKey: string, + public readonly contextAlias: string, public readonly callback: UmbContextCallback ) { - super(umbContextRequestType, {bubbles: true, composed: true, cancelable: true }); + super(umbContextRequestEventType, {bubbles: true, composed: true, cancelable: true }); } } -export const isUmbContextRequestEvent = (event: Event): event is UmbContextRequestEvent => { - return event.type === umbContextRequestType; +export const isUmbContextRequestEvent = (event: Event): event is UmbContextRequestEventImplementation => { + return event.type === umbContextRequestEventType; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-requester.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-requester.ts deleted file mode 100644 index 99d9051659..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-requester.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { UmbContextRequestEvent, UmbContextCallback } from './context-request.event'; - -/** - * @export - * @class UmbContextRequester - */ -export class UmbContextRequester { - - /** - * Creates an instance of UmbContextRequester. - * @param {HTMLElement} element - * @param {string} _contextKey - * @param {UmbContextCallback} _callback - * @memberof UmbContextRequester - */ - constructor ( - protected element: HTMLElement, - private _contextKey: string, - private _callback: UmbContextCallback - ) { - this.dispatchRequest(); - } - - /** - * @memberof UmbContextRequester - */ - dispatchRequest() { - const event = new UmbContextRequestEvent(this._contextKey, this._callback); - this.element.dispatchEvent(event); - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx b/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx index 8311719077..565c6ea567 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx +++ b/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx @@ -46,22 +46,22 @@ class UMBAppElement extends UMBContextMixin(LitElement) { ### Request context -Contexts shared through the Context API are async. To request a new context send a context request using the ```requestContext``` method from the ```UMBContextRequestMixin```. +Contexts shared through the Context API are async. To request a new context send a context request using the ```consumeContext``` method from the ```UMBContextConsumerMixin```. When a context is resolved the callback given to the method is called with the context. Until a requested context is resolved you should make sure to disable or query any functionality using that context. ```ts import { html, LitElement } from 'lit'; import type { UMBNotificationService } from './shell/shared/notification'; -import { UMBContextRequestMixin } from './utils/context'; +import { UMBContextConsumerMixin } from './utils/context'; -class MyElement extends UMBContextRequestMixin(LitElement) { +class MyElement extends UMBContextConsumerMixin(LitElement) { private _notificationService: UMBNotificationService; constructor () { super(); - this.requestContext('umbNotificationService', (api) => { + this.consumeContext('umbNotificationService', (api) => { this._notificationService = api; }); } @@ -118,7 +118,7 @@ This example shows how to use the ContextRequester class directly. ```ts import { html, LitElement } from 'lit'; import { UMBNotificationService } from './notification.service'; -import { UMBContextRequest } from './utils/context'; +import { UMBContextConsumer } from './utils/context'; class MyElement extends LitElement { @@ -127,7 +127,7 @@ class MyElement extends LitElement { connectedCallback(): void { super.connectedCallback(); - new UMBContextRequest(this, 'umbNotificationService', (_instance: UMBNotificationService) => { + new UMBContextConsumer(this, 'umbNotificationService', (_instance: UMBNotificationService) => { this._notificationService = _instance; }); } diff --git a/src/Umbraco.Web.UI.Client/src/core/context/index.ts b/src/Umbraco.Web.UI.Client/src/core/context/index.ts index 1ceeedee45..1753ff9e1c 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/index.ts @@ -1,5 +1,5 @@ -export * from './context-requester'; +export * from './context-consumer'; export * from './context-request.event'; export * from './context-provider'; -export * from './context-provide.mixin'; -export * from './context-inject.mixin'; \ No newline at end of file +export * from './context-provider.mixin'; +export * from './context-consumer.mixin'; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts index a8fa7c2779..58fd60b50c 100644 --- a/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/core/extension/extension.registry.ts @@ -21,15 +21,15 @@ export class UmbExtensionRegistry { public readonly extensions: Observable>> = this._extensions.asObservable(); register (manifest: UmbExtensionManifest) { - const extensions = this._extensions.getValue(); - const extension = extensions.find(extension => extension.alias === manifest.alias); + const extensionsValues = this._extensions.getValue(); + const extension = extensionsValues.find(extension => extension.alias === manifest.alias); if (extension) { console.error(`Extension with alias ${manifest.alias} is already registered`); return; } - this._extensions.next([...extensions, manifest]); + this._extensions.next([...extensionsValues, manifest]); } // TODO: implement unregister of extension From 53ea7cf5924051fc25e587fcf6f579433a5c1425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 25 May 2022 12:37:34 +0200 Subject: [PATCH 2/4] context api story update --- .../src/core/context/context.stories.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx b/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx index 565c6ea567..bab182feb0 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx +++ b/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx @@ -7,8 +7,8 @@ This system is based on the **Provider Pattern**, This is an event-based protoco This consists of a provider and a requester: -**Context Requester** -* A component requiring some data fires a ```umb:context-request``` event. +**Context Consumer** +* A component requsting an API fires a ```umb:context-request``` event. * The event carries a context value that denotes the data requested and a callback which will receive the data. * Providers can attach event listeners for ```umb:context-request``` events to handle them and provide the requested data. * Once a provider satisfies a request it calls stopPropagation() on the event. @@ -26,10 +26,10 @@ For other components to consume a context we need to provide it from a parent co ```ts import { html, LitElement } from 'lit'; -import { UMBContextMixin } from './utils/context'; +import { ContextProviderMixin } from './utils/context'; import { UMBNotificationService } from './shell/shared/notification'; -class UMBAppElement extends UMBContextMixin(LitElement) { +class UMBAppElement extends ContextProviderMixin(LitElement) { notificationService: UMBNotificationService = new UMBNotificationService(); From fcba97811d10097632806d664b262bf287ad92c6 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 25 May 2022 13:07:02 +0200 Subject: [PATCH 3/4] update examples to match new naming --- .../src/core/context/context.stories.mdx | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx b/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx index bab182feb0..67e876a0c8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx +++ b/src/Umbraco.Web.UI.Client/src/core/context/context.stories.mdx @@ -1,7 +1,7 @@ # Context API -In order to provide contextual shared logic or data we have established asystem called Context API. +In order to provide contextual shared logic or data we have established a system called Context API. This system is based on the **Provider Pattern**, This is an event-based protocol that components can use to retrieve data from any location in the DOM. @@ -26,20 +26,13 @@ For other components to consume a context we need to provide it from a parent co ```ts import { html, LitElement } from 'lit'; -import { ContextProviderMixin } from './utils/context'; -import { UMBNotificationService } from './shell/shared/notification'; - -class UMBAppElement extends ContextProviderMixin(LitElement) { - - notificationService: UMBNotificationService = new UMBNotificationService(); +import { UmbContextProviderMixin } from './context'; +import { UmbNotificationService } from './notification'; +class UmbAppElement extends UmbContextProviderMixin(LitElement) { constructor () { super(); - this.provideContext('umbNotificationService', notificationService); - } - - render() { - return html``; + this.provideContext('umbNotificationService', new UmbNotificationService()); } } ``` @@ -52,12 +45,12 @@ Until a requested context is resolved you should make sure to disable or query a ```ts import { html, LitElement } from 'lit'; -import type { UMBNotificationService } from './shell/shared/notification'; -import { UMBContextConsumerMixin } from './utils/context'; +import { UmbContextConsumerMixin } from './context'; +import type { UmbNotificationService } from './notification'; -class MyElement extends UMBContextConsumerMixin(LitElement) { +class MyElement extends UmbContextConsumerMixin(LitElement) { - private _notificationService: UMBNotificationService; + private _notificationService: UmbNotificationService; constructor () { super(); @@ -67,7 +60,7 @@ class MyElement extends UMBContextConsumerMixin(LitElement) { } private _handleClick () { - const data: UMBNotificationDefaultData = { message: 'Notification message' }; + const data: UmbNotificationDefaultData = { message: 'Notification message' }; this._notificationService?.peek('positive', { data }); } @@ -87,13 +80,12 @@ This example shows how to use the ContextProvider class directly. ```ts import { html, LitElement } from 'lit'; -import { UMBContextProvider } from './utils/context'; -import { UMBNotificationService } from './notification.service'; +import { UmbContextProvider } from './context'; +import { UmbNotificationService } from './notification'; -class UMBAppElement extends LitElement { +class UmbAppElement extends LitElement { - notificationService: UMVNotificationService = new UMBNotificationService(); - private _notificationProvider = new UMBContextProvider(this, 'umbNotificationService', this.notificationService); + private _notificationProvider = new UmbContextProvider(this, 'umbNotificationService', new UMBNotificationService()); connectedCallback(): void { super.connectedCallback(); @@ -104,10 +96,6 @@ class UMBAppElement extends LitElement { super.disconnectedCallback(); this._notificationProvider.detach(); } - - render() { - return html``; - } } ``` @@ -117,17 +105,17 @@ This example shows how to use the ContextRequester class directly. ```ts import { html, LitElement } from 'lit'; -import { UMBNotificationService } from './notification.service'; -import { UMBContextConsumer } from './utils/context'; +import { UmbNotificationService } from './notification'; +import { UmbContextConsumer } from './context'; class MyElement extends LitElement { - private _notificationService: UMBNotificationService; + private _notificationService: UmbNotificationService; connectedCallback(): void { super.connectedCallback(); - new UMBContextConsumer(this, 'umbNotificationService', (_instance: UMBNotificationService) => { + new UmbContextConsumer(this, 'umbNotificationService', (_instance: UmbNotificationService) => { this._notificationService = _instance; }); } From e1a73d62be656605261f6cf23df9960d04cbcba5 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 25 May 2022 13:16:17 +0200 Subject: [PATCH 4/4] prefix unused vars --- .../src/core/context/context-consumer.mixin.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts index 311f0be711..1a7e2f6598 100644 --- a/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/core/context/context-consumer.mixin.ts @@ -68,13 +68,12 @@ export const UmbContextConsumerMixin = >(supe this._resolved.clear(); } - - _consumeContextCallback(newAlias, newInstance) { + _consumeContextCallback(_newAlias: string, _newInstance: unknown) { // TODO: do be done. } // might return a object, so you can unsubscribe. - whenAvailableOrChanged(contextAliases: string[]) { + whenAvailableOrChanged(_contextAliases: string[]) { // TODO: To be done. } };