Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/172 #199

Merged
merged 4 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -516,15 +516,15 @@ applies to the `@scion/microfrontend-platform` version.
return messageClient.onMessage(topic, callback);
}
```
See https://scion-microfrontend-platform-developer-guide.vercel.app/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular for more information.
See https://scion-microfrontend-platform-developer-guide-v1-0-0-rc-10.vercel.app/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular for more information.

- If an Angular project, add the method `onIntent` to your `NgZone` intent client decorator, as following:
```typescript
public onIntent<IN = any, OUT = any>(selector: IntentSelector, callback: (intentMessage: IntentMessage<IN>) => Observable<OUT> | Promise<OUT> | OUT | void): Subscription {
return intentClient.onIntent(selector, callback);
}
```
See https://scion-microfrontend-platform-developer-guide.vercel.app/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular for more information.
See https://scion-microfrontend-platform-developer-guide-v1-0-0-rc-10.vercel.app/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular for more information.
* **platform:** Enabling the message/intent replier to control the requestor’s Observable lifecycle introduced a breaking change in the host/client communication protocol.

> Note: The messaging protocol between host and client HAS CHANGED for registering/unregistering capabilities/intentions using the `ManifestService`. Therefore, you must update the host and affected clients to the new version together. The API has not changed; the breaking change only applies to the `@scion/microfrontend-platform` version.
Expand Down Expand Up @@ -649,7 +649,7 @@ Renamed options object of the following methods:
- _IntentClient#request$_: _MessageOptions_ -> _IntentOptions_

#### Breaking change for decorating MessageClient and IntentClient bean
For Angular developers, see [Preparing the MessageClient and IntentClient for use with Angular](https://scion-microfrontend-platform-developer-guide.vercel.app/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular) how to decorate the `MessageClient` and `IntentClient` for making Observables to emit inside the Angular zone.
For Angular developers, see [Preparing the MessageClient and IntentClient for use with Angular](https://scion-microfrontend-platform-developer-guide-v1-0-0-rc-10.vercel.app/#chapter:angular-integration-guide:preparing-messaging-for-use-with-angular) how to decorate the `MessageClient` and `IntentClient` for making Observables to emit inside the Angular zone.

#### Breaking change for disabling messaging in tests
Messaging can now be deactivated via options object when starting the platform. Previously you had to register a `NullMessageClient` bean.
Expand Down
18 changes: 6 additions & 12 deletions apps/microfrontend-platform-devtools/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
* SPDX-License-Identifier: EPL-2.0
*/
import {BrowserModule} from '@angular/platform-browser';
import {APP_INITIALIZER, NgModule, NgZone} from '@angular/core';
import {APP_INITIALIZER, inject, NgModule, NgZone} from '@angular/core';

import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {IntentClient, ManifestService, MessageClient, MicrofrontendPlatform, OutletRouter, PlatformState} from '@scion/microfrontend-platform';
import {NgZoneIntentClientDecorator, NgZoneMessageClientDecorator} from './ng-zone-decorators';
import {IntentClient, ManifestService, MessageClient, MicrofrontendPlatform, ObservableDecorator, OutletRouter} from '@scion/microfrontend-platform';
import {NgZoneObservableDecorator} from './ng-zone-observable-decorator';
import {AppDetailsComponent} from './app-details/app-details.component';
import {AppListComponent} from './app-list/app-list.component';
import {SciViewportModule} from '@scion/components/viewport';
Expand Down Expand Up @@ -87,7 +87,6 @@ import {CustomParamMetadataPipe} from './custom-param-metadata.pipe';
provide: APP_INITIALIZER,
useFactory: providePlatformInitializerFn,
multi: true,
deps: [NgZoneMessageClientDecorator, NgZoneIntentClientDecorator, NgZone],
},
{provide: MessageClient, useFactory: () => Beans.get(MessageClient)},
{provide: IntentClient, useFactory: () => Beans.get(IntentClient)},
Expand All @@ -99,15 +98,10 @@ import {CustomParamMetadataPipe} from './custom-param-metadata.pipe';
export class AppModule {
}

export function providePlatformInitializerFn(ngZoneMessageClientDecorator: NgZoneMessageClientDecorator, ngZoneIntentClientDecorator: NgZoneIntentClientDecorator, zone: NgZone): () => Promise<void> {
export function providePlatformInitializerFn(): () => Promise<void> {
const zone = inject(NgZone);
return (): Promise<void> => {
// Make the platform to run with Angular
MicrofrontendPlatform.whenState(PlatformState.Starting).then(() => {
Beans.registerDecorator(MessageClient, {useValue: ngZoneMessageClientDecorator});
Beans.registerDecorator(IntentClient, {useValue: ngZoneIntentClientDecorator});
});

// Run the microfrontend platform as client app
Beans.register(ObservableDecorator, {useValue: new NgZoneObservableDecorator(zone)});
return zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost('devtools').catch(() => null));
};
}
90 changes: 0 additions & 90 deletions apps/microfrontend-platform-devtools/src/app/ng-zone-decorators.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2018-2022 Swiss Federal Railways
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/

import {NgZone} from '@angular/core';
import {ObservableDecorator} from '@scion/microfrontend-platform';
import {Observable} from 'rxjs';
import {observeInside, subscribeInside} from '@scion/toolkit/operators';

/**
* Mirrors the source, but ensures subscription and emission {@link NgZone} to be identical.
*
* Angular applications expect an RxJS Observable to emit in the same Angular zone in which the subscription was
* performed. That is, if subscribing inside the Angular zone, emissions are expected to be received inside the
* Angular zone. Otherwise, the UI may not be updated as expected but delayed until the next change detection cycle.
* Similarly, if subscribing outside the Angular zone, emissions are expected to be received outside the Angular
* zone. Otherwise, this would cause unnecessary change detection cycles resulting in potential performance degradation.
*/
export class NgZoneObservableDecorator implements ObservableDecorator {

constructor(private _zone: NgZone) {
}

public decorate$<T>(source$: Observable<T>): Observable<T> {
return new Observable<T>(observer => {
const insideAngular = NgZone.isInAngularZone();
const subscription = source$
.pipe(
subscribeInside(fn => this._zone.runOutsideAngular(fn)),
observeInside(fn => insideAngular ? this._zone.run(fn) : this._zone.runOutsideAngular(fn)),
)
.subscribe(observer);
return () => subscription.unsubscribe();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<img *ngIf="isPlatformHost" class="banner" src="assets/scion-microfrontend-platform-banner.svg" alt="SCION Microfrontend Platform">
<span *ngIf="pageTitle" class="page-title">{{pageTitle}}</span>
</div>
<span class="chip focus-within e2e-focus-within" title="This document or its embedded web content has received focus" *ngIf="isFocusWithin">focus-within</span>
<span class="angular-change-detection-indicator" title="Indicates that Angular is currently checking the application for changes." #angular_change_detection_indicator></span>
<span class="chip focus-within e2e-focus-within" title="This document or its embedded web content has received focus" *ngIf="focusMonitor.focusWithin$ | async">focus-within</span>
<span class="chip focus e2e-has-focus" *ngIf="focusMonitor.focus$ | async">has-focus</span>
<span class="chip app-name">{{appSymbolicName}}</span>
<span class="chip devtools" *ngIf="isDevtoolsEnabled" (click)="onDevToolsToggle()">
<span>DevTools</span>
Expand All @@ -20,3 +22,6 @@
<app-devtools></app-devtools>
</ng-template>
</sci-sashbox>

<!-- Captures Angular change detection cycles -->
{{onAngularChangeDetectionCycle}}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@

> div.title {
flex: auto;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;

> img.banner {
height: 35px;
Expand All @@ -33,18 +36,35 @@
}
}

> span.angular-change-detection-indicator {
width: 1em;
height: 1em;
min-width: 1em;
min-height: 1em;
border: 1px solid var(--sci-color-primary);
border-radius: 50%;
background-color: var(--sci-color-P50);
position: relative;
top: .5em;
margin: 0 .5em;

&:not(.active) {
visibility: hidden;
}
}

> span.chip {
flex: none;

&.focus-within {
&.focus-within, &.focus {
@include sci-ɵcomponents.theme-chip(var(--sci-color-accent), null, var(--sci-color-accent));
padding: .25em 1.5em;
padding: .25em .75em;
font-size: 1.1rem;
}

&.app-name {
@include sci-ɵcomponents.theme-chip(var(--sci-color-accent), var(--sci-color-A50), var(--sci-color-accent));
padding: .25em 1.5em;
padding: .25em .75em;
font-size: 1.1rem;
font-weight: bold;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
import {Component, HostBinding, OnDestroy} from '@angular/core';
import {asapScheduler, delay, EMPTY, from, mergeMap, of, Subject, switchMap, withLatestFrom} from 'rxjs';
import {Component, ElementRef, HostBinding, NgZone, OnDestroy, ViewChild} from '@angular/core';
import {asapScheduler, debounceTime, delay, EMPTY, from, mergeMap, of, Subject, switchMap, withLatestFrom} from 'rxjs';
import {APP_IDENTITY, ContextService, FocusMonitor, IS_PLATFORM_HOST, OUTLET_CONTEXT, OutletContext} from '@scion/microfrontend-platform';
import {takeUntil} from 'rxjs/operators';
import {takeUntil, tap} from 'rxjs/operators';
import {ActivatedRoute} from '@angular/router';
import {Defined} from '@scion/toolkit/util';
import {Beans} from '@scion/toolkit/bean-manager';
Expand All @@ -25,18 +25,24 @@ export class AppShellComponent implements OnDestroy {

private _destroy$ = new Subject<void>();
private _routeActivate$ = new Subject<void>();
private _angularChangeDetectionCycle$ = new Subject<void>();

public appSymbolicName: string;
public pageTitle: string;
public isFocusWithin: boolean;
public isDevToolsOpened = false;
public isPlatformHost = Beans.get<boolean>(IS_PLATFORM_HOST);
public focusMonitor: FocusMonitor;

@ViewChild('angular_change_detection_indicator', {static: true})
private _changeDetectionElement: ElementRef<HTMLElement>;

constructor() {
constructor(private _zone: NgZone) {
this.appSymbolicName = Beans.get<string>(APP_IDENTITY);
this.focusMonitor = Beans.get(FocusMonitor);

this.installFocusWithinListener();
this.installRouteActivateListener();
this.installKeystrokeRegisterLogger();
this.installAngularChangeDetectionIndicator();
}

private installRouteActivateListener(): void {
Expand Down Expand Up @@ -70,11 +76,16 @@ export class AppShellComponent implements OnDestroy {
});
}

private installFocusWithinListener(): void {
Beans.get(FocusMonitor).focusWithin$
.pipe(takeUntil(this._destroy$))
.subscribe(isFocusWithin => {
this.isFocusWithin = isFocusWithin;
private installAngularChangeDetectionIndicator(): void {
this._angularChangeDetectionCycle$
.pipe(
tap(() => NgZone.assertNotInAngularZone()),
tap(() => this._changeDetectionElement.nativeElement.classList.add('active')),
debounceTime(500),
takeUntil(this._destroy$),
)
.subscribe(() => {
this._changeDetectionElement.nativeElement.classList.remove('active');
});
}

Expand Down Expand Up @@ -110,6 +121,14 @@ export class AppShellComponent implements OnDestroy {
this.isDevToolsOpened = !this.isDevToolsOpened;
}

/**
* Method invoked on each Angular change detection cycle.
*/
public get onAngularChangeDetectionCycle(): void {
this._zone.runOutsideAngular(() => this._angularChangeDetectionCycle$.next());
return undefined as void;
}

public ngOnDestroy(): void {
this._destroy$.next();
}
Expand Down
Loading