相互に依存するフィールドを定義したいことがあります。
n個のフィールドのうち、n-1個のフィールドが決定すれば、残り1個も自動的に算出される場合などです。
例として、BMIを計算するプログラムを考えてみましょう。身長、体重、BMIの3つのフィールドがあり、うち2つを入力すると残り1個が決定されます。
理想としては、以下のようなクラスを定義すると、よしなに計算してくれると便利なところです。
App.bmi = Ember.Controller.create
height: (->
weight = @get('weight')
bmi = @get('bmi')
return Math.sqrt(weight / bmi) * 100
).property('bmi', 'weight'),
weight: (->
meter = @get('height') / 100.0
bmi = @get('bmi')
return bmi * (meter * meter)
).property('height', 'bmi')
bmi: (->
meter = @get('height') / 100.0
return @get('weight') / (meter * meter)
).property('height', 'weight')
しかし、このプログラムはうまく動きません。プログラムを読み込んだ段階でstack over flowが起こってしまいます。
Ember.jsのpropertyは、循環参照があると、処理できません。
この挙動はEmber.jsに限らず、データバインディング機能を備えているフレームワークに共通する動作です。
経験上、今回のような挙動をさせたい時には、テキストフィールドが設定されたタイミングで、計算可能なら値をセットするという処理を走らせるのが良いようです。
propertyは今回のケースでは使えません。
具体的な実装を見ていきます。テキストフィールドが設定されたタイミングを取るために、focusOutイベントをキャッチして計算を行うようにします。
focusOutを捉えるために、Ember.TextFieldの子クラスを定義します。
App.CalcTextField = Ember.TextField.extend
init: (->
@_super()
@on("focusOut", this, this.recalc)
),
recalc: (->
App.bmi.tryToCalcRemain()
)
テンプレート定義は以下のとおりです。
体重{{view App.CalcTextField valueBinding="weight"}}kg /
(身長{{view App.CalcTextField valueBinding="height"}}cm ** 2)
=
BMI{{view App.CalcTextField valueBinding="bmi"}}
tryToCalcRemainのコードも掲載します。もし1つだけフィールドが設定されてなければ、そのフィールドを設定します。
App.bmi = Ember.Controller.create
height: null,
weight: null,
bmi: null,
calcHeight: (->
weight = @get('weight')
bmi = @get('bmi')
@set('height', Math.sqrt(weight / bmi) * 100)
),
calcWeight: (->
meter = @get('height') / 100.0
bmi = @get('bmi')
@set('weight', bmi * (meter * meter))
),
calcBmi: (->
meter = @get('height') / 100.0
@set('bmi', @get('weight') / (meter * meter))
),
tryToCalcRemain: (->
height = @get('height')
weight = @get('weight')
bmi = @get('bmi')
if (@isPresent(height) && @isPresent(weight) && !@isPresent(bmi))
@calcBmi()
else if (@isPresent(height) && !@isPresent(weight) && @isPresent(bmi))
@calcWeight()
else if (!@isPresent(height) && @isPresent(weight) && @isPresent(bmi))
@calcHeight()
),
isPresent: ((v) ->
v && v.length > 0
)
実際の動作を以下のページで確認できます。
以上です。
今回のような、いつも自動で計算する、というわけではないケースは、データバインディング機能を使って自動計算するのは意外と相性が悪い気がします。
このケースに関しては、慣れないうちはうまく処理を組むのが難しいと思います。