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; }
サンプルのリポジトリ
参考
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アニメーションをさせてみたいと思います。 具体的には次のようにぬるっと表示される詳細検索パネル(風)のものです。
事前準備
今回は、次のコマンドでサンプルのプロジェクトを作成しました。
$ ng new expansion-panel --inline-style --inline-template
また、app.moduleにBrowserAnimationsModule
をインポートしておきます。
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; ... imports: [ BrowserModule, BrowserAnimationsModule ], ...
検索ボックス
まずは検索ボックスです。
テンプレートには、input要素と、buttonを配置しているだけです。
「詳細」ボタンを押した際に、親のコンポーネントにボタンが押されたことを通知したため、Output
とEventEmitter
を使っています。
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
はコンポーネントが表示されていない状態を表しています。
- アニメーション前の初期状態のstyleを定義しています。
- state('*', style({ opacity: 1 })),
- アニメーション後のstyleを定義しています。
*
はコンポーネントが表示された際の状態を示しています。
- アニメーション後の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に設定を記述
事前にプロジェクトの設定ファイルに記述しておく方法です。
... "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を作成してみます。
作成されたコンポーネントを確認すると、一つの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」を選択
node_modulesを検索できるようになるので、module名を入力するか候補から選択することでパッケージ内のファイルにアクセすることができます。
終わりに
このように、エクスプローラーを開いて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タグにプレビューした画像
参考
ServerlessのTypeScript公式テンプレートを使ってみる
Serverlessをしばらく触ってこなかったので、気づかなかったのですが、v1.21.0からsls create
の際に指定する公式テンプレートにaws-nodejs-typescript
と言うものが追加されたようです。(結構前ですね...)
以前から、プラグインとして、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のエンドポイントを叩いてみると...
動いてますね。
所感
webpackの設定なしに、気軽にTypeScriptが使えるようになってて最高です。
Angularでテキストファイルを読み込む
Angularでブラウザから読み込んだローカルのテキストファイルを表示する方法です。
HTML5のFile APIの基本的な使い方が分かれば簡単な内容ですが、メモ程度に残しておきます。
早速、実装していきたいと思います。
以下で紹介する内容は、angluar-cliでng 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; }
試してみる
試しに、次のようなファイルをアップロードしてみます。
用意したテキストファイル
inputから用意したテキストファイルを選択した結果
以上です。