(function($, window, document, undefined){
// ユーティリティ用のメソッドを定義
$.extend($.fn, {
// 要素に紐付けられたモデルデータを返す
item: function(){
// 紐付けられたモデルデータを取得
var item = $(this).tmplItem().data;
// モデルデータを返す(reloadで再取得してから)
// http://spinejs.com/api/models内のreloadAPI参照
return ($.isFunction(item.reload) ? item.reload() : null);
},
// 渡されたモデルデータを持つ要素を返す
forItem: function(item){
return this.filter(function(){
// 紐付けられたもモデルデータを代入
var compare = $(this).tmplItem().data;
// モデルデータがあり、モデルデータと一致すればその要素を返す
// http://spinejs.com/api/models内のeqlAPI参照
if (item.eql && item.eql(compare) || item === compare) return true;
});
},
// 指定された要素内の配列を返す
serializeForm: function(){
// 返り値用にオブジェクトを生成
var result = {};
// 指定された要素をシリアライズし、配列を返す
$.each($(this).find('input, textarea').serializeArray(), function(i, item){
result[item.name] = item.value;
});
// JSONオブジェクトを返す(e.x.{ email: '...', first_name: '...', last_name: '...' })
return result;
}
});
// Contactモデル
// Spineの最新版だと設定方法は変更されているので注意
var Contact = Spine.Model.setup('Contact', ['first_name', 'last_name', 'email']);
// ローカルストレージを利用するのでLocalを継承(要local.js)
Contact.extend(Spine.Model.Local);
// テンプレート内で使用するためにインスタンスメソッドを追加
Contact.include({
fullName: function(){
// ファーストネーム、ラストネームが設定されていない場合はfalseを返す
if (!this.first_name && !this.last_name) return;
return (this.first_name + ' ' + this.last_name);
}
});
// Sidebarコントローラー
var Sidebar = Spine.Controller.create({
// インスタンス変数を追加
elements: {
'.items': 'items'
},
// buttonをclickするとcreateメソッドを叩く
events: {
'click button': 'create'
},
// イベントのコールバックとして関数が呼び出された際に、
// 正しいコンテキストのもとで実行されることを保証
proxied: ['render'],
// テンプレートを描画
template: function(items){
return ($('#contactsTemplate').tmpl(items));
},
// 初期化処理
init: function(){
// レコード一覧を作成するクラスからインスタンスを作成
// .itemクラスが必須(選択された要素には.currentクラスが付与される)
// https://raw.github.com/maccman/book-assets/master/ch11/spine.contacts/lib/spine.list.js
this.list = Spine.List.init({
el: this.items,
template: this.template
});
// リスト上で異なる項目が選択された際に、該当する連絡先を表示
this.list.bind('change', this.proxy(function(item){
// グローバルなSpine.Appに対してshow:contactイベントを発火(選択された項目を渡す)
this.App.trigger('show:contact', item);
}));
// 連絡先が変更(あるいは新規作成)された場合に、
// リスト上で選択されている項目を切り替えます
// グローバルなSpine.Appに対してイベントを登録
this.App.bind('show:contact edit:contact', this.list.change);
// リストが更新あるいは変更された場合に再描画
Contact.bind('refresh change', this.render);
},
// リストを描画
render: function(){
// 全モデルインスタンス取得
var items = Contact.all();
// listのrenderメソッドを実行(全モデルインスタンスを渡す)
this.list.render(items);
},
// 新規作成のボタンがクリックされた際に呼ばれる
create: function(){
// 新規連絡先を追加
var item = Contact.create();
// グローバルなSpine.Appに対してedit:contactイベントを発火(新規モデルインスタンスを渡す)
this.App.trigger('edit:contact', item);
}
});
// Contactsコントローラー
var Contacts = Spine.Controller.create({
// インスタンス変数を追加
elements: {
'.show': 'showEl',
'.edit': 'editEl',
'.show .content': 'showContent',
'.edit .content': 'editContent'
},
// イベントの委譲
events: {
'click .optEdit': 'edit',
'click .optDestroy': 'destroy',
'click .optSave': 'save'
},
// イベントのコールバックとして関数が呼び出された際に、
// 正しいコンテキストのもとで実行されることを保証
proxied: ['render', 'show', 'edit'],
// 初期化処理
init: function(){
// 初期表示では連絡先が表示される
this.show();
// リストが変更された場合に再描画
Contact.bind('change', this.render);
// サイドバーで異なる項目が押下されると発火(連絡先画面を表示)
// グローバルなSpine.Appに対してイベントを登録
this.App.bind('show:contact', this.show);
// サイドバーで新規作成ボタンが押下されると発火(連絡先編集画面を表示)
// グローバルなSpine.Appに対してイベントを登録
this.App.bind('edit:contact', this.edit);
},
// currentプロパティに選択されたモデルを代入
change: function(item){
this.current = item;
this.render();
},
// 選択されている要素の連絡先画面、連絡先編集画面を選択されたモデルを元に描画
render: function(){
this.showContent.html($('#contactTemplate').tmpl(this.current));
this.editContent.html($('#editContactTemplate').tmpl(this.current));
},
// 連絡先画面を表示
show: function(item){
// モデルが渡されていればchangeイベントを叩く
if (item && item.model) this.change(item);
this.showEl.show();
this.editEl.hide();
},
// 連絡先編集画面を表示
edit: function(item){
// モデルが渡されていればchangeイベントを叩く
if (item && item.model) this.change(item);
this.showEl.hide();
this.editEl.show();
},
// 連絡先を削除
destroy: function(){
// 選択されているモデルデータを削除
this.current.destroy();
},
// 連絡先を保存
save: function(){
// 指定された要素をシリアライズし、配列を返す
var atts = this.editEl.serializeForm();
// 選択されているモデルデータを更新
// http://spinejs.com/api/models内のupdateAttributesAPI参照
this.current.updateAttributes(atts);
// 連絡先画面を表示
this.show();
}
});
// Appコントローラー
var App = Spine.Controller.create({
// el要素を設定
el: $('body'),
// インスタンス変数を追加
elements: {
'#sidebar': 'sidebarEl',
'#contacts': 'contactsEl'
},
// 初期化処理
init: function(){
// Sidebarコントローラを初期化
this.sidebar = Sidebar.init({ el: this.sidebarEl });
// Contactsコントローラを初期化
this.contact = Contacts.init({ el: this.contactsEl });
// モデルデータをローカルストレージから取得
Contact.fetch();
}
});
// DOMContentLoaded後にinit()関数を叩く
$(function(){
App.init();
});
}(jQuery, window, this.document));
<script type="text/x-jquery-tmpl" id="contactsTemplate">
<li class="item">
{{if fullName()}}
<div>${fullName()}</div>
{{else}}
<div>名前なし</div>
{{/if}}
</li>
</script>
<script type="text/x-jquery-tmpl" id="contactTemplate">
<dl>
<dt>名前</dt>
<dd>${first_name} ${last_name}</dd>
<dt>メールアドレス</dt>
{{if email}}
<dd>${email}</dd>
{{else}}
<dd>なし</dd>
{{/if}}
</dl>
</script>
<script type="text/x-jquery-tmpl" id="editContactTemplate">
<dl>
<dt>ファーストネーム</dt>
<dd><input type="text" name="first_name" value="${first_name}" autofocus></dd>
<dt>ラストネーム</dt>
<dd><input type="text" name="last_name" value="${last_name}"></dd>
<dt>メールアドレス</dt>
<dd><input type="text" name="email" value="${email}"></dd>
</dl>
</script>
<div id="sidebar">
<ul class="items"></ul>
<footer>
<button>新規連絡先</button>
</footer>
</div>
<div id="contacts">
<div class="show">
<ul class="options">
<li class="optEdit">連絡先の編集</li>
</ul>
<div class="content"></div>
</div>
<div class="edit">
<ul class="options">
<li class="optSave default">連絡先の保存</li>
<li class="optDestroy">連絡先の削除</li>
</ul>
<div class="content"></div>
</div>
</div>
body{
line-height:2;
}
#sidebar{
width:20%;
float:left;
}
#sidebar .current{
background:#000;
color:#fff;
}
#contacts{
width:75%;
float:right;
}
#contacts dt{
background:#efefef;
}
#contacts .options{
margin-bottom:1em;
overflow:hidden;
}
#contacts .options li{
width:10em;
margin-right:1em;
float:left;
background:#000;
color:#fff;
text-align:center;
}
External resources loaded into this fiddle: