From 94230a2e3c1f4247ad34ec4f6c21eac2ae53e646 Mon Sep 17 00:00:00 2001 From: ThibaudAv Date: Fri, 15 Jan 2021 19:53:57 +0100 Subject: [PATCH] feat(angular): add inline stories support in the docs addon The configuration has to be set up in each angular project to avoid forcing dependencies to `@angular/elements` ans `@webcomponents/custom-elements` --- addons/docs/README.md | 4 +- addons/docs/angular/README.md | 24 ++++++++ addons/docs/angular/inline.js | 1 + addons/docs/package.json | 5 ++ .../frameworks/angular/prepareForInline.ts | 47 +++++++++++++++ app/angular/element-renderer.d.ts | 1 + app/angular/element-renderer.js | 1 + app/angular/package.json | 10 ++++ .../angular-beta/ElementRendererService.ts | 59 +++++++++++++++++++ .../preview/angular-beta/RendererService.ts | 5 +- app/angular/src/element-renderer.ts | 1 + examples/angular-cli/.storybook/preview.ts | 7 ++- examples/angular-cli/package.json | 2 + .../__snapshots__/core.stories.storyshot | 2 +- yarn.lock | 12 ++++ 15 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 addons/docs/angular/inline.js create mode 100644 addons/docs/src/frameworks/angular/prepareForInline.ts create mode 100644 app/angular/element-renderer.d.ts create mode 100644 app/angular/element-renderer.js create mode 100644 app/angular/src/client/preview/angular-beta/ElementRendererService.ts create mode 100644 app/angular/src/element-renderer.ts diff --git a/addons/docs/README.md b/addons/docs/README.md index bf29ade7155a..2bd4dfe66004 100644 --- a/addons/docs/README.md +++ b/addons/docs/README.md @@ -87,9 +87,9 @@ Storybook Docs supports all view layers that Storybook supports except for React | Source | + | + | + | + | + | + | + | + | + | + | + | | Notes / Info | + | + | + | + | + | + | + | + | + | + | + | | Props table | + | + | + | + | + | | | | | | | -| Props controls | + | + | | | | | | | | | | +| Props controls | + | + | + | | | | | | | | | | Description | + | + | + | + | + | | | | | | | -| Inline stories | + | + | | | + | + | | | | | | +| Inline stories | + | + | + | | + | + | | | | | | **Note:** `#` = WIP support diff --git a/addons/docs/angular/README.md b/addons/docs/angular/README.md index e5ad349a6834..0a62e2399682 100644 --- a/addons/docs/angular/README.md +++ b/addons/docs/angular/README.md @@ -206,6 +206,30 @@ And for `MDX` you can modify it as an attribute on the `Story` element: {...} ``` +## Inline Stories + +Storybook Docs renders all Angular stories inside IFrames by default. But it is possible to use an inline rendering: + +To get this, you'll first need to install Angular elements: + +```sh +yarn add -D @angular/elements @webcomponents/custom-elements +``` + +Then update `.storybook/preview.js`: + +```js +import { addParameters } from '@storybook/angular'; +import { prepareForInline } from '@storybook/addon-docs/angular/inline'; + +addParameters({ + docs: { + inlineStories: true, + prepareForInline, + }, +}); +``` + ## More resources Want to learn more? Here are some more articles on Storybook Docs: diff --git a/addons/docs/angular/inline.js b/addons/docs/angular/inline.js new file mode 100644 index 000000000000..5e94eb81966a --- /dev/null +++ b/addons/docs/angular/inline.js @@ -0,0 +1 @@ +module.exports = require('../dist/esm/frameworks/angular/prepareForInline'); diff --git a/addons/docs/package.json b/addons/docs/package.json index cd237cc5a914..c955ef319398 100644 --- a/addons/docs/package.json +++ b/addons/docs/package.json @@ -92,6 +92,7 @@ "@babel/core": "^7.12.10", "@emotion/core": "^10.1.1", "@emotion/styled": "^10.0.27", + "@storybook/angular": "6.2.0-alpha.18", "@storybook/react": "6.2.0-alpha.18", "@storybook/vue": "6.2.0-alpha.18", "@storybook/web-components": "6.2.0-alpha.18", @@ -123,6 +124,7 @@ }, "peerDependencies": { "@babel/core": "^7.11.5", + "@storybook/angular": "6.2.0-alpha.18", "@storybook/vue": "6.2.0-alpha.18", "babel-loader": "^8.0.0", "react": "^16.8.0 || ^17.0.0", @@ -132,6 +134,9 @@ "webpack": ">=4" }, "peerDependenciesMeta": { + "@storybook/angular": { + "optional": true + }, "@storybook/vue": { "optional": true }, diff --git a/addons/docs/src/frameworks/angular/prepareForInline.ts b/addons/docs/src/frameworks/angular/prepareForInline.ts new file mode 100644 index 000000000000..0f85b8469d54 --- /dev/null +++ b/addons/docs/src/frameworks/angular/prepareForInline.ts @@ -0,0 +1,47 @@ +import React from 'react'; +import { IStory, StoryContext } from '@storybook/angular'; +import { ElementRendererService } from '@storybook/angular/element-renderer'; +import { StoryFn } from '@storybook/addons'; + +const customElementsVersions: Record = {}; + +/** + * Uses angular element to generate on-the-fly web components and reference it with `customElements` + * then it is added into react + */ +export const prepareForInline = (storyFn: StoryFn, { id, parameters }: StoryContext) => { + // Upgrade story version in order that the next defined component has a unique key + customElementsVersions[id] = + customElementsVersions[id] !== undefined ? customElementsVersions[id] + 1 : 0; + + const customElementsName = `${id}_${customElementsVersions[id]}`; + + const Story = class Story extends React.Component { + wrapperRef: React.RefObject; + + elementName: string; + + constructor(props: any) { + super(props); + this.wrapperRef = React.createRef(); + } + + async componentDidMount() { + // eslint-disable-next-line no-undef + customElements.define( + customElementsName, + await new ElementRendererService().renderAngularElement({ + storyFnAngular: storyFn(), + parameters, + }) + ); + } + + render() { + return React.createElement(customElementsName, { + ref: this.wrapperRef, + }); + } + }; + return React.createElement(Story); +}; diff --git a/app/angular/element-renderer.d.ts b/app/angular/element-renderer.d.ts new file mode 100644 index 000000000000..83074120d822 --- /dev/null +++ b/app/angular/element-renderer.d.ts @@ -0,0 +1 @@ +export * from './dist/ts3.9/element-renderer.d'; diff --git a/app/angular/element-renderer.js b/app/angular/element-renderer.js new file mode 100644 index 000000000000..9c6e428015d8 --- /dev/null +++ b/app/angular/element-renderer.js @@ -0,0 +1 @@ +module.exports = require('./dist/ts3.9/element-renderer'); diff --git a/app/angular/package.json b/app/angular/package.json index 51200ccf435c..fedae656416b 100644 --- a/app/angular/package.json +++ b/app/angular/package.json @@ -71,12 +71,14 @@ "@angular/compiler": "^11.1.0", "@angular/compiler-cli": "^11.1.0", "@angular/core": "^11.1.0", + "@angular/elements": "^11.1.0", "@angular/forms": "^11.1.0", "@angular/platform-browser": "^11.1.0", "@angular/platform-browser-dynamic": "^11.1.0", "@nrwl/workspace": "^11.1.5", "@types/autoprefixer": "^9.7.2", "@types/jest": "^25.2.3", + "@webcomponents/custom-elements": "^1.4.3", "jest": "^26.6.3", "jest-preset-angular": "^8.3.2", "ts-jest": "^26.4.4" @@ -88,18 +90,26 @@ "@angular/compiler": ">=6.0.0", "@angular/compiler-cli": ">=6.0.0", "@angular/core": ">=6.0.0", + "@angular/elements": ">=6.0.0", "@angular/forms": ">=6.0.0", "@angular/platform-browser": ">=6.0.0", "@angular/platform-browser-dynamic": ">=6.0.0", "@babel/core": "*", "@nrwl/workspace": ">=11.1.0", + "@webcomponents/custom-elements": ">=1.4.3", "rxjs": "^6.0.0", "typescript": "^3.4.0 || >=4.0.0", "zone.js": "^0.8.29 || ^0.9.0 || ^0.10.0 || ^0.11.0" }, "peerDependenciesMeta": { + "@angular/elements": { + "optional": true + }, "@nrwl/workspace": { "optional": true + }, + "@webcomponents/custom-elements": { + "optional": true } }, "engines": { diff --git a/app/angular/src/client/preview/angular-beta/ElementRendererService.ts b/app/angular/src/client/preview/angular-beta/ElementRendererService.ts new file mode 100644 index 000000000000..16d7cfb524d4 --- /dev/null +++ b/app/angular/src/client/preview/angular-beta/ElementRendererService.ts @@ -0,0 +1,59 @@ +// 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 platform = RendererService.getInstance().platform; + + /** + * Returns a custom element generated by Angular elements + */ + public async renderAngularElement({ + storyFnAngular, + parameters, + }: { + storyFnAngular: StoryFnAngularReturnType; + parameters: Parameters; + }): Promise { + const ngModule = getStorybookModuleMetadata( + { storyFnAngular, parameters }, + new BehaviorSubject(storyFnAngular.props) + ); + + return this.platform + .bootstrapModule(createElementsModule(ngModule)) + .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/RendererService.ts b/app/angular/src/client/preview/angular-beta/RendererService.ts index 558a1347de8a..bd7ce441bf14 100644 --- a/app/angular/src/client/preview/angular-beta/RendererService.ts +++ b/app/angular/src/client/preview/angular-beta/RendererService.ts @@ -23,7 +23,7 @@ export class RendererService { return RendererService.instance; } - private platform: PlatformRef; + public platform: PlatformRef; private staticRoot = document.getElementById('root'); @@ -43,6 +43,7 @@ export class RendererService { } // platform should be set after enableProdMode() this.platform = platformBrowserDynamic(); + this.initAngularBootstrapElement(); } /** @@ -83,7 +84,7 @@ export class RendererService { ); } - initAngularBootstrapElement() { + private initAngularBootstrapElement() { // Adds DOM element that angular will use as bootstrap component const storybookWrapperElement = document.createElement( RendererService.SELECTOR_STORYBOOK_WRAPPER diff --git a/app/angular/src/element-renderer.ts b/app/angular/src/element-renderer.ts new file mode 100644 index 000000000000..e49b411e80a2 --- /dev/null +++ b/app/angular/src/element-renderer.ts @@ -0,0 +1 @@ +export { ElementRendererService } from './client/preview/angular-beta/ElementRendererService'; diff --git a/examples/angular-cli/.storybook/preview.ts b/examples/angular-cli/.storybook/preview.ts index 55e7cb6c6436..ea3ecd41c449 100644 --- a/examples/angular-cli/.storybook/preview.ts +++ b/examples/angular-cli/.storybook/preview.ts @@ -1,5 +1,6 @@ -import { addParameters, addDecorator } from '@storybook/angular'; +import { addParameters } from '@storybook/angular'; import { setCompodocJson } from '@storybook/addon-docs/angular'; +import { prepareForInline } from '@storybook/addon-docs/angular/inline'; import addCssWarning from '../src/cssWarning'; // @ts-ignore @@ -18,7 +19,7 @@ addCssWarning(); addParameters({ docs: { - // inlineStories: true, - iframeHeight: '60px', + inlineStories: true, + prepareForInline, }, }); diff --git a/examples/angular-cli/package.json b/examples/angular-cli/package.json index 135e7ec2ee2b..395f0da96053 100644 --- a/examples/angular-cli/package.json +++ b/examples/angular-cli/package.json @@ -36,6 +36,7 @@ "@angular-devkit/core": "^11.1.0", "@angular/cli": "^11.1.0", "@angular/compiler-cli": "^11.1.0", + "@angular/elements": "^11.1.0", "@compodoc/compodoc": "^1.1.11", "@storybook/addon-a11y": "6.2.0-alpha.18", "@storybook/addon-actions": "6.2.0-alpha.18", @@ -54,6 +55,7 @@ "@types/jest": "^25.2.3", "@types/node": "^14.14.20", "@types/webpack-env": "^1.16.0", + "@webcomponents/custom-elements": "^1.4.3", "babel-plugin-require-context-hook": "^1.0.0", "global": "^4.4.0", "jasmine-core": "~3.6.0", diff --git a/examples/angular-cli/src/stories/__snapshots__/core.stories.storyshot b/examples/angular-cli/src/stories/__snapshots__/core.stories.storyshot index 44a39058994a..791e5a49d4a8 100644 --- a/examples/angular-cli/src/stories/__snapshots__/core.stories.storyshot +++ b/examples/angular-cli/src/stories/__snapshots__/core.stories.storyshot @@ -13,7 +13,7 @@ exports[`Storyshots Core/Parameters passed to story 1`] = ` > Parameters are { "docs": { - "iframeHeight": "60px" + "inlineStories": true }, "globalParameter": "globalParameter", "framework": "angular", diff --git a/yarn.lock b/yarn.lock index 16f7391e9f95..373d9c0a9cf3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -280,6 +280,13 @@ dependencies: tslib "^2.0.0" +"@angular/elements@^11.1.0": + version "11.1.0" + resolved "https://registry.yarnpkg.com/@angular/elements/-/elements-11.1.0.tgz#fd81e8dc01d4fa0ab36ee1ad2778ff5886ff8fd1" + integrity sha512-Bj6lwVg/uILRt2fFSKW+suezYJMXEkVJx+D+2a486ZnD/jT1UN5kJ21GfGy9c4tY4XtHlWTWc2CMPXGKDAhd7A== + dependencies: + tslib "^2.0.0" + "@angular/forms@^11.1.0": version "11.1.0" resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.1.0.tgz#6ab2e81df80dc9a9d0898d2a57270de5d9eb30e5" @@ -6482,6 +6489,11 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@webcomponents/custom-elements@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.4.3.tgz#1800d49f38bb4425ebfd160b50115e62776109d7" + integrity sha512-iD0YW46SreUQANGccywK/eC+gZELNHocZZrY2fGwrIlx/biQOTkAF9IohisibHbrmIHmA9pVCIdGwzfO+W0gig== + "@webpack-contrib/schema-utils@^1.0.0-beta.0": version "1.0.0-beta.0" resolved "https://registry.yarnpkg.com/@webpack-contrib/schema-utils/-/schema-utils-1.0.0-beta.0.tgz#bf9638c9464d177b48209e84209e23bee2eb4f65"