Help us understand the problem. What is going on with this article?

【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

実装

モデル

src/app/shared/models/product.ts
/**
 * 商品のモデル
 */
export class Product {
  /** ID */
  id: number;
  /** 品名 */
  name: string;
  /** 入荷日 */
  arrivalAt: Date;
  /** 作成者ID */
  createUserId: number;
  /** 作成日 */
  createdAt: string;
  /** 更新者ID */
  updateUserId: number;
  /** 更新日 */
  updatedAt: string;
  /** 削除フラグ */
  isDeleted: boolean;
}
src/app/shared/models/search-conditions/products-sc.ts
/**
 * 商品検索条件のモデル
 */
export interface ProductsSC {
  /** ID */
  id: number;
  /** 商品名 */
  name: string;
  /** 入荷日 */
  arrivalAt: {
    /** from */
    from: Date;
    /** to */
    to: Date;
  };
}

サービス

src/app/core/http/products-http.service.ts
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'])
      );
  }
}

モジュール

src/app/features/products/products.module.ts
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 { }

ルーティング

遅延ロードで実装します。
詳細

src/app/features/products/products-routing.module.ts
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 { }

コンポーネント

遅延モジュールのルーティング用のコンポーネント

src/app/features/products/product.conponent.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-product',
  template: `
    <router-outlet></router-outlet>
  `
})
export class ProductComponent implements OnInit {
  constructor() { }
  ngOnInit() { }
}

コンテナコンポーネント

src/app/features/products/pages/product-list/product-list.smart.conponent.ts
// .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);
  }
}

プレゼンテーショナルコンポーネント

src/app/features/products/pages/product-list/product-list.conponent.ts
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() { }
}
src/app/features/products/pages/product-list/product-list.conponent.html
<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>
src/app/features/products/conponents/search-products-form/search-products-form.conponent.ts
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});
  }
}
src/app/features/products/conponents/search-products-form/search-products-form.conponent.html
<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>

コンテナコンポーネントでformを持つ場合

src/app/features/products/pages/product-list/product-list.smart.conponent.ts
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});
  }
}
src/app/features/products/conponents/search-products-form/search-products-form.conponent.ts
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() { }
}
src/app/features/products/conponents/search-products-form/search-products-form.conponent.html
<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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした