I have defined various widget type components, and I'm using ng-dynamic-component so that each widget is dynamically associated with the component at runtime. One of these components is a youtube iframe widget component, i.e. all widgets that embed youtube videos will use this component. For now, the HTML of this component is simply
<iframe width={{iframeWidth}} height={{iframeHeight}} [src]="getUrl()" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
The issue I'm having is that whenever any widget is dragged, resized, or even clicked (so simply when a widget gains focus), all of the iframe widgets refresh their iframe. You can see an example here. Note that this does not happen when I click anywhere else, such as on the background grid. I imagine all widget content is being refreshed, not just the iframes, but since iframes take longer to load it's only noticeable for them.
I have a feeling it's something related to ng-dynamic-component and the Angular lifecycle, but I'm new to Angular so it might be something simple I'm not setting up correctly. I'll share the relevant pieces of code in case that might help.
I use a similar dashboard array as the Home example in dashboard-component.ts:
this.dashboard = [
{cols: 2, rows: 1, y: 0, x: 0, widgetType: 'iframe', widgetData: {src: "https://www.youtube.com/embed/H-WEhug-up8", iframeWidth: 240, iframeHeight: 160}},
{cols: 2, rows: 2, y: 0, x: 2, widgetType: 'gettingStarted'},
{cols: 1, rows: 1, y: 0, x: 4, widgetType: 'numericQuery'},
{cols: 1, rows: 1, y: 2, x: 5, widgetType: 'favorites'},
{cols: 1, rows: 1, y: 1, x: 0, widgetType: 'numericQuery'},
{cols: 1, rows: 1, y: 1, x: 0, widgetType: 'iframe', widgetData: {src: "https://www.youtube.com/embed/IDaqFiLvcB0", iframeWidth: 640, iframeHeight: 360}},
{cols: 2, rows: 2, y: 3, x: 5, widgetType: 'gettingStarted'},
{cols: 2, rows: 2, y: 2, x: 0, widgetType: 'numericQuery'},
{cols: 2, rows: 1, y: 2, x: 2, widgetType: 'favorites'},
{cols: 1, rows: 1, y: 2, x: 4, widgetType: 'numericQuery'},
{cols: 1, rows: 1, y: 2, x: 6, widgetType: 'iframe', widgetData: {src: "https://www.youtube.com/embed/PYOSKYWg-5E", iframeWidth: 340, iframeHeight: 260}}
];
I made a WidgetItemComponent which acts as the ng-dynamic-component facilitator for choosing the correct dynamic component:
@Component({
selector: 'app-widget-item',
template: `<ndc-dynamic [ndcDynamicComponent]="component" [ndcDynamicInputs]="inputs"></ndc-dynamic>`,
styleUrls: ['./widget-item.component.css']
})
export class WidgetItemComponent implements OnInit {
component : any;
inputs = { widgetData: {} };
constructor() { }
ngOnInit() {
this.component = this.getComponent();
if (this.dashboardItem != null) {
this.inputs.widgetData = this.dashboardItem.widgetData;
}
}
@Input() dashboardItem: GridsterItem;
getComponent() {
if (this.dashboardItem != null) {
if (this.dashboardItem.widgetType == 'iframe') {
return IframeWidgetComponent;
}
else if (this.dashboardItem.widgetType == 'numericQuery') {
return NumberQueryWidgetComponent;
}
else if (this.dashboardItem.widgetType == 'favorites') {
return FavoritesWidgetComponent;
}
else if (this.dashboardItem.widgetType == 'gettingStarted') {
return GettingStartedWidgetComponent;
}
}
}
}
And finally in iframe-widget-component.ts I have:
@Component({
selector: 'app-iframe-widget',
templateUrl: './iframe-widget.component.html',
styleUrls: ['./iframe-widget.component.css']
})
export class IframeWidgetComponent implements OnInit {
src: string;
iframeWidth: number;
iframeHeight: number;
@Input() widgetData: any;
constructor(public sanitizer: DomSanitizer) { }
ngOnInit() {
if (this.widgetData != null) {
this.src = this.widgetData.src;
this.iframeWidth = this.widgetData.iframeWidth;
this.iframeHeight = this.widgetData.iframeHeight;
}
}
getUrl() {
return this.sanitizer.bypassSecurityTrustResourceUrl(this.src);
}
}
I really appreciate the example and the code you posted. I didn't see anything in your code that would be causing your problem, but I have a strong feeling this is not a problem with angular-gridster2. If you are able to go here: https://stackblitz.com/ and make a minimal reproduction of the issue, please post it and I'll spend time playing around with it to see if I can't find a solution.
Thanks for offering to look at it, I appreciate it. I've never used StackBlitz before so I hope I've done this right. I've created a simpler version of what I have: https://stackblitz.com/edit/angular-htjysy. Please let me know if I'm missing anything and feel free to edit stuff if you need.
I think you're right about it not being an issue with angular-gridster2. I don't think it's even an issue with ng-dynamic-component. I believe my other component types do not refresh. I tested this by having the number widget generate a random number but the number doesn't change while the iframe refreshes. My suspicion is that it's to do with this binding in iframe-widget.component.html: [src]="getUrl()", and perhaps the DomSanitizer module, specifically this function:
getUrl() {
return this.sanitizer.bypassSecurityTrustResourceUrl(this.src);
}
I think I have to use this though if I want to bind the URL that's passed in to the DOM. If you can confirm (or at least strongly suspect) that it's a binding or DomSanitizer issue, or any other non-Angular Gridster 2 issue for that matter, feel free to close this issue. Thank you!
I am 99% sure it's due to [src]="getUrl()". For the random number widget I mentioned above, I changed it so the binding is similar to the iframe binding: <p>{{getNumber()}}</p>, where getNumber() is defined as:
getNumber() {
return this.queryResult * Math.random();
}
Now the numbers change whenever widgets are clicked. Also, if you click and hold the cursor on a widget, the number continuously changes and the iframe continuously refreshes. I suppose this would be obvious knowledge for people familiar with Angular and web programming. I guess I'll have to find a different way to bind the URL to the iframe.
Actually I'd like to reopen the issue because even though it's due to the binding, it doesn't explain why the function is called whenever a widget is selected. I'm wondering if the way gridster-item or another component in angular gridster2 is implemented may be the root cause of this.
I think you've stumbled upon a very interesting bug. I've taken a look at it and I think that this might actually be related to angular gridster2 as I can't reproduce any of this behavior without it. I was able to strip out most of your code and still reproduce the issue here: https://stackblitz.com/edit/angular-dktb63.
I removed all the dynamic component related code as well as all styling, all of your gridster options, and all the logic in the iframe component except the getUrl() function. You can still see the bug by checking the console after/during a screen resize.
I also tested in both Chrome and Edge just to check if it was browser specific, and it seems it isn't. Interestingly, simply having a gridster element in the dom seems to trigger the bug, refreshing elements that are not even inside the grid. This merits more looking into.
Very interesting, thank you for taking time to look into it and finding out more. I haven't looked more at this issue yet but I'll see if I can find anything when I do.
Hello guys! A little late to the party, but here's what I think it's happening. Gridster components are all made with the changeDetectionStrategy set to OnPush if I remember correctly. Which means that they trigger a change detection when their input changes or when an external event affects them (such as a click or a resize). Which means that on every click/resize a change detection will be triggered in the component.
Now, you are trying to get the url by binding it with the getUrl() method. The problem here is that Angular doesn't know what the result of a function changes, therefore, when a change detection is triggered, the function is executed again to get the result. To avoid this kind of behavior, you will have to execute the function at an earlier point (such as in the constructor or ngOnInit hook) and save the result in a property on the class.
// inside the class
public urlResource: any;
constructor () {...}
ngOnInit(): void {
this.url = this.sanitizer.bypassSecurityTrustResourceUrl("https://i.ytimg.com/vi/J9QOB6hSI-c/maxresdefault.jpg");
}
Now, even if the change detection is triggered, Angular knows that the url hasn't changed (since it's an object with a proper reference) and it won't try to rerender the component to which the url is binded again.
I hope this helps. ^.^
Thank you, NoMercy! Great explanation and this fixed the issue for me.
Most helpful comment
Hello guys! A little late to the party, but here's what I think it's happening. Gridster components are all made with the
changeDetectionStrategyset toOnPushif I remember correctly. Which means that they trigger a change detection when their input changes or when an external event affects them (such as a click or a resize). Which means that on every click/resize a change detection will be triggered in the component.Now, you are trying to get the url by binding it with the
getUrl()method. The problem here is that Angular doesn't know what the result of a function changes, therefore, when a change detection is triggered, the function is executed again to get the result. To avoid this kind of behavior, you will have to execute the function at an earlier point (such as in the constructor orngOnInithook) and save the result in a property on the class.Now, even if the change detection is triggered, Angular knows that the url hasn't changed (since it's an object with a proper reference) and it won't try to rerender the component to which the url is binded again.
I hope this helps. ^.^