Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
2
Help us understand the problem. What is going on with this article?
@pg_yamaton

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

More than 1 year has passed since last update.

この記事の概要

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

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

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

イメージ

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

2
Help us understand the problem. What is going on with this article?
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
pg_yamaton
フリーランス Angular大好きおじさん 自称RxJSマスター React学習中 バックエンドはNode.js(NestJS)が得意 その他経歴・スキル: https://qiita.com/pg_yamaton/private/662add8109f08ed47d29

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
2
Help us understand the problem. What is going on with this article?