在前面的單元,我們已經完成了開發單一元件Task,也完成了複合元件 TaskList,也把 TaskList 拆分為容器型(TaskListComponent) 及 表現型(PureTaskListComponent),接下來要做的是把元件組合成頁面 - InboxScreen。
元件驅動開發 Component-Driven Development(CDD),是指從元件開始向上開發,逐步的擴充及組合元件,到最後完成一個頁面。
在 InboxScreen 除了使用 TaskList,也會在當頁面出問題時呈現錯誤的樣式。InboxScreen 也會拆分成容器型(InboxScreenComponent) 及表現型 (PureInboxScreenComponent)
增加 Error 相關的欄位及操作
// src/app/state/task.state.ts
import { State, Selector, Action, StateContext } from '@ngxs/store';
import { Task } from '../models/task.model';
// defines the actions available to the app
export const actions = {
ARCHIVE_TASK: 'ARCHIVE_TASK',
PIN_TASK: 'PIN_TASK',
// defines the new error field we need
ERROR: 'APP_ERROR',
};
export class ArchiveTask {
static readonly type = actions.ARCHIVE_TASK;
constructor(public payload: string) {}
}
export class PinTask {
static readonly type = actions.PIN_TASK;
constructor(public payload: string) {}
}
// the class definition for our error field
export class AppError {
static readonly type = actions.ERROR;
constructor(public payload: boolean) {}
}
// The initial state of our store when the app loads.
// Usually you would fetch this from a server
const defaultTasks = {
1: { id: '1', title: 'Something', state: 'TASK_INBOX' },
2: { id: '2', title: 'Something more', state: 'TASK_INBOX' },
3: { id: '3', title: 'Something else', state: 'TASK_INBOX' },
4: { id: '4', title: 'Something again', state: 'TASK_INBOX' },
};
export class TaskStateModel {
entities: { [id: number]: Task };
error: boolean;
}
// sets the default state
@State<TaskStateModel>({
name: 'tasks',
defaults: {
entities: defaultTasks,
error: false,
},
})
export class TasksState {
@Selector()
static getAllTasks(state: TaskStateModel) {
const entities = state.entities;
return Object.keys(entities).map(id => entities[+id]);
}
// defines a new selector for the error field
@Selector()
static getError(state: TaskStateModel) {
const { error } = state;
return error;
}
//
// triggers the PinTask action, similar to redux
@Action(PinTask)
pinTask({ patchState, getState }: StateContext<TaskStateModel>, { payload }: PinTask) {
const state = getState().entities;
const entities = {
...state,
[payload]: { ...state[payload], state: 'TASK_PINNED' },
};
patchState({
entities,
});
}
// triggers the PinTask action, similar to redux
@Action(ArchiveTask)
archiveTask({ patchState, getState }: StateContext<TaskStateModel>, { payload }: ArchiveTask) {
const state = getState().entities;
const entities = {
...state,
[payload]: { ...state[payload], state: 'TASK_ARCHIVED' },
};
patchState({
entities,
});
}
// function to handle how the state should be updated when the action is triggered
@Action(AppError)
setAppError({ patchState, getState }: StateContext<TaskStateModel>, { payload }: AppError) {
const state = getState();
patchState({
error: !state.error,
});
}
}
表現型會做DOM markup及樣式呈現,如果有 error 就顯示錯誤區塊,否則就呈現 TaskListComponent,使用@Input()來接收值
// src/app/components/pure-inbox-screen.component.ts
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-pure-inbox-screen',
template: `
<div *ngIf="error" class="page lists-show">
<div class="wrapper-message">
<span class="icon-face-sad"></span>
<div class="title-message">Oh no!</div>
<div class="subtitle-message">Something went wrong</div>
</div>
</div>
<div *ngIf="!error" class="page lists-show">
<nav>
<h1 class="title-page">
<span class="title-wrapper">Taskbox</span>
</h1>
</nav>
<app-task-list></app-task-list>
</div>
`,
})
export class PureInboxScreenComponent implements OnInit {
@Input() error: any;
constructor() {}
ngOnInit() {}
}
容器型會做資料及狀態管理,把內容利用property binding傳給表現型
// src/app/components/inbox-screen.component.ts
import { Component, OnInit } from '@angular/core';
import { Select } from '@ngxs/store';
import { TasksState } from '../state/task.state';
import { Observable } from 'rxjs';
@Component({
selector: 'app-inbox-screen',
template: `
<app-pure-inbox-screen [error]="error$ | async"></app-pure-inbox-screen>
`,
})
export class InboxScreenComponent implements OnInit {
@Select(TasksState.getError) error$: Observable<any>;
constructor() {}
ngOnInit() {}
}
AppComponent 使用 InboxScreenComponent (容器型)
//src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-inbox-screen></app-inbox-screen>
`,
})
export class AppComponent {
title = 'taskbox';
}
...
import { InboxScreenComponent } from './components/inbox-screen.component';
import { PureInboxScreenComponent } from './components/pure-inbox-screen.component';
@NgModule({
declarations: [
AppComponent,
InboxScreenComponent,
PureInboxScreenComponent
],
...
})
export class AppModule { }
// src/app/components/pure-inbox-screen.stories.ts
import { moduleMetadata } from '@storybook/angular';
import { PureInboxScreenComponent } from './pure-inbox-screen.component';
import { TaskModule } from './task.module';
export default {
title: 'PureInboxScreen',
decorators: [
moduleMetadata({
imports: [TaskModule],
}),
],
};
// inbox screen default state
export const Default = () => ({
component: PureInboxScreenComponent,
});
// inbox screen error state
export const error = () => ({
component: PureInboxScreenComponent,
props: {
error: true,
},
});
現在Storybook有出現PureInboxScreen的元件清單,但Default Story,還需要把內容傳給它做呈現
// src/app/components/pure-inbox-screen.stories.ts
import { moduleMetadata } from '@storybook/angular';
import { PureInboxScreenComponent } from './pure-inbox-screen.component';
import { TaskModule } from './task.module';
import { Store, NgxsModule } from '@ngxs/store';
import { TasksState } from '../state/task.state';
export default {
title: 'PureInboxScreen',
decorators: [
moduleMetadata({
imports: [TaskModule, NgxsModule.forRoot([TasksState])],
providers: [Store],
}),
],
};
// inbox screen default state
export const Default = () => ({
component: PureInboxScreenComponent,
});
// inbox screen error state
export const error = () => ({
component: PureInboxScreenComponent,
props: {
error: true,
},
});
本篇修改的程式碼:https://git.io/JUSl2
接下來要介紹如何使用Storybook做出文件,讓程式碼即是可以使用的說明文檔。
Construct a screen - Angular