在這個單元我們使用官網的教學搭配Angular的框架,STEP BY STEP的撰寫Component及Stories。(如果是使用Vue的讀者,可以跳過這個單元)
官網教學參考:Angular
# Create our application, using a preset that contains jest:
$ ng new ng-taskbox --style css
$ cd ng-taskbox
# Add Storybook:
$ npx sb init
依照需要,在開發時期你可以會執行三種模式
# 執行 Unit Test
$ npm run test:unit
# 執行 Storybook
$ npm run storybook
# 執行專案網頁
$ npm run serve
因為這個單元是專注於開發 Single Component,所以目前我們只要執行 Storybook。
範例提供的CSS:請按此
把這個CSS檔案下載到src/styles.css
$ npx degit chromaui/learnstorybook-code/public/font src/assets/font
$ npx degit chromaui/learnstorybook-code/public/icon src/assets/icon
官網教學參考:Angular
在這個範例中,關鍵的元件是Task,每種狀態的Task都有其代表的意思,用Checkbox來表示是否完成任務,用Pin Button(圖上星星符號)來做重要任務置頂,以及中間要顯示Task的文字。經過分析後,Task元件需要二個props
// src/app/components/task.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-task',
template: `
<div class="list-item">
<input type="text" [value]="task.title" readonly="true" />
</div>
`,
})
export class TaskComponent implements OnInit {
title: string;
@Input() task: any;
// tslint:disable-next-line: no-output-on-prefix
@Output() onPinTask: EventEmitter<any> = new EventEmitter();
// tslint:disable-next-line: no-output-on-prefix
@Output() onArchiveTask: EventEmitter<any> = new EventEmitter();
constructor() {}
ngOnInit() {}
}
// src/app/components/task.stories.ts
import { action } from '@storybook/addon-actions';
import { TaskComponent } from './task.component';
export default {
title: 'Task',
excludeStories: /.*Data$/,
};
export const actionsData = {
onPinTask: action('onPinTask'),
onArchiveTask: action('onArchiveTask'),
};
export const taskData = {
id: '1',
title: 'Test Task',
state: 'Task_INBOX',
updated_at: new Date(2019, 0, 1, 9, 0),
};
export const Default = () => ({
component: TaskComponent,
props: {
task: taskData,
onPinTask: actionsData.onPinTask,
onArchiveTask: actionsData.onArchiveTask,
},
});
// pinned task state
export const Pinned = () => ({
component: TaskComponent,
props: {
task: {
...taskData,
state: 'TASK_PINNED',
},
onPinTask: actionsData.onPinTask,
onArchiveTask: actionsData.onArchiveTask,
},
});
// archived task state
export const Archived = () => ({
component: TaskComponent,
props: {
task: {
...taskData,
state: 'TASK_ARCHIVED',
},
onPinTask: actionsData.onPinTask,
onArchiveTask: actionsData.onArchiveTask,
},
});
目前Storybook運行畫面
接下來要做一些調整,讓Storybook只顯示components
目錄下的stories
因為是修改.storybook
config設定檔案,調整完成後記得要重啟Storybook
目前運行畫面
已經只有變成我們要看的components
目錄下的元件
// src/app/models/task.model.ts
export interface Task {
id: string;
title: string;
state: string;
}
// src/app/components/task.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../models/task.model';
@Component({
selector: 'app-task',
template: `
<div class="list-item {{ task?.state }}">
<label class="checkbox">
<input
type="checkbox"
[defaultChecked]="task?.state === 'TASK_ARCHIVED'"
disabled="true"
name="checked"
/>
<span class="checkbox-custom" (click)="onArchive(task.id)"></span>
</label>
<div class="title">
<input type="text" [value]="task?.title" readonly="true" placeholder="Input title" />
</div>
<div class="actions">
<a *ngIf="task?.state !== 'TASK_ARCHIVED'" (click)="onPin(task.id)">
<span class="icon-star"></span>
</a>
</div>
</div>
`,
})
export class TaskComponent implements OnInit {
title: string;
@Input() task: Task;
// tslint:disable-next-line: no-output-on-prefix
@Output() onPinTask: EventEmitter<any> = new EventEmitter();
// tslint:disable-next-line: no-output-on-prefix
@Output() onArchiveTask: EventEmitter<any> = new EventEmitter();
constructor() {}
ngOnInit() {}
onPin(id: any) {
this.onPinTask.emit(id);
}
onArchive(id: any) {
this.onArchiveTask.emit(id);
}
}
目前運行畫面
整個元件建置的過程,並沒有啟動專案本身的運行環境,僅僅只有啟動Storybook!
這也就表示在元件建置階段的時候我們只要關心元件的開發就可以了!
tag: simple-component https://git.io/JU0Dj
官網教學參考:Angular
可以把TaskList規劃成四種狀態
接下來就可以來建立 TaskList.vue 及 TaskList.stories.vue
// src/app/components/task-list.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../models/task.model';
@Component({
selector: 'app-task-list',
template: `
<div class="list-items">
<div *ngIf="loading">loading</div>
<div *ngIf="!loading && tasks.length === 0">empty</div>
<app-task
*ngFor="let task of tasks"
[task]="task"
(onArchiveTask)="onArchiveTask.emit($event)"
(onPinTask)="onPinTask.emit($event)"
>
</app-task>
</div>
`,
})
export class TaskListComponent implements OnInit {
@Input() tasks: Task[] = [];
@Input() loading = false;
// tslint:disable-next-line: no-output-on-prefix
@Output() onPinTask: EventEmitter<any> = new EventEmitter();
// tslint:disable-next-line: no-output-on-prefix
@Output() onArchiveTask: EventEmitter<any> = new EventEmitter();
constructor() {}
ngOnInit() {}
}
因為TaskList會用到Task,因此我們要把它們都宣告在Module中
TypeScript就不會發生引用的問題
另外因為未來元件可能會在不同的Module使用
所以也一併把它們export出來
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TaskComponent } from './task.component';
import { TaskListComponent } from './task-list.component';
const components = [
TaskComponent,
TaskListComponent
];
@NgModule({
declarations: [...components],
imports: [
CommonModule
],
exports: [...components]
})
export class ComponentsModule { }
// src/app/components/task-list.stories.ts
import { moduleMetadata } from '@storybook/angular';
import { CommonModule } from '@angular/common';
import { TaskListComponent } from './task-list.component';
import { taskData, actionsData } from './task.stories';
import { ComponentsModule } from './components.module';
export default {
title: 'TaskList',
excludeStories: /.*Data$/,
decorators: [
moduleMetadata({
imports: [CommonModule, ComponentsModule],
}),
],
};
export const defaultTasksData = [
{ ...taskData, id: '1', title: 'Task 1' },
{ ...taskData, id: '2', title: 'Task 2' },
{ ...taskData, id: '3', title: 'Task 3' },
{ ...taskData, id: '4', title: 'Task 4' },
{ ...taskData, id: '5', title: 'Task 5' },
{ ...taskData, id: '6', title: 'Task 6' },
];
export const withPinnedTasksData = [
...defaultTasksData.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];
// default TaskList state
export const Default = () => ({
component: TaskListComponent,
template: `
<div style="padding: 3rem">
<app-task-list [tasks]="tasks" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-task-list>
</div>
`,
props: {
tasks: defaultTasksData,
onPinTask: actionsData.onPinTask,
onArchiveTask: actionsData.onArchiveTask,
},
});
// tasklist with pinned tasks
export const WithPinnedTasks = () => ({
component: TaskListComponent,
template: `
<div style="padding: 3rem">
<app-task-list [tasks]="tasks" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-task-list>
</div>
`,
props: {
tasks: withPinnedTasksData,
onPinTask: actionsData.onPinTask,
onArchiveTask: actionsData.onArchiveTask,
},
});
// tasklist in loading state
export const Loading = () => ({
template: `
<div style="padding: 3rem">
<app-task-list [tasks]="[]" loading="true" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-task-list>
</div>
`,
});
// tasklist no tasks
export const Empty = () => ({
template: `
<div style="padding: 3rem">
<app-task-list [tasks]="[]" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-task-list>
</div>
`,
});
在以上範例有一個decorators的設定,在Vue的建立元件的單元我們是用來做增加額外的版型設定,在Angular這裡,則是用來增加Context的設定,使用
moduleMetadata
,讓Stories檔案知道要import哪些module,因為我們有設定好ComponentsModule
,所以只要import它即可。
程式碼設置完成後,目前運行的畫面如下圖
Default Task 的部分是完整呈現了
但 WithPinnedTasks 的 Pinned Task 沒有出現在最上面
而 Loading 及 Empty 則還需要加上樣式調整
// src/app/components/task-list.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../models/task.model';
@Component({
selector: 'app-task-list',
template: `
<div class="list-items">
<app-task
*ngFor="let task of tasksInOrder"
[task]="task"
(onArchiveTask)="onArchiveTask.emit($event)"
(onPinTask)="onPinTask.emit($event)"
>
</app-task>
<div *ngIf="tasksInOrder.length === 0 && !loading" class="wrapper-message">
<span class="icon-check"></span>
<div class="title-message">You have no tasks</div>
<div class="subtitle-message">Sit back and relax</div>
</div>
<div *ngIf="loading">
<div *ngFor="let i of [1, 2, 3, 4, 5, 6]" class="loading-item">
<span class="glow-checkbox"></span>
<span class="glow-text"> <span>Loading</span> <span>cool</span> <span>state</span> </span>
</div>
</div>
</div>
`,
})
export class TaskListComponent implements OnInit {
tasksInOrder: Task[] = [];
@Input() loading = false;
// tslint:disable-next-line: no-output-on-prefix
@Output() onPinTask: EventEmitter<any> = new EventEmitter();
// tslint:disable-next-line: no-output-on-prefix
@Output() onArchiveTask: EventEmitter<any> = new EventEmitter();
@Input()
set tasks(arr: Task[]) {
this.tasksInOrder = [
...arr.filter(t => t.state === 'TASK_PINNED'),
...arr.filter(t => t.state !== 'TASK_PINNED'),
];
}
constructor() {}
ngOnInit() {}
}
從差異修改的地方可以看到 Loading 及 Empty 已加上樣式調整
當設定 tasks 時也會把 tasksInOrder 做出來
把 Pinned Task 放在資料的最前頭
目前運行畫面:
整個元件建置的過程,也沒有啟動專案本身的運行環境,僅僅只有啟動Storybook!
tag: composite-component https://git.io/JU0yf
Get started:Angular
Simple Component:Angular
Composite Component:Angular