読者です 読者をやめる 読者になる 読者になる

Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

JavaScriptMVCフレームワーク「Spine.js」のRoute & HTML5 HistoryAPI を使ってみる

javascript Spine.js CoffeeScript

むらた(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点です。

  1. constructorにて、activeメソッドにchangeメソッドを指定することで、本Controllerが表示される際に、changeメソッドを呼び出すように指定している
  2. 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点です。

  1. Spine.Listを使っている。これはSpineが提供するユーティリティControllerで、今回のようなリスト表現をする際に利用できる。
    1. リスト要素の選択状態を管理しており、要素の選択状態が変化した場合のイベント発火や選択状態表示制御をしてくれます。
  2. 検索フィールドのkeyupイベントを補足して、filterメソッドを呼び出し、最終的にContact Modelのfilterメソッドを呼び出し、入力されている文字列で検索して結果のリストを表示する。
  3. 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を作成し、組み合わせていく感じが非常に分かりやすくてよいと感じています。

それでは!