src/app/shared/widgets/widget.registry.ts
[key: string]:
|
Defined in src/app/shared/widgets/widget.registry.ts:62
|
import {
of as observableOf,
Subject,
ReplaySubject,
Observable,
Subscription,
} from "rxjs";
import { filter } from "rxjs/operators";
import { Injectable } from "@angular/core";
import { FormGroup } from "@angular/forms";
import {
Channels,
WidgetController,
WidgetConfig,
InputParameterDefinition,
WidgetMetadata,
ChannelDescriptors,
} from "./index";
import { NmWidgetMetadataRegistry } from "./widget.metadata.registry";
import { SimpleChannelMapping } from "./widget.configuration";
import { HalService } from "../components/hal/hal.service";
import { CurrentLocaleService } from "../components/i18n/currentLocale.service";
import { ActionForm } from "../components/hal/actionForm";
const NG_ON_INIT = "ngOnInit";
class ChannelBridge {
private _subject: ReplaySubject<any>;
private _sources: Observable<any>[] = [];
private _destinations: Subject<any>[] = [];
public subscriptions: Subscription[] = [];
public constructor(
public sourcePath: string,
public destinationPath: string
) {
this._subject = new ReplaySubject<any>(1);
}
public addSource(source: Observable<any>, path: string) {
this._sources.push(source);
let subscription = source.subscribe(this._subject);
this.subscriptions.push(subscription);
}
public addDestination(destination: Subject<any>, path: string) {
this._destinations.push(destination);
let subscription = this._subject.subscribe(destination);
this.subscriptions.push(subscription);
}
public dispose(): void {
if (this.subscriptions) {
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
}
}
}
interface BridgeMap {
[key: string]: ChannelBridge[];
}
export function widgetRegistryFactory(
halService: HalService,
currentLocaleService: CurrentLocaleService
): WidgetRegistry {
return new WidgetRegistry(halService, currentLocaleService);
}
class DefaultWidgetController implements WidgetController {
onInit(channels: Channels): void {}
}
@Injectable()
export class WidgetRegistry {
private _stack: string[] = [];
private _inputParameterDefinition: InputParameterDefinition = {};
private _controller: WidgetController;
private _form: FormGroup;
private _channels: BridgeMap = {};
private _action: ActionForm;
// WARNING:
// WidgetRegistry is re-created for every instance of NmPageComponent
// using the factory injection pattern.
// All new dependencies must be registered in the factory in page.component.ts
// before they can be used here
constructor(
private _halService: HalService,
private _currentLocaleService: CurrentLocaleService
) {
this._controller = new DefaultWidgetController();
}
public dispose() {
for (let channelId in this._channels) {
this._channels[channelId].forEach((bridge) => bridge.dispose());
}
if (this._controller && this._controller.onDestroy) {
this._controller.onDestroy();
}
}
public parseConfiguration(configuration: WidgetConfig) {
this.parseConfigurationStart(configuration, this.prepareChannels);
this.parseConfigurationStart(configuration, this.connectLinks);
this.connectInputParameters();
this.connectActionEvents();
this.connectLocaleEvents();
this.connectEcho();
this.connectController();
console.log(this._channels);
}
private parseConfigurationStart(
configuration: WidgetConfig,
callback: (
path: string,
configuration: WidgetConfig,
descriptor: WidgetMetadata
) => void
) {
this.parseConfigurationRecursive("", "root", configuration, callback);
}
private parseConfigurationRecursive(
path: string,
name: string,
configuration: WidgetConfig,
callback: (
path: string,
configuration: WidgetConfig,
descriptor: WidgetMetadata
) => void
) {
let subPath = path + "/" + name;
let descriptor = NmWidgetMetadataRegistry.getWidgetDescriptor(
configuration.component
);
callback.apply(this, [subPath, configuration, descriptor]);
if (configuration._embedded) {
for (let child in configuration._embedded) {
this.parseConfigurationRecursive(
subPath,
child,
configuration._embedded[child],
callback
);
}
}
}
private connectInputParameters(): void {
for (let parameter in this._inputParameterDefinition) {
let key = "@page:" + parameter;
let observable = this._inputParameterDefinition[parameter];
if (this._channels[key]) {
this._channels[key].forEach((bridge) =>
bridge.addSource(observable, key)
);
}
}
}
private connectActionEvents(): void {
for (let channel in this._channels) {
if (channel.indexOf("@action") < 0) {
continue;
}
let action = channel.replace("@action:", "");
let observable = this._halService
.getActionEvents()
.pipe(filter((event) => event.name === action));
this._channels[channel].forEach((bridge) =>
bridge.addSource(observable, channel)
);
}
}
private connectLocaleEvents(): void {
for (let channel in this._channels) {
if (channel.indexOf("@locale") < 0) {
continue;
}
let observable = this._currentLocaleService.getCurrentLocale();
this._channels[channel].forEach((bridge) =>
bridge.addSource(observable, channel)
);
}
}
private connectEcho(): void {
let echos = this._channels["@echo"];
let subject = new Subject();
if (echos) {
echos.forEach((bridge) => {
let subscription = subject.subscribe((data) =>
console.log("@echo", bridge.sourcePath, data)
);
bridge.subscriptions.push(subscription);
bridge.addDestination(subject, "@echo");
});
}
}
private connectController(): void {
let channels: Channels = {};
for (let channel in this._channels) {
if (channel.indexOf("@controller") < 0) {
continue;
}
let name = channel.replace("@controller:", "");
this._channels[channel].forEach((bridge) => {
let subject = new Subject();
channels[name] = subject;
if (bridge.sourcePath === channel) {
bridge.addSource(subject, channel);
} else if (bridge.destinationPath === channel) {
bridge.addDestination(subject, channel);
}
});
}
if (this._controller) {
this._controller.onInit(channels);
}
}
private prepareChannels(
path: string,
configuration: WidgetConfig,
descriptor: WidgetMetadata
) {
if (configuration.channels) {
configuration.channels.forEach((mapping) => {
this.configureChannelMapping(path, mapping);
});
}
}
private connectLinks(
path: string,
configuration: WidgetConfig,
descriptor: WidgetMetadata
) {
if (!configuration._links) {
return;
}
for (let name in configuration._links) {
let key = path + ":@link:" + name;
let link = configuration._links[name];
let observable = observableOf(link["href"]);
if (this._channels[key]) {
if (this._channels[key]) {
this._channels[key].forEach((bridge) =>
bridge.addSource(observable, key)
);
}
}
}
}
private configureChannelMapping(path: string, mapping: SimpleChannelMapping) {
// check for channels and create bridges where possible
let sourcePath = this.resolveChannelPath(path, mapping.source);
let destinationPath = this.resolveChannelPath(path, mapping.destination);
let bridge = new ChannelBridge(sourcePath, destinationPath);
this.addChannel(this._channels, sourcePath, bridge);
this.addChannel(this._channels, destinationPath, bridge);
}
private addChannel(channels: BridgeMap, path: string, bridge: ChannelBridge) {
let array = channels[path] || [];
array.push(bridge);
channels[path] = array;
}
private resolveChannelPath(currentPath: string, channel: string): string {
// reference to something else
if (channel.match(/^(@page|@action|@echo|@locale|@controller)/)) {
return channel;
}
let parts = channel.split(":");
if (parts.length == 2) {
parts = [currentPath].concat(parts);
}
var _path = parts[0];
var _prefix = parts[1];
var _name = parts[2];
let char = _path.charAt(0);
if (char !== "/" && char !== "*") {
_path = currentPath + "/" + _path;
}
return [_path, _prefix, _name].join(":");
}
public configureWidget(
parent: string,
id: string,
configuration: WidgetConfig,
widget: any,
descriptor: WidgetMetadata
) {
while (
this._stack.length > 0 &&
this._stack[this._stack.length - 1] !== parent
) {
this._stack.pop();
}
this._stack.push(id);
let path = "/" + this._stack.join("/");
console.log(
"widget path",
configuration.id,
configuration.component,
parent,
path
);
if (!descriptor.propertyConfiguration && !descriptor.methodConfigure) {
throw new Error(
'either a configuration property or a configure method is required for widget "' +
id +
'"'
);
}
if (descriptor.propertyConfiguration) {
widget[descriptor.propertyConfiguration] = configuration;
}
if (descriptor.propertyId) {
widget[descriptor.propertyId] = id;
}
if (descriptor.methodConfigure) {
widget[descriptor.methodConfigure](
configuration,
this._form,
this._action
);
}
let inputChannels = descriptor.inputs;
let outputChannels = descriptor.outputs;
WidgetRegistry.prepareChannels(widget, inputChannels);
WidgetRegistry.prepareChannels(widget, outputChannels);
let _service = this;
let onInit = widget[NG_ON_INIT];
let wrappedOnInit = function () {
try {
if (onInit) {
onInit.call(this);
}
_service.connectInputChannels(widget, path, inputChannels);
_service.connectOutputChannels(widget, path, outputChannels);
} catch (err) {
throw new Error(
"exception during onInit or connectInput/-Output " +
JSON.stringify(err)
);
}
};
// if the widget contains a ngOnInit-method, we initialize as part of the normal
// angular lifecycle
if (onInit) {
widget[NG_ON_INIT] = wrappedOnInit;
}
// otherwise, we just initalize after the constructor and possibly the configure method have finished
else {
wrappedOnInit.call(widget);
}
}
private connectOutputChannels(
widget: any,
path: string,
channels: ChannelDescriptors
) {
if (!channels) {
return;
}
for (let channelId in channels) {
let channelDescriptor = channels[channelId];
let subject = widget[channelDescriptor.property];
let channelPath = path + ":@output:" + channelId;
if (this._channels[channelPath]) {
this._channels[channelPath].forEach((bridge) =>
bridge.addSource(subject, channelPath)
);
}
let defaultPath = "*:@output:" + channelId;
if (this._channels[defaultPath]) {
this._channels[defaultPath].forEach((bridge) =>
bridge.addSource(subject, channelPath)
);
}
}
}
private connectInputChannels(
widget: any,
path: string,
channels: ChannelDescriptors
) {
if (!channels) {
return;
}
for (let channelId in channels) {
let channelDescriptor = channels[channelId];
let subject = widget[channelDescriptor.property];
let channelPath = path + ":@input:" + channelId;
if (this._channels[channelPath]) {
this._channels[channelPath].forEach((bridge) =>
bridge.addDestination(subject, channelPath)
);
}
let defaultPath = "*:@input:" + channelId;
if (this._channels[defaultPath]) {
this._channels[defaultPath].forEach((bridge) =>
bridge.addDestination(subject, channelPath)
);
}
}
}
private static prepareChannels(widget: any, channels: ChannelDescriptors) {
if (!channels) {
return;
}
for (let channelId in channels) {
let channelDescriptor = channels[channelId];
let subject = widget[channelDescriptor.property] || new Subject<any>();
if (!widget[channelDescriptor.property]) {
widget[channelDescriptor.property] = subject;
}
}
}
public registerInputParameters(inputParameters: InputParameterDefinition) {
this._inputParameterDefinition = Object.assign(
this._inputParameterDefinition,
inputParameters
);
}
public registerController(controller: WidgetController) {
this._controller = controller;
}
public registerFormGroup(formGroup: FormGroup, action: ActionForm) {
this._form = formGroup;
this._action = action;
}
}