JavaScript」カテゴリーアーカイブ

[Vue] 親コンポーネントのデータ/メソッドを子コンポーネントから使う

前提

以下の環境で動作確認

要素 バージョン
debian 8.6
node 8.6.0
Vue 2.6.1

概要

題の通り、Vueの親子コンポーネント間でのデータとメソッドのやり取りを行う方法の備忘録。
本記事では、以下のようなVueコンポーネントの親子関係を用意し、子コンポーネントから親コンポーネントのデータ、メソッドを利用する。

  • 親コンポーネント(parent)
    — 文字列データ及びそれを書き換えるメソッドを定義
  • 子コンポーネント(child)
    — テキストボックスとボタンと持ち、親のデータの読み書きを行う

親コンポーネント

<template>
  <div>
    <child :data="data" @set="setData"/>
  </div>
</template>

<script>
  export default {
    name: 'parent',
    data() {
      return {
        data: '',
      }
    },
    methods: {
      setData(data) {
        this.data = data
      }
    }
  }
</script>

子コンポーネント(child)に、属性dataとメソッドsedDataを渡す。
:dataと@setは省略記法で、それぞれv-bind:dataと、v-on:setを省略したもの

子コンポーネント

<template>
  <div>
    <input type="text" ref="text">
    <button @click="$emit('set', $refs.text.value)">Set</button>
    <p>{{ data }}</p>
  </div>
</template>

<script>
  export default {
    name: 'child',
    props: ['data'],
  }
</script>

親コンポーネントから受け取る属性は、propsで定義しなければならない。一見面倒だが、これによって不要な属性を受け取らずに済む。

buttonクリック時に、親コンポーネントのイベントに伝搬するために、$emitを用いる。$emitの第二引数以降が、元のイベントに対する引数になる。

実行結果

画像のように、テキストボックスに文字列を入力し、ボタンをクリックすると、親コンポーネントのsetDataメソッドが実行され、dataが書き換わる。

備考

  • Vueではコンポーネントの親子関係が深くなるとバケツリレーが始まるので、Vuexを導入が推奨されるみたい
  • VuexはReactにおけるReduxのような、Vueでストアを一元化するためのフレームワーク

WebPack + Vueで、「Element」導入時にハマった点

前提

以下環境にて確認

要素 バージョン
debian 8.6
node 8.2.1
npm 5.3.0

概要

Webpackを用いてVueアプリを開発している中で、Vue2.0用のUIライブラリであるElementを導入しようとしたら、Webpack周りでハマったので導入手順と解決手順を備忘録に残す。

備忘録が主な目的なので、Webpack,Vue,Elementなどに関する説明は割愛

Elementの導入

npmでインストールする

npm install element-ui -s

エントリポイントでElement関連をimport

import ElementUI  from 'element-ui'
import locale     from 'element-ui/lib/locale/lang/ja'
import 'element-ui/lib/theme-default/index.css'

Elementの利用を宣言。localeを指定することで日本語が有効になるらしい

Vue.use(ElementUI, {locale});

エラー① Element内のCSSファイルが解決できない

この状態でwebpackを回すと、以下のエラーが出る

ERROR in ./node_modules/element-ui/lib/theme-default/index.css
Module parse failed: /root/degulog/node_modules/element-ui/lib/theme-default/index.css Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type.

どうやらElementに付随するCSSファイルをバンドルできないらしい。

原因は単純に、これまでscssだけ使ってたのでcssに対するローダーをwebpackで設定していなかったため。
なのでwebpack.config.jsのloadersに以下を追加すれば解決

{ test: /\.css$/, loader: 'style-loader!css-loader' },

エラー② Element内のwoffファイルが解決できない

続けて以下のエラーが発生した

ERROR in ./node_modules/element-ui/lib/theme-default/fonts/element-icons.woff?t=1472440741
Module parse failed: /root/degulog/node_modules/element-ui/lib/theme-default/fonts/element-icons.woff?t=1472440741 Unexpected character '' (1:4)
You may need an appropriate loader to handle this file type.

問題の本筋は前項と同様。woffを解決するローダが設定されていなかった。

url-loaderを適用すれば良いが、まずそれが入っていなかたのでインストールする。

$ npm install url-loader

で、webpack.config.jsonのloadersに以下を追加。woff意外にも対応できるように汎用的に記述。

{
  test: /\.(otf|eot|svg|ttf|woff|woff2)(\?.+)?$/,
  loader: 'url-loader'
},

動作確認

依存解決も上手く行って、以下のようにElementが利用できた。
例はDatetimePickerコンポーネント

<el-date-picker
  v-model="value1"
  type="datetime"
  placeholder="Select date and time">
</el-date-picker>

Bowerを使ってフロントエンドの依存関係を管理する

前提

要素 バージョン
debian 8.7
node 7.4.0
npm 4.1.2
bower 1.8.0

概要

Debianにnpmを用いてBowerをインストールし、Bowerを簡単に使ってみる

Bowerとは

Bower(バウアー)はフロントエンド開発の依存関係(JavaScript/CSS/HTMLなど)を管理してくれるフロントエンド用パッケージ管理システム。

PHPのComposer、RubyのGem、iOSのcocoaPodsなどの仲間だと思えば良い。

ライブラリなどのインストールをコマンドで行えるようになり、インストールした内容及びその依存関係が、bowser.jsonに書き込まれる。

同一のbowser.jsonさえあれば、他環境にライブラリをインストールする際にコマンド一発で解決する。

Bowerのインストール

npmを用いてインストールを行う。パッケージ管理システム(npm)でパッケージ管理システム(bower)をインストールするのがちょっと面白い。ここではver1.8.0がインストールされる。

$ sudo npm install bower -g
/usr/local/bin/bower -> /usr/local/lib/node_modules/bower/bin/bower
/usr/local/lib
└── bower@1.8.0

bower.jsonの作成

初めにパッケージの依存関係などを記述するbower.jsonを作成する。といっても、JSONファイルを直接作成するのでなく、以下のコマンドを実行すると、対話式に入力を促されるのでそれに従っていけば自動的にbower.jsonが作成される

$ bower init

以下のように、名称、説明文、メインファイル(省略可)、キーワード(省略可)、作者、ライセンス、Webサイト(省略可)などを入力することで、bower.jsonが生成される。ここで入力される情報のうちのいくつかは、自分でbowerにライブラリを公開する場合に必要になるものなので、単に開発で試用するライブラリを管理するという場合は省略して良い。

$ bower init
? name qs-test
? description bower test directory
? main file
? keywords
? authors Sa2Knight <shingo.sasaki.0529@gmail.com>
? license MIT
? homepage
? set currently installed components as dependencies? No
? add commonly ignored files to ignore list? No
? would you like to mark this package as private which prevents it from being accidentally published to the registry? No

{
  name: 'qs-test',
  authors: [
    'Sa2Knight <shingo.sasaki.0529@gmail.com>'
  ],
  description: 'bower test directory',
  main: '',
  license: 'MIT',
  homepage: ''
}

? Looks good? Yes

Bowerを用いてjQueryUIパッケージをインストールする

Bowerを用いたパッケージのインストールは、下記のコマンドで行える。 –save を付与することで、インストール結果をbower.jsonに記録してくれるので実質必須。

$ bower install [パッケージ名] –save

jQueryUIのパッケージ名はjquery-uiなので、以下のように、jQueryUIをインストールする

$ bower install jquery-ui --save
bower jquery-ui#*               cached https://github.com/components/jqueryui.git#1.12.1
bower jquery-ui#*             validate 1.12.1 against https://github.com/components/jqueryui.git#*
bower jquery#>=1.6              cached https://github.com/jquery/jquery-dist.git#3.2.1
bower jquery#>=1.6            validate 3.2.1 against https://github.com/jquery/jquery-dist.git#>=1.6
bower jquery-ui#^1.12.1        install jquery-ui#1.12.1
bower jquery#>=1.6             install jquery#3.2.1

bower.jsonの内容を確認すると、jQueryUI1.12.1に依存している状態にあることが追記されている

$ cat bower.json
{
  "name": "qs-test",
  "authors": [
    "Sa2Knight <shingo.sasaki.0529@gmail.com>"
  ],
  "description": "bower test directory",
  "main": "",
  "license": "MIT",
  "homepage": "",
  "dependencies": {
    "jquery-ui": "^1.12.1"
  }
}

インストールされたパッケージは、bower_componentsディレクトリ以下に配置されるのだが、ここを見てみると、jQueryUIが依存しているjQuery本体もインストールされていることがわかる。

$ ls bower_components/
jquery  jquery-ui

このように、Bowerでは、インストール対象のパッケージが依存している他のパッケージについても自動的にインストールしてくれる。(composerやnpm、gemなどに慣れている人にとっては当然だが)

所感

  • npmの影響を大きく受けているらしく、全体的な仕様やコマンドの使い方がnpmに限りなく近い
  • bowerでは、対象パッケージをgitからcloneしてくるだけなので、実際に必要なるファイルはその中の極一部である。それをどのように扱うかはまた別の手段を用いる必要があるようなので、即戦力という感じでは無さそうだ
  • scriptタグでロードする時に依存関係に基づいた順番でロードしなきゃならないって問題は解決できてない
  • と言うかJavaScriptの外部ライブラリってそんな依存管理が必要なほど沢山使うかな
  • bowerの触りをやった程度なのでメリットがあまり掴めてない。詳しい方がいたらご教授願う。
  • bowerのサブコマンドが大量にあるので、余裕があれば色々見てみたい

参考

Reactで作る簡易的なTodoList

前提

要素 バージョン
react 15.3.0
babel 5.8.34
Chrome 57.0

概要

  • インフルエンザで死んでたのでリハビリにコーディング
  • Reactをせっかく勉強したので、よく技術のお試し教材に登場する簡易TodoListを作ってみる
  • Reactに関する説明はほぼ無いが、React未経験者でも雰囲気わかる程度には書く
  • JSXを使用する。JavaScriptコード中にXMLが出てくるのは気持ち悪いが、Reactを使うには避けては通れないし手放せないと思う
  • ES6を使用する。というかReactなんてモダンな技術を使ってES6を使わないなんてナンセンスなことはありえないと思う

実装したTodoListは以下の通り。Todoを表示して、追加できて、削除できる。それだけ。編集とか保存とかそんなものはない。

基本的な状態遷移を記述できて、かつ目的がわかりやすいからこうやって教材に選ばれやすいんだと思う。

HTML

特に複雑な内容はないが、やってることは以下の通り

  • React及びReactDOMをロード
  • JSXをトランスパイルするためのbabelをロード
  • UIを動的配置するためのDOM(#content)を定義
  • 最後にscript.jsxをロード
<html lang="ja">

<head>
  <meta charset="utf-8">
  <script src="https://fb.me/react-15.3.0.js"></script>
  <script src="https://fb.me/react-dom-15.3.0.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.min.js"></script>
  <title>TodoList</title>
</head>

<body>
  <div id="content"></div>
  <script src="script.jsx" type="text/babel"></script>
</body>

</html>

スクリプト(JSX)

  • タスクの一覧と新規タスク(入力内容)をステートとして保持
  • 「追加」ボタン押下時にタスクに新規タスクをpushし、新規タスクをクリア
  • 各タスクのボタン押下で該当タスクを削除
let TodoList = React.createClass({

  /*
   * ステートの初期値を定義
   */
  getInitialState() {
    return {
      newTask: '',
      tasks: [
        '夕飯作る',
        'ケージの掃除する',
        'プログラミングする',
      ]
    };
  },

  /*
   * 新規タスクの入力内容を変更
   */
  changeText(e) {
    this.setState({newTask: e.target.value});
  },

  /*
   * 新規タスクを作成
   */
  addNewTask() {
    let newTasks = this.state.tasks;
    newTasks.push(this.state.newTask);
    this.setState({
      newTask: '',
      tasks: newTasks
    });
  },

  /*
   * 既存タスクを削除
  */
  removeTask(idx) {
    let newTasks = this.state.tasks;
    newTasks.splice(idx, 1);
    this.setState({tasks: newTasks});
  },

  /*
   * UIの描画
   */
  render() {
    let tasks = this.state.tasks.map((t, idx) =>
      <li key={idx}>
        {t}
        <input type='button' value='☓' onClick={() => this.removeTask(idx)} />
      </li>
    );
    return (
      <div>
        <input type='text' value={this.state.newTask} onChange={this.changeText} />
        <input type='button' onClick={this.addNewTask} value="追加"/>
        <ul>
          { tasks }
        </ul>
      </div>
    );
  },
});

/* DOMの描画 */
ReactDOM.render(<TodoList/>, document.getElementById('content'));

「React開発入門」レビュー

書籍について

タイトル WebデベロッパーのためのReact開発入門 JavaScript UIライブラリの基本と活用
著者 著: 柴田 文彦
発行日 2016/12/01
ページ数 231ページ
価格 2500円
Amazon https://www.amazon.co.jp/dp/4295000337
読了日 2017/03/15

評価

良い点

  • Reactに関して基本的なことが整理されている
  • コード例の構成が全体的に一貫しており、理解しやすい
  • コード例はWebでダウンロードできるので、コード写経もしやすかった
  • React用Chrome拡張の使い方も掲載されており、これがかなり使いやすい。巻末だったが最初に掲載するべき

悪い点

  • 説明が冗長。一度説明した内容をほぼ同じようにまた説明するということがチラホラ
  • 説明がコードの内容をそのまま和訳したようなモノが多い。コードと実行結果を見れば説明を読む必要がない感じ
  • 応用的な例がほぼ無い。Reactがどんなものかはわかったが、それによってどんな楽が出来るかが伝わらない
  • 元々ページ数が少ない上、説明が冗長的なので事実上のコンテンツ量はかなり少ない
  • それで2500円はちょっと

その他

  • 序盤はJSX不使用だが、中盤以降はJSXが必須
  • JSX自体の説明に結構なページ数を割いている
  • Fluxに関しては巻末で簡単に説明する程度

内容とメモなど

1. Reactとは

Reactに関する概要がズラッと書かれている。Reactにおけるキーワード、専門用語も一通り書かれているので、最初に一読、最後にもう一読すると良さそう

JavaScriptフレームワーク全体でのReactの位置付けについても触れているが、比較対象がAngularしか無いのは少し寂しい

2. Reactのコンポーネント

Reactの基本的な使い方全般。この章ではJSXは使われておらず、純粋なReactの機能だけでReactコンポーネントを構築している。

順を追って読んでいるとなんだか不便だなと感じるが、次章でJSXが登場すると突然便利さを感じることができるので、すぐにJSXに飛びつかずにこの章をしっかりやると良さそう

3. JSXの基本

JSXに関する説明、JSXの利用方法、JSXを用いたReactコードの書き方をそれなりのページ数を割いてまとめている。

基本的には前章で作成したコードをJSXを用いたものに書き換えることで、JSXがどれほど便利なのかを主張している。

個人的に、通読前はJavaScript内にXMLが突然あらわれることに強い抵抗を持っていたが、この章を読んでみるとJSXなしのReactは考えられないなと思った

4. Reactを使いこなす

本章では、フォーム要素を対象に、プロパティやステート、単方向データバインディングなど、Reactの具体的な部分に触れている。

Angularの双方向データバインディングに慣れていた自分にとってはすんなり受け入れられたが、そうでもない人にとってはかなり違和感のある章だと思う

5. Reactの一歩進んだ使い方

本章では、以下のようなReact本体ではない外側の要素について触れている

  • アドオン
  • React DeveloperTools
  • Flux

どれも触り程度で具体的な内容はほぼ書かれていないが、React自体に慣れ、ステップアップを詰むためのヒントとなる内容がかかれている。意欲のある人が本書をきっかけに次のステップに進むための足掛けになるとは思う。

所感

本書でなくReactを使ってみた感想

  • 全体的に「あ、この動き、Angularで見たやつだ」となる部分が多かった
  • その上でもReactはUI操作に特化しているので、Angularだけではできそうにない実装ができそう
  • JavaScript中にXMLを書くのに強い抵抗があったが、やってみると別に気にならなくなった
  • JavaScript中にXMLを書くため、デザイナとの作業の分離が難しくなる気はする
  • システム全体で使うよりも、システム内の、UI色の強い一部機能にのみ採用するという使い方ができそう
  • ReactというよりVirtualDOMの考えがスゴイんだ
  • サードパーティ製のアドオンが豊富なようなので、スタイリッシュなUIなど探せば沢山ありそう
  • ChromeのReact Developer Toolsがスゴイ。これはデバッグも捗るし使ってて楽しい
  • jQueryという闇を払拭できそう

MEANでデグー飼育アプリを作ってみた

概要

  • 前々からMEANスタックに興味があった
  • 2017/01/22ぐらいからチマチマMEANアプリ作り始めた
  • とりあえず自分にとって実用的であるのが望ましいので、デグー飼育支援アプリを作った
  • MEANだとこんなことができるよの紹介を含め、動作画面を掲載する
  • 完全に個人用なのでサービスの公開はしないが、ソースコードはGithubで公開している

システム構成

OS + MEAN構成は以下の通り

構成要素 バージョン
Debian 8.7
Mongodb 3.2.9
Express 4.14.0
AngularJS 1.4.1
NodeJS 7.4.0

その他、主に開発に用いたツールなどは以下の通り

構成要素 バージョン
npm 4.1.2
babel 6.22.2
eslint 3.0.1
less 3.3.0
gulp 3.9.1
jQuery 3.1.1
jQUeryUI 1.8.19
bootstrap 3.3.7
d3.js 3.5.16
c3.js 0.4.1

ソースコード

https://github.com/Sa2Knight/degu-log よりクローン可能。

環境構築手順も一応READMEにあるので、頑張ればローカルでも動作させられる(多分)
Zaimアカウント及びZaimAPIを利用するためのトークンが必要だったり敷居は高い。

機能一覧

高速な画面遷移

初っ端から機能というより特徴の話だが、本アプリはSPA(っぽい何か)でありながら、URLを変更しながら画面遷移を行える。
そのため、ブラウザの「戻る」「進む」も利用可能であり、URLを直接指定して特定のリソースにアクセスすることもできる。

AngularJSのオプションモジュールである、ng-routeを用いることで、それを簡単に実装できた。
しかも画面遷移が非常に早い。これだけでMEANで開発してよかったと思えるレベル。

簡易ブログ機能

飼育に関する簡易的なブログっぽいものを投稿することができる。
ブログは通常のCRUDの他、カレンダーを表示して記事を一覧することができる。

体重管理機能

デグーの体重を時系列で管理することができる。便宜上、我が家のデグー2匹(パズー、メイ)に特化しており、それぞれ日付に対する体重を記録することができる。
また、時系列に記録した体重を元に、折れ線グラフをリアルタイムで描画することができる。

ペット関連支出管理機能

本機能は、クラウド家計簿サービスであるZaim及び入力した家計簿記録を取得するZaimAPIを用いて実装している。
日頃から入力している家計簿データを元に、月ごとの支出額の一覧及び特定月の支払内容を閲覧できる。

外部APIの利用もやはりSPAとの相性が抜群だ

写真管理機能

写真をアップロードして管理することができる。写真には、タイトル及び1つ以上のタグを付与することができ、それらで検索することができる。
また、写真アップロード時にオリジナルと別にサムネイルを生成し、写真一覧画面にはサムネイルが表示され、クリックすることでオリジナルサイズを確認できるようにアンっている。

所感

MEANという、MもEもAもNもほぼ使ったこと無い状態から、1つずつ勉強しながらなんとか実用性も多少あるWebアプリを作ることができた。特にSPAを1から作るのは初めての経験だったので、従来型のWebアプリとの違いを感じながら楽しく開発することができた。以下、主な所感を列挙する

  • MongoDBはJavaScriptライクに読み書きを行え、スキーマに依存せずにデータを管理できるので手軽で楽しい
  • AngularJSは双方向データバインディングが一番強いと思ったが、フルスタックのMVCであればAngularJSでなくても良いかなとは思う。
  • Nodeを使えばだいたいのことは対応するNodeモジュールがあって、サーバサイドを一通りJavaScriptで書ける。強い。
  • Expressについては、今回はRestfulなAPIサーバとしてしか使わなかったので触り程度しかわからなかった
  • SPAはクライアントの待機時間が極端に短くなるのでストレス無く利用できることを強く実感
  • クライアントサイドの負担を増やし、サーバサイドの負担を減らす新しい分散システムだと感じた
  • SPAでも画面遷移ごとにURLを変化させたほうが絶対便利だと思った
  • サーバサイドはRestfulなAPIに特化させるように実装したほうが分担が明確で良い
  • JavaScriptはBabelを導入して、全てES6で記述した。やはり最新仕様のJavaScriptは書きやすい。もう戻りたくない。
  • ESLintを導入してコーディング規約を強制した。おかげで比較的高品質のコードを終始簡単に弄るすことができた
  • Bootstrapに頼りきりだった割にUIが雑で残念
  • 一応LESSも導入したが特に使いこなせてない
  • テスト(ユニット,E2E)を書きたかったが、その余裕がなかった。そのうち書きたい
  • とにかく新しい技術/ツールで溢れた開発で楽しい1ヶ月半だった

次はTypeScript及びAngular2に手を付けてみたい。Angular2のほうが良さげであれば、本アプリを移植することも検討する。

express + mongodb でページャ機能を実装

前提

要素 バージョン
node 7.4.0
express 4.14.0
mongodb 3.2.9

概要

  • expressで動作しているAPIサーバに対して、mongodbに格納されているデータをリクエストする際に、ページを指定して取得する要素を絞り込む。
  • ページ番号はクライアントからサーバに対して送信する
  • 1ページあたりのデータ件数はサーバ側で固定する(今回は5件)
  • expressでmongodbを利用するための前準備などは割愛する

使用するmongodbのメソッド

メソッド名 説明
find(query) queryで指定したデータを取得する
skip(n) データを取得する際に先頭N件をスキップする
limit(n) データを取得する際に最大N件までとする

これらを組み合わせて以下のようにチェインすることでページングが実装できる

find(検索条件).skip((ページ番号 – 1) * 1ページあたりの件数).limit(1ページあたりの件数)

実装

今回は、Photoコレクションに含まれる全ての写真データを対象に、ページ番号のみをクライアントから指定して取得する

/* リクエストからページ番号を取得(デフォルト1) */
let page = Number(req.body.page) || 1;

/* 1ページあたりのデータ件数を5件にする */
let numPerPage = 5;

/* 全ての写真を取得 */
let c = collection('photo').find({});

/* 写真の枚数を取得 */
c.count().then(function(count) {

  /* 総ページ数を計算 */
  let pageNum = Math.ceil(count / numPerPage);

  /* 指定されたページ番号が総ページを上回っていた場合総ページ番号で置き換え */
  if (page > pageNum) { page = pageNum; }

  /* ページングを実行 */
  c.skip((page - 1) * numPerPage).limit(numPerPage).toArray(function(err, docs) {
    /* 写真一覧及びページング情報をクライアントに返却 */
    res.send({
      page: page,
      pageNum: pageNum,
      count: count,
      photo: docs,
    });
  });
});

所感

  • ページング処理とSPAの相性は抜群。僅かな待機時間もなくページを切り替えることができて気持ちいい
  • countとtoArrayで非同期処理が2回続いているので、Promissを使えばもっとキレイなコードになると思うが、Promissの使い方が上手く掴めなかったので今回は普通に入れ子を利用した

参考

AngularJSでjQuery datetimepickerを使うためのディレクティブの作成

前提

要素 バージョン
AngularJS 1.4.1
jQuery 3.1.1
jQuery datetimepicker 2.5.4

概要

初めてAngularJSのディレクティブを自作したので舞い上がって投稿する。

本記事では。属性ディレクティブ”dateTimePicker”を定義し、以下のように利用できるようにする

呼び出し

<date-time-picker ng-model="datetime" format="Y/m/d" time=false />

パラメータ

パラメータ名 入力例 説明
ng-model datetime バインディングするAngularJSのデータモデル
format Y/m/d 入力される日時文字列のフォーマット
time true 時刻の入力を行うか

実行例

ディレクティブの作成

パラメータで指定しない部分については、以下の固定値を与えている
– bootstrap用のclassを設定する(form-control)
– AngularJS用のバリデーションを設定(required)
– 各種スクロールをオフにする

angular.module('app').directive('dateTimePicker', function() {
  return {
    restrict: 'E',
    require: 'ngModel',
    scope: {
      format: "@",
      time: "@",
    },
    replace: true,
    template: '<input type="text" class="form-control date-time-picker" required>',
    link(scope, element, attrs, controller) {
      if (scope.format === undefined) {
        scope.format = 'Y/m/d H:i';
      }
      scope.time = scope.time === 'true' ? true : false;
      $(element).datetimepicker({
        format: scope.format,
        timepickerScrollbar: false,
        timepicker: scope.time,
        scrollMonth: false,
        scrollTime: false,
        scrollInput: false,
        onChangeDateTime(dp, $input) {
          controller.$setViewValue($input.val());
        },
      });
    },
  };
});

ソースコード解説

行番号 抜粋 説明
01 directive(‘dateTimePicker’… ディレクティブ dateTimePickerを作成する
03 restrict: ‘E’ <dateTimePicker>の形式で利用できるようにする
04 require: ‘ngModel’ ng-model属性の指定を必須にする
05-08 scope: { 属性(format/time)を文字列で受け取る
09 replace: true, タグをtemplateで置き換える
10 template: ‘<input type=”text”… タグを置き換えるテンプレート
11-27 link(scope, element, attrs, controller) { ディレクティブの挙動を定義
16-26 $(element).datetimepicker({ datetimepickerの呼び出し
24 controller.$setViewValue($input.val()); datetimepickerによって値が書き換わった時に手動でデータバインディングする

実行結果

実行前

<date-time-picker format="Y/m/d H:i" time=true ng-model="blog.post.datetime"/>

実行後

<input type="text" class="form-control date-time-picker ng-isolate-scope ng-valid ng-valid-required ng-touched" required="" format="Y/m/d" time="false" ng-model="weight.post.date">

AngularJSからexpressに画像をアップロードする

前提

要素 バージョン
node 7.4.0
npm 4.1.2
express 4.14.0
AngularJS 1.4.1

概要

  • AngularJSのページから画像ファイルをアップロードし、express側で保存させる
  • AngularJSには(少なくとも標準モジュールには)ファイルアップロードの仕組みが存在しないので、ディレクティブを自作して対応する

1.クライアントサイド

添付ファイルをモデルで扱うためのディレクティブを定義

app.directive('fileModel', ['$parse', function($parse) {
  return function(scope, element, attrs) {
    const model = $parse(attrs.fileModel);
    element.bind('change', function() {
      scope.$apply(() => {
        model.assign(scope, element[0].files[0]);
      });
    });
  };
}]);

上記ディレクティブは、以下のように利用する

<input type="file" name="myfile" file-model="my-file">

上記ディレクティブをinput[type=file]にバインドすることで、ファイル選択時にファイルの内容をAngularモデルにバインディングする。
これによって、添付されたファイルをAngularJSのモデルとして操作できるようになる。

ファイルアップロード用のサービスを定義

app.factory('fileUpload' , ['$http' , function($http) {
  return {
    upload(photo) {
      let formData = new FormData();
      formData.append('file' , photo);
      $http.post('/rest/photo/put', formData, {
        headers: {'Content-Type': undefined} ,
        transformRequest: null
      });
    },
  };
}]);

APIがあるであろう、’/rest/photo/put’に対して添付ファイルをPOSTする。
Content-Typeをundefinedに明示的に設定しておくと、AngularJS側で良い感じに設定してくれるので今回はおまかせする。
transformRequestをnullにしておかないと、POSTデータがJSONに変換される可能性があるので、一応明示的にNULLにしておく。

上記サービスを、添付ファイルを指定して実行することで、APIサーバに添付ファイルがPOSTされる

2.サーバサイド

Multerモジュールをインストール

Multerモジュールは、multipart/form-data形式のリクエストを扱うためのNodeJSのミドルウェア。これを用いることでexpress側で添付ファイルを処理することができる。

$ npm install --save multer

POSTされたファイルを保存する

Multerモジュールがインストールできたら、以下のコードをexpress側に追加する(※appはexpressオブジェクト)

var multer  = require('multer');
app.use(multer({ dest: './uploads/'}).any());

非常にシンプルだが、これだけで、アップロードファイルがPOSTされた際に、そのファイルを./uploadsに保存する処理が自動で行われる。

アップロードファイルの検証

アップロードされたファイルは、requestオブジェクトのfiles属性に含まれている。filesは配列であることから、複数ファイルの同時アップロードにも対応できる。

以下のコードでは、アップロードされたファイル(1件)の内容を出力し、無条件でsuccessを戻している。

app.post('/rest/photo/put' , function(req, res) {
  console.log(req.files[0]);
  res.send('success');
});

‘/rest/photo/put’に対して、画像ファイルを1件アップロードすると、以下のログが出力される

{ fieldname: 'file',
  originalname: '10.jpeg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: './uploads/',
  filename: '652aa507d6e70b89e064bbddd3b33224',
  path: 'uploads/652aa507d6e70b89e064bbddd3b33224',
  size: 5297 }

ユニークなランダム文字列であるfilenameを含んだpathが既に出力に含まれているように、この時点で既に画像ファイルは保存されている。

もちろんファイル名を変更したり、保存する条件を指定したりもできるが、ここでは割愛する。

参考

AngularJSの専門用語整理その2

まさかの続き(前回)

digestサイクル

  • AngularJSがデータ双方向バインディングを実現するために、UIとモデルの差異を検証するサイクル

式(Expressions)

  • Angular式とか呼ばれてる
  • {{ 式 }} の形式でビューで展開できるJavaScriptの式っぽいもの
  • JavaScriptっぽいだけで別物
  • 全ての評価はデフォルトでスコープのプロパティに対して行われるので
  • undefinedをオブジェクトとして参照できる(無論結果は全てundefined)
  • フィルタが使える

$parse

  • Angular式を関数に変換するサービス
  • 対象のスコープを指定して関数を実行できる
    var getter = $parse('user.name');
    var context = {user:{name:'sasaki'}};
    console.log(getter(context)); // sasaki
    

$watch

  • 特定のオブジェクトやプロパティの変更を監視し、変更後に任意の処理を行わせるサービス
    $scope.hoge = 'foo';
    $scope.$watch('hoge' , function(oldValue, newValue) {
      console.log('hogeの内容が' + oldValue + 'から' + newValue + 'に変わりました');
    });
    
  • 関数を指定して、その戻り値の変化も検知できる
    $scope.$watch(function() {
      return $location.path();
    }, function() {
      console.log('パスが変わりました');
    });
    
  • watchはdigestサイクルの中で定期的に行われる

$apply

  • UIとモデルの値を同期させるサービス
  • digestサイクルの中で呼び出されるが、手動で呼び出すことも可能
  • 特にjQueryなどによってUIの値を直接書き換えた場合など、digestサイクルで検知できない場合に手動で同期させる必要がある

$inject

  • AngularJSにおける依存性注入のためのサービス
    コンストラクタ’My’に対して、依存するオブジェクト$scopeを注入してコントローラを作成する

    var My = function(s) {
      s.msg = 'Hello, AngularJS!';
    };
    My.$inject = ['$scope'];
    angular.module('myApp').controller('myController', My);