mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-17 05:02:23 +08:00
Merge pull request #2554 from ralzinov/component-props-assignment-improvements
Component props assignment improvements
This commit is contained in:
commit
c07b15a9e4
6
app/angular/index.d.ts
vendored
6
app/angular/index.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import {NgModuleMetadata } from './dist/client/preview/angular/types';
|
||||
import {NgModuleMetadata, ICollection} from './dist/client/preview/angular/types';
|
||||
|
||||
export interface IStorybookStory {
|
||||
name: string,
|
||||
@ -11,8 +11,8 @@ export interface IStoribookSection {
|
||||
}
|
||||
|
||||
export type IGetStory = () => {
|
||||
props?: {[p: string]: any};
|
||||
moduleMetadata?: {[p: string]: NgModuleMetadata};
|
||||
props?: ICollection;
|
||||
moduleMetadata?: Partial<NgModuleMetadata>;
|
||||
component: any
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
// 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 * as _ from 'lodash';
|
||||
import {
|
||||
Component,
|
||||
Inject,
|
||||
@ -11,46 +11,86 @@ import {
|
||||
ViewContainerRef,
|
||||
ComponentFactoryResolver,
|
||||
OnDestroy,
|
||||
EventEmitter
|
||||
} from "@angular/core";
|
||||
import { STORY } from "../app.token";
|
||||
import { NgStory } from "../types";
|
||||
EventEmitter,
|
||||
SimpleChanges,
|
||||
SimpleChange
|
||||
} from '@angular/core';
|
||||
import { STORY } from '../app.token';
|
||||
import { NgStory, ICollection } from '../types';
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
template: "<ng-template #target></ng-template>"
|
||||
selector: 'app-root',
|
||||
template: '<ng-template #target></ng-template>'
|
||||
})
|
||||
export class AppComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild("target", { read: ViewContainerRef })
|
||||
@ViewChild('target', { read: ViewContainerRef })
|
||||
target: ViewContainerRef;
|
||||
constructor(
|
||||
private cfr: ComponentFactoryResolver,
|
||||
@Inject(STORY) private data: NgStory
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
ngAfterViewInit(): void {
|
||||
this.putInMyHtml();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
this.target.clear();
|
||||
}
|
||||
|
||||
putInMyHtml() {
|
||||
private putInMyHtml(): void {
|
||||
this.target.clear();
|
||||
const { component, props = {}, propsMeta = {} } = this.data;
|
||||
let compFactory = this.cfr.resolveComponentFactory(component);
|
||||
const compFactory = this.cfr.resolveComponentFactory(this.data.component);
|
||||
const instance = this.target.createComponent(compFactory).instance;
|
||||
|
||||
Object.keys(propsMeta).map(key => {
|
||||
const value = (<any>props)[key];
|
||||
const property = (<any>instance)[key];
|
||||
this.setProps(instance, this.data);
|
||||
}
|
||||
|
||||
if (!(property instanceof EventEmitter)) {
|
||||
(<any>instance)[key] = (<any>props)[key];
|
||||
} else if (typeof value === 'function') {
|
||||
property.subscribe((<any>props)[key]);
|
||||
/**
|
||||
* Set inputs and outputs
|
||||
*/
|
||||
private setProps(instance: any, {props = {}, propsMeta = {}}: NgStory): void {
|
||||
const changes: SimpleChanges = {};
|
||||
const hasNgOnChangesHook = _.has(instance, 'ngOnChanges');
|
||||
|
||||
_.forEach(propsMeta, (meta, key) => {
|
||||
const value = props[key];
|
||||
const instanceProperty = <any>_.get(instance, key);
|
||||
|
||||
if (!(instanceProperty instanceof EventEmitter) && !_.isUndefined(value)) {
|
||||
_.set(instance, key, value);
|
||||
if (hasNgOnChangesHook) {
|
||||
changes[key] = new SimpleChange(undefined, value, instanceProperty === undefined);
|
||||
}
|
||||
} else if (_.isFunction(value) && (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 (!_.isEmpty(changes)) {
|
||||
_.invoke(instance, 'ngOnChanges', changes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If component implements ControlValueAccessor interface try to set ngModel
|
||||
*/
|
||||
private setNgModel(instance: any, props: ICollection): void {
|
||||
if (_.has(props, 'ngModel')) {
|
||||
_.invoke(instance, 'writeValue', props.ngModel);
|
||||
}
|
||||
|
||||
if (_.isFunction(props.ngModelChange)) {
|
||||
_.invoke(instance, 'registerOnChange', props.ngModelChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,16 +4,17 @@ import {
|
||||
NgModule,
|
||||
Component,
|
||||
NgModuleRef
|
||||
} from "@angular/core";
|
||||
} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms'
|
||||
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { AppComponent } from "./components/app.component";
|
||||
import { ErrorComponent } from "./components/error.component";
|
||||
import { NoPreviewComponent } from "./components/no-preview.component";
|
||||
import { STORY } from "./app.token";
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AppComponent } from './components/app.component';
|
||||
import { ErrorComponent } from './components/error.component';
|
||||
import { NoPreviewComponent } from './components/no-preview.component';
|
||||
import { STORY } from './app.token';
|
||||
import { getAnnotations, getParameters, getPropMetadata } from './utils';
|
||||
import { NgModuleMetadata, NgStory, IGetStoryWithContext, IContext, NgProvidedData } from "./types";
|
||||
import { NgModuleMetadata, NgStory, IGetStoryWithContext, IContext, NgProvidedData } from './types';
|
||||
|
||||
let platform: any = null;
|
||||
let promises: Promise<NgModuleRef<any>>[] = [];
|
||||
@ -37,15 +38,19 @@ const debounce = (func: IRenderStoryFn | IRenderErrorFn,
|
||||
immediate: boolean = false): () => void => {
|
||||
let timeout: any;
|
||||
return function () {
|
||||
let context = this, args = arguments;
|
||||
let later = function () {
|
||||
const context = this, args = arguments;
|
||||
const later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
if (!immediate) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
let callNow = immediate && !timeout;
|
||||
const callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
if (callNow) {
|
||||
func.apply(context, args);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -57,8 +62,9 @@ const getComponentMetadata = (
|
||||
providers: []
|
||||
} }: NgStory
|
||||
) => {
|
||||
if (!component || typeof component !== "function")
|
||||
throw new Error("No valid component provided");
|
||||
if (!component || typeof component !== 'function') {
|
||||
throw new Error('No valid component provided');
|
||||
}
|
||||
|
||||
const componentMetadata = getAnnotations(component)[0] || {};
|
||||
const propsMetadata = getPropMetadata(component);
|
||||
@ -118,7 +124,7 @@ moduleMetadata: NgModuleMetadata = {
|
||||
}): IModule => {
|
||||
const moduleMeta = new NgModule({
|
||||
declarations: [...declarations, ...moduleMetadata.declarations],
|
||||
imports: [BrowserModule, ...moduleMetadata.imports],
|
||||
imports: [BrowserModule, FormsModule, ...moduleMetadata.imports],
|
||||
providers: [{ provide: STORY, useValue: Object.assign({}, data) }, ...moduleMetadata.providers],
|
||||
entryComponents: [...entryComponents],
|
||||
schemas: [...moduleMetadata.schemas],
|
||||
@ -140,7 +146,9 @@ const initModule = (currentStory: IGetStoryWithContext, context: IContext, reRen
|
||||
moduleMeta
|
||||
} = getComponentMetadata(currentStory(context));
|
||||
|
||||
if (!componentMeta) throw new Error("No component metadata available");
|
||||
if (!componentMeta) {
|
||||
throw new Error('No component metadata available');
|
||||
}
|
||||
|
||||
const AnnotatedComponent = getAnnotatedComponent(
|
||||
componentMeta,
|
||||
@ -176,9 +184,9 @@ const draw = (newModule: IModule, reRender: boolean = true): void => {
|
||||
Promise.all(promises)
|
||||
.then((modules) => {
|
||||
modules.forEach(mod => mod.destroy());
|
||||
|
||||
|
||||
const body = document.body;
|
||||
const app = document.createElement("app-root");
|
||||
const app = document.createElement('app-root');
|
||||
body.appendChild(app);
|
||||
promises = [];
|
||||
promises.push(platform.bootstrapModule(newModule));
|
||||
@ -213,4 +221,4 @@ export const renderNoPreview = debounce(() => {
|
||||
|
||||
export const renderNgApp = debounce((story, context, reRender) => {
|
||||
draw(initModule(story, context, reRender), reRender);
|
||||
});
|
||||
});
|
||||
|
@ -4,11 +4,13 @@ export interface NgModuleMetadata {
|
||||
schemas: Array<any>,
|
||||
providers: Array<any>,
|
||||
}
|
||||
|
||||
|
||||
export interface ICollection {[p: string]: any}
|
||||
|
||||
export interface NgStory {
|
||||
component: any,
|
||||
props: {[p: string]: any},
|
||||
propsMeta: {[p: string]: any},
|
||||
props: ICollection,
|
||||
propsMeta: ICollection,
|
||||
moduleMetadata?: NgModuleMetadata
|
||||
}
|
||||
|
||||
@ -23,4 +25,4 @@ export interface IContext {
|
||||
[p: string]: any
|
||||
}
|
||||
|
||||
export type IGetStoryWithContext = (context: IContext) => NgStory
|
||||
export type IGetStoryWithContext = (context: IContext) => NgStory
|
||||
|
@ -4,6 +4,7 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": true,
|
||||
"lib": [
|
||||
"es2016",
|
||||
"dom"
|
||||
|
@ -0,0 +1,17 @@
|
||||
import {storiesOf} from '@storybook/angular';
|
||||
import {action} from '@storybook/addon-actions';
|
||||
import {withNotes} from '@storybook/addon-notes';
|
||||
import {CustomCvaComponent} from './custom-cva.component';
|
||||
|
||||
const description = `
|
||||
This is an example of component that implements ControlValueAccessor interface
|
||||
`;
|
||||
|
||||
storiesOf('ngModel', module)
|
||||
.add('custom ControlValueAccessor', withNotes(description)(() => ({
|
||||
component: CustomCvaComponent,
|
||||
props: {
|
||||
ngModel: 'Type anything',
|
||||
ngModelChange: action('ngModelChnange')
|
||||
}
|
||||
})));
|
@ -0,0 +1,55 @@
|
||||
import { Component, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
const NOOP = () => {};
|
||||
|
||||
@Component({
|
||||
selector: 'custom-cva-component',
|
||||
template: `
|
||||
<div>{{value}}</div>
|
||||
<input type="text" [(ngModel)]="value" />
|
||||
`,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => CustomCvaComponent),
|
||||
multi: true,
|
||||
}
|
||||
]
|
||||
})
|
||||
export class CustomCvaComponent implements ControlValueAccessor {
|
||||
disabled: boolean;
|
||||
|
||||
protected onChange: (value: any) => void = NOOP;
|
||||
protected onTouch: () => void = NOOP;
|
||||
protected internalValue: any;
|
||||
|
||||
get value(): any {
|
||||
return this.internalValue;
|
||||
}
|
||||
|
||||
set value(value: any) {
|
||||
if (value !== this.internalValue) {
|
||||
this.internalValue = value;
|
||||
this.onChange(value);
|
||||
}
|
||||
}
|
||||
|
||||
writeValue(value: any): void {
|
||||
if (value !== this.internalValue) {
|
||||
this.internalValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouch = fn;
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
}
|
@ -25,6 +25,8 @@ import { ServiceComponent } from './moduleMetadata/service.component'
|
||||
import { NameComponent } from './name.component';
|
||||
import { CustomPipePipe } from './custom.pipe';
|
||||
|
||||
import './customControlValueAccessor/custom-cva-component.stories';
|
||||
|
||||
storiesOf('Welcome', module)
|
||||
.add('to Storybook', () => ({
|
||||
component: Welcome,
|
||||
|
@ -31,6 +31,7 @@
|
||||
"test-latest-cra": "npm --prefix lib/cli run test-latest-cra"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.91",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.3",
|
||||
|
@ -260,6 +260,10 @@
|
||||
version "2.5.54"
|
||||
resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.5.54.tgz#a6b5f2ae2afb6e0307774e8c7c608e037d491c63"
|
||||
|
||||
"@types/lodash@^4.14.91":
|
||||
version "4.14.91"
|
||||
resolved "https://artifactory.iponweb.net:443/artifactory/api/npm/npm/@types/lodash/-/lodash-4.14.91.tgz#794611b28056d16b5436059c6d800b39d573cd3a"
|
||||
|
||||
"@types/mz@0.0.32":
|
||||
version "0.0.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/mz/-/mz-0.0.32.tgz#e8248b4e41424c052edc1725dd33650c313a3659"
|
||||
|
Loading…
x
Reference in New Issue
Block a user