Skip to content

Commit

Permalink
feat: allow programmatic registration of activities
Browse files Browse the repository at this point in the history
This feature will be used for the upcoming micro frontend integration to register activities of micro frontends.

Further changes:
- activity route provides the activity in its routing context
- activity actions can be specified as component or template
- activity visibility is controlled on the activity

Closes #28

BREAKING CHANGE:

Use added `visible` property over `ngIf` directive to show or hide an activity based on a conditional <wb-activity [visible]="conditional"></wb-activity>
  • Loading branch information
danielwiehl committed Dec 11, 2018
1 parent 3561660 commit efc1344
Show file tree
Hide file tree
Showing 15 changed files with 475 additions and 161 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
<!-- Activity Bar -->
<ul class="activity-bar">
<li *ngFor="let activity of activityPartService.activities">
<li *ngFor="let activity of activities">
<a href=""
(click)="onActivate(activity);"
[class.active]="activityPartService.isActive(activity)"
(click)="onActivate(activity)"
[class.active]="activity.active"
[ngClass]="activity.cssClass"
[attr.title]="activity.title">
{{activity.label || ''}}
</a>
<div class="diamond" *ngIf="activityPartService.isActive(activity)"></div>
<div class="diamond" *ngIf="activity.active"></div>
</li>
</ul>

<!-- Activity Panel -->
<div *ngIf="activityPartService.activeActivity as activity"
<div *ngIf="activeActivity as activity"
#panel
class="activity-panel"
[style.width.px]="panelWidth + activity.panelWidthDelta"
[@panel-enter-or-leave]>
[@panel-enter-or-leave]
(@panel-enter-or-leave.done)="onPanelAnimationDone()">

<header>
<h1>{{activity.title}}</h1>
<ul>
<li *ngFor="let action of activity.actions" [ngTemplateOutlet]="action"></li>
<li *ngFor="let action of activity.actions">
<ng-container *ngIf="action.component">
<ng-container *ngComponentOutlet="action.component; injector: action.injector"></ng-container>
</ng-container>
<ng-container *ngIf="action.template">
<ng-container *ngTemplateOutlet="action.template"></ng-container>
</ng-container>
</li>
</ul>
</header>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { Component, ElementRef, ViewChild } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
import { animate, AnimationBuilder, AnimationPlayer, style, transition, trigger } from '@angular/animations';
import { WorkbenchActivityPartService } from './workbench-activity-part.service';
import { WorkbenchLayoutService } from '../workbench-layout.service';
import { noop, Observable, Subject } from 'rxjs';
import { ACTIVITY_OUTLET_NAME, ROUTER_OUTLET_NAME } from '../workbench.constants';
import { WbActivityDirective } from './wb-activity.directive';
import { Activity } from './activity';

@Component({
selector: 'wb-activity-part',
Expand Down Expand Up @@ -55,7 +55,17 @@ export class ActivityPartComponent {
constructor(public host: ElementRef<HTMLElement>,
public activityPartService: WorkbenchActivityPartService,
private _workbenchLayout: WorkbenchLayoutService,
private _animationBuilder: AnimationBuilder) {
private _animationBuilder: AnimationBuilder,
private _cd: ChangeDetectorRef) {
}

public get activities(): Activity[] {
return this.activityPartService.activities.filter(it => it.visible);
}

public get activeActivity(): Activity {
const activeActivity = this.activityPartService.activeActivity;
return activeActivity && activeActivity.visible ? activeActivity : null;
}

public set panelWidth(panelWidth: number) {
Expand All @@ -71,7 +81,7 @@ export class ActivityPartComponent {
return this._panelWidth$.asObservable();
}

public onActivate(activity: WbActivityDirective): false {
public onActivate(activity: Activity): false {
this.activityPartService.activateActivity(activity).then(noop);
return false; // prevent UA to follow 'href'
}
Expand All @@ -93,6 +103,10 @@ export class ActivityPartComponent {
this.panelWidth = ActivityPartComponent.PANEL_INITIAL_WIDTH;
}

public onPanelAnimationDone(): void {
this._cd.detectChanges();
}

private ensureMinimalPanelWidth(): void {
if (this.panelWidth >= ActivityPartComponent.PANEL_MIN_WIDTH) {
return;
Expand Down
169 changes: 169 additions & 0 deletions projects/scion/workbench/src/lib/activity-part/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Injector, TemplateRef } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ComponentType, PortalInjector } from '@angular/cdk/portal';
import { Disposable } from '../disposable';
import { InternalWorkbenchRouter } from '../routing/workbench-router.service';

/**
* Represents an activity in the activity panel.
*
* Register this activity in {WorkbenchActivityPartService} and its route as primary Angular route.
*/
export abstract class Activity {

/**
* Specifies the title of the activity.
*/
public title: string;

/**
* Use in combination with an icon font to specify the icon.
*/
public label: string;

/**
* Specifies the CSS class(es) used for the icon, e.g. 'material-icons' when using Angular Material Design.
*/
public cssClass: string | string[];

/**
* Controls whether to open this activity in the activity panel or to open it in a separate view.
*/
public target: 'activity-panel' | 'view';

/**
* Controls whether to show or hide this activity. By default, this activity is showing.
*/
public visible: boolean;

/**
* Specifies where to insert this activity in the list of activities.
*/
public position: number;

/**
* Specifies the number of pixels added to the activity panel width if this is the active activity.
*/
public panelWidthDelta: number;

/**
* Specifies the routing commands used by Angular router to navigate when this activity is activated.
* The route must be registered as primary Angular route.
*/
public abstract set routerLink(routerLink: any[] | string);

/**
* Returns the routing commands of this activity.
*/
public abstract get commands(): any[];

/**
* Returns the routing path of this activity.
*/
public abstract get path(): string;

/**
* Emits upon activation change of this activity.
*/
public abstract get active$(): Observable<boolean>;

/**
* Indicates if this activity is currently active.
*/
public abstract get active(): boolean;

/**
* Returns the actions associated with this activity.
*/
public abstract get actions(): ActivityAction[];

/**
* Associates an action with this activity. When this activity is active, it is displayed in the activity panel header.
*
* Either provide a template or component to render the action, and optionally an injector to provide data to the component.
*/
public abstract registerAction(action: TemplateRef<void> | ComponentType<any>, injector?: Injector): Disposable;
}

export class InternalActivity implements Activity {

private _commands: any[] = [];
private _actions: ActivityAction[] = [];
private _path: string;
private _active$ = new BehaviorSubject<boolean>(false);

public panelWidthDelta = 0;
public title: string;
public label: string;
public cssClass: string | string[];
public target: 'activity-panel' | 'view' = 'activity-panel';
public visible = true;
public position: number;

constructor(private _wbRouter: InternalWorkbenchRouter, private _injector: Injector) {
}

public set routerLink(routerLink: any[] | string) {
this._commands = this._wbRouter.normalizeCommands(routerLink ? (Array.isArray(routerLink) ? routerLink : [routerLink]) : []);
this._path = this.commands.filter(it => typeof it === 'string').join('/');
}

public get actions(): ActivityAction[] {
return this._actions;
}

public get commands(): any[] {
return this._commands;
}

public get path(): string {
return this._path;
}

public get active$(): Observable<boolean> {
return this._active$.asObservable();
}

public get active(): boolean {
return this._active$.getValue();
}

public set active(active: boolean) {
this._active$.next(active);
}

public registerAction(templateOrComponent: TemplateRef<void> | ComponentType<any>, injector?: Injector): Disposable {
const action = ((): ActivityAction => {
if (templateOrComponent instanceof TemplateRef) {
return {template: templateOrComponent, injector: injector};
} else {
const injectionTokens = new WeakMap();
injectionTokens.set(Activity, this);
const portalInjector = new PortalInjector(injector || this._injector, injectionTokens);
return {component: templateOrComponent, injector: portalInjector};
}
})();
this._actions.push(action);

return {
dispose: (): void => {
const index = this._actions.indexOf(action);
if (index === -1) {
throw Error('[IllegalStateError] Action not registered in activity');
}
this._actions.splice(index, 1);
}
};
}
}

/**
* Action to be added to an activity.
*
* Specify either a template or a component to render the action.
*/
export interface ActivityAction {
template?: TemplateRef<void>;
component?: ComponentType<any>;
injector?: Injector;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
* SPDX-License-Identifier: EPL-2.0
*/

import { Directive, OnDestroy, Optional, TemplateRef } from '@angular/core';
import { WbActivityDirective } from './wb-activity.directive';
import { Directive, OnDestroy, TemplateRef } from '@angular/core';
import { WorkbenchActivityPartService } from './workbench-activity-part.service';
import { Activity } from './activity';
import { ActivatedRoute } from '@angular/router';
import { Disposable } from '../disposable';

/**
* Use this directive to model an action for an activity, which is displayed
Expand All @@ -32,19 +34,21 @@ import { WorkbenchActivityPartService } from './workbench-activity-part.service'
})
export class WbActivityActionDirective implements OnDestroy {

private readonly _activity: WbActivityDirective;
private readonly _activity: Activity;
private readonly _action: Disposable;

constructor(@Optional() private _template: TemplateRef<void>,
activityService: WorkbenchActivityPartService) {
if (!this._template) {
throw Error('Illegal usage: Host element of this modelling directive must be a <ng-template>');
constructor(private _template: TemplateRef<void>,
activityService: WorkbenchActivityPartService,
route: ActivatedRoute) {
this._activity = activityService.getActivityFromRoutingContext(route.snapshot);
if (!this._activity) {
throw Error('[RoutingContextError] Route not in the context of an activity');
}

this._activity = activityService.activeActivity;
this._activity.registerAction(this._template);
this._action = this._activity.registerAction(this._template);
}

public ngOnDestroy(): void {
this._activity.unregisterAction(this._template);
this._action.dispose();
}
}
Loading

0 comments on commit efc1344

Please sign in to comment.