LoginSignup
0
2

More than 3 years have passed since last update.

【Angular】検索一覧画面をコンポーネントを分割して作ってみる

Posted at

この記事の概要

コンテナ(スマート)コンポーネントとプレゼンテーショナルコンポーネントの概念に沿ってコンポーネントを分割した検索一覧画面を作成します。

コンテナ(スマート)コンポーネント、プレゼンテーショナルコンポーネントについて

フォームはテンプレート駆動で作成します。

イメージ

スクリーンショット 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

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2