Takumon/mean-blog

View on GitHub
src/app/articles/article-list/article-list.component.ts

Summary

Maintainability
A
1 hr
Test Coverage
import {
  Component,
  OnInit,
  OnDestroy,
  ChangeDetectionStrategy
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PageEvent, MatPaginatorIntl } from '@angular/material';
import { Store } from '@ngrx/store';
import { Actions, ofType } from '@ngrx/effects';
import * as moment from 'moment';
import { Subject, Observable } from 'rxjs';
import { takeUntil, map, tap } from 'rxjs/operators';

import * as fromArticle from '../../state';
import { Constant } from '../../shared/constant';
import {
  PaginatorService,
  AuthenticationService,
  UserService,
  SearchArticlesCondition,
} from '../../shared/services';
import { ArticleWithUserModel } from '../../shared/models';

import { ScrollService } from '../shared/scroll.service';
import {
  LoadArticles,
  ArticleActionTypes,
  LoadArticlesSuccess
} from '../../state/article.actions';

export enum ArticleSearchMode {
  ALL = 100,
  FAVORITE = 200,
  USER = 300,
  VOTER = 400,
}

enum SortDirection {
  ASC, DESC, NONE
}

interface SortFactor {
  label: string;
  value: string;
  direction: SortDirection;
}

const SortFactors: {[key: string]: SortFactor} = {
  CREATE_DATE: {
    label: '登録日',
    value: 'created',
    direction: SortDirection.NONE,
  },
  UPDATE_DATE: {
    label: '更新日',
    value: 'updated',
    direction: SortDirection.NONE
  },
};


@Component({
  selector: 'app-article-list',
  templateUrl: './article-list.component.html',
  styleUrls: ['./article-list.component.scss'],
  changeDetection: ChangeDetectionStrategy.Default,
})
export class ArticleListComponent implements OnInit, OnDestroy {
  /** 定数クラス、HTMLで使用するのでコンポーネントのメンバとしている */
  public Constant = Constant;


  /**
   * プログレスバーを表示するか
   */
  public showPrograssBar = false;
  public direction = SortDirection;
  public sortFactors = SortFactors;
  public sortFactorKeys = Object.keys(SortFactors);

  // ページング用プロパティ
  public pageSize = Constant.DEFAULT_PER_PAGE;
  public pageIndex = 0;


  /** 検索条件(ソート、ページング用に保持しておく) */
  private searchCondition: SearchArticlesCondition;

  private destroyed$ = new Subject();
  private mode;

  public articles$: Observable<ArticleWithUserModel[]>;
  public count$: Observable<number>;
  loading$: Observable<boolean>;

  constructor(
    private route: ActivatedRoute,
    private actions$: Actions,
    private store: Store<fromArticle.State>,
    public paginatorService: MatPaginatorIntl,
    public auth: AuthenticationService,
    private userService: UserService,
    private scrollService: ScrollService
  ) {
    this.loading$ = this.store.select(fromArticle.getLoading);
    this.articles$ = this.store.select(fromArticle.getArticles).pipe(
      map(articles => articles as ArticleWithUserModel[])
    );
    this.count$ = this.store.select(fromArticle.getCount);

    this.actions$.pipe(
      takeUntil(this.destroyed$),
      ofType<LoadArticlesSuccess>(ArticleActionTypes.LoadArticlesSuccess),
      tap(action => {
        this.showPrograssBar = false;

        // 画面条文にスクロールする
        setTimeout(function() {
          this.scrollService.scrollToTop();
        }.bind(this), 0);
      })
    ).subscribe();



  }

  ngOnInit() {
    this.route.data
    .pipe(takeUntil(this.destroyed$))
    .subscribe((data: any) => {
      // モードを保持
      this.mode = data['mode'];

      // ログイン時かつお気に入り検索時は
      // 検索条件初期化が終わったら、そのイベントを検知して
      // getArticlesを呼ぶためここでは呼び出さない
      if (this.mode === ArticleSearchMode.FAVORITE && this.auth.isLogin()) {
        // プログレスバーを表示しておく
        this.showPrograssBar = true;
      } else {
        this.init();
      }
    });
  }

  ngOnDestroy() {
    this.destroyed$.next();
    this.destroyed$.complete();
  }

  /**
   * 記事検索時に検索条件がないか<br>
   * 検索結果が0件時の文言出し分けのために使用する
   **/
  public hasNoSearchCondition() {
    return this.searchCondition && Object.keys(this.searchCondition).length === 0;
  }

  /**
   * コンポーネント初期化時の処理
   */
  init(): void {
    this.initPaging();
    this.initSort();

    this.showPrograssBar = true;

    this.constructSearchCondition(searchCondition => {
      this.searchCondition = searchCondition;
      this.getArticles(this.searchCondition);
    });
  }

  onChangeSearchCondition(searchCondition: SearchArticlesCondition) {

    this.initPaging();
    this.initSort();

    this.showPrograssBar = true;

    this.searchCondition = searchCondition;
    this.getArticles(this.searchCondition);
  }

  /**
   * modeがお気に入り検索か判定する
   *
   * @return modeがお気に入り検索の場合true. それ以外の場合false
   */
  isFavoriteMode(): boolean {
    return this.mode && this.mode === ArticleSearchMode.FAVORITE;
  }

  /**
   * ページング時に記事一覧を再検索する.
   *
   * @param event ページングイベント
   */
  refreshPage(event: PageEvent): void {
    // ページサイズを変更する時は現在のページが存在しない可能性があるので1ページに初期化
    if (this.pageSize !== event.pageSize) {
      this.pageSize = event.pageSize;
      this.pageIndex = 0;
      this.getArticles(this.searchCondition);
      return;
    }

   this.pageSize = event.pageSize;
    this.pageIndex = event.pageIndex;
    this.getArticles(this.searchCondition);
  }

  /**
   * ソート時に記事一覧を再建策する.
   *
   * @param selectedKey
   */
  sortAndRefresh(selectedKey): void {
    // ソート時は最初のページに戻る
    this.initPaging();

    // ソート条件設定
    for (const key of this.sortFactorKeys) {
      const factor = this.sortFactors[key];

      // 指定したソートキー以外はdirectionを初期化
      if (selectedKey !== key) {
        factor.direction = SortDirection.NONE;
        continue;
      }

      // 指定したソートキーのdirectionをトグルする
      // 特にdirectionが未指定の場合はDESCにする
      switch (factor.direction) {
        case SortDirection.NONE:
        case SortDirection.ASC:
          factor.direction = SortDirection.DESC;
          break;
        case SortDirection.DESC:
          factor.direction = SortDirection.ASC;
          break;
      }
    }

    this.getArticles(this.searchCondition);
  }

  /**
   * ページ数初期化
   */
  private initPaging(): void {
    this.pageIndex = 0;
  }

  /**
   * ソート条件初期化
   */
  private initSort(): void {
    for (const key of this.sortFactorKeys) {
      const factor = this.sortFactors[key];
      // 作成日を降順にしてその他のdirectionを解除する
      factor.direction = factor === SortFactors.CREATE_DATE
        ? SortDirection.DESC
        : SortDirection.NONE;
    }
  }

  /**
   * URLやmodeに応じた検索条件を組み立てる.
   * <p>
   * MODEがFAVORITの場合はonChangeSearchConditionで検索条件を設定するので本メソッドは実行されない
   * </p>
   *
   * @param cb 検索条件を引数に渡すコールバック関数
   */
  private constructSearchCondition(cb: (searchCondition: SearchArticlesCondition) => void ): void {
    switch (this.mode) {
      case ArticleSearchMode.ALL:
        cb({});
        break;

      // 見ログイン時は全件検索と同様
      case ArticleSearchMode.FAVORITE:
        if (!this.auth.isLogin()) {
          cb({});
        }
        break;

      case ArticleSearchMode.USER:
        this.route.parent.params
        .pipe(takeUntil(this.destroyed$))
        .subscribe( params => cb({author: { userId: params['_userId'] }}));
        break;

      case ArticleSearchMode.VOTER:
        this.route.parent.params
        .pipe(
          takeUntil(this.destroyed$)
        )
        .subscribe( params => {
          this.userService
          .getById(params['_userId'])
          .subscribe(user => cb({ voter: user._id.toString()}));
        });
        break;
    }
  }

  /**
   * 指定したソート項目を取得する
   *
   * @return 指定したソート項目
   */
  private getSelectedSortFactor(): SortFactor {
    for (const key of this.sortFactorKeys) {
      const factor = this.sortFactors[key];
      if (factor.direction !== SortDirection.NONE) {
        return factor;
      }
    }

    // 必ずソート項目は指定しているので
    // ここに到達した場合は例外をスローする
    throw new Error('ソート項目が選択されていない情報');
  }

  /**
   * 保持している検索条件と、ページングとソートの設定に従って記事一覧を取得する
   *
   * @param searchCondition 記事検索条件
   */
  private getArticles(searchCondition: SearchArticlesCondition): void {
    const selectedSortFactor = this.getSelectedSortFactor();

    this.getPagingRang().subscribe(range => {

      const pageingAndSortOption = {
        sort: {
          [selectedSortFactor.value]: selectedSortFactor.direction === SortDirection.ASC ? 1 : -1
        },
        skip: range.startIndex,
        limit: range.endIndex - range.startIndex
      };

      // withUserをtrueに設定しているので戻り値の型を絞ってかえす
      this.store.dispatch(new LoadArticles({
        condition: searchCondition,
        paginAndSortOptions: pageingAndSortOption,
        withUser: true
      }));

    });
  }

  /**
   * 記事検索時に必要なページング情報を取得する.
   */
  private getPagingRang(): Observable<{ startIndex: number; endIndex: number; }> {
    return this.count$.pipe(
      map(count => (this.paginatorService as PaginatorService).calcRange(this.pageIndex, this.pageSize, count))
    );
  }
}