diff --git a/apps/demo/src/app/modules/doc/modules/extension/component/widget.example.component.html b/apps/demo/src/app/modules/doc/modules/extension/component/widget.example.component.html
new file mode 100644
index 000000000..fba9df617
--- /dev/null
+++ b/apps/demo/src/app/modules/doc/modules/extension/component/widget.example.component.html
@@ -0,0 +1,8 @@
+
+
Example Widget - Type: "{{ type }}"
+
+ Icon:
+ {{ icon }}
+
+
Data: {{ data | json }}
+
diff --git a/apps/demo/src/app/modules/doc/modules/extension/component/widget.example.component.ts b/apps/demo/src/app/modules/doc/modules/extension/component/widget.example.component.ts
new file mode 100644
index 000000000..660460be6
--- /dev/null
+++ b/apps/demo/src/app/modules/doc/modules/extension/component/widget.example.component.ts
@@ -0,0 +1,20 @@
+import { OnInit, Component } from '@angular/core';
+import { AbstractDbxWidgetComponent } from '@dereekb/dbx-web';
+
+export const DOC_EXTENSION_WIDGET_EXAMPLE_TYPE = 'widgetExample';
+
+export interface DocExtensionWidgetExampleData {
+ icon: string;
+ data: object;
+}
+
+@Component({
+ templateUrl: './widget.example.component.html'
+})
+export class DocExtensionWidgetExampleComponent extends AbstractDbxWidgetComponent {
+ readonly type = DOC_EXTENSION_WIDGET_EXAMPLE_TYPE;
+
+ get icon() {
+ return this.data.icon;
+ }
+}
diff --git a/apps/demo/src/app/modules/doc/modules/extension/container/widget.component.html b/apps/demo/src/app/modules/doc/modules/extension/container/widget.component.html
new file mode 100644
index 000000000..88cb13f65
--- /dev/null
+++ b/apps/demo/src/app/modules/doc/modules/extension/container/widget.component.html
@@ -0,0 +1,18 @@
+
+
+
+
+ Example with data
+ The input config has a type that instructs which widget to inject into the view, and passes the data field directly.
+ Input Config: {{ examplePair | json }}
+
+
+
+ Example without data
+ When not given content, the widget produces no visual output.
+
+
+
+
+
+
diff --git a/apps/demo/src/app/modules/doc/modules/extension/container/widget.component.ts b/apps/demo/src/app/modules/doc/modules/extension/container/widget.component.ts
new file mode 100644
index 000000000..c29b5ec5a
--- /dev/null
+++ b/apps/demo/src/app/modules/doc/modules/extension/container/widget.component.ts
@@ -0,0 +1,20 @@
+import { OnInit, Component } from '@angular/core';
+import { DbxWidgetDataPair } from '@dereekb/dbx-web';
+import { DocExtensionWidgetExampleData, DOC_EXTENSION_WIDGET_EXAMPLE_TYPE } from '../component/widget.example.component';
+
+@Component({
+ templateUrl: './widget.component.html'
+})
+export class DocExtensionWidgetComponent implements OnInit {
+ readonly examplePair: DbxWidgetDataPair = {
+ type: DOC_EXTENSION_WIDGET_EXAMPLE_TYPE,
+ data: {
+ icon: 'code',
+ data: {
+ test: true
+ }
+ } as DocExtensionWidgetExampleData
+ };
+
+ ngOnInit(): void {}
+}
diff --git a/apps/demo/src/app/modules/doc/modules/extension/doc.extension.module.ts b/apps/demo/src/app/modules/doc/modules/extension/doc.extension.module.ts
index c181f5cb3..c66da3f88 100644
--- a/apps/demo/src/app/modules/doc/modules/extension/doc.extension.module.ts
+++ b/apps/demo/src/app/modules/doc/modules/extension/doc.extension.module.ts
@@ -5,22 +5,34 @@ import { DocSharedModule } from '../shared/doc.shared.module';
import { DocExtensionLayoutComponent } from './container/layout.component';
import { DocExtensionCalendarComponent } from './container/calendar.component';
import { STATES } from './doc.extension.router';
-import { DbxCalendarRootModule } from '@dereekb/dbx-web';
+import { DbxCalendarRootModule, DbxWidgetModule, DbxWidgetService } from '@dereekb/dbx-web';
+import { DocExtensionWidgetComponent } from './container/widget.component';
+import { DOC_EXTENSION_WIDGET_EXAMPLE_TYPE, DocExtensionWidgetExampleComponent } from './component/widget.example.component';
@NgModule({
imports: [
DocSharedModule,
DbxCalendarRootModule,
+ DbxWidgetModule,
UIRouterModule.forChild({
states: STATES
})
],
declarations: [
// component
+ DocExtensionWidgetExampleComponent,
// container
DocExtensionLayoutComponent,
DocExtensionHomeComponent,
- DocExtensionCalendarComponent
+ DocExtensionCalendarComponent,
+ DocExtensionWidgetComponent
]
})
-export class DocExtensionModule {}
+export class DocExtensionModule {
+ constructor(dbxWidgetService: DbxWidgetService) {
+ dbxWidgetService.register({
+ type: DOC_EXTENSION_WIDGET_EXAMPLE_TYPE,
+ componentClass: DocExtensionWidgetExampleComponent
+ });
+ }
+}
diff --git a/apps/demo/src/app/modules/doc/modules/extension/doc.extension.router.ts b/apps/demo/src/app/modules/doc/modules/extension/doc.extension.router.ts
index 36253b5da..a1421dc37 100644
--- a/apps/demo/src/app/modules/doc/modules/extension/doc.extension.router.ts
+++ b/apps/demo/src/app/modules/doc/modules/extension/doc.extension.router.ts
@@ -2,6 +2,7 @@ import { Ng2StateDeclaration } from '@uirouter/angular';
import { DocExtensionLayoutComponent } from './container/layout.component';
import { DocExtensionCalendarComponent } from './container/calendar.component';
import { DocExtensionHomeComponent } from './container/home.component';
+import { DocExtensionWidgetComponent } from './container/widget.component';
export const layoutState: Ng2StateDeclaration = {
url: '/extension',
@@ -22,4 +23,16 @@ export const docExtensionCalendarState: Ng2StateDeclaration = {
component: DocExtensionCalendarComponent
};
-export const STATES: Ng2StateDeclaration[] = [layoutState, homeState, docExtensionCalendarState];
+export const docExtensionWidgetState: Ng2StateDeclaration = {
+ url: '/widget',
+ name: 'doc.extension.widget',
+ component: DocExtensionWidgetComponent
+};
+
+export const STATES: Ng2StateDeclaration[] = [
+ //
+ layoutState,
+ homeState,
+ docExtensionCalendarState,
+ docExtensionWidgetState
+];
diff --git a/apps/demo/src/app/modules/doc/modules/extension/doc.extension.ts b/apps/demo/src/app/modules/doc/modules/extension/doc.extension.ts
index ad5b49272..61977e6f5 100644
--- a/apps/demo/src/app/modules/doc/modules/extension/doc.extension.ts
+++ b/apps/demo/src/app/modules/doc/modules/extension/doc.extension.ts
@@ -4,6 +4,12 @@ export const DOC_EXTENSION_ROUTES = [
title: 'Calendar',
detail: 'dbx-calendar',
ref: 'doc.extension.calendar'
+ },
+ {
+ icon: 'code',
+ title: 'Widget',
+ detail: 'dbx-widget-view',
+ ref: 'doc.extension.widget'
}
];
diff --git a/packages/dbx-core/src/lib/storage/storage.accessor.simple.ts b/packages/dbx-core/src/lib/storage/storage.accessor.simple.ts
index f1257aa1d..0c2dbe409 100644
--- a/packages/dbx-core/src/lib/storage/storage.accessor.simple.ts
+++ b/packages/dbx-core/src/lib/storage/storage.accessor.simple.ts
@@ -1,6 +1,6 @@
import { Observable, map } from 'rxjs';
import { timeHasExpired, unixTimeNumberForNow } from '@dereekb/date';
-import { DataDoesNotExistError, DataIsExpiredError, ReadStoredData, StoredData, StoredDataStorageKey, StoredDataString, Maybe, hasNonNullValue } from '@dereekb/util';
+import { DataDoesNotExistError, DataIsExpiredError, ReadStoredData, StoredData, StoredDataStorageKey, StoredDataString, Maybe, hasNonNullValue, splitJoinRemainder } from '@dereekb/util';
import { StorageAccessor } from './storage.accessor';
// MARK: SimpleStorageAccessor
@@ -223,7 +223,7 @@ export class SimpleStorageAccessor implements StorageAccessor {
}
protected decodeStorageKey(storageKey: StoredDataStorageKey): string {
- const split = storageKey.split(this._config.prefixSplitter, 2);
+ const split = splitJoinRemainder(storageKey, this._config.prefixSplitter, 2);
return split[1];
}
diff --git a/packages/dbx-web/src/lib/extension/index.ts b/packages/dbx-web/src/lib/extension/index.ts
index edaf8f07a..1787ef57a 100644
--- a/packages/dbx-web/src/lib/extension/index.ts
+++ b/packages/dbx-web/src/lib/extension/index.ts
@@ -1 +1,2 @@
export * from './calendar';
+export * from './widget';
diff --git a/packages/dbx-web/src/lib/extension/widget/index.ts b/packages/dbx-web/src/lib/extension/widget/index.ts
new file mode 100644
index 000000000..83c50f680
--- /dev/null
+++ b/packages/dbx-web/src/lib/extension/widget/index.ts
@@ -0,0 +1,6 @@
+export * from './widget';
+export * from './widget.component';
+export * from './widget.directive';
+// export * from './widget.list.component';
+export * from './widget.module';
+export * from './widget.service';
diff --git a/packages/dbx-web/src/lib/extension/widget/widget.component.ts b/packages/dbx-web/src/lib/extension/widget/widget.component.ts
new file mode 100644
index 000000000..453ce4b3b
--- /dev/null
+++ b/packages/dbx-web/src/lib/extension/widget/widget.component.ts
@@ -0,0 +1,52 @@
+import { Observable, BehaviorSubject, map } from 'rxjs';
+import { Component, Input, OnDestroy } from '@angular/core';
+import { Maybe } from '@dereekb/util';
+import { DbxInjectionComponentConfig } from '@dereekb/dbx-core';
+import { DbxWidgetDataPair } from './widget';
+import { DbxWidgetService } from './widget.service';
+
+/**
+ * Used to display a corresponding widget based on the input data.
+ */
+@Component({
+ selector: 'dbx-widget-view',
+ template: `
+
+ `,
+ host: {
+ class: 'dbx-widget-view'
+ }
+})
+export class DbxWidgetViewComponent implements OnDestroy {
+ private _config = new BehaviorSubject>(undefined);
+
+ readonly config$: Observable> = this._config.pipe(
+ map((pair: Maybe) => {
+ let config: Maybe;
+
+ if (pair != null) {
+ const entry = this.dbxWidgetService.getWidgetEntry(pair.type);
+
+ if (entry) {
+ config = {
+ componentClass: entry.componentClass,
+ data: pair.data
+ };
+ }
+ }
+
+ return config;
+ })
+ );
+
+ constructor(readonly dbxWidgetService: DbxWidgetService) {}
+
+ ngOnDestroy(): void {
+ this._config.complete();
+ }
+
+ @Input()
+ set config(config: Maybe) {
+ this._config.next(config);
+ }
+}
diff --git a/packages/dbx-web/src/lib/extension/widget/widget.directive.ts b/packages/dbx-web/src/lib/extension/widget/widget.directive.ts
new file mode 100644
index 000000000..5ea1eba7c
--- /dev/null
+++ b/packages/dbx-web/src/lib/extension/widget/widget.directive.ts
@@ -0,0 +1,7 @@
+import { Directive, Inject } from '@angular/core';
+import { DBX_INJECTION_COMPONENT_DATA } from '@dereekb/dbx-core';
+
+@Directive()
+export abstract class AbstractDbxWidgetComponent {
+ constructor(@Inject(DBX_INJECTION_COMPONENT_DATA) readonly data: T) {}
+}
diff --git a/packages/dbx-web/src/lib/extension/widget/widget.list.component.ts b/packages/dbx-web/src/lib/extension/widget/widget.list.component.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/dbx-web/src/lib/extension/widget/widget.module.ts b/packages/dbx-web/src/lib/extension/widget/widget.module.ts
new file mode 100644
index 000000000..ce7284b3c
--- /dev/null
+++ b/packages/dbx-web/src/lib/extension/widget/widget.module.ts
@@ -0,0 +1,18 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { DbxInjectionComponentModule } from '@dereekb/dbx-core';
+import { DbxWidgetViewComponent } from './widget.component';
+
+/**
+ * Contains components related to displaying "widgets" for pieces of data.
+ */
+@NgModule({
+ imports: [
+ //
+ CommonModule,
+ DbxInjectionComponentModule
+ ],
+ declarations: [DbxWidgetViewComponent],
+ exports: [DbxWidgetViewComponent]
+})
+export class DbxWidgetModule {}
diff --git a/packages/dbx-web/src/lib/extension/widget/widget.service.ts b/packages/dbx-web/src/lib/extension/widget/widget.service.ts
new file mode 100644
index 000000000..9a1964737
--- /dev/null
+++ b/packages/dbx-web/src/lib/extension/widget/widget.service.ts
@@ -0,0 +1,52 @@
+import { Inject, Injectable, Optional, Type } from '@angular/core';
+import { Maybe, filterMaybeValues, mapIterable } from '@dereekb/util';
+import { DbxWidgetType } from './widget';
+
+export interface DbxWidgetEntry {
+ /**
+ * Widget type to respond to.
+ */
+ readonly type: DbxWidgetType;
+ /**
+ * Widget component class to use.
+ */
+ readonly componentClass: Type;
+}
+
+/**
+ * Service used to register widgets.
+ */
+@Injectable({
+ providedIn: 'root'
+})
+export class DbxWidgetService {
+ private _entries = new Map();
+
+ /**
+ * Used to register an entry. If an entry with the same type is already registered, this will override it by default.
+ *
+ * @param entry
+ * @param override
+ */
+ register(entry: DbxWidgetEntry, override: boolean = true): boolean {
+ if (override || !this._entries.has(entry.type)) {
+ this._entries.set(entry.type, entry);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // MARK: Get
+ getWidgetIdentifiers(): DbxWidgetType[] {
+ return Array.from(this._entries.keys());
+ }
+
+ getWidgetEntry(type: DbxWidgetType): Maybe {
+ return this._entries.get(type);
+ }
+
+ getWidgetEntries(types: Iterable): DbxWidgetEntry[] {
+ return filterMaybeValues(mapIterable(types ?? [], (x) => this._entries.get(x)));
+ }
+}
diff --git a/packages/dbx-web/src/lib/extension/widget/widget.ts b/packages/dbx-web/src/lib/extension/widget/widget.ts
new file mode 100644
index 000000000..09f56a623
--- /dev/null
+++ b/packages/dbx-web/src/lib/extension/widget/widget.ts
@@ -0,0 +1,16 @@
+import { ModelTypeDataPair, TypedModel, MapFunction, ReadKeyFunction } from '@dereekb/util';
+
+/**
+ * Widget type identifier
+ */
+export type DbxWidgetType = string;
+
+/**
+ * Type and data pair for a DbxWidget.
+ */
+export type DbxWidgetDataPair = ModelTypeDataPair;
+
+/**
+ * Used for converting the input data into a DbxWidgetDataPair value.
+ */
+export type DbxWidgetDataPairFactory = MapFunction>;