尝试通过将搜索列表页面分割为组件来创建【Angular】
这篇文章的摘要
按照容器(智能)组件和展示性组件的概念,创建一个按照组件方式分割的搜索列表界面。
关于容器(智能)组件和呈现组件
表单是以模板驱动创建的。
形象
版本
"@angular-devkit/schematics": "^9.0.1",
"@angular/animations": "~9.0.0",
"@angular/cdk": "^9.0.0",
"@angular/common": "~9.0.0",
"@angular/compiler": "~9.0.0",
"@angular/core": "~9.0.0",
"@angular/forms": "~9.0.0",
"@angular/localize": "^9.0.0",
"@angular/platform-browser": "~9.0.0",
"@angular/platform-browser-dynamic": "~9.0.0",
"@angular/router": "~9.0.0",
"@angular-devkit/build-angular": "~0.900.1",
"@angular/cli": "~9.0.1",
"@angular/compiler-cli": "~9.0.0",
"@angular/language-service": "~9.0.0",
"typescript": "^3.7.5"
文件夹结构(摘要)
src
┗app
┣features
┃ ┗products
┃ ┣components
┃ ┃ ┗search-products-form
┃ ┃ ┗search-products-form.component.html/scss/spec.ts/ts
┃ ┣pages
┃ ┃ ┗product-list
┃ ┃ ┣product-list.component.html/scss/spec.ts/ts
┃ ┃ ┗product-list.smart.component.ts
┃ ┣product.component.ts
┃ ┣products-routing.module.ts
┃ ┗products.module.ts
┣app-routing.module.ts
┣app.component.html/scss/spec.ts/ts
┗app.module.ts
实施
模特儿 (Mó Tè Ér)
/**
* 商品のモデル
*/
export class Product {
/** ID */
id: number;
/** 品名 */
name: string;
/** 入荷日 */
arrivalAt: Date;
/** 作成者ID */
createUserId: number;
/** 作成日 */
createdAt: string;
/** 更新者ID */
updateUserId: number;
/** 更新日 */
updatedAt: string;
/** 削除フラグ */
isDeleted: boolean;
}
/**
* 商品検索条件のモデル
*/
export interface ProductsSC {
/** ID */
id: number;
/** 商品名 */
name: string;
/** 入荷日 */
arrivalAt: {
/** from */
from: Date;
/** to */
to: Date;
};
}
服务
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Product } from '@shared/models/product.model';
import { ProductsSC } from '@shared/models/search-conditions/products-sc.model';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
const URL = '/products';
/**
* 商品API通信サービス.
*/
@Injectable({
providedIn: 'root'
})
export class ProductsHttpService {
constructor(private http: HttpClient) { }
search(conditions: ProductsSC): Observable<Product[]> {
const API_URL = `${environment.apiBaseUrl}${URL}`;
return this.http
.post<Product[]>(API_URL, conditions)
.pipe(
map(res => res['payload'])
);
}
}
模块
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { SearchProductsFormComponent } from './components/search-products-form/search-products-form.component';
import { PreProductEditComponent } from './pages/pre-product-edit/pre-product-edit.component';
import { ProductListComponent } from './pages/product-list/product-list.component';
import { ProductListSmartComponent } from './pages/product-list/product-list.smart.component';
import { ProductComponent } from './product.component';
import { ProductsRoutingModule } from './products-routing.module';
const COMPONENTS = [
ProductComponent,
ProductListComponent,
ProductListSmartComponent,
SearchProductsFormComponent
];
@NgModule({
imports: [
SharedModule,
ProductsRoutingModule
],
declarations: COMPONENTS,
exports: COMPONENTS
})
export class ProductsModule { }
路由系统
使用延迟加载进行实施。
详细说明
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductListSmartComponent } from './pages/product-list/product-list.smart.component';
import { ProductComponent } from './product.component';
const routes: Routes = [
{
path: '',
component: ProductComponent,
children: [
{
path: 'list',
component: ProductListSmartComponent,
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }
组件
用于延迟模块路由的组件
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-product',
template: `
<router-outlet></router-outlet>
`
})
export class ProductComponent implements OnInit {
constructor() { }
ngOnInit() { }
}
容器组件
// .smart.conponent.tsの「.smart」は任意です
import { Component, OnInit } from '@angular/core';
import { ProductsHttpService } from '@core/http/products-http.service';
import { Product } from '@shared/models/product.model';
import { ProductsSC } from '@shared/models/search-conditions/products-sc.model';
import { Observable } from 'rxjs';
/**
* コンテナ(スマート)コンポーネント
*/
@Component({
selector: 'app-product-list-smart',
template: `
<!-- search-products-formコンポーネントから検索ボタン押下の@Outputイベントを受け取ります。引数にフォームの値を取得します。 -->
<app-search-products-form (search)="searchProducts($event)"></app-search-products-form>
<!-- product-listコンポーネントへ商品の検索結果のリストを渡します。asyncパイプを利用ます。 -->
<app-product-list [products]="products$|async"></app-product-list>
`
})
export class ProductListSmartComponent implements OnInit {
products$: Observable<Product[]>
constructor(private productHttpService: ProductsHttpService) { }
ngOnInit() { }
/**
* 検索処理.
* プレゼンテーショナルコンポーネントからの@Outputイベントで呼ばれます.
*/
searchProducts(searchFormValue: ProductsSC): void {
// asyncパイプを使うのでsubscribeは不要です。Observableのまま変数に格納します。
this.products$ = this.productHttpService.search(searchFormValue);
}
}
演示组件
import { Component, Input, OnInit } from '@angular/core';
import { Product } from '@shared/models/product.model';
/**
* 商品一覧のプレゼンテーショナルコンポーネント.
*/
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit {
// コンテナコンポーネントから商品リストを受け取ります.
@Input()
products: Product[];
constructor() { }
ngOnInit() { }
}
<ng-container *ngFor="let data of products">
<div class="d-flex">
<div>{{data.id}}|</div>
<div>{{data.name}}|</div>
<div>
{{data.arrivalAt | date: "yyyy/MM/dd"}}
</div>
</div>
</ng-container>
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ProductsSC } from '@shared/models/search-conditions/products-sc.model';
@Component({
selector: 'app-search-products-form',
templateUrl: './search-products-form.component.html',
styleUrls: ['./search-products-form.component.scss']
})
export class SearchProductsFormComponent implements OnInit {
@Output()
private search = new EventEmitter<ProductsSC>();
constructor() { }
ngOnInit() { }
/**
* 検索ボタン押下時の処理.
* コンテナコンポーネントへイベントを通知します。
*/
onSearch(searchForm: NgForm): void {
// スプレッドでformの値を展開します。
this.search.emit({...searchForm.value});
}
}
<form
#searchForm="ngForm"
(ngSubmit)="onSearch(searchForm)">
<div>
<label>ID:</label>
<input
type="text"
name="id"
ngModel>
</div>
<div>
<label>商品名:</label>
<input
type="text"
name="name"
ngModel>
</div>
<fieldset ngModelGroup="arrivalAt">
<div>
<label>入荷日:</label>
<input
type="text"
name="from"
ngModel>
</div>
<div>
<label>〜</label>
<input
type="text"
name="to"
ngModel>
</div>
</fieldset>
<button type="submit">検索</button>
</form>
如果在容器组件中使用表单。
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ProductsHttpService } from '@core/http/products-http.service';
import { Product } from '@shared/models/product.model';
import { Observable } from 'rxjs';
/**
* コンテナ(スマート)コンポーネント
*/
@Component({
selector: 'app-product-list-smart',
template: `
<!-- プレゼンテーショナルコンポーネントではなく自分のコンポーネントで検索ボタンのイベントを定義します。 -->
<form #searchForm="ngForm" (ngSubmit)="onSearchProducts(searchForm)">
<app-search-products-form></app-search-products-form>
<button type="submit">検索</button>
</form>
<app-product-list [products]="products$|async"></app-product-list>
`
})
export class ProductListSmartComponent implements OnInit {
products$: Observable<Product[]>
constructor(private productHttpService: ProductsHttpService) { }
ngOnInit() { }
/**
* 検索処理.
*/
onSearchProducts(searchForm: NgForm): void {
this.products$ = this.productHttpService.search({...searchForm.value});
}
}
import { Component, OnInit } from '@angular/core';
import { ControlContainer, NgForm } from '@angular/forms';
@Component({
selector: 'app-search-products-form',
templateUrl: './search-products-form.component.html',
styleUrls: ['./search-products-form.component.scss'],
// htmlのfieldsetで定義したngModelGroupディレクティブが、NgFormプロバイダーを見つける為に下記を定義します。
viewProviders: [{provide: ControlContainer. useExisting: NgForm}]
})
export class SearchProductsFormComponent implements OnInit {
constructor() { }
ngOnInit() { }
}
<div>
<label>ID:</label>
<input
type="text"
name="id"
ngModel>
</div>
<div>
<label>商品名:</label>
<input
type="text"
name="name"
ngModel>
</div>
<fieldset ngModelGroup="arrivalAt">
<div>
<label>入荷日:</label>
<input
type="text"
name="from"
ngModel>
</div>
<div>
<label>〜</label>
<input
type="text"
name="to"
ngModel>
</div>
</fieldset>
参考
viewProviders
useExisting
https://medium.com/@a.yurich.zuev/angular-nested-template-driven-form-4a3de2042475
参见
viewProviders
useExisting
https://medium.com/@a.yurich.zuev/angular-nested-template-driven-form-4a3de2042475