コンテンツを画面に表示するとき、下にスクロールすれば自動的に次のコンテンツをロードしてくれる、という「loadMore」機能を考える
例えば、クラウド上に保存してある写真を画面に表示するような Webアプリを作るとする。
第1案
MVC を少しかじると、次のような実装が頭に浮かぶ。
# apis/image.coffee
angular.module("app").factory "getImageAPI", ->
getImageAPI = (userId, offset, limit) ->
# call API and return promise
return getImageAPI
# models/image.coffee
angular.module("app").factory "ImageModel", ["getImageAPI", (getImageAPI) ->
ImageModel = (userId) ->
_pageNo = 0
_pageSize = 10
_images = []
_isComplete
self =
loadMore: ->
promise = getImageAPI userId, (_pageNo++) * _pageSize, _pageSize
.then (response) ->
data = response.data
Array.prototype.push.apply _images, data.images
_isComplete = data.isComplete
return response
return promise
getUserId: -> userId
getImages: -> _images
isComplete: -> _isComplete
return self
return ImageModel
]
# controllers/image.coffee
angular.module("app").controller "ImageController", ["$scope", "$stateParams", "ImageModel", ($scope, $stateParams, ImageModel) ->
userId = queryParams.userId
imageModel = ImageModel userId
$scope.images = imageModel.getImages()
$scope.isComplete = -> imageModel.isComplete()
$scope.loadMore = ->
imageModel.loadMore().then (data) ->
$scope.$broadcast 'scroll.infiniteScrollComplete'
]
-# templates/image.haml
%ion-list
%ion-item(ng-repeat="image in images")
%img(ng-src="{{ image.src }}")
%ion-infinite-scroll(on-infinite="loadMore()" ng-if="!isComplete()")
angularJS と ionic を使っているが他のライブラリでもこんな構成になるはずだ。
- M
- getImageAPI
- API 通信を行う
- ImageModel
- ユーザーの写真を管理するモデルを提供する
- loadMore メソッドを呼ぶと、内部で API 通信を行い、画像を追加で読み込む
- getImageAPI
- C
- ImageController
- モデルである ImageModel と、ビューである templates/image.haml をつなぎとめるコントローラ
- $scope に渡した値が、view にバインドされる。
- $stateParams で、queryString を受け取ることができる
- ImageController
- V
- templates/image.haml
- ion-list
- コンテンツをリスト表示する
- ion-item
- 1つのコンテンツを表す
- ion-infinite-scroll
- このタグが画面上に現れ、かつ
ng-if
がtrue
を返す限り、on-infinite
を実行する - on-infinite には、loadMore の実行が指示されている
- このタグが画面上に現れ、かつ
- ion-list
- templates/image.haml
第1案の問題点
第1案は、実は完全な MVC 構成とは言えない。何がおかしいのだろう。
一言で言えば、「loadMore は view の事情であって、 Model が管理するべきではない」ということである。
- クラウド上に保存されている画像をどう表示するかは、view が決める
- view 側の実装は、例えば次のようなものが考えられる
- スクロールすれば自動的につぎの10件がロードされる
- ページャーを用意し、次へボタンを押せば次の10件がロードされる
- 画像が1枚だけ表示され、3秒ごとに次の画像に切り替わる
- loadMore は、その中の1つの選択肢に過ぎない。
- つまり、loadMore 関数が ImageModel の中にあるのはおかしい。
ImageModel は、あくまで offset, limit を受け取り、その分のデータを表示するべきだろう。
第2案
そこで、次のような util 関数を用意して、この切り分けを実現する。
# apis/image.coffee
angular.module("app").factory "getImageAPI", ->
getImageAPI = (userId, offset, limit) ->
# call API and return promise
return getImageAPI
# models/image.coffee
angular.module("app").factory "ImageModel", ["getImageAPI", (getImageAPI) ->
ImageModel = (userId) ->
self =
getImages: (offset, limit) ->
promise = getImageAPI userId, offset, limit
.then (response) ->
return {
images: response.data.images
isComplete: response.data.isComplete
}
return promise
getUserId: -> userId
return self
return ImageModel
]
# views/loadMore.coffee
angular.module("app").factory "view.loadMoreTask", ->
return (startPage, loader) ->
self =
pageNo: startPage
items: []
isComplete: false
loadMore: ->
loader(self.pageNo++).then (response) ->
Array.prototype.push.apply(self.items, response.items)
self.isComplete = response.isComplete
return self
# controllers/image.coffee
angular.module("app").controller "ImageController", ["$scope", "$stateParams", "ImageModel", "view.loadMoreTask", ($scope, $stateParams, ImageModel, loadMoreTask) ->
userId = queryParams.userId
imageModel = ImageModel userId
pageSize = 10
$scope.loadMoreTask = loadMoreTask 0, (pageNo) ->
imageModel.getImages(pageNo * pageSize, pageSize).then (data) ->
$scope.$broadcast 'scroll.infiniteScrollComplete'
return {
items: data.images
isComplete: data.isComplete
}
]
-# templates/image.haml
%ion-list
%ion-item(ng-repeat="item in loadMoreTask.items")
%img(ng-src="{{ item.src }}")
%ion-infinite-scroll(on-infinite="loadMoreTask.loadMore()" ng-if="!loadMoreTask.isComplete")
- M は、V がどのように表示しているかは知らない
- offset, limit を受け取って 画像を返すだけである
- V は、M から受け取ったデータを表示するためのプレゼンテーションロジックを提供する。
- このロジックは、データの中身が画像なのか、ツイートなのかなどは知らない。
- C は、M と V とを結びつけ、M から受け取ったデータを V が提供するプレゼンテーションロジックに渡す。