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