@storybook/angular
Version:
Storybook for Angular: Develop Angular components in isolation with hot reloading.
181 lines (180 loc) • 8.02 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AbstractRenderer = void 0;
const core_1 = require("@angular/core");
const platform_browser_1 = require("@angular/platform-browser");
const rxjs_1 = require("rxjs");
const telejson_1 = require("telejson");
const StorybookModule_1 = require("./StorybookModule");
const StorybookProvider_1 = require("./StorybookProvider");
const StorybookWrapperComponent_1 = require("./StorybookWrapperComponent");
const PropertyExtractor_1 = require("./utils/PropertyExtractor");
const applicationRefs = new Map();
class AbstractRenderer {
/**
* Wait and destroy the platform
*/
static resetApplications(domNode) {
StorybookWrapperComponent_1.componentNgModules.clear();
applicationRefs.forEach((appRef, appDOMNode) => {
if (!appRef.destroyed && (!domNode || appDOMNode === domNode)) {
appRef.destroy();
}
});
}
constructor() {
this.previousStoryRenderInfo = new Map();
if (typeof NODE_ENV === 'string' && NODE_ENV !== 'development') {
try {
// platform should be set after enableProdMode()
(0, core_1.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 {Component}
*/
async render({ storyFnAngular, forced, component, targetDOMNode, }) {
const targetSelector = this.generateTargetSelectorFromStoryId(targetDOMNode.id);
const newStoryProps$ = new rxjs_1.BehaviorSubject(storyFnAngular.props);
if (!this.fullRendererRequired({
targetDOMNode,
storyFnAngular,
moduleMetadata: {
...storyFnAngular.moduleMetadata,
},
forced,
})) {
this.storyProps$.next(storyFnAngular.props);
return;
}
await this.beforeFullRender(targetDOMNode);
// Complete last BehaviorSubject and set a new one for the current module
if (this.storyProps$) {
this.storyProps$.complete();
}
this.storyProps$ = newStoryProps$;
this.initAngularRootElement(targetDOMNode, targetSelector);
const analyzedMetadata = new PropertyExtractor_1.PropertyExtractor(storyFnAngular.moduleMetadata, component);
const application = (0, StorybookModule_1.getApplication)({
storyFnAngular,
component,
targetSelector,
analyzedMetadata,
});
const applicationRef = await (0, platform_browser_1.bootstrapApplication)(application, {
...storyFnAngular.applicationConfig,
providers: [
(0, StorybookProvider_1.storyPropsProvider)(newStoryProps$),
...analyzedMetadata.applicationProviders,
...(storyFnAngular.applicationConfig?.providers ?? []),
],
});
applicationRefs.set(targetDOMNode, applicationRef);
await this.afterFullRender();
}
/**
* Only ASCII alphanumerics can be used as HTML tag name.
* https://html.spec.whatwg.org/#elements-2
*
* Therefore, stories break when non-ASCII alphanumerics are included in target selector.
* https://github.com/storybookjs/storybook/issues/15147
*
* This method returns storyId when it doesn't contain any non-ASCII alphanumerics.
* Otherwise, it generates a valid HTML tag name from storyId by removing non-ASCII alphanumerics from storyId, prefixing "sb-", and suffixing "-component"
* @protected
* @memberof AbstractRenderer
*/
generateTargetSelectorFromStoryId(id) {
const invalidHtmlTag = /[^A-Za-z0-9-]/g;
const storyIdIsInvalidHtmlTagName = invalidHtmlTag.test(id);
return storyIdIsInvalidHtmlTagName ? `sb-${id.replace(invalidHtmlTag, '')}-component` : id;
}
initAngularRootElement(targetDOMNode, targetSelector) {
// Adds DOM element that angular will use as bootstrap component
// eslint-disable-next-line no-param-reassign
targetDOMNode.innerHTML = '';
targetDOMNode.appendChild(document.createElement(targetSelector));
}
fullRendererRequired({ targetDOMNode, storyFnAngular, moduleMetadata, forced, }) {
const previousStoryRenderInfo = this.previousStoryRenderInfo.get(targetDOMNode);
const currentStoryRender = {
storyFnAngular,
moduleMetadataSnapshot: (0, telejson_1.stringify)(moduleMetadata),
};
this.previousStoryRenderInfo.set(targetDOMNode, currentStoryRender);
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 &&
previousStoryRenderInfo?.storyFnAngular?.template !== storyFnAngular.template;
if (hasChangedTemplate) {
return true;
}
// force the rendering if the metadata structure has changed
const hasChangedModuleMetadata = currentStoryRender.moduleMetadataSnapshot !== previousStoryRenderInfo?.moduleMetadataSnapshot;
return hasChangedModuleMetadata;
}
}
exports.AbstractRenderer = AbstractRenderer;
_a = AbstractRenderer;
/**
* Reset compiled components because we often want to compile the same component with
* more than one NgModule.
*/
AbstractRenderer.resetCompiledComponents = async () => {
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
const { ɵresetCompiledComponents } = await Promise.resolve().then(() => __importStar(require('@angular/core')));
ɵresetCompiledComponents();
}
catch (e) {
/**
* noop catch
* This means angular removed or modified ɵresetCompiledComponents
*/
}
};