むらた(id:KenichiroMurata)です。
SpineのTodosアプリに続き、今度はClientRoutingを使う、Contactsアプリを作ってみましょう。基本は本家のチュートリアル(Contacts Example - Documentation - Spine)に従って進めます。
完成すると以下のようになります。
本アプリケーションは以下の機能を持ちます。
- ContactデータのCRUD
- 文字入力毎にContactリストをリアルタイム検索する検索フィールド
- pushStateを使ったHistory
アプリケーションの準備
まずは、前回と同様に、Spine.appによってアプリケーションとModel, Controllerのひな形を生成します。Spine.appとHemはインストール済みという前提です。
./node_modules/spine.app/bin/spine app contacts cd contacts npm install .
../node_modules/spine.app/bin/spine model contact ../node_modules/spine.app/bin/spine controller contacts ../node_modules/spine.app/bin/spine controller contacts_main ../node_modules/spine.app/bin/spine controller contacts_sidebar
今回は、Contact Modelと、アプリケーション全体を管理するController(contacts.coffee)、左の検索フィールドおよびリストを管理するController(contatcs_sidebar.coffee)、参照・編集のペインを管理するController(contacts_main.coffee)を作ります。
Modelを作る
早速コードを見てみましょう。
models/contact.coffee
Spine = require 'spine' class Contact extends Spine.Model @configure('Contact', 'name', 'email') @extend @Local @filter: (query) -> return @all() unless query query = query.toLowerCase() @select (item) -> item.name?.toLowerCase().indexOf(query) isnt -1 or item.email?.toLowerCase().indexOf(query) isnt -1 module.exports = Contact
Contact Modelにはnameとemailのプロパティがあり、HTML5のLocalStorageに保存するように指定しています。また、今回リアルタイム検索で使うstaticなfilterメソッドを定義します。
参照・編集用Controllerを作る
次にリストで選択されたContactデータを参照、編集するためのControllerを作ります。ここは少し複雑で構成は以下の通りです。
- 参照用のController: Show
- 編集用のController: Edit
- ShowとEditを管理するController(Stack): Main
MainはSpine.Stackというクラスを継承しています。StackはControllerの一種ですが、特に今回のようなタブ表現をするような片方を選択したら、他は表示しない、というようなControllerグループを管理するために使います。
まずは参照用のShow Controllerを作ります。
controllers/contacts_main.coffee(抜粋)
class Show extends Spine.Controller logPrefix: '(Show)' className: 'show' events: 'click .edit': 'edit' constructor: -> @log 'constructor' super @active @change render: -> @log 'render' @html require('views/show')(@item) change: (params) => @log 'change' @item = Contact.find(params.id) @render() edit: -> @log 'edit' @navigate('/contacts', @item.id, 'edit')
ポイントは2点です。
- constructorにて、activeメソッドにchangeメソッドを指定することで、本Controllerが表示される際に、changeメソッドを呼び出すように指定している
- editメソッドにて、編集画面へのリンクが押されたら、navigateメソッドにより、Contactデータを指定して、Edit Controllerを呼び出している(ここで指定するパスは後ほど定義するroutesの内容となる)
次に編集用のEdit Controllerを作ります。
controllers/contacts_main.coffee(抜粋)
class Edit extends Spine.Controller logPrefix: '(Edit)' className: 'edit' events: 'submit form': 'submit' 'click .save': 'submit' 'click .delete': 'delete' elements: 'form': 'form' constructor: -> @log 'constructor' super @active @change render: -> @log 'render' @html require('views/form')(@item) change: (params) => @log 'change' @item = Contact.find(params.id) @render() submit: (e) -> @log 'submit' e.preventDefault() @item.fromForm(@form).save() @navigate('/contacts', @item.id) delete: -> @log 'delete' @item.destroy() if confirm('Are you sure?')
内容が編集用になっているだけで、ポイントは同じです。
最後に、これら2つのControllerを管理するMain Stackを作ります。
controllers/contacts_main.coffee(抜粋)
class Main extends Spine.Stack className: 'main stack' controllers: show: Show edit: Edit module.exports = Main
controllersというプロパティに、2つのControllerを定義していますね。
リスト表示用のControllerを作る
次に、左の検索フィールド、およびリストを管理するためのControllerを作ります。
controllers/contacts_sidebar.coffee
Spine = require 'spine' Contact = require 'models/contact' List = Spine.List $ = Spine.$ class Sidebar extends Spine.Controller logPrefix: '(Sidebar)' className: 'sidebar' events: 'keyup input[type=search]': 'filter' 'click footer button': 'create' elements: '.items': 'items' 'input[type=search]': 'search' constructor: -> @log 'constructor' super @html require('views/sidebar')() @list = new List el: @items template: require('views/item') selectFirst: true @list.bind('change', @change) Contact.bind('refresh change', @render) filter: -> @log 'filter' @query = @search.val() @render() render: => @log 'render' contacts = Contact.filter(@query) @list.render(contacts) change: (item) => @log 'change: ' + item @navigate('/contacts', item.id) create: -> @log 'create' item = Contact.create() @navigate('/contacts', item.id, 'edit') module.exports = Sidebar
ポイントは3点です。
- Spine.Listを使っている。これはSpineが提供するユーティリティControllerで、今回のようなリスト表現をする際に利用できる。
- リスト要素の選択状態を管理しており、要素の選択状態が変化した場合のイベント発火や選択状態表示制御をしてくれます。
- 検索フィールドのkeyupイベントを補足して、filterメソッドを呼び出し、最終的にContact Modelのfilterメソッドを呼び出し、入力されている文字列で検索して結果のリストを表示する。
- changeやcreateメソッドにて、navigateメソッドにより参照用Controllerまたは、編集用のControllerをactiveにするように指定している。
アプリケーション全体管理用のControllerを作る
Main(Show & Edit)、SlidebarというControllerを全て管理するContacts Controllerを作ります。
Spine = require 'spine' Contact = require 'models/contact' $ = Spine.$ Main = require 'controllers/contacts_main' Sidebar = require 'controllers/contacts_sidebar' class Contacts extends Spine.Controller logPrefix: '(Contacts)' className: 'contacts' constructor: -> @log 'constructor' super @sidebar = new Sidebar @main = new Main @routes '/contacts/:id/edit': (params) -> @sidebar.active(params) @main.edit.active(params) '/contacts/:id': (params) -> @sidebar.active(params) @main.show.active(params) divide = $('<div />').addClass('vdivide') @append(@sidebar, divide, @main) Contact.fetch() module.exports = Contacts
ポイントは1点で、Main, Slidebarのオブジェクトを生成し、routesの定義を行っています。routesの指定では、パスに対してコールバック関数を指定し、その中で、どのControllerを表示(active)にするか定義します。
BootstrapとなるApp Controllerを作成する
最後にApp Controllerを作成します。
require 'lib/setup' Spine = require 'spine' Contacts = require 'controllers/contacts' class App extends Spine.Controller logPrefix: '(App)' constructor: -> @log 'constructor' super @contacts = new Contacts @append @contacts Spine.Route.setup(history: true) module.exports = App
ポイントは1点で、Spine.Route.setup()を呼び出し、Routing機能を初期化しています。引数に何も指定しない場合は#フラグメントを使ったURLとなり、上記のようにhistoryをtrueに指定すると、HTML5のHistoryAPIに対応しているブラウザであれば、#フラグメントなしのURLになります。
まとめ
少し長かったですが、SpineでRouterを使ってページ遷移を制御するか分かったと思います。SpineにはRouter機能はController内に存在しており、routes, active, navigateの3要素を使って実装します。
各パーツのControllerを作成し、組み合わせていく感じが非常に分かりやすくてよいと感じています。
それでは!