Merge branch 'main' into router-updates

This commit is contained in:
Mads Rasmussen
2022-05-25 13:37:07 +02:00
20 changed files with 331 additions and 283 deletions

View File

@@ -12,11 +12,11 @@ import { UmbSectionContext } from './section.context';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { getInitStatus } from './api/fetcher';
import { isUmbRouterBeforeEnterEvent, UmbRoute, UmbRouteLocation, UmbRouter, UmbRouterBeforeEnterEvent, umbRouterBeforeEnterEventType } from './core/router';
import { UmbContextProvideMixin } from './core/context';
import { Subscription } from 'rxjs';
import { UmbContextProviderMixin } from './core/context';
const routes: Array<UmbRoute> = [
{
@@ -38,7 +38,7 @@ const routes: Array<UmbRoute> = [
// Import somewhere else?
@customElement('umb-app')
export class UmbApp extends UmbContextProvideMixin(LitElement) {
export class UmbApp extends UmbContextProviderMixin(LitElement) {
static styles = css`
:host,
#outlet {
@@ -63,8 +63,8 @@ export class UmbApp extends UmbContextProvideMixin(LitElement) {
super.connectedCallback();
const { extensionRegistry } = window.Umbraco;
this.provide('umbExtensionRegistry', window.Umbraco.extensionRegistry);
this.provide('umbSectionContext', new UmbSectionContext(extensionRegistry));
this.provideContext('umbExtensionRegistry', window.Umbraco.extensionRegistry);
this.provideContext('umbSectionContext', new UmbSectionContext(extensionRegistry));
}
private _onBeforeEnter = (event: Event) => {
@@ -88,7 +88,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);
// TODO: this is a temporary routing solution for shell elements
try {

View File

@@ -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<string, any>): void {
if (contexts.has('umbRouter')) {
this._router = contexts.get('umbRouter');
}
this.consumeContext('umbRouter', (api: unknown) => {
this._router = api as UmbRouter;
});
}
private _handleSubmit = (e: SubmitEvent) => {

View File

@@ -5,15 +5,15 @@ 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 { UmbExtensionManifest, UmbManifestSectionMeta } from '../core/extension';
import { UmbRouteLocation, UmbRouter } from '../core/router';
import { UmbSectionContext } from '../section.context';
import { UmbContextConsumerMixin } from '../core/context';
// 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`
@@ -104,6 +104,21 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
private _locationSubscription?: Subscription;
private _location? : UmbRouteLocation;
constructor () {
super();
this.consumeContext('umbRouter', (_instance: UmbRouter) => {
this._router = _instance;
this._useLocation();
});
this.consumeContext('umbSectionContext', (_instance: UmbSectionContext) => {
this._sectionContext = _instance;
this._useCurrentSection();
this._useSections();
});
}
private _handleMore(e: MouseEvent) {
e.stopPropagation();
this._open = !this._open;
@@ -134,26 +149,6 @@ export class UmbBackofficeHeader extends UmbContextInjectMixin(LitElement) {
this._open = false;
}
connectedCallback() {
super.connectedCallback();
this.requestContext('umbRouter');
this.requestContext('umbSectionContext');
}
contextInjected(contexts: Map<string, any>): void {
if (contexts.has('umbRouter')) {
this._router = contexts.get('umbRouter');
this._useLocation();
}
if (contexts.has('umbSectionContext')) {
this._sectionContext = contexts.get('umbSectionContext');
this._useCurrentSection();
this._useSections();
}
}
private _useLocation () {
this._locationSubscription?.unsubscribe();

View File

@@ -0,0 +1,89 @@
import { UmbContextConsumer } from './context-consumer';
type Constructor<T = HTMLElement> = 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 = <T extends Constructor<HTMLElement>>(superClass: T) => {
class UmbContextConsumerClass extends superClass {
// all context requesters in the element
_consumers: Map<string, UmbContextConsumer> = new Map();
// all successfully resolved context requests
_resolved: Map<string, unknown> = 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: string, _newInstance: unknown) {
// 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<UmbContextConsumerInterface> & T;
}
declare global {
interface HTMLElement {
connectedCallback(): void;
disconnectedCallback(): void;
}
}

View File

@@ -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();
});

View File

@@ -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();
}
}
}

View File

@@ -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<TBase extends Constructor>(Base: TBase) {
return class Providing extends Base {
// all context requesters in the element
_requesters: Map<string, UmbContextRequester> = new Map();
// all successfully resolved context requests
_resolved: Map<string, any> = 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<string, any>) {
// This is a stub
}
};
}
declare global {
interface HTMLElement {
connectedCallback(): void;
disconnectedCallback(): void;
}
}

View File

@@ -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;
});
});

View File

@@ -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;
}

View File

@@ -1,36 +0,0 @@
import { UmbContextProvider } from './context-provider';
type Constructor = new (...args: any[]) => HTMLElement;
export function UmbContextProvideMixin<TBase extends Constructor>(Base: TBase) {
return class Providing extends Base {
_providers: Map<string, UmbContextProvider> = 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;
}
}

View File

@@ -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`<my-test-provider-element></my-test-provider-element>`);
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');
});
});

View File

@@ -0,0 +1,52 @@
import { UmbContextProvider } from './context-provider';
type Constructor<T = HTMLElement> = new (...args: any[]) => T;
export declare class UmbContextProviderMixinInterface {
provideContext(alias: string, instance: unknown):void;
}
export const UmbContextProviderMixin = <T extends Constructor>(superClass: T) => {
class UmbContextProviderClass extends superClass {
_providers: Map<string, UmbContextProvider> = 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<UmbContextProviderMixinInterface> & T;
}
declare global {
interface HTMLElement {
connectedCallback(): void;
disconnectedCallback(): void;
}
}

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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;
});
});

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -1,14 +1,14 @@
# 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.
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,48 +26,41 @@ 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 { UMBNotificationService } from './shell/shared/notification';
class UMBAppElement extends UMBContextMixin(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`<slot></slot>`;
this.provideContext('umbNotificationService', new UmbNotificationService());
}
}
```
### 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 './context';
import type { UmbNotificationService } from './notification';
class MyElement extends UMBContextRequestMixin(LitElement) {
class MyElement extends UmbContextConsumerMixin(LitElement) {
private _notificationService: UMBNotificationService;
private _notificationService: UmbNotificationService;
constructor () {
super();
this.requestContext('umbNotificationService', (api) => {
this.consumeContext('umbNotificationService', (api) => {
this._notificationService = api;
});
}
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`<slot></slot>`;
}
}
```
@@ -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 { UMBContextRequest } 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 UMBContextRequest(this, 'umbNotificationService', (_instance: UMBNotificationService) => {
new UmbContextConsumer(this, 'umbNotificationService', (_instance: UmbNotificationService) => {
this._notificationService = _instance;
});
}

View File

@@ -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';
export * from './context-provider.mixin';
export * from './context-consumer.mixin';

View File

@@ -21,15 +21,15 @@ export class UmbExtensionRegistry {
public readonly extensions: Observable<Array<UmbExtensionManifest<unknown>>> = this._extensions.asObservable();
register (manifest: UmbExtensionManifest<unknown>) {
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