Merge branch 'main' into router-updates
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user