fix(angular): correctly destroy angular application between each render

Avoids a lot of memory leakage 😵
This commit is contained in:
ThibaudAv 2021-02-18 19:54:47 +01:00
parent 3a9ef82903
commit 890cb09c52
4 changed files with 92 additions and 8 deletions

View File

@ -19,7 +19,7 @@ import { RendererService } from './RendererService';
* Bootstrap angular application to generate a web component with angular element * Bootstrap angular application to generate a web component with angular element
*/ */
export class ElementRendererService { export class ElementRendererService {
private platform = RendererService.getInstance().platform; private rendererService = RendererService.getInstance();
/** /**
* Returns a custom element generated by Angular elements * Returns a custom element generated by Angular elements
@ -36,7 +36,8 @@ export class ElementRendererService {
new BehaviorSubject<ICollection>(storyFnAngular.props) new BehaviorSubject<ICollection>(storyFnAngular.props)
); );
return this.platform return this.rendererService
.newPlatformBrowserDynamic()
.bootstrapModule(createElementsModule(ngModule)) .bootstrapModule(createElementsModule(ngModule))
.then((m) => m.instance.ngEl); .then((m) => m.instance.ngEl);
} }

View File

@ -111,5 +111,33 @@ describe('RendererService', () => {
expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe('🍺'); expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe('🍺');
}); });
}); });
it('should properly destroy angular platform between each render', async () => {
let countDestroy = 0;
await rendererService.render({
storyFnAngular: {
template: '🦊',
props: {},
},
forced: false,
parameters: {} as any,
});
rendererService.platform.onDestroy(() => {
countDestroy += 1;
});
await rendererService.render({
storyFnAngular: {
template: '🐻',
props: {},
},
forced: false,
parameters: {} as any,
});
expect(countDestroy).toEqual(1);
});
}); });
}); });

View File

@ -35,15 +35,13 @@ export class RendererService {
constructor() { constructor() {
if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') { if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') {
try { try {
// platform should be set after enableProdMode()
enableProdMode(); enableProdMode();
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.debug(e); console.debug(e);
} }
} }
// platform should be set after enableProdMode()
this.platform = platformBrowserDynamic();
this.initAngularBootstrapElement();
} }
/** /**
@ -76,15 +74,32 @@ export class RendererService {
} }
this.storyProps$ = new BehaviorSubject<ICollection>(storyFnAngular.props); this.storyProps$ = new BehaviorSubject<ICollection>(storyFnAngular.props);
this.initAngularBootstrapElement(); await this.newPlatformBrowserDynamic().bootstrapModule(
await this.platform.bootstrapModule(
createStorybookModule( createStorybookModule(
getStorybookModuleMetadata({ storyFnAngular, parameters }, this.storyProps$) getStorybookModuleMetadata({ storyFnAngular, parameters }, this.storyProps$)
) )
); );
} }
private initAngularBootstrapElement() { public newPlatformBrowserDynamic() {
// Before creating a new platform, we destroy the previous one cleanly.
this.destroyPlatformBrowserDynamic();
this.initAngularRootElement();
this.platform = platformBrowserDynamic();
return this.platform;
}
public destroyPlatformBrowserDynamic() {
if (this.platform && !this.platform.destroyed) {
// Destroys the current Angular platform and all Angular applications on the page.
// So call each angular ngOnDestroy and avoid memory leaks
this.platform.destroy();
}
}
private initAngularRootElement() {
// Adds DOM element that angular will use as bootstrap component // Adds DOM element that angular will use as bootstrap component
const storybookWrapperElement = document.createElement( const storybookWrapperElement = document.createElement(
RendererService.SELECTOR_STORYBOOK_WRAPPER RendererService.SELECTOR_STORYBOOK_WRAPPER

View File

@ -0,0 +1,40 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Story, Meta } from '@storybook/angular';
@Component({
selector: 'on-destroy',
template: `Current time: {{ time }} <br />
📝 The current time in console should no longer display after a change of stroy`,
})
class OnDestroyComponent implements OnInit, OnDestroy {
time: string;
interval: any;
ngOnInit(): void {
const myTimer = () => {
const d = new Date();
this.time = d.toLocaleTimeString();
console.info(`Current time: ${this.time}`);
};
myTimer();
this.interval = setInterval(myTimer, 3000);
}
ngOnDestroy(): void {
clearInterval(this.interval);
}
}
export default {
title: 'Basics / Component / with ngOnDestroy',
component: OnDestroyComponent,
parameters: {
storyshots: { disable: true }, // disabled due to new Date()
},
} as Meta;
export const SimpleComponent: Story = () => ({
component: OnDestroyComponent,
});