daikiojm’s diary

ブログを書くときがやってきたどん!

Angularで画像の遅延読み込み(ng-lazyload-image)

ng-lazyload-imageを使って、スクロールに応じた画像の遅延読み込みをしてみる。
今回はわかり易い例として、スクロールに応じてグリッド配置した画像を例にした。
レスポンシブのグリッドを作るために、AngularMaterialの grid-list も使っています。

デモ: AngularLazyloadGridimageDemo

ng-lazyload-imageの導入

まずは、Angularプロジェクトにインストール

$ npm install ng-lazyload-image --save

app.moduleにインポートしておく。
(別途モジュール分割している場合は、インポートする先が変わってくる)

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { LazyLoadImageModule } from 'ng-lazyload-image'; // 追加
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    LazyLoadImageModule // 追加
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

グリッド画像に遅延ローディングを設定する

テンプレート側では、<img> タグのlazyLoadディレクティブに画像URLを指定します。
src属性の代わりに、lazyLoadを使うだけなので、使い方はいたって簡単です。

app.component.html

<mat-grid-list cols="4" rowHeight="1:1">
  <mat-grid-tile *ngFor="let item of range(150)">
    <img lazyLoad="assets/images/nattouIMGL3800_TP_V.jpg">
  </mat-grid-tile>
</mat-grid-list>

また、lazyloadの文脈からは外れますが、サンプルの画像をリピートするためのメソッドは、次の記事を参考にAngular 2以降で動作するようにしています。
AngularJS tips - ng-repeat で配列ではなく数値で for ループする方法

app.component.ts

...
export class AppComponent {

  range(n): number[] {
    const arr = [];
    for (let i = 0; i < n; ++i) {
      arr.push(i);
    }
    return arr;
  }
}

ここまでの手順で、スクロールに応じて画像を遅延読込することが出来るのですが、「ふわっ」と徐々に画像を表示させるアニメーションを加えるためのCSSを追加します。
ng-lazyloaded というクラスは、ng-lazyload-imageによって画像が完全に読み込まれたタイミングで動的に付加されるクラスです。

app.component.css

img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* lazyload */
  transition: opacity 1s;
  opacity: 0;
}

img.ng-lazyloaded {
  opacity: 1;
}

サンプルのリポジトリ

GitHub - daikiojm/angular-lazyload-gridimage-demo: A sample repository for ng-lazyload-image + Grid list(AngularMaterial)

参考

https://www.pakutaso.com/20180132010nato.html
http://phiary.me/angularjs-ng-repeat-for-loop-with-numbers/
https://www.webcreatorbox.com/tech/object-fit

Angular Animationsを初めて使ってみた

Angularを使ったWebアプリを作り際も、CSSのアニメーションしか使わない場合が多かったのですが、Angular Routerと連携したアニメーションや複数の要素に対するアニメーションなどを実現するためにAngular Animationsを使ってみることにしました。

そもそも今まで触ったことなかったので、気にしてなかったけど、どうやらv4.2以上ではアニメーション関連が強化されているらしい。 今回紹介する内容では、v5.1.1を使っています。

今回は、単一のコンポーネントにごく単純なFade In-Outアニメーションをさせてみたいと思います。 具体的には次のようにぬるっと表示される詳細検索パネル(風)のものです。

f:id:daikiojm:20171223230440g:plain

事前準備

今回は、次のコマンドでサンプルのプロジェクトを作成しました。

$ ng new expansion-panel --inline-style --inline-template

また、app.moduleにBrowserAnimationsModuleをインポートしておきます。

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

...
  imports: [
    BrowserModule,
    BrowserAnimationsModule
  ],
...

検索ボックス

まずは検索ボックスです。
テンプレートには、input要素と、buttonを配置しているだけです。 「詳細」ボタンを押した際に、親のコンポーネントにボタンが押されたことを通知したため、OutputEventEmitterを使っています。  

search.component.ts

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-search',
  template: `
    <div>
      <form>
        <input type="text">
      </form>
      <button (click)="onClickPanelOpen()">詳細</button>
    </div>
  `,
  styles: [`
    :host {
      position: fixed;
      top: 50;
      z-index: 99;
      left: 50%;
      margin-left: -40%;
      width: 80%;
    }
    input {
      padding: 0;
      font-size: 1.3em;
      font-family: Arial, sans-serif;
      color: #aaa;
      border: solid 1px #ccc;
      margin: 0;
      height: 38px;
      width: 100%;
      background-color: #ffffff;
    }
    button {
      width: 40px;
      font-size: .3em;
      position: relative;
      top: -36px;
      left: 85%;
    }
  `]
})
export class SearchComponent {
  @Output() panelOpen = new EventEmitter<boolean>();
  private panelState = false;

  onClickPanelOpen() {
    this.panelState = !this.panelState;
    this.panelOpen.emit(this.panelState);
  }
}

詳細検索パネル

こちらは、詳細検索パネル(風)です。今回は、アニメーションの例なので詳細検索に関する機能は省いていますが...
親のコンポーネントで見通しが良くなるよう別ファイルにしていますが、特にロジックを持たないコンポーネントです。

panel.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-panel',
  template: `
    <p>詳細検索</p>
  `,
  styles: [`
    :host {
      position: fixed;
      top: 48px;
      z-index: 99;
      left: 50%;
      padding: 0;
      margin-left: -40%;
      width: 80%;
      height: 400px;
      border: 1px solid #cccccc;
      box-shadow: 0px 4px 4px -2px #cccccc;
    }
    p {
      text-align: center;
      margin-top: 180px;
    }
  `]
})
export class PanelComponent { }

app.component

次にapp.componentです。これは、ng newした際に作成されるものを編集して使っています。
アニメーションの定義もこのコンポーネントで行っています。
検索ボックスから受けたpanelOpenイベントに対してonChangePanelState()メソッドをバインドしています。

app.component.ts

import { Component } from '@angular/core';
import { trigger, state, transition, style, animate } from '@angular/animations';

@Component({
  selector: 'app-root',
  template: `
    <app-search (panelOpen)="onChangePanelState()"></app-search>
    <app-panel @panelOpenTrigger *ngIf="panelState"></app-panel>
  `,
  styles: [],
  animations: [
    trigger('panelOpenTrigger', [
      state('void', style({ opacity: 0.4 })),
      state('*', style({ opacity: 1 })),
      transition('* <=> *', animate('0.2s ease-in-out'))
    ])
  ]
})
export class AppComponent {
  panelState: boolean;

  onChangePanelState() {
    this.panelState = !this.panelState;
  }
}
  • テンプレート内の@panelOpenTrigger、triggerのpanelOpenTrigger
    • テンプレート内にトリガーを定義してanimationsのtriggerと関連付けを行っています。
  • state('void', style({ opacity: 0.4 })),
    • アニメーション前の初期状態のstyleを定義しています。 state()の第一引数voidコンポーネントが表示されていない状態を表しています。
  • state('*', style({ opacity: 1 })),
    • アニメーション後のstyleを定義しています。*コンポーネントが表示された際の状態を示しています。
  • transition(' <=> ', animate('0.2s ease-in-out'))
    • transition()メソッドの第一引数には上記のstateに定義した状態の移り変わりを定義しますが、今回は* <=> *で全ての状態の移り変わりをハンドリングするよう設定しています。今回の場合、パネルを開く時、閉じるときで同様のアニメーションを実行します。
    • animate()メソッドで実際のアニメーションを定義しています。ここでは、0.2秒かけてstyleがstate()で定義した状態に変化します。ease-in-outはイージングの種類です。

所感

Angular CDKのExpansion Panelでもそれぽいことができたかもしれない。
そもそもこれぐらいならCSSだけでも出来るかもしれないけど...Angular Animationsの取っ付きとしてはよかったかなと。

参考

AngularCLIで単一ファイルコンポーネント

AngluarCLIでComponentを作成する際にVue.jsの単一ファイルコンポーネントっぽく、1Component1ファイルとする方法を紹介します。

やってみる

コマンド実行時のオプションで指定

コンポーネント作成(デフォルト)

$ ng g c <コンポーネント名>

コンポーネント作成(単一ファイルコンポーネント)

$ ng g c --inline-style --inline-template <コンポーネント名>

--inline-style --inline-templateオプションを付けて実行することでcss、htmlの内容がtsファイルにインラインで記述されます。

※ gはgenerateのエイリアス
※ cはcomponentのエイリアス

.angular-cli.jsonに設定を記述

事前にプロジェクトの設定ファイルに記述しておく方法です。

.angular-cli.json(デフォルト)

...
  "defaults": {
    "styleExt": "css",
    "class": {
      "spec": false
    },
    "component": {}
  }
}

.angular-cli.json(単一ファイルコンポーネント)

...
  "defaults": {
    "styleExt": "css",
    "class": {
      "spec": false
    },
    "component": {
      "inlineStyle": true,
      "inlineTemplate": true
    }
  }
}

以上です。

確認

オプション指定、もしくは.angular-cli.jsonに設定を記述した状態でComponentを作成してみます。

f:id:daikiojm:20171202140127p:plain

作成されたコンポーネントを確認すると、一つのtsファイルにstyleとテンプレートがまとまっているのが分かるかと思います。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-one-file',
  template: `
    <p>
      one-file works!
    </p>
  `,
  styles: []
})
export class OneFileComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

参考

https://github.com/angular/angular-cli/wiki/generate-component

VSCodeでnode_modules以下を素早く覗ける「Search node_modules」が便利

JavaScriptでフロントエンドの開発やNode.jsでの開発をしている時、npm パッケージの中を覗きたいことがあると思います。
そんな時、Visual Studio Codeのプラグイン Search node_modulesを入れておくと素早くnpmパッケージ内のファイルにアクセスすることができて便利だったので紹介します。

VisualStudioCodeでは、プロジェクトの.gitignoreに登録されているファイル、ディレクトリは検索や移動の対象にならないようになっています。 その為、node_modules以下のファイルにアクセスしたい場合、サイドパネルのエクスプローラーを開いてnode_modules以下に移動して...という作業が発生しがちでした。

Search node_modulesを使う

Visual Studio Code内の「拡張機能」メニューからか、Search node_modulesからインストールしておきます。

Cmd + Shift + Pでコマンドパレットを表示して、「Search node_modules」を選択

f:id:daikiojm:20171201015027p:plain

node_modulesを検索できるようになるので、module名を入力するか候補から選択することでパッケージ内のファイルにアクセすることができます。

f:id:daikiojm:20171201015336p:plain

終わりに

このように、エクスプローラーを開いてnode_modules以下に移動して...という作業を減らすことができます。
Node.jsでの開発が捗りますね💪

Angularでプレビュー画像を自動回転する

Angularで画像プレビューを行う際に、画像のExif(Orientation)情報を見て自動で向きを回転させる方法です。

JavaScript-Load-Imageという便利なライブラリと、Reactでの使い方を紹介したありがたいブログ記事があったので、かなり簡単に実装することができました。 早速、実装していきたいと思います。

JavaScript-Load-Imageの導入

javascript exifとかでググるexif-jsというライブラリが真っ先にヒットするが、JavaScript-Load-Imageの方が断然使いやすいAPIが用意されている印象でした。

まずは、Angularプロジェクトにインストール

$ npm i --save blueimp-load-image

画像プレビュー&回転を行いたいコンポーネントに次のようにしてインポート(モジュール全体をまとめてインポートする必要あり)

import * as loadImage from 'blueimp-load-image';

Exifを見て自動で回転するプレビューを実装

まずは、テンプレート側

<!-- 画像を選択 -->
<input type="file" accept="image/*" (change)="onInputChange($event)"/>

<!-- 選択した画像を表示 -->
<img [src]="image" style="margin-top: 24px">

画像を選択させるinput要素のchangのイベントをイベントハンドラonInputChangeで受けています。

次に、クラス側

...
export class RotateComponent {
  public image: any = '';

  onInputChange(event: any) {
    const file = event.target.files[0];
    loadImage.parseMetaData(file, (data) => {
      const options = {
        orientation: null,
        canvas: true
      };
      if (data.exif) {
        options.orientation = data.exif.get('Orientation');
      }
      this.getDataUrl(file, options)
      .then(result => {
        this.image = result;
      });
    });
  }

  getDataUrl(blobImage: Blob, options: Object): Promise<any> {
    return new Promise((resolve) => {
      loadImage(blobImage, (canvas) => {
        resolve(canvas.toDataURL(blobImage.type));
      }, options);
    });
  }
}

イベントハンドラの引数から取得したfileをloadImageのparseMetaDataメソッドに渡し、Exif情報の取得を行った後、getDataUrlメソッドでは、loadImageで得られるcanvasをimgタグで表示できるData URL形式に変換しています。

試してみる

試しに、Exif(Orientation)情報が含まれる画像をアップロードしてみます。 ※ 右はJavaScript-Load-Imageを使わすにimgタグにプレビューした画像

f:id:daikiojm:20171123222415p:plain

参考

JavaScript(ES2015&React)で画像を扱う:リサイズとプレビュー表示

ServerlessのTypeScript公式テンプレートを使ってみる

Serverlessをしばらく触ってこなかったので、気づかなかったのですが、v1.21.0からsls createの際に指定する公式テンプレートにaws-nodejs-typescriptと言うものが追加されたようです。(結構前ですね...)

f:id:daikiojm:20171105164504p:plain

以前から、プラグインとして、serverless-webpackというものがあり、デプロイコマンド実行時にwebpackを使ってTypeScript→JavaScriptへのビルドタスクを実行するという方法がありましたが、公式テンプレートでもserverless-webpackを使う際の一連のセットアップなどが済んだテンプレートを提供しているようです。

使ってみる

テンプレートからプロジェクトを作成して、とりあえずデプロイしてみます。

プロジェクトの作成

まずは、serverlessのインストールから。 インストールしたバージョンは、現時点での最新版1.24.0になります。

$ npm i -g serverless

createコマンドでテンプレートからプロジェクトを作成してみます。

$ sls create -t aws-nodejs-typescript -p sls-ts

作成したプロジェクトの内容を確認ると、次のファイルが作成されていました。

./sls-ts/
├── handler.ts
├── package.json
├── serverless.yml
├── tsconfig.json
└── webpack.config.js

aws-nodejsのテンプレートで作成した際に作成される、ファイルに加え、package.json、tsconfig.json、webpack.config.jsが作成されるようです。 それぞれのファイルの内容を見ていきます。

package.json

プラグインであるserverless-webpack、webpackで使うTypeScriptのローダーなどがインストールされているようです。

{
  "name": "aws-nodejs-typescript",
  "version": "1.0.0",
  "description": "Serverless webpack example using Typescript",
  "main": "handler.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "devDependencies": {
    "serverless-webpack": "^3.0.0",
    "ts-loader": "^2.3.7",
    "typescript": "^2.5.2",
    "webpack": "^3.6.0"
  },
  "author": "The serverless webpack authors (https://github.com/elastic-coders/serverless-webpack)",
  "license": "MIT"
}

serverless.yml

pluginsにserverless-webpackが設定されています。

service:
  name: aws-nodejs-typescript

# Add the serverless-webpack plugin
plugins:
  - serverless-webpack

provider:
  name: aws
  runtime: nodejs6.10

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          method: get
          path: hello

webpack.config.js

entry にslsw.lib.entriesが指定されていますが、これはserverless-webpack側でエントリポイントを自動で解決してくれているらしいです。(./handler.tsを指定したい気分ですが、どんな動きになっているかは後々調べてみよう...) ts-loaderの設定もされた状態ですね。

const path = require('path');
const slsw = require('serverless-webpack');

module.exports = {
  entry: slsw.lib.entries,
  resolve: {
    extensions: [
      '.js',
      '.jsx',
      '.json',
      '.ts',
      '.tsx'
    ]
  },
  output: {
    libraryTarget: 'commonjs',
    path: path.join(__dirname, '.webpack'),
    filename: '[name].js',
  },
  target: 'node',
  module: {
    loaders: [
      { test: /\.ts(x?)$/, loader: 'ts-loader' },
    ],
  },
};

その他のファイルに関しては、割愛します。

とりあえずデプロイしてみる

AWS CLIで必要なcredentが設定されている前提で、デプロイをしてみます。

$ sls deploy

デプロイ中のコンソールを眺めていると、まずwebpack(ts-loader)でTypeScriptが実行され、終わったタイミングでServerlessの実行結果が流れ始めるのがわかります。 ※ グローバルにTypeScriptがインストールされた環境で実行した場合は、そちらが使われるようです。

また、デプロイが完了後に.serverlessディレクトリ以下に生成されている.zipファイルの内容を確認してみると、JavaScriptにビルド済みのhandler.jsが確認できるかと思います。

作成されたAPI Gatewayのエンドポイントを叩いてみると...

f:id:daikiojm:20171105164532p:plain

動いてますね。

所感

webpackの設定なしに、気軽にTypeScriptが使えるようになってて最高です。

Angularでテキストファイルを読み込む

Angularでブラウザから読み込んだローカルのテキストファイルを表示する方法です。
HTML5のFile APIの基本的な使い方が分かれば簡単な内容ですが、メモ程度に残しておきます。

早速、実装していきたいと思います。
以下で紹介する内容は、angluar-cling newしたプロジェクトのapp.componentにべた書きしているので、試しに動かす際はコピペすれば動くはずです。

環境

この記事で紹介する内容は、以下の環境で試しています。

$ ng -v
@angular/cli: 1.4.1
node: 8.1.3

実装例

まずは、テンプレート側です。
テキストファイルを読み込むための<input>と、読み込んだテキストの内容を表示するための<p>タグを配置しました。

<!-- テキストファイルを選択するinput -->
<input type="file" (change)="onChangeInput($event)">

<!-- テキストファイルの内容を表示するエリア -->
<p>{{readText}}</p>

次に、クラス側です。 まず、inputのchangeイベントにバインドされたonChangeInput()メソッド内で、fileオブジェクトをfileToText()メソッドに渡しています。
fileToText()メソッドは、FileReaderの結果をPromiseで返します。
※ここではエンコーディング未指定なので、デフォルトのUTF-8 で解釈されます。

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  public readText: string = null;

  onChangeInput(evt) {
    const file = evt.target.files[0];
    this.fileToText(file)
      .then(text => {
        this.readText = text;
      })
      .catch(err => console.log(err));
  }

  fileToText(file): Promise<string> {
    const reader = new FileReader();
    reader.readAsText(file);
    return new Promise((resolve, reject) => {
      reader.onload = () => {
        resolve(reader.result);
      };
      reader.onerror = () => {
        reject(reader.error);
      };
    });
  }
}

読み込んだテキストが正しく改行されるように、cssも修正しておきます。

p {
  white-space: pre-wrap;
}

試してみる

試しに、次のようなファイルをアップロードしてみます。

用意したテキストファイル

f:id:daikiojm:20171128235722p:plain

inputから用意したテキストファイルを選択した結果

f:id:daikiojm:20171129000712p:plain

以上です。

参考

FileReader -MDN web docs
white-space -HTMLクイックリファレンス