2011年2月26日更新
SproutCoreのGUIデザイン
アプリケーションタイトルの変更
main.jsなどから、document.titleに設定します。
document.title = 'blog'.loc()
ビューの作成
プロジェクトディレクトリに移動して、以下を実行すると、 views/以下にそのファイルが作成されます。
$ sc-gen view Blog.CategoryView
でもだいたいmain_page.jsで事足りますので、 あまり使う機会はないかも。
参考
ボタン
postView: SC.ButtonView.design({
title: 'post',
isEnabled: YES,
isDefault: YES
target: 'Blog.articleController',
action: 'createArticle'
})
ボタンを押したときに、 Blog.articleController.createArticle()を実行します。 アイコンボタンを作る場合はtitleをnullにして、 iconプロパティに特定の名前を設定します。
isDefaultをYESにすると、Enterキーを押したときに 実行されるボタンになります。
リスト
categoriesView: SC.ScrollView.design({
contentView: SC.SourceListView.design({
exampleView: Blog.CategoryView
})
})
articlesView: SC.ScrollView.design({
contentView: SC.ListView.design({
})
})
上記画像の左側がSourceListViewで、右が普通のListViewです。
2行で表示する、とか、そういうCSSで対応できないような範囲で 個々の見た目を変えたければ、SC.ListItemViewを拡張した型を作って、 SC.ListView#exampleViewに設定します。
参考
ドロップダウンメニュー
selectionView: SC.SelectButtonView.design({
title: 'title',
objects: [
{ name: 'PC等', value: 'pc' },
{ name: 'Web製作', value: 'web' }
],
value: 'pc',
theme: 'square',
nameKey: 'name',
valueKey: 'value'
})
nameKeyを省略したときは、objects[i].toString()の値を使います。 同様に、valueKeyを省略したときは、objects[i]を使います。
valueの値がobjectsに含まれていない場合、 titleの値がボタンのタイトルに使われます。 このとき、valueの比較は===演算子により行われますので注意です。
調べると、SC.SelectViewがSC.SelectButtonViewに代わるようです。 ですが使ってみると、ドロップダウンから選択すると、 parentMenuがnullまたはundefinedだというエラーで停止します。 SproutCoreのメーリングリストで2010年2月頃に話題に挙がり、 修正もされているような記述がありましたが、 SproutCoreの1.4.5でもまだバグが残っている状態です。
タブ
nowShowingをnullにすると、どのタブも選択されていない状態になります。
middle: SC.View.design({
layout: { top: 75, bottom: 115, left: 0, right: 0 },
childViews: 'tab'.w(),
tab: SC.TabView.design({
nowShowing: 'Blog.publicPage.mainView',
items: [
{ title: 'public'.loc(),
value: 'Blog.publicPage.mainView'
},
{ title: 'acquisition'.loc(),
value: 'Blog.privatePage.mainView'
}
],
itemTitleKey: 'title',
itemValueKey: 'value'
})
})
ここで、valueのパスはSC.Pageの名前で、 それはふつうに書いてもいいですし、sc-genを使ってもいいです。
$ sc-gen design Blog.publicPage
並び替え
まずはSC.Query等のクエリで対応するのが正解かなあと思いますが、 使えない場合は、SC.ArrayController#orderByが便利かもしれません。
Blog.articlesController = SC.ArrayController.create({
orderBy: 'createdDate DESC, ...'
})
テーブル
使う前に、Buildfileへsproutcore/tableを追加しておきます。
required => [:sproutcore, 'sproutcore/table']
テーブルもスクロールさせる場合は、 ListViewと同じようにScrollViewに含めるといいです。
フォーマット
TableViewでフォーマットするには、 TableColumnのオプションにformatterを渡します。 これが関数なら、その戻り値をテーブルデータに表示します。 以下はSC.DateTimeを表示する場合。一部抜粋。
tableView: SC.TableView.design({
columns: [
SC.TableColumn.create({
label: 'created date',
key: 'createdDate',
formatter: function(d){
return d.toFormattedString('%Y/%m/%d')
}
}),
...
],
contentBinding: 'Blog.articlesController.arrangedObjects',
selectionBinding: 'Blog.articlesController.selection',
selectOnMouseDown: YES,
exampleView: SC.TableRowView
})
多言語対応
ビューとはちょっと違うかもしれませんが、まあ見た目なので。 まず言語ファイルを作成するには、以下のようにします。
$ sc-gen language Blog Japanese
sc-genのヘルプでは、
# エラーが出るよ
$ sc-gen language Language
となっていますが、実際は違うので注意です。いちおうWiki では修正されているみたいですが。。
あとは生成した言語ファイルを修正して、String#locを呼び出すだけです。 個人的によく使うのは、
'%Y-%m-%d': '%Y/%m/%d'
と定義しておいて、'%Y-%m-%d'.loc()とか。 置換パラメータを持っている場合は、
'post failed[code=%@]': '書き込みに失敗しました[コード=%@]'
で、locに引数を渡します。
throw 'post failed[code=%@]'.loc(statusCode)
こうしておくと、未対応の言語でも最低限の表示ができますので。
ドラッグアンドドロップ(並び替え)
コントローラと連携して対応するっぽいです。 まず、コントローラをSC.CollectionViewDelegateで拡張します。
Blog.articlesController = SC.ArrayController.create(
SC.CollectionViewDelegate, {
...
})
次に、View側でcanReorderContentとisEditableをYESに設定します。 一部抜粋。
articlesView: SC.ScrollView.design({
contentView: SC.ListView.design({
contentBinding: 'Blog.articlesController.arrangedObjects',
selectionBinding: 'Blog.articlesController.selection',
canReorderContent: YES,
isEditable: YES
})
})
こうすると、該当するビュー内で、ドラッグアンドドロップを使った 並び替えができるようになります。あとは必要に応じて、 以下の関数を実装すれば終わりです。
- collectionViewDragDataTypes(view)
- ドラッグ開始時に実行される
- collectionViewDragDataForType(view, drag, dataType)
- 上記が成功したとき
- collectionViewPerformDragOperation(view, drag, op, i, p)
- ドロップ時に発生
参考ページ
ドラッグアンドドロップ(他のリストへドロップ)
並び替えとは少し違う形になります。
ドラッグ対象側のリスト
contentView: SC.ListView.design({
dragDataTypes: [Blog.Article]
})
ドロップされる側のリスト
contentView: SC.ListView.design({
isDropTarget: YES,
computeDragOperation: function(drag, e){
return SC.DRAG_ANY
},
acceptDragOperation: function(drag, op){
...
}
})
ドロップしたときの動作は、acceptDragOperationに記述します。
ドラッグアンドドロップ(他リスト中のアイテムへドロップ)
ドロップ先となるアイテムのビューをsc-genで新しく作り、 それをSC.ListItemViewで拡張したものに置き換えます。 で、他のリストへドロップする場合と同じように、 作成したリストへプロパティを追加すればいいです。 以下ではdragEnteredとdragExitedも実装していますが、 これはあってもなくてもいいです。 また、performDragOperationでは、thisはドロップ先のアイテムです。
Blog.CategoryView = SC.ListItemView.extend({
isDropTarget: YES,
computeDragOperations: function(drag, e){
return SC.DRAG_ANY
},
acceptDragOperation: function(drag, op){
var ctlr = drag.get('source')
var article = ctlr.get('selection').firstObject()
article.set('category', this.get('content'))
return YES
},
dragEntered: function(drag, e){
this.$().addClass('drop-target')
},
dragExited: function(drag, e){
this.$().removeClass('drop-target')
}
});
最後に、上記で作成したビューを ドロップ先のSC.ListView#exampleViewに設定します。
categoriesView: SC.ScrollView.design({
contentView: SC.SourceListView.design({
exampleView: Blog.CategoryView
})
})
Validator
詳しく調べていないのでざっくりと。 validatorはSC.Validatorを拡張して作ります。 sc-genを使えないので、自分で作らなければいけません。 とりあえず、main_page.jsの先頭に書くようにしています。
Blog.dateTimeValidator = SC.Validator.extend({
...
})
- validate(form, field): bool
- 変更時に実行される
- 変更時とはchange, submit, partialのこと; partial?
- 違反している場合はfalseを返すように作る
- validateError(form, field)
- validate()がfalseの時
- fieldValueForObject(obj, form, field)
- objectForFieldValue(value, form, field)
- いろいろ
- イメージ的にはkeydownイベントで発生してるっぽい
ビューへ適用するには、validatorに設定します。
createdDateView: SC.LabelView.design({
validator: Blog.dateTimeValidator.create({...})
})
メールアドレスやクレジットカードなど、一般的なものは SC.Validator以下に最初から用意されています。
observes式
ほぼメモ。あとで清書。
- observes('a')のような名前だけ
- 自分自身のaプロパティが変更されたら通知
- observes('.a')またはobserves('this.a')
- 同上
- observes('a.b.c')
- グローバル変数aのプロパティbから、cが変更されたら通知
- observes('*a.b')
- 自分自身の、aまたはa.bのどちらかが変更されたら通知
- おそらく*で開始した場合のみ自分自身を起点とする
- observes('a.b*c.d')
- グローバル変数aのプロパティbを起点として、cかc.dに変更があれば通知
こんなのも書けるっぽい。
function(){}.observes('this')
トラブルシューティング
selectObjectしても関連データが更新されていない
SC.ArrayController#selectObjectの直後、 関連するSC.Bindingが更新されるわけではないようです。
Blog.articleController = SC.ObjectController.create({
contentBinding: SC.Binding
.single('Blog.articlesController.selection')
})
ここで、
Blog.articlesController.selectObject(article)
var p = Blog.articleController.get('content')
pの値は直前に選んでいた記事か、 選んでいなければnullが設定されます。 正しく動かすには、invoke系関数を使います。
Blog.articlesController.selectObject(article)
Blog.articlesController.invokeLater(function(){
var p = Blog.articleController.get('content')
})
または、自分でSC.RunLoop.beginとSC.RunLoop.endを 呼び出して処理してもいいのかも。
追加や削除してもリストに反映されない
リレーションを張った状態で、 多側の内容をコントローラに設定、 それをリストにバインドしていると、 ビューの更新がされません。具体的に書くと、
contentBinding: SC.Binding
.single('Blog.categoriesController.selection')
.transform(function(value, binding){
return value ? value.get('articles') : null
})
ここで、contentはSC.ManyArray型になります。 この場合は変更が通知されません。 ちなみに、ビューが更新されないだけで、 内部のデータは変更されています。
次に、
contentBinding: SC.Binding
.single('Blog.categoriesController.selection')
.transform(function(value, binding){
var q = SC.Query.local(Blog.Article, 'category = {target}', {
target: value
})
return Blog.store.find(q)
})
この場合は、contentがSC.RecordArray型になり、 変更を反映するようになります。
変更していないけど変更したことにしたい
obj.propertyDidChange('name')
enumerableContentDidChangeも気になります。
SC.DropTargetがundefined
SC.DropTargetを拡張してドラッグアンドドロップを実装していると、 sc-serverで動かすぶんには普通に動くのですが、 sc-buildして展開した際に、
Uncaught SC.Object.extend expects a non-null value. Did you forget to 'sc_require' something? Or were you passing a Protocol to extend() as if it were a mixin?
というエラーが出るみたいです。 あきらめてisDropTarget版を作り直してください。
SC.SelectButtonViewの表示がvalueの値にならない
SC.SelectButtonViewの比較は===演算子なので、 オブジェクトのアドレスが異なれば違うものとして扱われます。
SC.SelectButtonView.design({
objectsBinding: 'Blog.categoriesController.arrangedObjects',
valueBinding: 'Blog.articleController.category',
theme: 'square'
})
これで通常は、valueの値にあわせて切り替わりますが、 SC.NestedStoreが関係する場合は混乱するかもしれません。 というのも、オリジナルのオブジェクトとNestedStoreから再取得した オブジェクトは異なるアドレスを持つので、 同じ値は無いというように扱われてしまうのですね。
なので、割と適当な回避策として、objectsのほうもNestedStoreから 再取得したものでバインドしてあげると期待通りに動きます。一部抜粋。
SC.SelectButtonView.design({
objectsBinding: 'Blog.altCategoriesController.arrangedObjects'
valueBinding: 'Blog.articleController.category'
})
呼び出す場所では以下のように。
var nstore = Blog.store.chain()
var q = SC.Query.local('Blog.Category')
Blog.altCategoriesController.set('content', nstore.find(q))
Blog.articleController.set('content', nstore.find(article))
SC.Validator.Numberのバグ
Validator.Numberはplacesで小数点以下の桁数を指定できます。
validator: SC.Validator.Number.extend({ places: 1 })
ですが、2011年2月時点でバグがあり、文字が一切入力できません。 仕方がないのでバグの部分だけを置き換えたものを作ります。
Vacations.DaysValidator = SC.Validator.Number.extend({
places: 1,
objectForFieldValue: function(value, form, field){
switch(SC.typeOf(value)){
case SC.T_STRING:
value = SC.uniJapaneseConvert(value)
value = value.replace(/,/g, '')
if(value.length === 0 || value.match(/^-$/))
value = null
else if(this.get('places') > 0)
value = parseFloat(value)
else
value = parseInt(value, 0)
if(isNaN(value))
value = ''
return value
case SC.T_NULL:
case SC.T_UNDEFINED:
default:
return null
}
},
validateKeyDown: function(form, field, charStr){
var text = field.$input().val()
if(!text)
text = ''
text += charStr
var pass = charStr.length === 0 || charStr === '-' || charStr === '.'
if(this.get('places') === 0){
if(pass)
return true
else{
var a = text.match(/^[\-{0,1}]?[0-9,\0]*/)
return a && a[0] === text
}
}else{
if(pass)
return true
else{
var a = text.match(/^[\-{0,1}]?[0-9,\0]*\.?[0-9\0]+/)
return a && a[0] === text
}
}
}
}
で、これをvalidatorに設定するとうまく動きます。