Skip to content

Instantly share code, notes, and snippets.

@thiagoelg
Last active February 3, 2022 20:18
Show Gist options
  • Save thiagoelg/20b408720d153262d1ab6d503c967b85 to your computer and use it in GitHub Desktop.
Save thiagoelg/20b408720d153262d1ab6d503c967b85 to your computer and use it in GitHub Desktop.
Kogito Tooling Examples - Implementing the Ping Pong View in Angular
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"cli": {
"packageManager": "yarn"
},
"projects": {
"ping-pong-view-angular": {
...
},
"ping-pong-view-wc": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/wc",
"index": "src/index.html",
"main": "src/app/web-component/web-component.main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.wc.json",
"aot": true,
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.css"],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
}
}
}
}
}
},
"defaultProject": "ping-pong-view-angular"
}
import { Injectable } from "@angular/core";
import { MessageBusClientApi } from "@kie-tools-core/envelope-bus/dist/api";
import { PingPongChannelApi, PingPongInitArgs } from "@kie-tools-examples/ping-pong-view/dist/api";
import { PingPongFactory } from "@kie-tools-examples/ping-pong-view/dist/envelope";
import { ReplaySubject, BehaviorSubject, Subject } from "rxjs";
declare global {
interface Window {
initArgs: PingPongInitArgs;
channelApi: PingPongChannelApi;
}
}
export interface LogEntry {
line: string;
time: number;
}
function getCurrentTime() {
return Date.now();
}
@Injectable({
providedIn: 'root',
})
export class PingPongApiService implements PingPongFactory {
channelApi: MessageBusClientApi<PingPongChannelApi>;
initArgs: PingPongInitArgs;
log = new ReplaySubject<LogEntry>(10);
logCleared = new Subject();
lastPingTimestamp = new BehaviorSubject<number>(0);
dotInterval?: number;
initialized = false;
pingSubscription?: (source: string) => void;
pongSubscription?: (source: string, replyingTo: string) => void;
constructor() {}
create(initArgs: PingPongInitArgs, channelApi: MessageBusClientApi<PingPongChannelApi>) {
// Making sure we don't subscribe more than once.
this.clearSubscriptions();
this.clearInterval();
this.initArgs = initArgs;
this.channelApi = channelApi;
// Subscribe to ping notifications.
this.pingSubscription = this.channelApi.notifications.pingPongView__ping.subscribe((pingSource) => {
// If this instance sent the PING, we ignore it.
if (pingSource === this.initArgs.name) {
return;
}
// Add a new line to our log, stating that we received a ping.
this.log.next({ line: `PING from '${pingSource}'.`, time: getCurrentTime() });
// Acknowledges the PING message by sending back a PONG message.
this.channelApi.notifications.pingPongView__pong.send(this.initArgs.name, pingSource);
});
// Subscribe to pong notifications.
this.pongSubscription = this.channelApi.notifications.pingPongView__pong.subscribe(
(pongSource: string, replyingTo: string) => {
// If this instance sent the PONG, or if this PONG was not meant to this instance, we ignore it.
if (pongSource === this.initArgs.name || replyingTo !== this.initArgs.name) {
return;
}
// Updates the log to show a feedback that a PONG message was observed.
this.log.next({ line: `PONG from '${pongSource}'.`, time: getCurrentTime() });
}
);
// Populate the log with a dot each 2 seconds.
this.dotInterval = window.setInterval(() => {
this.log.next({ line: ".", time: getCurrentTime() });
}, 2000);
this.initialized = true;
return () => ({
clearLogs: () => {
this.log = new ReplaySubject<LogEntry>(10);
// Emit a value to logCleared so we can re-subscribe to this.log wherever needed.
this.logCleared.next(null);
},
getLastPingTimestamp: () => {
return Promise.resolve(this.lastPingTimestamp.value);
},
});
}
// Send a ping to the channel.
ping() {
this.channelApi.notifications.pingPongView__ping.send(this.initArgs.name);
this.lastPingTimestamp.next(getCurrentTime());
}
clearSubscriptions() {
this.pingSubscription && this.channelApi.notifications.pingPongView__ping.unsubscribe(this.pingSubscription);
this.pongSubscription && this.channelApi.notifications.pingPongView__pong.unsubscribe(this.pongSubscription);
}
clearInterval() {
window.clearInterval(this.dotInterval);
}
ngOnDestroy() {
this.clearSubscriptions();
this.clearInterval();
}
}
<div class="ping-pong-view--main">
<h2>This is an implementation of Ping-Pong View in Angular</h2>
<p class="ping-pong-view--p-iframe">
The envelope boundary border is green. It can be an iFrame or a Div. (It's possible to use Div if using web
components made from Angular components)
</p>
<p class="ping-pong-view--p-ping-pong">The Ping-Pong View implementation border is red</p>
<div class="ping-pong-view--container">
<i>#{{ pingPongApiService.initArgs?.name }}</i>
<div class="ping-pong-view--header">
<span>Hello from Angular!</span>
<button (click)="pingPongApiService.ping()">Ping others!</button>
</div>
<div class="ping-pong-view--log">
<p *ngFor="let entry of log | async" class="ping-pong-view--line">
{{ entry.line }}
</p>
</div>
</div>
</div>
import { PingPongApiService, LogEntry } from "./ping-pong-api.service";
import { Component, Input, OnInit } from "@angular/core";
import * as PingPongViewEnvelope from "@kie-tools-examples/ping-pong-view/dist/envelope";
import { ContainerType } from "@kie-tools-core/envelope/dist/api";
import { Observable, scan } from "rxjs";
@Component({
selector: "app-ping-pong",
templateUrl: "./ping-pong.component.html",
styleUrls: ["./ping-pong.component.css"],
providers: [],
})
export class PingPongComponent implements OnInit {
@Input() containerType: ContainerType = ContainerType.IFRAME;
@Input() envelopeId?: string;
constructor(public pingPongApiService: PingPongApiService) {}
log: Observable<LogEntry[]>;
subscribeToLogUpdates() {
this.log = this.pingPongApiService.log.asObservable().pipe(scan((acc, curr) => [...acc.slice(-9), curr], []));
}
ngOnInit() {
// Initialize log with a starting message.
this.pingPongApiService.log.next({ line: "Logs will show up here", time: 0 });
// Initialize envelope with the container config, the bus,
// and factory (in this case, a service that implements the "create" method).
PingPongViewEnvelope.init({
config: { containerType: this.containerType, envelopeId: this.envelopeId! },
bus: { postMessage: (message, _targetOrigin, transfer) => window.parent.postMessage(message, "*", transfer) },
pingPongViewFactory: this.pingPongApiService,
});
// Create an observable variable with the 10 latest values of the log.
this.subscribeToLogUpdates();
this.pingPongApiService.logCleared.subscribe(() => this.subscribeToLogUpdates());
}
}
import { PingPongApiService } from "./ping-pong-api.service";
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { PingPongComponent } from "./ping-pong.component";
@NgModule({
declarations: [PingPongComponent],
imports: [BrowserModule],
exports: [PingPongComponent],
providers: [PingPongApiService],
bootstrap: [PingPongComponent],
})
export class PingPongModule {}
/**
* The API of a PingPongViewApi.
*
* These methods are what the "external world" knows about this component.
*/
export interface PingPongApi {
clearLogs(): void;
getLastPingTimestamp(): Promise<number>;
}
/**
* Methods provided by the Envelope that can be consumed by the Channel.
*/
export interface PingPongEnvelopeApi {
pingPongView__init(association: Association, initArgs: PingPongInitArgs): Promise<void>;
pingPongView__clearLogs(): Promise<void>;
pingPongView__getLastPingTimestamp(): Promise<number>;
}
export class PingPongEnvelopeApiImpl implements PingPongEnvelopeApi {
constructor(
private readonly args: EnvelopeApiFactoryArgs<PingPongEnvelopeApi, PingPongChannelApi, void, {}>,
private readonly pingPongViewFactory: PingPongFactory
) {}
pingPongApi?: () => PingPongApi | null;
public async pingPongView__init(association: Association, initArgs: PingPongInitArgs) {
this.args.envelopeClient.associate(association.origin, association.envelopeServerId);
this.pingPongApi = this.pingPongViewFactory.create(initArgs, this.args.envelopeClient.manager.clientApi);
}
public async pingPongView__clearLogs() {
this.pingPongApi?.()?.clearLogs();
}
public async pingPongView__getLastPingTimestamp() {
const api = this.pingPongApi?.();
if (!api) return Promise.resolve(0);
return api.getLastPingTimestamp();
}
}
export class PingPongApiService implements PingPongFactory {
...
create(initArgs: PingPongInitArgs, channelApi: MessageBusClientApi<PingPongChannelApi>) {
...
return () => {
...
} as PingPongApi;
}
pingPongApiService = new PingPongApiService();
// Initialize envelope with the container config, the bus,
// and factory (in this case, a service that implements the "create" method).
// This should be called after the view is rendered,
// inside a `ngOnInit` or `useEffect` for example.
PingPongViewEnvelope.init({
config: { containerType: this.containerType, envelopeId: this.envelopeId! },
bus: { postMessage: (message, _targetOrigin, transfer) => window.parent.postMessage(message, "*", transfer) },
pingPongViewFactory: this.pingPongApiService,
});
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./dist/wc",
"types": []
},
"files": ["src/app/web-component/web-component.main.ts", "src/polyfills.ts"],
"include": ["src/app/web-component/*.d.ts"]
}
import { Component, Input } from "@angular/core";
import { ContainerType } from "@kie-tools-core/envelope/dist/api";
@Component({
selector: "ping-pong-wc",
template: `<app-ping-pong [containerType]="containerType" [envelopeId]="envelopeId"></app-ping-pong>`,
})
export class PingPongWcComponent {
@Input("containertype") containerType: ContainerType;
@Input("envelopeid") envelopeId: string;
}
import { WebComponentModule } from "./web-component.module";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
const bootstrap = () => platformBrowserDynamic().bootstrapModule(WebComponentModule);
bootstrap().catch((err) => console.error(err));
import { NgModule, Injector, DoBootstrap } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { createCustomElement } from "@angular/elements";
import { PingPongModule } from "../ping-pong/ping-pong.module";
import { PingPongWcComponent } from "./web-component.component";
@NgModule({
declarations: [PingPongWcComponent],
imports: [BrowserModule, PingPongModule],
entryComponents: [PingPongWcComponent],
providers: [],
})
export class WebComponentModule implements DoBootstrap {
constructor(private injector: Injector) {}
ngDoBootstrap() {
const element = createCustomElement(PingPongWcComponent, { injector: this.injector });
customElements.define("ping-pong-angular", element);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment