From 3446b77d7d6c172bacb3031ec3c685d9cf680d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 31 Mar 2025 10:32:36 +0200 Subject: [PATCH] Feature: context consumer auto destroys if no callback + getContext options (#18835) * consumer destroys when no callback * getContext can be simplification * JSDocs * implement options to skipHost and pass context alias matches --- .../src/libs/class-api/class.interface.ts | 9 ++- .../src/libs/class-api/class.mixin.ts | 24 ++++---- .../consume/context-consumer.test.ts | 57 ++++++++++++------- .../context-api/consume/context-consumer.ts | 37 ++++++++---- .../src/libs/element-api/element.mixin.ts | 29 ++++------ 5 files changed, 95 insertions(+), 61 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts index a70f20916e..e25e08831c 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts @@ -9,6 +9,11 @@ import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoff import type { ObserverCallback, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +export interface UmbClassGetContextOptions extends UmbContextConsumerAsPromiseOptionsType { + skipHost?: boolean; + passContextAliasMatches?: boolean; +} + export interface UmbClassInterface extends UmbControllerHost { /** * @description Observe an Observable. An Observable is a declared source of data that can be observed. An observables is declared from a UmbState. @@ -60,11 +65,11 @@ export interface UmbClassInterface extends UmbControllerHost { /** * @description Retrieve a context. Notice this is a one time retrieving of a context, meaning if you expect this to be up to date with reality you should instead use the consumeContext method. * @param {string} alias - * @returns {Promise} A Promise with the reference to the Context Api Instance + * @returns {Promise} A Promise with the reference to the Context Api Instance * @memberof UmbClassInterface */ getContext( alias: string | UmbContextToken, - options?: UmbContextConsumerAsPromiseOptionsType, + options?: UmbClassGetContextOptions, ): Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts index 607cca29c1..3249c22356 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts @@ -1,4 +1,5 @@ import type { UmbClassMixinInterface } from './class-mixin.interface.js'; +import type { UmbClassGetContextOptions } from './class.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; import { @@ -11,7 +12,6 @@ import { type UmbContextCallback, UmbContextConsumerController, UmbContextProviderController, - type UmbContextConsumerAsPromiseOptionsType, } from '@umbraco-cms/backoffice/context-api'; import { type ObserverCallback, UmbObserverController, simpleHashCode } from '@umbraco-cms/backoffice/observable-api'; @@ -99,20 +99,18 @@ export const UmbClassMixin = >(superClas async getContext( contextAlias: string | UmbContextToken, - options?: UmbContextConsumerAsPromiseOptionsType, + options?: UmbClassGetContextOptions, ): Promise { const controller = new UmbContextConsumerController(this, contextAlias); - const promise = controller - .asPromise(options) - .then((result) => { - controller.destroy(); - return result; - }) - .catch(() => { - controller.destroy(); - return undefined; - }); - return promise; + if (options) { + if (options.passContextAliasMatches) { + controller.passContextAliasMatches(); + } + if (options.skipHost) { + controller.skipHost(); + } + } + return controller.asPromise(options); } public override destroy(): void { diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts index 57f9f0339b..bd3115eab3 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.test.ts @@ -100,6 +100,23 @@ describe('UmbContextConsumer', () => { provider.hostConnected(); }); + it('auto destroys when no callback provided', async () => { + const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass()); + + const localConsumer = new UmbContextConsumer(element, testContextAlias); + expect((localConsumer as any)._retrieveHost).to.not.be.undefined; + localConsumer.hostConnected(); + provider.hostConnected(); + const instance = await localConsumer.asPromise().catch(() => { + expect.fail('Promise should not reject'); + }); + expect(instance?.prop).to.eq('value from provider'); + provider.hostDisconnected(); + + await Promise.resolve(); + expect((localConsumer as any)._retrieveHost).to.be.undefined; + }); + it('gets rejected when using asPromise that does not resolve', (done) => { const localConsumer = new UmbContextConsumer(element, testContextAlias); @@ -116,29 +133,31 @@ describe('UmbContextConsumer', () => { localConsumer.hostConnected(); }); - it('never gets rejected when using asPromise that is set not to timeout and never will resolve', (done) => { + it('never gets rejected when using asPromise that is set to prevent timeout and never will resolve', (done) => { const localConsumer = new UmbContextConsumer(element, testContextAlias); localConsumer.hostConnected(); - const timeout = setTimeout(() => { - localConsumer.hostDisconnected(); - done(); - }, 200); + let acceptedRejection = false; - try { - localConsumer - .asPromise({ preventTimeout: true }) - .then((instance) => { - clearTimeout(timeout); - expect.fail('Promise should not resolve'); - }) - .catch(() => { - clearTimeout(timeout); + const timeout = setTimeout(() => { + acceptedRejection = true; + localConsumer.hostDisconnected(); + }, 100); + + localConsumer + .asPromise({ preventTimeout: true }) + .then((instance) => { + clearTimeout(timeout); + expect.fail('Promise should not resolve'); + }) + .catch((e) => { + clearTimeout(timeout); + if (acceptedRejection === true) { + done(); + } else { expect.fail('Promise should not reject'); - }); - } catch (e) { - console.log('e', e); - } + } + }); }); it('works with host as a method', (done) => { @@ -375,7 +394,7 @@ describe('UmbContextConsumer', () => { }); }); - it('context api of same context alias will NOT prevent request from propagating when set to exactMatch', (done) => { + it('context api of same context alias will NOT prevent request from propagating when set to passContextAliasMatches', (done) => { const provider = new UmbContextProvider(document.body, testContextAlias, new UmbTestContextConsumerClass()); provider.hostConnected(); diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts index 1d7b8ca359..872ff24fb4 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts @@ -23,6 +23,7 @@ export class UmbContextConsumer; #promise?: Promise; + #promiseOptions?: UmbContextConsumerAsPromiseOptionsType; #promiseResolver?: (instance: ResultType) => void; #promiseRejecter?: (reason: string) => void; @@ -115,9 +116,13 @@ export class UmbContextConsumer ${hostElement?.nodeName}`, ); this.#promise = undefined; + this.#promiseOptions = undefined; this.#promiseResolver = undefined; this.#promiseRejecter = undefined; } + if (!this.#callback) { + this.destroy(); + } } /** @@ -145,12 +154,14 @@ export class UmbContextConsumer((resolve, reject) => { if (this.#instance) { + this.#promiseOptions = undefined; this.#promiseResolver = undefined; this.#promiseRejecter = undefined; resolve(this.#instance); } else { + this.#promiseOptions = options; this.#promiseResolver = resolve; - this.#promiseRejecter = options?.preventTimeout ? undefined : reject; + this.#promiseRejecter = reject; } })) ); @@ -181,11 +192,13 @@ export class UmbContextConsumer { - // For unproviding, then setInstance to undefined here. [NL] - this.#rejectPromise(); - this.#raf = undefined; - }); + if (this.#promiseResolver && this.#promiseOptions?.preventTimeout !== true) { + this.#raf = requestAnimationFrame(() => { + // For unproviding, then setInstance to undefined here. [NL] + this.#rejectPromise(); + this.#raf = undefined; + }); + } } public hostConnected(): void { @@ -199,11 +212,14 @@ export class UmbContextConsumer(superClass: T) => { class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement { @@ -78,20 +75,18 @@ export const UmbElementMixin = (superClass: T) async getContext( contextAlias: string | UmbContextToken, - options?: UmbContextConsumerAsPromiseOptionsType, + options?: UmbClassGetContextOptions, ): Promise { const controller = new UmbContextConsumerController(this, contextAlias); - const promise = controller - .asPromise(options) - .then((result) => { - controller.destroy(); - return result; - }) - .catch(() => { - controller.destroy(); - return undefined; - }); - return promise; + if (options) { + if (options.passContextAliasMatches) { + controller.passContextAliasMatches(); + } + if (options.skipHost) { + controller.skipHost(); + } + } + return controller.asPromise(options); } }