尝试通过将搜索列表页面分割为组件来创建【Angular】

这篇文章的摘要

按照容器(智能)组件和展示性组件的概念,创建一个按照组件方式分割的搜索列表界面。

关于容器(智能)组件和呈现组件

表单是以模板驱动创建的。

形象

スクリーンショット 2020-02-22 17.37.58.png

版本

"@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

广告
将在 10 秒后关闭
bannerAds