テキストを色々なケースに変換する Angular アプリを作った

最近精力的に Angular Utilities を作っていた。今回は、入力したテキストをスネークケースにしたり、キャメルケースにしたりするツールを作った。

ケース変換に使ったのは Lodash。Underscore.js をベースにした汎用ライブラリの、文字列操作部分を利用した。

まんま Lodash#camelCase() などのメソッドが存在しているので、ほとんどコレを叩いてやるだけ。

ただ、表示欄を沢山作って、それぞれに値をバインドするのをベタ書きで作るのは面倒なので、以下のようなオブジェクトを用意し、ループによって描画や処理をさせるようにしてみた。

@Component({ /* 省略 */ })
export class CaseConverterComponent implements DoCheck {
  /** 入力値 */
  public input: string = '';
  
  /**
   * 様々なケースに変換するオブジェクトたち
   * 
   * - title : 画面に表示する「変換方法」のラベル
   * - value : テキストエリアに表示する「変換結果」
   * - transform : 行データを変換し返却する関数
   * - isCollapsed : 行を縮小表示するかどうか
   */
  public cases: any = {
    pascal    : { title: 'パスカルケース',         value: '', transform: line => _.upperFirst(_.camelCase(line)), isCollapsed: false },
    camel     : { title: 'キャメルケース',         value: '', transform: line => _.camelCase(line)              , isCollapsed: false },
    kebab     : { title: 'ハイフンケース',         value: '', transform: line => _.kebabCase(line)              , isCollapsed: false },
    snake     : { title: 'スネークケース',         value: '', transform: line => _.snakeCase(line)              , isCollapsed: false },
    upperSnake: { title: 'アッパースネークケース', value: '', transform: line => _.toUpper(_.snakeCase(line))   , isCollapsed: false },
    lower     : { title: 'ロウワーケース',         value: '', transform: line => _.lowerCase(line)              , isCollapsed: false },
    upper     : { title: 'アッパーケース',         value: '', transform: line => _.upperCase(line)              , isCollapsed: false }
  };
  
  /**
   * 変更を監視してケースを変換する
   */
  public ngDoCheck(): void {
    // 入力値を行ごとに配列に分割する
    const lines = this.input.split('\n');
    // ケースごとに処理する
    Object.keys(this.cases).forEach((key) => {
      this.cases[key].value = lines.map((line) => {
        // 行ごとに所定の変換メソッドを利用する
        return this.cases[key].transform(line);
      }).join('\n');  // 配列を結合して返す
    });
  }
}

このようにすると、cases.pascal.valuecases.camel.value が変換後の値を持つことになる。

ただ、今回、メンバ変数 cases を Array ではなく Object で作ったので、画面側で *ngFor によるループ表示ができない。連想配列を順に操作する時は、

Object.keys(this.cases).forEach((key) => {
  this.cases[key];  // コレで「pascal」や「camel」にアクセスできる
});

このように Object.keys() を使う必要がある。

コレを画面側で実現するために、連想配列を Angular パイプで渡し、キーの配列を返す、独自の「KeyPipe」なるものを作った。

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

/** 連想配列のキーの配列を返すパイプ : オブジェクトを *ngFor でループさせたい時に */
@Pipe({ name: 'keys' })
export class KeysPipe implements PipeTransform {
  transform(value: any, args?: any): any {
    return Object.keys(value);
  }
}

パイプはコレだけ。

HTML 側では、このパイプを利用して以下のようにケース変換結果欄を作っている。

<!-- さきほどのパイプを使い、cases のキーを順にループする -->
<tr *ngFor="let key of cases | keys">
  <!-- ラベルのセルをクリックすると、その行の高さを縮めたりできる -->
  <td (click)="cases[key].isCollapsed = !cases[key].isCollapsed">
    <!-- ラベルを表示する -->
    {{ cases[key].title }}
  </td>
  <td>
    <!-- 変換結果を表示。ngStyle を利用して height 値を動的に書き換える -->
    <textarea [value]="cases[key].value" readonly
              [ngStyle]="{ 'height': cases[key].isCollapsed ? '2.5em' : '4em' }">
    </textarea>
  </td>
</tr>

こんな感じ。KeyPipe なんて Angular 組み込みで作っておいてくれても良さそうなのにね〜。というか cases を連想配列にする必要がなかった…!?w