-
-
Notifications
You must be signed in to change notification settings - Fork 9.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2564 from storybooks/angular-storyshots
Angular and Vue storyshots
- Loading branch information
Showing
53 changed files
with
2,386 additions
and
139 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// We could use NgComponentOutlet here but there's currently no easy way | ||
// to provide @Inputs and subscribe to @Outputs, see | ||
// https://github.com/angular/angular/issues/15360 | ||
// For the time being, the ViewContainerRef approach works pretty well. | ||
import { | ||
Component, | ||
Inject, | ||
OnInit, | ||
ViewChild, | ||
ViewContainerRef, | ||
ComponentFactoryResolver, | ||
OnDestroy, | ||
EventEmitter, | ||
SimpleChanges, | ||
SimpleChange, | ||
} from '@angular/core'; | ||
import { STORY } from './app.token'; | ||
import { NgStory, ICollection } from './types'; | ||
|
||
@Component({ | ||
selector: 'storybook-dynamic-app-root', | ||
template: '<ng-template #target></ng-template>', | ||
}) | ||
export class AppComponent implements OnInit, OnDestroy { | ||
@ViewChild('target', { read: ViewContainerRef }) | ||
target: ViewContainerRef; | ||
constructor(private cfr: ComponentFactoryResolver, @Inject(STORY) private data: NgStory) {} | ||
|
||
ngOnInit(): void { | ||
this.putInMyHtml(); | ||
} | ||
|
||
ngOnDestroy(): void { | ||
this.target.clear(); | ||
} | ||
|
||
private putInMyHtml(): void { | ||
this.target.clear(); | ||
const compFactory = this.cfr.resolveComponentFactory(this.data.component); | ||
const instance = this.target.createComponent(compFactory).instance; | ||
|
||
this.setProps(instance, this.data); | ||
} | ||
|
||
/** | ||
* Set inputs and outputs | ||
*/ | ||
private setProps(instance: any, { props = {} }: NgStory): void { | ||
const changes: SimpleChanges = {}; | ||
const hasNgOnChangesHook = !!instance['ngOnChanges']; | ||
|
||
Object.keys(props).map((key: string) => { | ||
const value = props[key]; | ||
const instanceProperty = instance[key]; | ||
|
||
if (!(instanceProperty instanceof EventEmitter) && !!value) { | ||
instance[key] = value; | ||
if (hasNgOnChangesHook) { | ||
changes[key] = new SimpleChange(undefined, value, instanceProperty === undefined); | ||
} | ||
} else if (typeof value === 'function' && key !== 'ngModelChange') { | ||
instanceProperty.subscribe(value); | ||
} | ||
}); | ||
|
||
this.callNgOnChangesHook(instance, changes); | ||
this.setNgModel(instance, props); | ||
} | ||
|
||
/** | ||
* Manually call 'ngOnChanges' hook because angular doesn't do that for dynamic components | ||
* Issue: [https://github.com/angular/angular/issues/8903] | ||
*/ | ||
private callNgOnChangesHook(instance: any, changes: SimpleChanges): void { | ||
if (!!Object.keys(changes).length) { | ||
instance.ngOnChanges(changes); | ||
} | ||
} | ||
|
||
/** | ||
* If component implements ControlValueAccessor interface try to set ngModel | ||
*/ | ||
private setNgModel(instance: any, props: ICollection): void { | ||
if (!!props['ngModel']) { | ||
instance.writeValue(props.ngModel); | ||
} | ||
|
||
if (typeof props.ngModelChange === 'function') { | ||
instance.registerOnChange(props.ngModelChange); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { InjectionToken } from '@angular/core'; | ||
import { NgStory } from './types'; | ||
|
||
export const STORY = new InjectionToken<NgStory>('story'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Component, Type } from '@angular/core'; | ||
import { FormsModule } from '@angular/forms'; | ||
import { BrowserModule } from '@angular/platform-browser'; | ||
import { AppComponent } from './app.component'; | ||
import { STORY } from './app.token'; | ||
import { NgStory } from './types'; | ||
|
||
const getModuleMeta = ( | ||
declarations: Array<Type<any> | any[]>, | ||
entryComponents: Array<Type<any> | any[]>, | ||
bootstrap: Array<Type<any> | any[]>, | ||
data: NgStory, | ||
moduleMetadata: any | ||
) => { | ||
return { | ||
declarations: [...declarations, ...(moduleMetadata.declarations || [])], | ||
imports: [BrowserModule, FormsModule, ...(moduleMetadata.imports || [])], | ||
providers: [ | ||
{ provide: STORY, useValue: Object.assign({}, data) }, | ||
...(moduleMetadata.providers || []), | ||
], | ||
entryComponents: [...entryComponents, ...(moduleMetadata.entryComponents || [])], | ||
schemas: [...(moduleMetadata.schemas || [])], | ||
bootstrap: [...bootstrap], | ||
}; | ||
}; | ||
|
||
const createComponentFromTemplate = (template: string): Function => { | ||
const componentClass = class DynamicComponent {}; | ||
|
||
return Component({ | ||
template: template, | ||
})(componentClass); | ||
}; | ||
|
||
export const initModuleData = (storyObj: NgStory): any => { | ||
const { component, template, props, moduleMetadata = {} } = storyObj; | ||
|
||
let AnnotatedComponent; | ||
|
||
if (template) { | ||
AnnotatedComponent = createComponentFromTemplate(template); | ||
} else { | ||
AnnotatedComponent = component; | ||
} | ||
|
||
const story = { | ||
component: AnnotatedComponent, | ||
props, | ||
}; | ||
|
||
const moduleMeta = getModuleMeta( | ||
[AppComponent, AnnotatedComponent], | ||
[AnnotatedComponent], | ||
[AppComponent], | ||
story, | ||
moduleMetadata | ||
); | ||
|
||
return { | ||
AppComponent, | ||
moduleMeta, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import runWithRequireContext from '../require_context'; | ||
import hasDependency from '../hasDependency'; | ||
import loadConfig from '../config-loader'; | ||
|
||
function setupAngularJestPreset() { | ||
// Angular + Jest + Storyshots = Crazy Shit: | ||
// We need to require 'jest-preset-angular/setupJest' before any storybook code | ||
// is running inside jest - one of the things that `jest-preset-angular/setupJest` does is | ||
// extending the `window.Reflect` with all the needed metadata functions, that are required | ||
// for emission of the TS decorations like 'design:paramtypes' | ||
require.requireActual('jest-preset-angular/setupJest'); | ||
} | ||
|
||
function test(options) { | ||
return ( | ||
options.framework === 'angular' || (!options.framework && hasDependency('@storybook/angular')) | ||
); | ||
} | ||
|
||
function load(options) { | ||
setupAngularJestPreset(); | ||
|
||
const { content, contextOpts } = loadConfig({ | ||
configDirPath: options.configPath, | ||
babelConfigPath: '@storybook/angular/dist/server/babel_config', | ||
}); | ||
|
||
runWithRequireContext(content, contextOpts); | ||
|
||
return { | ||
framework: 'angular', | ||
renderTree: require.requireActual('./renderTree').default, | ||
renderShallowTree: () => { | ||
throw new Error('Shallow renderer is not supported for angular'); | ||
}, | ||
storybook: require.requireActual('@storybook/angular'), | ||
}; | ||
} | ||
|
||
export default { | ||
load, | ||
test, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import AngularSnapshotSerializer from 'jest-preset-angular/AngularSnapshotSerializer'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import HTMLCommentSerializer from 'jest-preset-angular/HTMLCommentSerializer'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { TestBed } from '@angular/core/testing'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { NO_ERRORS_SCHEMA } from '@angular/core'; | ||
import { addSerializer } from 'jest-specific-snapshot'; | ||
import { initModuleData } from './helpers.ts'; | ||
|
||
addSerializer(HTMLCommentSerializer); | ||
addSerializer(AngularSnapshotSerializer); | ||
|
||
function getRenderedTree(story, context) { | ||
const currentStory = story.render(context); | ||
|
||
const { moduleMeta, AppComponent } = initModuleData(currentStory); | ||
|
||
TestBed.configureTestingModule({ | ||
imports: [...moduleMeta.imports], | ||
declarations: [...moduleMeta.declarations], | ||
providers: [...moduleMeta.providers], | ||
schemas: [NO_ERRORS_SCHEMA, ...moduleMeta.schemas], | ||
bootstrap: [...moduleMeta.bootstrap], | ||
}); | ||
|
||
TestBed.overrideModule(BrowserDynamicTestingModule, { | ||
set: { | ||
entryComponents: [...moduleMeta.entryComponents], | ||
}, | ||
}); | ||
|
||
return TestBed.compileComponents().then(() => { | ||
const tree = TestBed.createComponent(AppComponent); | ||
tree.detectChanges(); | ||
|
||
return tree; | ||
}); | ||
} | ||
|
||
export default getRenderedTree; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export interface NgModuleMetadata { | ||
declarations?: Array<any>; | ||
entryComponents?: Array<any>; | ||
imports?: Array<any>; | ||
schemas?: Array<any>; | ||
providers?: Array<any>; | ||
} | ||
|
||
export interface ICollection { | ||
[p: string]: any; | ||
} | ||
|
||
export interface NgStory { | ||
component?: any; | ||
props: ICollection; | ||
propsMeta?: ICollection; | ||
moduleMetadata?: NgModuleMetadata; | ||
template?: string; | ||
} |
Oops, something went wrong.