はじめに
Angular + Reduxでストップウオッチを作りました
- デモ
https://l08084.github.io/ng-stopwatch/
- ソースコード
実装
開発環境
Angular: 5.2.3
angular-redux/store: 7.1.0
OS: darwin x64
Node: 8.1.4
概要
angular-redux/store
を使ったReduxのアーキテクチャを採用しました。
ストップウオッチについては、Observable.interval(1000)
を使用して、STARTボタンを押下してから、何秒経過したかを計測した後、計測結果をmoment.js
とAngularのPipe
で、HH : mm : ss
フォーマットに変更するといった流れになっています。
Store
Storeでは、何秒経過したかを記録するtime
と、STOPボタンが押されているかどうかを記録するisStop
の2つのプロパティを定義しています。
src/state/store.ts
:
import { Action } from 'redux'; import { TimerActions } from '../state/action'; export interface IAppState { time: number; isStop: boolean; } export const INITIAL_STATE: IAppState = { time: 0, isStop: false, };
Action
Actionでは、3つあるボタン(START, STOP, RESET)に対応した3メソッドを定義しています。
src/state/action.ts
:
import { Injectable } from '@angular/core'; import { Action, Store } from 'redux'; import { FluxStandardAction } from 'flux-standard-action'; import { dispatch, NgRedux } from '@angular-redux/store'; import { IAppState } from './store'; export type VoidAction = FluxStandardAction<void, void>; @Injectable() export class TimerActions { static readonly COUNT = 'COUNT'; static readonly STOP = 'STOP'; static readonly RESET = 'RESET'; @dispatch() count = (): VoidAction => ({ type: TimerActions.COUNT, payload: undefined, meta: undefined }) @dispatch() stop = (): VoidAction => ({ type: TimerActions.STOP, payload: undefined, meta: undefined }) @dispatch() reset = (): VoidAction => ({ type: TimerActions.RESET, payload: undefined, meta: undefined }) }
Reducer
count()
が呼び出されるたびに、Storeのtime
プロパティを+1ずつインクリメントしています。
src/state/reducer.ts
:
import { IAppState } from './store'; import { Action } from 'redux'; import { TimerActions, VoidAction, } from './action'; export function rootReducer( lastState: IAppState, action: Action ): IAppState { switch (action.type) { case TimerActions.COUNT: return { time: lastState.time += 1, isStop: false, }; case TimerActions.STOP: return { time: lastState.time, isStop: true, }; case TimerActions.RESET: return { time: 0, isStop: false, }; default: return lastState; } }
Appcomponent
本アプリケーションのキモであるAppComponentです。
isStop
がtrue
になっている状態(STOPボタンが押された)では、this.action.count()
を呼び出さないようにしています。
src/app/app.component.ts
:
import { Component, OnInit } from '@angular/core'; import { select } from '@angular-redux/store'; import { Observable } from 'rxjs/Observable'; import { TimerActions } from '../state/action'; import 'rxjs/add/observable/interval'; import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { @select() readonly time$: Observable<number>; @select() readonly isStop$: Observable<boolean>; subscription: Subscription; isStop: boolean; constructor( private action: TimerActions ) {} ngOnInit() { this.isStop$.subscribe(isStop => this.isStop = isStop); } countStart = (): void => { if (this.subscription) { this.subscription.unsubscribe(); } this.isStop = false; this.subscription = Observable.interval(1000) .subscribe(() => { if (this.isStop) { return; } this.action.count(); }); } callReset = (): void => { if (this.subscription) { this.subscription.unsubscribe(); } this.action.reset(); } }
概要で説明した通り、Observable.interval()
でカウントした結果をPipe
で変換することによって、デジタル形式で時刻を表示しています(例えば、カウント結果が70
だとするとデジタル形式では、00: 01 : 10
になる)。
src/app/app.component.html
:
<div class="timer-box"> <h1 class="timer">{{ (time$ | async) | digitalTimer }}</h1> <div> <button *ngIf="((isStop$ | async) || (time$ | async) === 0) else elseBlock" (click)="countStart()" class="start">START</button> <ng-template #elseBlock> <button (click)="action.stop()" class="stop">STOP</button> </ng-template> <button *ngIf="(isStop$ | async)" (click)="callReset()" class="reset">RESET</button> </div> </div>
Pipe
JavaScriptのDateを素のままで使うと少々厳しいので、moment.js
ライブラリーを使用しています。
# moment.jsをインストール $ npm install --save moment
import * as moment from 'moment';
の行でmoment.jsをインポートした後、
moment().hour(0).minute(0).second(time).format('HH : mm : ss')
の行でフォーマット変換を行なっています。
src/app/digital-timer-pipe.ts
:
import { Pipe, PipeTransform } from '@angular/core'; import * as moment from 'moment'; @Pipe({ name: 'digitalTimer' }) export class DigitalTimerPipe implements PipeTransform { transform(time: number): string { return moment().hour(0).minute(0).second(time).format('HH : mm : ss'); } }