AngularJS使ってます。
よくあるフォームの処理で、半日ほどハマッたのでメモ。
やりたいことは、
- サーバからデータを取得
- フォームに読み込み
- ユーザーがなんか入力
- 送信ボタンをアクティブにする
って、よくあるパターンのやつ。
HTML
サンプルの構成はindex.htmlとapp.jsのみ。
angularjs,bootstrapは面倒なんでCDNを使わせていただきました。
HTMLはこんな感じ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>watch test</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
<!-- AngularJS -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script>
<script src="./app.js"></script>
</head>
<body>
<h3>Test</h3>
<div ng-app="app">
<div ng-controller="FormTestController">
<form>
<input type="text" ng-model="Model.Name" ng-change="changed($event)" />
<input type="text" ng-model="Model.Age" />
<button ng-click="reload($event);">Reload!</button>
<br />
<a class="btn btn-primary" href="" role="button" ng-class="{disabled : !Modified}">変更したらアクティブになるボタン</a>
</form>
変更済み?{{Modified}}<br />
氏名:{{Model.Name}}<br />
年齢:{{Model.Age}}<br />
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
|
ブラウザで見るとこんな感じ。(app.jsでModelをセットしてます)
Modifiedフラグでボタンのactive/inactiveを制御します。
面白くもなんとも無いですが、ま、サンプルですし。
うまくいかなかったapp.js
最初app.jsはこんな感じで書いたんですが、期待した動作にはなりませんでした。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
//このスクリプトはちゃんと動きません。
(function () {
var app = angular.module("app",[]);
angular.module("app").controller("FormTestController", ['$scope', function ($scope) {
//メンバー
var loadTimes = 0; //ダミー生成用
$scope.Model = null; //編集対象
$scope.Modified = false; //編集されたか?フラグ
// 初期化
loadModel(); //初期値ロード
function loadModel() {
//web apiとかでモデルをゲット(今回はダミー)
$scope.Model = { Name: "氏名", Age: 18 + (loadTimes++)};
//クライアント上では更新されてないのでModifiedをfalseにセット
$scope.Modified = false;
}
//監視対象が更新されたらModifiedをtrueにする
$scope.$watchCollection("Model", function (newVal, oldVal) {
if (newVal != oldVal) {
$scope.Modified = true;
}
});
//ダミー更新用イベントハンドラ
$scope.reload = function () {
loadModel(); //データがリフレッシュされて、変更済みフラグがリセットされるはず!
};
}]);
})();
|
やってることは単純で、$scope.$watchCollectionでModelの変更を監視して、変更があったらModified をTrueにするだけ。
これ、最初はうまくいくんですが、reloadしてもModified はtrueのままになってしまいます。
loadModelでModel変更後にModifiedをリセットしてるんで、falseのままなはずと思ってたんですが、デバッグしてみるとreloadイベント終了後に$watchCollectionが発火しているご様子。しかもnewVal != oldValで。
なぜ?
理由はangularの仕様で、watch関係はイベントハンドラ処理が終わった後に実行されるから。
$applyとそれに続くLife cycleの説明をよーーーーく読むと解るような気がします。
angularでのイベント処理は
- Formイベントとかタイマーイベントとかのときはangularが$apply経由でハンドラ呼ぶよ。
- $applyの中ではハンドラ実行後に$digest呼ぶよ。
- $digestでwatch処理するよ。
ということなので、ハンドラ内で処理順を気にしても無駄なのでした。
結局どうしたか
ロード直後のModelと変更後のModelを比較するという、とても効率の悪そうな方法しか思い浮かびませんでした。
ただwatch内で比較するので、変更されたっぽいタイミングのみでの比較になるので、そんなに負荷はかからないはずと信じます。
んで、改良版がこちら
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
(function () {
var app = angular.module("app",[]);
angular.module("app").controller("FormTestController", ['$scope', function ($scope) {
//メンバー
var loadTimes = 0; //ダミー生成用
var originModel=null; //編集前Model(シリアライズ済み)
$scope.Model = null; //編集対象
$scope.Modified = false; //編集されたか?フラグ
// 初期化
loadModel(); //初期値ロード
function loadModel() {
//web apiとかでモデルをゲット(今回はダミー)
$scope.Model = { Name: "氏名", Age: 18 + (loadTimes++)};
//編集前モデルを保存
originModel=JSON.stringify($scope.Model );
//クライアント上では更新されてないのでModifiedをfalseにセット
$scope.Modified = false;
}
//監視対象が更新されたらModifiedをtrueにする
$scope.$watchCollection("Model", function (newVal, oldVal) {
if (newVal != oldVal) {
var curModel=JSON.stringify(newVal); //現在のModelをシリアライズ
if(originModel!=curModel){ //シリアライズした値を比較(Deep Compare)
$scope.Modified = true; //変更あり!
} else{
$scope.Modified = false; //読み込んだ当時の姿です。
}
}
});
//ダミー更新用イベントハンドラ
$scope.reload = function () {
loadModel(); //データがリフレッシュされて、変更済みフラグがリセットされるはず!
};
}]);
})();
|
読み込み直後にJSON.stringifyでオリジナルを文字列(originModel)として保存して、watchで比較してます。
この方法だと、フォーム上で変更しても元の値を入力しなおすとModifiedをfalseになるような処理もかけます。
angularの理解が進めば、もっと良い方法があるような気もしますが、現時点ではこれで。
だれか良い方法おしえてー。