カレンダー生成

生成の流れ

  1. 対象月の日数を調べる
  2. カレンダー上で、1日の前に何日(何マス)あるか
  3. カレンダー上で、最終日の後ろに何日(何マス)あるか
  4. カレンダーに表示される日数を調べる
    • 前月、翌月も含んだ数
  5. 日数分の配列を生成
  6. chunkする
    • 最終的に[Array(7), Array(7), Array(7), Array(7), Array(7)]のような配列になる

サンプル

週の開始を変更したい場合

import chunk from 'lodash/fp/chunk';
import compose from 'lodash/fp/compose';
import map from 'lodash/fp/map';
import range from 'lodash/fp/range';
import * as moment from 'moment';

const DAYS_PER_WEEK = 7;

export function createCalendar(
  m = moment(),

  // 週の最初の曜日
  // default(0)は日曜
  startDayOfWeek = 0) {

  // 指定した月の1日
  const startOfMonth: moment.Moment = m.clone().startOf('month');

  // 月の日数
  const daysInMonth: number = m.clone().endOf('month').date();

  // カレンダー上で、1日の前に何日(何マス)あるか
  const daysBefore: number = getDaysBefore(startOfMonth, startDayOfWeek);

  // カレンダー上で、最終日の後ろに何日(何マス)あるか
  const daysAfter: number = getDaysAfter(daysBefore, daysInMonth);

  // カレンダーに表示する日数分の配列を生成
  const diffs: number[] = getDiffs(daysBefore, daysInMonth, daysAfter);

  return compose(
    chunk(DAYS_PER_WEEK),
    map((diff) => startOfMonth.clone().add(diff, 'd'))
  )(diffs);
}

function getDaysBefore(startOfMonth: moment.Moment, startDayOfWeek: number): number {
  const dayOfWeek = startOfMonth.day();
  return (dayOfWeek + DAYS_PER_WEEK - startDayOfWeek) % DAYS_PER_WEEK;
  // if (dayOfWeek > startDayOfWeek) {
  //   return dayOfWeek - startDayOfWeek;
  // } else {
  //   return dayOfWeek + DAYS_PER_WEEK - startDayOfWeek;
  // }
}

function getDaysAfter(daysBefore: number, daysInMonth: number): number {
  return DAYS_PER_WEEK - (daysBefore + daysInMonth) % DAYS_PER_WEEK;
}

function getDiffs(daysBefore, daysInMonth, daysAfter): number[] {
  return range((daysBefore * -1), daysInMonth + daysAfter)
}

AngularのPipe

(Angular v.4.3.6を利用)

これは何か

Angular (4+)を始めたのでドキュメントを読んでいます。今回はPipeについて。
内容は以下ドキュメントからの抜粋のようなものです。

概要

  • PipeはHtmlテンプレート内でデータを意図した形式に変換する仕組み
<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>

Built-in

カスタムパイプ

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'custom'
})
export class CustomPipe implements PipeTransform {

  transform(value: any, args?: any): any {
    return null;
  }

}
  • Style Guide的にはShared feature moduleに入れる

Change detection

  • Pipeは通常のChange detectionと仕様が異なる
  • 例えば、配列に要素を追加しただけではPipeは実行されない
    • 参照を変更する必要がある。新しい配列で置き換えるた場合、Pipeは実行される
<!-- heroが追加されれば新しいhero.nameが表示される -->
<div *ngFor="let hero of heroes">
  {{hero.name}}
</div>

<!-- heroが追加されても表示は変わらない -->
<div *ngFor="let hero of (heroes | fooPipe)">
  {{hero.name}}
</div>

上記のような場合の対応策

  • impure pipeにする
  • Pipeを利用しないでComponent内で処理をする
    • The Angular team and many experienced Angular developers strongly recommend moving filtering and sorting logic into the component itself

Impure pipes

  • pure: falseにする
@Pipe({
  name: 'fooImpure',
  pure: false
})
export class FooImpurePipe extends FooPipe { }
  • pure pipepure changeを見つけたときに実行される
    • A pure change is either a change to a primitive input value (String, Number, Boolean, Symbol) or a changed object reference (Date, Array, Function, Object).

  • impure pipeの場合
    • Angular executes an impure pipe during every component change detection cycle. An impure pipe is called often, as often as every keystroke or mouse-move.

AsyncPipe

ServiceやComponent内でも利用したい

import { DatePipe } from '@angular/common';
class MyService {

  constructor(private datePipe: DatePipe) {}

  transformDate(date) {
    this.datePipe.transform(myDate, 'yyyy-MM-dd');
  }
}

DatePipeのlocaleを変更したい場合

その他

AngularのHttpClientメモ

(Angular v.4.3.6を利用)

参考

データを取得する

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';

export interface Book {
  id?: number;
  title: string;
  author: string;
}

const url = 'http://localhost:3000';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  book: Book;

  constructor(
    private http: HttpClient
  ) { }

  ngOnInit(): void {
  }

  get(id: number) {
    this.http.get<Book>(`${url}/posts/${id}`)
      .subscribe(data => {
        this.book = data;
      }, (err: HttpErrorResponse) => {

        // エラー処理
        if (err.error instanceof Error) {

          // A client-side or network error occurred. Handle it accordingly.
          console.log('An error occurred:', err.error.message);
        } else {

          // The backend returned an unsuccessful response code.
          // The response body may contain clues as to what went wrong,
          console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
        }
      });
  }
}

レスポンスデータの詳細を見る

this.http
  .get<Book>(`${url}/posts/1`, { observe: 'response' })
  .subscribe(resp => {
    console.log(resp);
  });

retry

RxJSにretryというオペレータがある。

import 'rxjs/add/operator/retry';
this.http
  .get<Book>(`${url}/posts/${id}`)

  // Retry this request up to 3 times.
  .retry(3)

  // Any errors after the 3rd retry will fall through to the app.
  .subscribe(...);

データを送信する

const body: Book = {
  title: 'foo',
  author: 'bar'
};
this.http
  .post<Book>(`${url}/posts`, body)
  .subscribe(resp => {
    this.book = resp;
  });

Headersを追加

this.http
  .post(`${url}/posts`, body, {
    headers: new HttpHeaders().set('Authorization', 'my-auth-token'),
  })
  // ...

URL Parametersを追加

this.http
  .post(`${url}/posts`, body, {
    params: new HttpParams().set('param', 'value')
  })
  // ...

進捗を確認

const body: Book = {
  title: 'foo' + new Date().valueOf(),
  author: 'bar' + new Date().valueOf()
};
const req = new HttpRequest('POST', `${url}/posts`, body, {
  reportProgress: true,
});

this.http.request(req).subscribe(event => {

  // Via this API, you get access to the raw event stream.
  // Look for upload progress events.
  if (event.type === HttpEventType.UploadProgress) {

    // This is an upload progress event. Compute and show the % done:
    const percentDone = Math.round(100 * event.loaded / event.total);
    console.log(`File is ${percentDone}% uploaded.`);
  } else if (event instanceof HttpResponse) {
    console.log('File is completely uploaded!');
  }
});

https://angular.io/guide/http#listening-to-progress-events

応用

Interceptorサービスを登録して、リクエスト・レスポンスに処理を追加できる。

import {NgModule} from '@angular/core';
import {HTTP_INTERCEPTORS} from '@angular/common/http';

@NgModule({
  // Interceptorは複数登録可能
  // 登録順に処理される
  providers: [{
    provide: HTTP_INTERCEPTORS,
    useClass: NoopInterceptor,
    multi: true,
  }],
})
export class AppModule {}

Interceptorを利用してヘッダーを追加

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private auth: AuthService) { }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authHeader = this.auth.getAuthorizationHeader();

    // HttpRequestはイミュータブル(直接、値を変更できない)
    const authReq = req.clone({ headers: req.headers.set('Authorization', authHeader) });
    return next.handle(authReq);
  }
}

Interceptorを利用してログを表示

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  const started = Date.now();
  return next
    .handle(req)

    // Rxのstreamに影響を与えずに処理を追加
    .do(event => {
      if (event instanceof HttpResponse) {
        const elapsed = Date.now() - started;

        // 経過時間を表示
        console.log(`Request for ${req.urlWithParams} took ${elapsed} ms.`);
      }
    });
}

https://angular.io/guide/http#logging

Interceptorを利用してキャッシュ機能を実装

constructor(private cache: HttpCache) { }

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

  if (req.method !== 'GET') {
    return next.handle(req);
  }

  const cachedResponse = this.cache.get(req);
  if (cachedResponse) {
    return Observable.of(cachedResponse);
  }

  // キャッシュがなかった場合
  return next.handle(req).do(event => {
    // Remember, there may be other events besides just the response.
    if (event instanceof HttpResponse) {
      this.cache.put(req, event);
    }
  });
}
/**
 * 以前にキャッシュされていれば、2つのResponseを返す
 */
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  if (req.method !== 'GET') {
    return next.handle(req);
  }

  // This will be an Observable of the cached value if there is one,
  // or an empty Observable otherwise. It starts out empty.
  let maybeCachedResponse: Observable<HttpEvent<any>> = Observable.empty();

  const cachedResponse = this.cache.get(req);
  if (cachedResponse) {
    maybeCachedResponse = Observable.of(cachedResponse);
  }

  const networkResponse = next.handle(req).do(event => {
    if (event instanceof HttpResponse) {
      this.cache.put(req, event);
    }
  });

  return Observable.concat(maybeCachedResponse, networkResponse);
}}

https://angular.io/guide/http#caching

XSRF

テスト

Angularでi18n

(Angular v.4.3.6を利用)

Angular CLI環境でアプリケーションのi18n対応をしました。

準備

翻訳対象(HTMLのタグ)にi18n属性を設定

<h1 i18n="site header|An introduction header for this sample@@introductionHeader">Hello i18n!</h1>
  • i18n=<meaning>|<description>@@<id>
  • 値は必須ではない
  • メンテナンスを考慮した場合、IDは入力しておいた方がいい(未入力の場合、メッセージがかわるとIDも変わってしまうので)

XLIFF出力例

<trans-unit id="introductionHeader" datatype="html">
  <source>Hello i18n!</source>
  <context-group purpose="location">
    <context context-type="sourcefile">app/app.component.ts</context>
    <context context-type="linenumber">4</context>
  </context-group>
  <note priority="1" from="description">An introduction header for this sample</note>
  <note priority="1" from="meaning">site header</note>
</trans-unit>

要素を追加しない方法

<!--i18n: optional meaning|optional description -->
I don't output any element either
<!--/i18n-->

属性値を翻訳

<img [src]="logo" i18n-title title="Angular logo">

複数形、代替テキスト

(あとで確認する)

ファイル(XLIFF)の書き出し

Angular CLI利用している場合。

ng xi18n --output-path src/locale

XLIFFファイルについて

In the real world, you send the messages.es.xlf file to a Spanish translator who fills in the translations using one of the many XLIFF file editors.

https://angular.io/guide/i18n#translate-text-nodes

Serve

ng serve --aot --locale en --i18n-format xlf --i18n-file src/locale/messages.en.xlf

  • --i18n-file Localization file to use for i18n.
  • —i18n-format Format of the localization file specified with –i18n-file.
  • —locale Locale to use for i18n.

Note: this only works for AOT, if you want to use i18n in JIT you will have to update your bootstrap file yourself.

https://github.com/angular/angular-cli/wiki/stories-internationalization#serve

Build

ng build -prod --output-path dist/en --locale en --i18n-format xlf --i18n-file src/locale/messages.en.xlf

  • 基本はServeと同じ
  • --output-path dist/enを追加
    • 出力先を変更

Report missing translations

(あとで確認する)

参考

(WIP) NgUpgradeのドキュメント読む

読んだドキュメント

実際に利用したか

  • すみません。自分たちのプロジェクトには利用していません。

理由

  • しばらくは大きな改善や機能追加がなさそうなので、移行作業に集中する時間を確保できそうだった。
    • これが主な理由
  • 今のAngularJSアプリケーションにAngularを導入することは結構簡単。ただ、同時にアップデートしたいことがいくつかあった。
    • Angualr CLI環境にしたい
    • 利用しているCSSフレームワークをアップデートしたい
    • UI RouterからAngular Routerに変更したい
  • 多言語対応しているアプリなので、対応が面倒になりそう。
    • (そうでもないかもしれない。この時点であまり深く考えなくなった。)

以下メモです。

NgUpgrade in Depth

NgUpgrade in Depth – nrwl

Bootstrapping

This is a good default because components upgraded from AngularJS to Angular require an AngularJS ancestor component, and this way of bootstrapping guarantees that.

コンポーネントツリーの上流にAngularJSコンポーネントが必要。

Capturing AngularJS and Lazy Loading

“@angular/upgrade/static” captures window.angular.

import 'angular';
import { UpgradeModule } from '@angular/upgrade/static';

angularを先に読み込む必要がある。

Component Integration

This creates an AngularJS directive with the appRoot selector. This directive will use AppComponent to render its template. Because of this indirection, we need to register AppComponent as an entry component.

(AngularJSにするコンポーネントをエントリーコンポーネントに登録する理由)

So the <app-root> element itself is owned by AngularJS, which means that we can apply other AngularJS directives to it. Its template, however, is rendered using Angular.

したがって、<app-root>要素自体はAngularJSによって所有されています。つまり、他のAngularJSディレクティブを適用することができます。ただし、そのテンプレートはAngularを使用してレンダリングされます。

UI Routerの場合

GitHub - ui-router/angular-hybrid: Upgrade an ng1 UI-Router app to a ng1+ng2 hybrid using ng-upgrade

We currently support routing either Angular (2+) or AngularJS (1.x) components into an AngularJS (1.x) ui-view. However, we do not support routing AngularJS (1.x) components into an Angular (2+) ui-view.

現在Angular(2+)またはAngularJS(1.x)コンポーネントをAngularJS(1.x)UIビューにルーティングしています。ただし、AngularJS(1.x)コンポーネントをAngular(2+)UIビューにルーティングすることはサポートしていません。

ルーターを利用する場合

記事ではルータを利用していない。実際のアプリケーションでは以下対応が必要。

Component Router does not work with Upgrade Adapter · Issue #9870 · angular/angular · GitHub

export class AppComponent implements OnInit {
  constructor(
    router: Router
  ) {
    router.initialNavigation();
  }
  // ...
}

Angular - Upgrading from AngularJS

Angular Docs

  • バージョン違いのコンポーネントをどのように両立、橋渡しするか
  • テンプレートの中の世界(AngularJSなのかAngualrなのか)は、そのコンポーネントが何でつくられているかによる
  • 「アップグレードする、ダウングレードするとは、対象のテンプレート内で利用できるようにすること」と考えるとすっきりする

The DOM element <a-component> will remain to be an AngularJS managed element, because it’s defined in an AngularJS template. That also means you can apply additional AngularJS directives to it, but not Angular directives. It is only in the template of the <a-component> where Angular steps in. This same rule also applies when you use AngularJS component directives from Angular.

DOM要素<a-component>は、AngularJSテンプレートで定義されているため、AngularJSが管理する要素のままです。 つまり、AngularJSディレクティブを追加できますが、Angularディレクティブは適用できません。 <a-component>のテンプレートにのみ、Angularが入る。これは、AngularのAngularJSコンポーネントディレクティブを使用する場合にも適用されます。

Note that you do not add a bootstrap declaration to the @NgModule decorator, since AngularJS will own the root template of the application.

AngularJSはアプリケーションのルートテンプレートを所有するためbootstrap、@NgModuleデコレータに宣言を追加しないことに注意してください。

TSlintで、特定ファイルや特定行のみ設定を変更したい

ライブラリやレスポンスデータの関係で対応が難しい場合や、他環境にあったファイルを一旦そのまま利用したい場合などに。

https://palantir.github.io/tslint/usage/rule-flags/

// ファイル全体に以下設定を反映
/* tslint:disable:no-null-keyword */
// 次の行のみ、以下設定を反映
/* tslint:disable-next-line:directive-selector */