Angular UI-Router v1さわる

  • 環境は angular-ui-router 1.0.0-rc.1。
  • v0.3からMigrateした
  • 新機能(uiCanExit, lazyLoadなど)も試したみた

ドキュメント読んだ

About

  • About - UI-Router
    • Core部分の実装をui-router-coreに分けた
    • AngularJS (1.x), Angular (2.0+), Reactもいける
  • UI-Router Features - UI-Router
    • 基本的な機能や名称は今までと同じ
      • States, Views, Urls, Parameters, Resolve などなど
    • Transitions
      • Stateの遷移をコントロールしたい場合Transition Lifecycle Hooksを使う(従来のState Change Eventsは利用しない)

Tutorials

ボリューム少ないので、さくっとさわれる。

Guide

  • URLs, ViewsがOptional
    • Although states generally have URLs, they are optional.

      You might create some child states without URLs, if it doesn’t make sense to make sense to bookmark those child states. The state machine transitions between url-less states as usual, but does not update the url when complete You still get all the other benefits of a state transition such as parameters, resolve, and lifecycle hooks.

Guide: UI-Router 1.0 Migration - UI-Router

New Features

HighChartsを動的読み込みする例

$stateProvider.state('chart', {
  url: 'chart',
  component: ChartComponent,

  // webpackを利用している場合
  lazyLoad: () => {
    return (<any>require).ensure([
      'highcharts'
      ], require => {
      (<any>window).Highcharts = require('highcharts');
    });
  }
});
  • redirectTo
  • uiCanExit
    • そのStateからExitできるのか、条件を設定できる
    • formが編集中なら確認画面をだす、などが可能(今まで$scope.$on('$stateChangeStart', (event, toState, toParams, fromState, fromParams) => ... などを使っていた場合は代替できる)
    • ルートに設定したComponent内で利用できる
  • Transition Hooks
    • 従来の$stateChangeStart/$stateChangeSuccessの代わりに利用できる
    • なれると便利
  • Resolveの実行順が整理された

Breaking changes

  • State Change Events($stateChangeStart/$stateChangeSuccessなど)が非推奨
    • デフォルトでは利用できなくなっている
    • 代わりにTransition Hookを利用する
    • 移行作業はここが一番大変(それでもそれほど時間がかかるわけではない)

Hookの設定

app.run(function($transitions) {
  /**
   * 第1引数で遷移対象を設定できる
   * 第2引数に`TransitionHookFn`を設定
   * `TransitionHookFn`は`HookResult`を返す
   *     HookResultについては -> https://ui-router.github.io/ng1/docs/latest/modules/transition.html#hookresult
   */
  $transitions.onStart({}, (trans) => {
    if (/* 何かしらの条件 */) {
      return /* true | false | void | TargetState | Promise<true | false | void | TargetState> */
    }
  });
})
  • onEnter/onExitの戻り値に意味があるようになったので、意図しない値を返却していないか確認
  • $stateParamsが非推奨になった
    • 動かないわけではないが、挙動がわかりにくいので代わりに$transition$を使ってね
  • Default Error Handler
    • 今まで見えなかったエラーがコンソールに表示されるかも
  • resolve inside views
    • マルチviewsを利用している場合に注意
    • 今までは各viewにresolveの設定が可能だったが、今後はviewsに一つのresolveになる(stateに一つのresolve)

などなど

Guide: Route to Component - UI-Router

  • コンポーネント指向のルーティングサンプル
  • Resolveされたデータ名とコンポーネントで利用する変数名が異なる場合
    • bindings: { users: 'userlist' }
    • (state定義内でマッピングする)
  • $transition$の渡し方
    • bindings: { $transition$: '<' }
  • 親子でのデータ、イベントのやり取り

webpackさわる

環境は webpack 2.2.1。

コード分割

アプリケーション用のjs (app.js)と、ライブラリをまとめたjs (vendor.js)を分割させたい場合。

webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry: {
    app: './src/app',

    // ここで設定したライブラリは、利用していなくても読み込まれるので注意
    vendor: [
      'lodash',
      'moment-timezone',
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: "[name].js",
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: Infinity
    })
  ]
};

動的読み込み

  • require.ensureを使えばおk
  • 古めのブラウザに対応させる場合、PromiseのPolyfillが必要なので注意
    • e.g. import 'es6-promise/auto';
  • オフィシャルドキュメント

webpack.config.js

var path = require('path');
var webpack = require('webpack');
module.exports = {
  entry: {
    app: './src/app',
  },
  output: {
    path: path.resolve(__dirname, 'dist'),

    // webpack.js.org/guides/code-splitting-require/#example
    // > output.publicPath is an important option when using code-splitting, it is used to tell webpack where to load your bundles on-demand, see the configuration documentation.
    publicPath: '/app/',

    filename: '[name].js',
    chunkFilename: '[name].js'
  }
};

app.js

// a.jsとb.jsを読み込む
// require.ensureはPromiseを返す
require.ensure(['./a', './b'], require => {
  // a.jsを実行
  // (requireしていないので、b.jsは実行されないまま)
  require('./a');
  console.log('loaded');
}, 'chunk');

TypeScriptを使う場合はrequireを定義する必要がある

// https://github.com/TypeStrong/ts-loader#loading-other-resources-and-code-splitting
declare var require: {
  <T>(path: string): T;
  (paths: string[], callback: (...modules: any[]) => void): void;
  ensure: (paths: string[], callback: (require: <T>(path: string) => T) => void) => void;
};

その他

moment読み込み時に全てのlocaleファイルを読み込んでしまう

  • ContextReplacementPluginを利用して回避
var webpack = require('webpack');
module.exports = {
  // ...
  plugins: [
    // moment/locale/ja.jsのみ読み込み
    new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /ja/)
  ]
};

シェルスクリプトを利用して、tsファイルの先頭にimport文を差し込む

tsファイル内に文字列angularが存在する場合、ファイルの先頭にimport * as angular from 'angular;を追加。

# 改行コード
LF=$'\\\x0A'

# 結果を配列に
files=(`find ./client/{app,components} -type f -name "*.ts" -print0 | xargs -0 grep "angular" -l`)

for i in "${files[@]}"

# 各ファイルの1行目に差し込む
do sed -i '' -e "1s/^/import * as angular from 'angular';"$LF"/" $i;

# 終了
done;

参考

GithubのLabel設定 Export, Import

Getting Started | GitHub Developer Guide

Create an OAuth token

2段階認証をしている場合。

curl -i -u ${your_username} -H "X-GitHub-OTP: ${your_2fa_OTP_code}" -d '{"scopes": ["repo"], "note": "labels"}' https://api.github.com/authorizations

https://github.com/settings/tokens からでもおk

Export

curl -i -H "Authorization: token ${token}" https://api.github.com/repos/:owner/:repo/labels

https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository

Import

curl -i -H "Authorization: token ${token}" -d '{ "name": "foo", "color": "ff0000" }' https://api.github.com/repos/${owner}/${repo}/labels
...

https://developer.github.com/v3/issues/labels/#create-a-label

テーブルとデータ

これは何か

データテーブルを表示する場合、どのようなデータの型が利用しやすいか、について整理してみました。

参考にしたのは以下2つのライブラリ

以下のようなテーブルを表示したい。

ID Date Name Age
1 March 5, 2017 Foo 26
2 March 6, 2017 Bar 31

データのみ用意

配列

const data = [
  [1, 'March 5, 2017', 'Foo', 26],
  [2, 'March 6, 2017', 'Bar', 31]
];
  • 一番シンプル
  • カラムタイトルなどは別途用意する必要がある(Htmlのテンプレート内など)

オブジェクト

const data = [
  { id: 1, date: 'March 5, 2017', name: 'foo', age: 26},
  { id: 2, date: 'March 6, 2017', name: 'bar', age: 31},
];
  • 1行目を利用して、カラムタイトルを作成
  • オブジェクトだからカラム項目の順番は保証できないはず

データとカラム用の設定データ

  • 汎用性が高いのはこっち

配列

const data = [
  [1, 'March 5, 2017', 'Foo', 26],
  [2, 'March 6, 2017', 'Bar', 31]
];
const columns = [
  { name: 'ID'},
  { name: 'Date'},
  { name: 'Name'},
  { name: 'Age'}
];

オブジェクト

const data = [
  { id: 1, date: 'March 5, 2017', name: 'foo', age: 26},
  { id: 2, date: 'March 6, 2017', name: 'bar', age: 31},
];
const columns = [
  { field: 'id', name: 'ID' },
  { field: 'date', name: 'Date' },
  { field: 'name', name: 'Name' },
  { field: 'age', name: 'Age' }
];
  • カラムタイトル(name)とプロパティ名(field)を別にできる
const data = [
  { id: 1, date: 'March 5, 2017', customer: { name: 'foo', age: 26 } },
  { id: 2, date: 'March 6, 2017', customer: { name: 'bar', age: 31} },
];
const columns = [
  { field: 'id', name: 'ID' },
  { field: 'date', name: 'Date' },
  { field: 'customer.name', name: 'Name' },
  { field: 'customer.age', name: 'Age' }
];
  • データのネストなども対応可能

複雑なテーブル

カラム設定データを用意する場合、以下の機能の実現も汎用的にできそう。

  • ソート機能のオンオフ
  • 合計値の表示
  • 平均値の表示
  • データと表示内容の変更
    • 2017-03-05 -> March 5, 2017 or 2017年3月5日

UI-Gridだとこのような感じで設定を追加しています。

// http://ui-grid.info/docs/#/tutorial/105_footer

$scope.gridOptions = {
    showGridFooter: true,
    showColumnFooter: true,
    enableFiltering: true,
    columnDefs: [
        { field: 'name', width: '13%' },
        { field: 'address.street',aggregationType: uiGridConstants.aggregationTypes.sum, width: '13%' },
        { field: 'age', aggregationType: uiGridConstants.aggregationTypes.avg, aggregationHideLabel: true, width: '13%' },
        { name: 'ageMin', field: 'age', aggregationType: uiGridConstants.aggregationTypes.min, width: '13%', displayName: 'Age for min' },
        { name: 'ageMax', field: 'age', aggregationType: uiGridConstants.aggregationTypes.max, width: '13%', displayName: 'Age for max' },
        { name: 'customCellTemplate', field: 'age', width: '14%', footerCellTemplate: '<div class="ui-grid-cell-contents" style="background-color: Red;color: White">custom template</div>' },
        { name: 'registered', field: 'registered', width: '20%', cellFilter: 'date', footerCellFilter: 'date', aggregationType: uiGridConstants.aggregationTypes.max }
    ],
    data: data,
    onRegisterApi: function(gridApi) {
            $scope.gridApi = gridApi;
    }
};

angular-translate読み直し

半年前にまとめたもの。こっちに転載。


これは何か

設定例

/*@ngInject*/
export function translateConfig($translateProvider) {
  $translateProvider

  // angular-translate-loader-static-files
  // assets/lang/[langKey].json から非同期で言語ファイルを読み込む
  .useStaticFilesLoader({
    prefix: 'assets/lang/',
    suffix: '.json'
  })

  // 言語keyの設定
  .registerAvailableLanguageKeys(['en', 'ja'], {
    // en_US などは en と認識させる
    'en_*': 'en',
    'ja_*': 'ja',
    // そのほかの言語keyは全て en にする
    '*': 'en'
  })

  // ブラウザ環境から優先言語を自動取得
  // (以前に設定されたkeyがあれば、そちらを優先する <- angular-translate-storage-local利用時)
  .determinePreferredLanguage()

  // enable escaping of HTML
  // https://angular-translate.github.io/docs/#/guide/19_security
  .useSanitizeValueStrategy('escape')

  // angular-translate-handler-log
  .useMissingTranslationHandlerLog()

  // angular-translate-storage-local
  .useLocalStorage();
}

以下3つのextensionを利用している

  • angular-translate-loader-static-files
    • 非同期読み込み
  • angular-translate-handler-log
    • 対応するメッセージがない場合にエラーを表示
  • angular-translate-storage-local
    • 前回選択した言語コードを保存
    • (fallback で cookie を利用しているので angular-translate-storage-cookie も読み込まれる)

逆引き

言語ファイル(JSON)の書き方

  • 通常:
{
  "TRANSLATION_ID": "This is a concrete translation for a specific language."
}
  • ショートカット:
// bar.foo でアクセス可
{
  "bar": {
    "foo": {
      "foo": "This is my text."
    }
  }
}
  • リンク(いい機能だなあ):
{
  "SOME_NAMESPACE": {
    "OK_TEXT": "OK"
  },
  "ANOTHER_NAMESPACE": {
    "OK_TEXT": "@:SOME_NAMESPACE.OK_TEXT"
  }
}

参考: https://angular-translate.github.io/docs/#/guide/02_getting-started

$translate利用時に、非同期読み込みされたメッセージを反映

  • Two-way binding されないので 明示的に処理を書く必要がある
    • $translateChangeSuccessを利用
/*@ngInject*/
app.controller('Ctrl', ($scope, $translate, $rootScope) => {
  $rootScope.$on('$translateChangeSuccess', () => {
    $translate('HEADLINE').then(
      // 成功
      translation => $scope.headline = translation,
      // 失敗
      translationId => $scope.headline = translationId
    );
  });
});

参考: https://angular-translate.github.io/docs/#/guide/03_using-translate-service

変数を利用

言語JSONファイル:

{
  "TRANSLATION_ID": "{{username}} is logged in."
}

$translate利用時

$translate('TRANSLATION_ID', { username: 'PascalPrecht' });

$filter利用時

パターン1:

<div>{{ 'TRANSLATION_ID' | translate:'{ username: "PascalPrecht" }' }}</div>

パターン2:

<div>{{ 'TRANSLATION_ID' | translate:translationData }}</div>
angular.module('myApp')
/*@ngInject*/
.controller('Ctrl', ($scope) => { 
  $scope.translationData = {
    username: 'PascalPrecht'
  };
});

そのほかの方法もある

参考: https://angular-translate.github.io/docs/#/guide/06_variable-replacement

設定されている言語を取得

$rootScope.$on('$translateChangeEnd', (event, args) => {
  // args.language
});
// or
$translate.proposedLanguage() ||  $translate.use()
// proposedLanguage() -> 非同期読み込み中の言語keyを取得

利用言語を設定する

  • preferredLanguageを利用
  • ブラウザ環境から優先言語を自動取得する場合はdeterminePreferredLanguageを利用
    • (内部的にnavigator.languages[0] navigator.languageなどを利用している)

参考: https://angular-translate.github.io/docs/#/guide/07_multi-language

en_US en_SGなどをenとして扱う

  • registerAvailableLanguageKeysを利用

参考: https://angular-translate.github.io/docs/#/guide/09_language-negotiation

Unit Testing

非同期読み込みをしている場合、ローダー書き換える

beforeEach(module('app', ($provide, $translateProvider) => {
  $provide.factory('customLoader', $q => 
    () =>  $q.resolve()
  );
  $translateProvider.useLoader('customLoader');
}));

アプリを起動させる時の流れ

  • we register a asynchronous loader
  • we define our preferred language
  • $translate service is instantiated the first time it gets injected
  • angular-translate notices that there’s no language locally available
  • it looks if there’s a registered asynchronous loader
  • the asynchronous loader is called with the preferred language locale
  • the translation data is loaded and ready to be used

参考: https://angular-translate.github.io/docs/#/guide/22_unit-testing-with-angular-translate