I was creating this stackblitz to get help with a different problem and ran into the problem I'm having now in the process. I'm using a BroadcastChannel
to send data to a component embedded inside of an <iframe>
with ViewContanierRef.createComponenent()
. Everything works as I expect it to however my *ngFor
condition isn't iterating the data.
If you open the stackblitz you'll see a show data
button inside the <iframe>
. I made that to console.log()
the data but clicking on it somehow makes it iterate in the *ngFor
. Beneath the <iframe>
you'll see the ControllerComponent
with a resend
button. The ControllerComponent
will ultimately be an interface for interacting with the component embedded into the <iframe>
through the BroadcastChannel
. Of course we have to consider the lifecycle hooks in doing things like this so the process I created flows as follows.
Pass a name and a
type
name into@Input()
s on both theControllerComponent
and theEmbeddedViewComponent
which will create theBroadcastChannel
s in their service files.In the
AfterViewChecked
hook of theEmbeddedViewComponent
I fire a function in the service file that sends a notification through theBroadcastChannel
to theControllerComponent
so that it knows the component already exists before it attempts to send data.Inside the
.onMessage()
method that receives the notification another function is fired that broadcasts the data to theEmbeddedViewComponent
where it's stored in aBehaviorSubject
which is subscribed to in theOnInit
hook and is what's iterated by the*ngFor
.
I tried adding an | async
pipe to the *ngFor
condition just to see if that made any difference and got an error as I expected. I tried moving things to different lifecycle hooks just for it to do the same thing or give an undefined
error as the component and BroadcastChannel
didn't exist yet.
As far as the mechanism I'm creating, I'm building a component for demonstrating different responsive design examples which I'm using the <iframe>
to do to take advantage of the separate DOM instance which the user can scaleup and down without having to scale the entire browser. Due to the purpose of this component I have to build it in consideration to the fact that it will never know what component it will have to embed or controller it will have to provide for the user. So the way I have it set up to work goes as follows.
ResponsiveFrameComponent.component.ts
@Component({
selector : 'responsive-frame',
templateUrl : './responsive-frame.component.html',
styleUrls : ['./responsive-frame.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResponsiveFrameComponent implements OnInit, AfterContentChecked {
// The component that will be embedded into the iframe.
@Input() ComponentToEmbed : Type<Component>;
// The name I want to use for the BroadcastChannel
@Input() DataChannelName : string = '';
// The name of the Type I want the .onMessage() method to check for
@Input() DataChannelType : string = '';
// The iframe the component gets embedded into
@ViewChild('responsiveWindow', {static : true} ) ResponsiveWindow : ElementRef;
constructor( private vcRef: ViewContainerRef, private cdr: ChangeDetectorRef ){}
ngOnInit():void {
this.embedComponent();
}
ngAfterContentChecked(): void {
this.cdr.detectChanges();
}
public embedComponent(): void {
// Targets the iframe
const frame = this.ResponsiveWindow?.nativeElement.contentDocument || this.ResponsiveWindow?.nativeElement.contentWindow;
// The instance of the component to be embedded
const componentInstance : ComponentRef<any> = this.vcRef.createComponent( this.ComponentToEmbed );
// Checks to see if the component being embedded has an @Input() named DataChannelName
if(
this.DataChannelName !== '' &&
(<ComponentRef<any>>componentInstance.instance).hasOwnProperty('DataChannelName')
){
// If the component has a DataChannelName @Input() then pass the channel name and
// type into the component instance.
componentInstance.instance.DataChannelName = this.DataChannelName;
componentInstance.instance.DataChannelType = this.DataChannelType;
}
// css stuff
const domStyles = document.createElement( 'style' );
domStyles.innerText = "* { padding : 0; margin : 0; box-sizing: border-box; overflow: hidden; } body { display : grid; place-items: center; min-height: 0px; max-height: 100vh; grid-template-columns: 1fr; grid-template-rows: 1fr; } ";
frame.head.appendChild( domStyles );
// Embeds component into iframe
frame.body.appendChild( componentInstance.location.nativeElement );
}
}
ResponsiveFrameComponent.component.html
<article class="iFrameShell">
<iframe #responsiveWindow class="iFrame"></iframe>
</article>
<!-- I use content projection to insert a controller component -->
<ng-content select="[controlFrame]"></ng-content>
and where I want to use the ResponsiveFrameComponent
I just do this
<!-- The EmbedElement is a variable where I store the component to embed -->
<responsive-frame
[ComponentToEmbed]="EmbedElement"
[DataChannelName]="'dataChannelA'"
[DataChannelType]="'propA'"
[FrameWidth]="400"
[FrameHeight]="400"
>
<!-- the DataObjectA is a variable that stores the data I want the controller to use -->
<controller-component
controlFrame
class="controlFrame"
[DataChannelName]="'dataChannelA'"
[DataChannelType]="'propA'"
[Value]="DataObjectA"
></controller-component>
</responsive-frame>
As I mentioned before the embedded component will be what sends a notification to the controller which is set up like this
EmbeddedViewComponent
@Component({
selector: 'embedded-view',
templateUrl: './embedded-view.component.html',
styleUrls: ['./embedded-view.component.css'],
providers: [EmbeddedViewService],
encapsulation: ViewEncapsulation.ShadowDom,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmbeddedViewComponent implements OnInit, AfterContentChecked {
@Input() DataChannelName : string = '';
@Input() DataChannelType : string = '';
// For indicating if the component is loaded or not
Loaded: boolean = false;
Data: string[] = [];
constructor(private service: EmbeddedViewService, private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
// creates the BroadcastChannel
this.service.initDataChannel(this.DataChannelName, this.DataChannelType);
// subscribes to the BehaviorSubject
this.service.getData().subscribe(a => this.Data = a);
}
ngAfterContentChecked(): void{
if(!this.Loaded){
this.Loaded = true;
// Sends the notification to the controller
this.service.sendNotification();
}
this.cdr.detectChanges();
}
public showData(){console.log(this.Data);}
}
EmbeddedViewComponent.service.ts
export class EmbeddedViewService{
// Stores data from controller
Data: BehaviorSubject<string[]> = new BehaviorSubject([]);
// returns Data as an Observable for subscription
DataObserver: Observable<string[]> = this.Data.asObservable();
// The Broadcast channel that will be created
DataChannel!: BroadcastChannel;
DataChannelName!: string;
DataChannelType!: string;
constructor(){}
// Fired in the OnInit hook to set everything up for communicating with the controller
public initDataChannel(channel: string, type: string):void {
// Creates the BroadcastChannel
this.DataChannel = new BroadcastChannel(channel);
// Stores channel name and type locally to be checked against later
this.DataChannelName = channel;
this.DataChannelType = type;
// creates onMessage event for handling data from the controller
this.listenForMessage();
}
// Updates the BehaviorSubject
private setData(value: string[]): void{ this.Data.next(value); }
// Returns the Observable of the BehaviorSubject
public getData(): Observable<string[]>{ return this.DataObserver; }
// sends a message through the BroadcastChannel to indicate
public sendNotification(): void{
this.DataChannel.postMessage({
type: 'verification',
data: 'complete'
});
}
public listenForMessage(): void{
this.DataChannel.onmessage = (ev) =>{
if(ev.data.type === this.DataChannelType){
this.setData(ev.data.data);
}
};
}
}
And the template looks like this
EmbeddedViewComponent.component.html
<article *ngFor="let i of Data">
<p>{{i}}</p>
</article>
<p>embed component</p>
<button type="button" (click)="showData()">show data</button>
The ControllerComponent
is pretty much the same type of setup which looks like this
ControllerComponent.component
export class ControllerComponent implements OnInit, AfterViewInit {
// The name of the broadcast channel to communicate through
@Input() DataChannelName : string = '';
// The type to use on the channel which will be explained further
@Input() DataChannelType : string = '';
// The data I want to use and cast to the channel
@Input() Value : string[] = [];
constructor(private service: ControllerService) {}
ngOnInit(): void {
// Sets up the BroadcastChannel in the service file and stores the type to a variable
this.service.setChannel(this.DataChannelName, this.DataChannelType);
// Sends data to service file to have available to send to the embedded component
this.service.setProperty(this.Value);
}
ngAfterViewInit(): void {}
public resendData():void{
this.service.sendData();
}
}
ControllerComponent.service
export class ControllerService {
// Same as in the EmbeddedViewComponent
DataChannel!: BroadcastChannel;
DataChannelName!: string;
DataChannelType!: string;
// The data set in the OnInit hook
DataToSend: string[] = [];
// Sets up the BroadcastChannel
public setChannel(channel: string, type: string): void {
this.DataChannel = new BroadcastChannel(channel);
this.DataChannelName = channel;
this.DataChannelType = type;
// Creates onMessage event to confirm when the EmbeddedViewComponent is loaded
this.listenForVerification();
}
// Stores Data sent in OnInit hook of the component
public setProperty(property: string[]): void {
this.DataToSend = property;
}
public listenForVerification(): void {
this.DataChannel.onmessage = (ev) => {
if (ev.data.type === 'verification' && ev.data.data === 'complete') {
// Sends the data to the EmbeddedViewComponent
this.sendData();
}
};
}
public sendData(): void {
this.DataChannel.postMessage({
type: this.DataChannelType,
data: this.DataToSend,
});
}
}
As I stated before, everything works as expected up to the point of the data being iterated with the *ngFor
. Does anybody see what causes this is happening?
As I was looking through my notebook at some other uses of
BroadcastChannel
I came across a function I made that usedNgZone
which reminded me that I needed to do that so Angular can know the data update is happening. So I refactored the.onMessage()
method like thisThat made it work as expected as you can see in this stackblitz.