diff --git a/app/angular/element-renderer.d.ts b/app/angular/element-renderer.d.ts deleted file mode 100644 index 83074120d822..000000000000 --- a/app/angular/element-renderer.d.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dist/ts3.9/element-renderer.d'; diff --git a/app/angular/element-renderer.js b/app/angular/element-renderer.js deleted file mode 100644 index 9c6e428015d8..000000000000 --- a/app/angular/element-renderer.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./dist/ts3.9/element-renderer'); diff --git a/app/angular/src/client/preview/angular-beta/ElementRendererService.ts b/app/angular/src/client/preview/angular-beta/ElementRendererService.ts deleted file mode 100644 index 0c85550efc6f..000000000000 --- a/app/angular/src/client/preview/angular-beta/ElementRendererService.ts +++ /dev/null @@ -1,65 +0,0 @@ -// Should be added first : -// Custom Elements polyfill. Required for browsers that do not natively support Custom Elements. -import '@webcomponents/custom-elements'; -// Custom Elements ES5 shim. Required when using ES5 bundles on browsers that natively support -// Custom Elements (either because the browser does not support ES2015 modules or because the app -// is explicitly configured to generate ES5 only bundles). -import '@webcomponents/custom-elements/src/native-shim'; - -import { Injector, NgModule, Type } from '@angular/core'; -import { createCustomElement, NgElementConstructor } from '@angular/elements'; - -import { BehaviorSubject } from 'rxjs'; -import { ICollection, StoryFnAngularReturnType } from '../types'; -import { Parameters } from '../types-6-0'; -import { getStorybookModuleMetadata } from './StorybookModule'; -import { RendererService } from './RendererService'; - -/** - * Bootstrap angular application to generate a web component with angular element - */ -export class ElementRendererService { - private rendererService = RendererService.getInstance(); - - /** - * Returns a custom element generated by Angular elements - */ - public async renderAngularElement({ - storyFnAngular, - parameters, - component, - }: { - storyFnAngular: StoryFnAngularReturnType; - parameters: Parameters; - component: any; - }): Promise { - const ngModule = getStorybookModuleMetadata( - { storyFnAngular, component, targetSelector: RendererService.SELECTOR_STORYBOOK_WRAPPER }, - new BehaviorSubject(storyFnAngular.props) - ); - - return this.rendererService - .newPlatformBrowserDynamic() - .bootstrapModule( - createElementsModule(ngModule), - parameters.bootstrapModuleOptions ?? undefined - ) - .then((m) => m.instance.ngEl); - } -} - -const createElementsModule = (ngModule: NgModule): Type<{ ngEl: CustomElementConstructor }> => { - @NgModule({ ...ngModule }) - class ElementsModule { - public ngEl: NgElementConstructor; - - constructor(private injector: Injector) { - this.ngEl = createCustomElement(ngModule.bootstrap[0] as Type, { - injector: this.injector, - }); - } - - ngDoBootstrap() {} - } - return ElementsModule; -}; diff --git a/app/angular/src/client/preview/angular-beta/RendererFactory.test.ts b/app/angular/src/client/preview/angular-beta/RendererFactory.test.ts index 01c53b86cb1b..237e9b2c9b92 100644 --- a/app/angular/src/client/preview/angular-beta/RendererFactory.test.ts +++ b/app/angular/src/client/preview/angular-beta/RendererFactory.test.ts @@ -73,6 +73,31 @@ describe('RendererFactory', () => { ); }); + it('should handle circular reference in moduleMetadata', async () => { + class Thing { + token: Thing; + + constructor() { + this.token = this; + } + } + const token = new Thing(); + + const render = await rendererFactory.getRendererInstance('my-story', rootTargetDOMNode); + await render.render({ + storyFnAngular: { + template: '🦊', + props: {}, + moduleMetadata: { providers: [{ provide: 'foo', useValue: token }] }, + }, + forced: false, + parameters: {}, + targetDOMNode: rootTargetDOMNode, + }); + + expect(document.body.getElementsByTagName('my-story')[0].innerHTML).toBe('🦊'); + }); + describe('when forced=true', () => { beforeEach(async () => { // Init first render diff --git a/app/angular/src/client/preview/angular-beta/RendererService.test.ts b/app/angular/src/client/preview/angular-beta/RendererService.test.ts deleted file mode 100644 index a878d7a7db6e..000000000000 --- a/app/angular/src/client/preview/angular-beta/RendererService.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { Component, ɵresetJitOptions } from '@angular/core'; -import { platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { Parameters } from '../types-6-0'; -import { RendererService } from './RendererService'; - -jest.mock('@angular/platform-browser-dynamic'); - -declare const document: Document; -describe('RendererService', () => { - let rendererService: RendererService; - - beforeEach(async () => { - document.body.innerHTML = '
'; - (platformBrowserDynamic as any).mockImplementation(platformBrowserDynamicTesting); - rendererService = new RendererService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - - // Necessary to avoid this error "Provided value for `preserveWhitespaces` can not be changed once it has been set." : - // Source: https://github.com/angular/angular/commit/e342ffd855ffeb8af7067b42307ffa320d82177e#diff-92b125e532cc22977b46a91f068d6d7ea81fd61b772842a4a0212f1cfd875be6R28 - ɵresetJitOptions(); - }); - - it('should initialize', () => { - expect(rendererService).toBeDefined(); - }); - - describe('render', () => { - it('should add storybook-wrapper for story template', async () => { - await rendererService.render({ - storyFnAngular: { - template: '🦊', - props: {}, - }, - forced: false, - parameters: {}, - }); - - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe('🦊'); - }); - - it('should add storybook-wrapper for story component', async () => { - @Component({ selector: 'foo', template: '🦊' }) - class FooComponent {} - - await rendererService.render({ - storyFnAngular: { - props: {}, - }, - forced: false, - component: FooComponent, - parameters: {}, - }); - - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe( - '🦊' - ); - }); - - it('should handle circular reference in moduleMetadata', async () => { - class Thing { - token: Thing; - - constructor() { - this.token = this; - } - } - const token = new Thing(); - - await rendererService.render({ - storyFnAngular: { - template: '🦊', - props: {}, - moduleMetadata: { providers: [{ provide: 'foo', useValue: token }] }, - }, - forced: false, - parameters: {}, - }); - - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe('🦊'); - }); - - describe('when forced=true', () => { - beforeEach(async () => { - // Init first render - await rendererService.render({ - storyFnAngular: { - template: '{{ logo }}: {{ name }}', - props: { - logo: '🦊', - name: 'Fox', - }, - }, - forced: true, - parameters: {}, - }); - }); - - it('should be rendered a first time', async () => { - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe( - '🦊: Fox' - ); - }); - - it('should not be re-rendered when only props change', async () => { - let countDestroy = 0; - - rendererService.platform.onDestroy(() => { - countDestroy += 1; - }); - // only props change - await rendererService.render({ - storyFnAngular: { - props: { - logo: '👾', - }, - }, - forced: true, - parameters: {}, - }); - expect(countDestroy).toEqual(0); - - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe( - '👾: Fox' - ); - }); - - it('should be re-rendered when template change', async () => { - await rendererService.render({ - storyFnAngular: { - template: '{{ beer }}', - props: { - beer: '🍺', - }, - }, - forced: true, - parameters: {}, - }); - - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe('🍺'); - }); - - it('should be re-rendered when moduleMetadata structure change', async () => { - let countDestroy = 0; - - rendererService.platform.onDestroy(() => { - countDestroy += 1; - }); - - // Only props change -> no full rendering - await rendererService.render({ - storyFnAngular: { - template: '{{ logo }}: {{ name }}', - props: { - logo: '🍺', - name: 'Beer', - }, - }, - forced: true, - parameters: {}, - }); - expect(countDestroy).toEqual(0); - - // Change in the module structure -> full rendering - await rendererService.render({ - storyFnAngular: { - template: '{{ logo }}: {{ name }}', - props: { - logo: '🍺', - name: 'Beer', - }, - moduleMetadata: { providers: [{ provide: 'foo', useValue: 42 }] }, - }, - forced: true, - parameters: {}, - }); - expect(countDestroy).toEqual(1); - }); - }); - - it('should properly destroy angular platform between each render', async () => { - let countDestroy = 0; - - await rendererService.render({ - storyFnAngular: { - template: '🦊', - props: {}, - }, - forced: false, - parameters: {}, - }); - - rendererService.platform.onDestroy(() => { - countDestroy += 1; - }); - - await rendererService.render({ - storyFnAngular: { - template: '🐻', - props: {}, - }, - forced: false, - parameters: {}, - }); - - expect(countDestroy).toEqual(1); - }); - - describe('bootstrap module options', () => { - async function setupComponentWithWhitespace(bootstrapModuleOptions: unknown) { - await rendererService.render({ - storyFnAngular: { - template: '
', - props: {}, - }, - forced: false, - parameters: { - bootstrapModuleOptions, - } as Parameters, - }); - } - - it('should preserve whitespaces', async () => { - await setupComponentWithWhitespace({ preserveWhitespaces: true }); - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe( - '
' - ); - }); - - it('should remove whitespaces', async () => { - await setupComponentWithWhitespace({ preserveWhitespaces: false }); - expect(document.body.getElementsByTagName('storybook-wrapper')[0].innerHTML).toBe( - '
' - ); - }); - }); - }); -}); diff --git a/app/angular/src/client/preview/angular-beta/RendererService.ts b/app/angular/src/client/preview/angular-beta/RendererService.ts deleted file mode 100644 index f0e451cdccad..000000000000 --- a/app/angular/src/client/preview/angular-beta/RendererService.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable no-undef */ -import { enableProdMode, NgModule, PlatformRef } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { BehaviorSubject, Subject } from 'rxjs'; -import { stringify } from 'telejson'; -import { ICollection, StoryFnAngularReturnType } from '../types'; -import { Parameters } from '../types-6-0'; -import { createStorybookModule, getStorybookModuleMetadata } from './StorybookModule'; - -/** - * Bootstrap angular application and allows to change the rendering dynamically - * To be used as a singleton so has to set global properties of render function - */ -export class RendererService { - private static instance: RendererService; - - public static SELECTOR_STORYBOOK_WRAPPER = 'storybook-wrapper'; - - public static getInstance() { - if (!RendererService.instance) { - RendererService.instance = new RendererService(); - } - return RendererService.instance; - } - - public platform: PlatformRef; - - private staticRoot = document.getElementById('root'); - - // Observable to change the properties dynamically without reloading angular module&component - private storyProps$: Subject; - - private currentStoryRender: { - storyFnAngular: StoryFnAngularReturnType; - moduleMetadataSnapshot: string; - }; - - constructor() { - if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') { - try { - // platform should be set after enableProdMode() - enableProdMode(); - } catch (e) { - // eslint-disable-next-line no-console - console.debug(e); - } - } - } - - /** - * Bootstrap main angular module with main component or send only new `props` with storyProps$ - * - * @param storyFnAngular {StoryFnAngularReturnType} - * @param forced {boolean} If : - * - true render will only use the StoryFn `props' in storyProps observable that will update sotry's component/template properties. Improves performance without reloading the whole module&component if props changes - * - false fully recharges or initializes angular module & component - * @param component {Parameters} - */ - public async render({ - storyFnAngular, - forced, - component, - parameters, - }: { - storyFnAngular: StoryFnAngularReturnType; - forced: boolean; - component?: any; - parameters: Parameters; - }) { - const storyProps$ = new BehaviorSubject(storyFnAngular.props); - const moduleMetadata = getStorybookModuleMetadata( - { storyFnAngular, component, targetSelector: RendererService.SELECTOR_STORYBOOK_WRAPPER }, - storyProps$ - ); - - if ( - !this.fullRendererRequired({ - storyFnAngular, - moduleMetadata, - forced, - }) - ) { - this.storyProps$.next(storyFnAngular.props); - - return; - } - - try { - // Clear global Angular component cache in order to be able to re-render the same component across multiple stories - // - // References: - // https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/build_angular/src/webpack/plugins/hmr/hmr-accept.ts#L50 - // https://github.com/angular/angular/blob/2ebe2bcb2fe19bf672316b05f15241fd7fd40803/packages/core/src/render3/jit/module.ts#L377-L384 - // eslint-disable-next-line global-require - const resetCompiledComponents = require('@angular/core').ɵresetCompiledComponents; - resetCompiledComponents(); - } catch (e) { - /** - * noop catch - * This means angular removed or modified ɵresetCompiledComponents - * - * Probably, they added a clearCache mechanism to platform.destroy() and - * we can simply remove this in case no errors are thrown during runtime - */ - } - - // Complete last BehaviorSubject and set a new one for the current module - if (this.storyProps$) { - this.storyProps$.complete(); - } - this.storyProps$ = storyProps$; - - await this.newPlatformBrowserDynamic().bootstrapModule( - createStorybookModule(moduleMetadata), - parameters.bootstrapModuleOptions ?? undefined - ); - } - - 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 - const storybookWrapperElement = document.createElement( - RendererService.SELECTOR_STORYBOOK_WRAPPER - ); - this.staticRoot.innerHTML = ''; - this.staticRoot.appendChild(storybookWrapperElement); - } - - private fullRendererRequired({ - storyFnAngular, - moduleMetadata, - forced, - }: { - storyFnAngular: StoryFnAngularReturnType; - moduleMetadata: NgModule; - forced: boolean; - }) { - const { currentStoryRender: lastStoryRender } = this; - - this.currentStoryRender = { - storyFnAngular, - moduleMetadataSnapshot: stringify(moduleMetadata), - }; - - if ( - // check `forceRender` of story RenderContext - !forced || - // if it's the first rendering and storyProps$ is not init - !this.storyProps$ - ) { - return true; - } - - // force the rendering if the template has changed - const hasChangedTemplate = - !!storyFnAngular?.template && - lastStoryRender?.storyFnAngular?.template !== storyFnAngular.template; - if (hasChangedTemplate) { - return true; - } - - // force the rendering if the metadata structure has changed - const hasChangedModuleMetadata = - this.currentStoryRender?.moduleMetadataSnapshot !== lastStoryRender?.moduleMetadataSnapshot; - return hasChangedModuleMetadata; - } -} diff --git a/app/angular/src/element-renderer.ts b/app/angular/src/element-renderer.ts deleted file mode 100644 index e49b411e80a2..000000000000 --- a/app/angular/src/element-renderer.ts +++ /dev/null @@ -1 +0,0 @@ -export { ElementRendererService } from './client/preview/angular-beta/ElementRendererService'; diff --git a/app/angular/src/renderer.ts b/app/angular/src/renderer.ts index cfc2f826e804..8f2b6a154dca 100644 --- a/app/angular/src/renderer.ts +++ b/app/angular/src/renderer.ts @@ -1,5 +1,4 @@ export { computesTemplateSourceFromComponent } from './client/preview/angular-beta/ComputesTemplateFromComponent'; -export { RendererService } from './client/preview/angular-beta/RendererService'; export { rendererFactory } from './client/preview/render'; export { AbstractRenderer } from './client/preview/angular-beta/AbstractRenderer'; export { getStorybookModuleMetadata } from './client/preview/angular-beta/StorybookModule';