官網的程式碼架構,會把元件分成二種類型-容器型(Container)及表現型(Presentational),因此可以做到更好的關注點分離,讓元件架構可以易於的被重複使用。
更多詳細說明請看此 BLOG
$ npm install @ngxs/store @ngxs/logger-plugin @ngxs/devtools-plugin
安裝完成後,需要加上 src/app/state/task.state.ts 這支檔案,然後就可以把 Task 的資料邏輯移至該檔案
// 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',
};
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 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 };
}
// sets the default state
@State<TaskStateModel>({
name: 'tasks',
defaults: {
entities: defaultTasks,
},
})
export class TasksState {
@Selector()
static getAllTasks(state: TaskStateModel) {
const entities = state.entities;
return Object.keys(entities).map(id => entities[+id]);
}
// 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 archiveTask 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,
});
}
}
原來的TaskListComponent 修改成為PureTaskListComponent (表現型),裡面的程式碼結構只專注於呈現
//src/app/components/pure-task-list.component.ts
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Task } from '../models/task.model';
@Component({
selector: 'app-pure-task-list',
// same content as before with the task-list.component.ts
})
export class PureTaskListComponent implements OnInit {
// same content as before with the task-list.component.ts
}
TaskListComponent (容器型) 使用 PureTaskListComponent (表現型),再加上 ngxs 做資料與狀態操作。
// src/app/components/task-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { TasksState, ArchiveTask, PinTask } from '../state/task.state';
import { Task } from '../models/task.model';
import { Observable } from 'rxjs';
@Component({
selector: 'app-task-list',
template: `
<app-pure-task-list
[tasks]="tasks$ | async"
(onArchiveTask)="archiveTask($event)"
(onPinTask)="pinTask($event)"
></app-pure-task-list>
`,
})
export class TaskListComponent implements OnInit {
@Select(TasksState.getAllTasks) tasks$: Observable<Task[]>;
constructor(private store: Store) {}
ngOnInit() {}
archiveTask(id: string) {
this.store.dispatch(new ArchiveTask(id));
}
pinTask(id: string) {
this.store.dispatch(new PinTask(id));
}
}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgxsModule } from '@ngxs/store';
import { TaskComponent } from './task.component';
import { TaskListComponent } from './task-list.component';
import { TasksState } from '../state/task.state';
import { PureTaskListComponent } from './pure-task-list.component';
@NgModule({
imports: [
CommonModule,
NgxsModule.forFeature([TasksState])
],
exports: [TaskComponent, TaskListComponent],
declarations: [TaskComponent, TaskListComponent, PureTaskListComponent],
})
export class TaskModule { }
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { TaskModule } from './components/task.module';
import { NgxsModule } from '@ngxs/store';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
TaskModule,
NgxsModule.forRoot([]),
NgxsReduxDevtoolsPluginModule.forRoot(),
NgxsLoggerPluginModule.forRoot(),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
把 TaskList 分成 容器型 (TaskListComponent) 及 表現型(PureTaskListComponent),會更容易去測試。
Stories 只專注於 UI 呈現的效果,所以我們要把 task-list.stories.js 改為 pure-task-list.stories.js,並調整程式碼如所示。
// src/app/components/pure-task-list.stories.ts
import { moduleMetadata } from '@storybook/angular';
import { CommonModule } from '@angular/common';
import { PureTaskListComponent } from './pure-task-list.component';
import { TaskComponent } from './task.component';
import { taskData, actionsData } from './task.stories';
export default {
title: 'PureTaskList',
excludeStories: /.*Data$/,
decorators: [
moduleMetadata({
// imports both components to allow component composition with storybook
declarations: [PureTaskListComponent, TaskComponent],
imports: [CommonModule],
}),
],
};
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: PureTaskListComponent,
template: `
<div style="padding: 3rem">
<app-pure-task-list [tasks]="tasks" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-pure-task-list>
</div>
`,
props: {
tasks: defaultTasksData,
onPinTask: actionsData.onPinTask,
onArchiveTask: actionsData.onArchiveTask,
},
});
// tasklist with pinned tasks
export const WithPinnedTasks = () => ({
component: PureTaskListComponent,
template: `
<div style="padding: 3rem">
<app-pure-task-list [tasks]="tasks" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-pure-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-pure-task-list [tasks]="[]" loading="true" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-pure-task-list>
</div>
`,
});
// tasklist no tasks
export const Empty = () => ({
template: `
<div style="padding: 3rem">
<app-pure-task-list [tasks]="[]" (onPinTask)="onPinTask($event)" (onArchiveTask)="onArchiveTask($event)"></app-pure-task-list>
</div>
`,
});
本篇修改的程式碼:https://git.io/JUSZW
完成了更複雜的元件結構分類,我們可以繼續把元件向上組裝成頁面。
Wire in data - Angular
Presentational and Container Components