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
This commit is contained in:
@@ -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<ContextType>} A Promise with the reference to the Context Api Instance
|
||||
* @returns {Promise<unknown>} A Promise with the reference to the Context Api Instance
|
||||
* @memberof UmbClassInterface
|
||||
*/
|
||||
getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
|
||||
alias: string | UmbContextToken<BaseType, ResultType>,
|
||||
options?: UmbContextConsumerAsPromiseOptionsType,
|
||||
options?: UmbClassGetContextOptions,
|
||||
): Promise<ResultType | undefined>;
|
||||
}
|
||||
|
||||
@@ -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 = <T extends ClassConstructor<EventTarget>>(superClas
|
||||
|
||||
async getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
|
||||
contextAlias: string | UmbContextToken<BaseType, ResultType>,
|
||||
options?: UmbContextConsumerAsPromiseOptionsType,
|
||||
options?: UmbClassGetContextOptions,
|
||||
): Promise<ResultType | undefined> {
|
||||
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 {
|
||||
|
||||
@@ -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<UmbTestContextConsumerClass>(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<UmbTestContextConsumerClass>(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<UmbTestContextConsumerClass>(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();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
|
||||
#stopAtContextMatch = true;
|
||||
#callback?: UmbContextCallback<ResultType>;
|
||||
#promise?: Promise<ResultType | undefined>;
|
||||
#promiseOptions?: UmbContextConsumerAsPromiseOptionsType;
|
||||
#promiseResolver?: (instance: ResultType) => void;
|
||||
#promiseRejecter?: (reason: string) => void;
|
||||
|
||||
@@ -115,9 +116,13 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
|
||||
if (this.#promiseResolver && this.#instance !== undefined) {
|
||||
this.#promiseResolver(this.#instance);
|
||||
this.#promise = undefined;
|
||||
this.#promiseOptions = undefined;
|
||||
this.#promiseResolver = undefined;
|
||||
this.#promiseRejecter = undefined;
|
||||
}
|
||||
if (!this.#callback) {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
#rejectPromise() {
|
||||
@@ -128,9 +133,13 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
|
||||
`Context could not be found. (Context Alias: ${this.#contextAlias} with API Alias: ${this.#apiAlias}). Controller is hosted on ${hostElement?.parentNode?.nodeName ?? 'Not attached node'} > ${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<BaseType = unknown, ResultType extends BaseType
|
||||
this.#promise ??
|
||||
(this.#promise = new Promise<ResultType | undefined>((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<BaseType = unknown, ResultType extends BaseType
|
||||
}
|
||||
*/
|
||||
|
||||
this.#raf = requestAnimationFrame(() => {
|
||||
// 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<BaseType = unknown, ResultType extends BaseType
|
||||
if (this.#raf !== undefined) {
|
||||
cancelAnimationFrame(this.#raf);
|
||||
this.#raf = undefined;
|
||||
this.#promiseRejecter?.('Context request was cancelled, host was disconnected.');
|
||||
this.#promiseResolver = undefined;
|
||||
this.#promiseRejecter = undefined;
|
||||
this.#promise = undefined;
|
||||
}
|
||||
|
||||
this.#promiseRejecter?.('Context request was cancelled, host was disconnected.');
|
||||
this.#promise = undefined;
|
||||
this.#promiseOptions = undefined;
|
||||
this.#promiseResolver = undefined;
|
||||
this.#promiseRejecter = undefined;
|
||||
|
||||
// TODO: We need to use closets application element. We need this in order to have separate Backoffice running within or next to each other.
|
||||
window.removeEventListener(UMB_CONTEXT_PROVIDE_EVENT_TYPE, this.#handleNewProvider);
|
||||
//window.removeEventListener(umbContextUnprovidedEventType, this.#handleRemovedProvider);
|
||||
@@ -242,6 +258,7 @@ export class UmbContextConsumer<BaseType = unknown, ResultType extends BaseType
|
||||
this._retrieveHost = undefined as any;
|
||||
this.#callback = undefined;
|
||||
this.#promise = undefined;
|
||||
this.#promiseOptions = undefined;
|
||||
this.#promiseResolver = undefined;
|
||||
this.#promiseRejecter = undefined;
|
||||
this.#instance = undefined;
|
||||
|
||||
@@ -3,14 +3,11 @@ import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { type UmbControllerAlias, UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
|
||||
import type {
|
||||
UmbContextToken,
|
||||
UmbContextCallback,
|
||||
UmbContextConsumerAsPromiseOptionsType,
|
||||
} from '@umbraco-cms/backoffice/context-api';
|
||||
import type { UmbContextToken, UmbContextCallback } from '@umbraco-cms/backoffice/context-api';
|
||||
import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/backoffice/context-api';
|
||||
import type { ObserverCallback } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbObserverController, simpleHashCode } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbClassGetContextOptions } from '@umbraco-cms/backoffice/class-api';
|
||||
|
||||
export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T) => {
|
||||
class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement {
|
||||
@@ -78,20 +75,18 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
|
||||
|
||||
async getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
|
||||
contextAlias: string | UmbContextToken<BaseType, ResultType>,
|
||||
options?: UmbContextConsumerAsPromiseOptionsType,
|
||||
options?: UmbClassGetContextOptions,
|
||||
): Promise<ResultType | undefined> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user