forked from sproutcore/sproutcore
-
Notifications
You must be signed in to change notification settings - Fork 1
/
binding.js
1053 lines (836 loc) · 36.2 KB
/
binding.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ==========================================================================
// Project: SproutCore Costello - Property Observing Library
// Copyright: ©2006-2011 Strobe Inc. and contributors.
// Portions ©2008-2011 Apple Inc. All rights reserved.
// License: Licensed under MIT license (see license.js)
// ==========================================================================
sc_require('ext/function');
sc_require('system/object');
/**
Debug parameter you can turn on. This will log all bindings that fire to
the console. This should be disabled in production code. Note that you
can also enable this from the console or temporarily.
@property {Boolean}
*/
SC.LOG_BINDINGS = NO ;
/**
Performance parameter. This will benchmark the time spent firing each
binding.
@property {Boolean}
*/
SC.BENCHMARK_BINDING_NOTIFICATIONS = NO ;
/**
Performance parameter. This will benchmark the time spend configuring each
binding.
@property {Boolean}
*/
SC.BENCHMARK_BINDING_SETUP = NO;
/**
Default placeholder for multiple values in bindings.
@property {String}
*/
SC.MULTIPLE_PLACEHOLDER = '@@MULT@@' ;
/**
Default placeholder for null values in bindings.
@property {String}
*/
SC.NULL_PLACEHOLDER = '@@NULL@@' ;
/**
Default placeholder for empty values in bindings.
@property {String}
*/
SC.EMPTY_PLACEHOLDER = '@@EMPTY@@' ;
/**
@class
A binding simply connects the properties of two objects so that whenever the
value of one property changes, the other property will be changed also. You
do not usually work with Binding objects directly but instead describe
bindings in your class definition using something like:
valueBinding: "MyApp.someController.title"
This will create a binding from "MyApp.someController.title" to the "value"
property of your object instance automatically. Now the two values will be
kept in sync.
Customizing Your Bindings
===
In addition to synchronizing values, bindings can also perform some basic
transforms on values. These transforms can help to make sure the data fed
into one object always meets the expectations of that object regardless of
what the other object outputs.
To customize a binding, you can use one of the many helper methods defined
on SC.Binding like so:
valueBinding: SC.Binding.single("MyApp.someController.title")
This will create a binding just like the example above, except that now the
binding will convert the value of MyApp.someController.title to a single
object (removing any arrays) before applying it to the "value" property of
your object.
You can also chain helper methods to build custom bindings like so:
valueBinding: SC.Binding.single("MyApp.someController.title").notEmpty("(EMPTY)")
This will force the value of MyApp.someController.title to be a single value
and then check to see if the value is "empty" (null, undefined, empty array,
or an empty string). If it is empty, the value will be set to the string
"(EMPTY)".
One Way Bindings
===
One especially useful binding customization you can use is the oneWay()
helper. This helper tells SproutCore that you are only interested in
receiving changes on the object you are binding from. For example, if you
are binding to a preference and you want to be notified if the preference
has changed, but your object will not be changing the preference itself, you
could do:
bigTitlesBinding: SC.Binding.oneWay("MyApp.preferencesController.bigTitles")
This way if the value of MyApp.preferencesController.bigTitles changes the
"bigTitles" property of your object will change also. However, if you
change the value of your "bigTitles" property, it will not update the
preferencesController.
One way bindings are almost twice as fast to setup and twice as fast to
execute because the binding only has to worry about changes to one side.
You should consider using one way bindings anytime you have an object that
may be created frequently and you do not intend to change a property; only
to monitor it for changes. (such as in the example above).
Adding Custom Transforms
===
In addition to using the standard helpers provided by SproutCore, you can
also defined your own custom transform functions which will be used to
convert the value. To do this, just define your transform function and add
it to the binding with the transform() helper. The following example will
not allow Integers less than ten. Note that it checks the value of the
bindings and allows all other values to pass:
valueBinding: SC.Binding.transform(function(value, isForward, binding) {
return ((SC.typeOf(value) === SC.T_NUMBER) && (value < 10)) ? 10 : value;
}).from("MyApp.someController.value")
If you would like to instead use this transform on a number of bindings,
you can also optionally add your own helper method to SC.Binding. This
method should simply return the value of this.transform(). The example
below adds a new helper called notLessThan() which will limit the value to
be not less than the passed minimum:
SC.Binding.notLessThan = function(minValue) {
return this.transform(function(value, isForward, binding) {
return ((SC.typeOf(value) === SC.T_NUMBER) && (value < minValue)) ? minValue : value ;
}) ;
} ;
You could specify this in your core.js file, for example. Then anywhere in
your application you can use it to define bindings like so:
valueBinding: SC.Binding.from("MyApp.someController.value").notLessThan(10)
Also, remember that helpers are chained so you can use your helper along with
any other helpers. The example below will create a one way binding that
does not allow empty values or values less than 10:
valueBinding: SC.Binding.oneWay("MyApp.someController.value").notEmpty().notLessThan(10)
Note that the built in helper methods all allow you to pass a "from"
property path so you don't have to use the from() helper to set the path.
You can do the same thing with your own helper methods if you like, but it
is not required.
Creating Custom Binding Templates
===
Another way you can customize bindings is to create a binding template. A
template is simply a binding that is already partially or completely
configured. You can specify this template anywhere in your app and then use
it instead of designating your own custom bindings. This is a bit faster on
app startup but it is mostly useful in making your code less verbose.
For example, let's say you will be frequently creating one way, not empty
bindings that allow values greater than 10 throughout your app. You could
create a binding template in your core.js like this:
MyApp.LimitBinding = SC.Binding.oneWay().notEmpty().notLessThan(10);
Then anywhere you want to use this binding, just refer to the template like
so:
valueBinding: MyApp.LimitBinding.beget("MyApp.someController.value")
Note that when you use binding templates, it is very important that you
always start by using beget() to extend the template. If you do not do
this, you will end up using the same binding instance throughout your app
which will lead to erratic behavior.
How to Manually Activate a Binding
===
All of the examples above show you how to configure a custom binding, but
the result of these customizations will be a binding template, not a fully
active binding. The binding will actually become active only when you
instantiate the object the binding belongs to. It is useful however, to
understand what actually happens when the binding is activated.
For a binding to function it must have at least a "from" property and a "to"
property. The from property path points to the object/key that you want to
bind from while the to path points to the object/key you want to bind to.
When you define a custom binding, you are usually describing the property
you want to bind from (such as "MyApp.someController.value" in the examples
above). When your object is created, it will automatically assign the value
you want to bind "to" based on the name of your binding key. In the
examples above, during init, SproutCore objects will effectively call
something like this on your binding:
binding = this.valueBinding.beget().to("value", this) ;
This creates a new binding instance based on the template you provide, and
sets the to path to the "value" property of the new object. Now that the
binding is fully configured with a "from" and a "to", it simply needs to be
connected to become active. This is done through the connect() method:
binding.connect() ;
Now that the binding is connected, it will observe both the from and to side
and relay changes.
If you ever needed to do so (you almost never will, but it is useful to
understand this anyway), you could manually create an active binding by
doing the following:
SC.Binding.from("MyApp.someController.value")
.to("MyApp.anotherObject.value")
.connect();
You could also use the bind() helper method provided by SC.Object. (This is
the same method used by SC.Object.init() to setup your bindings):
MyApp.anotherObject.bind("value", "MyApp.someController.value") ;
Both of these code fragments have the same effect as doing the most friendly
form of binding creation like so:
MyApp.anotherObject = SC.Object.create({
valueBinding: "MyApp.someController.value",
// OTHER CODE FOR THIS OBJECT...
}) ;
SproutCore's built in binding creation method make it easy to automatically
create bindings for you. You should always use the highest-level APIs
available, even if you understand how to it works underneath.
@since SproutCore 1.0
*/
SC.Binding = /** @scope SC.Binding.prototype */{
/**
This is the core method you use to create a new binding instance. The
binding instance will have the receiver instance as its parent which means
any configuration you have there will be inherited.
The returned instance will also have its parentBinding property set to the
receiver.
@param {String} [fromPath]
@returns {SC.Binding} new binding instance
*/
beget: function(fromPath) {
var ret = SC.beget(this) ;
ret.parentBinding = this;
if (fromPath !== undefined) ret = ret.from(fromPath) ;
return ret ;
},
/**
Returns a builder function for compatibility.
*/
builder: function() {
var binding = this,
ret = function(fromProperty) { return binding.beget().from(fromProperty); };
ret.beget = function() { return binding.beget(); } ;
return ret ;
},
/**
This will set "from" property path to the specified value. It will not
attempt to resolve this property path to an actual object/property tuple
until you connect the binding.
The binding will search for the property path starting at the root level
unless you specify an alternate root object as the second parameter to this
method. Alternatively, you can begin your property path with either "." or
"*", which will use the root object of the to side be default. This special
behavior is used to support the high-level API provided by SC.Object.
@param {String|Tuple} propertyPath A property path or tuple
@param {Object} [root] root object to use when resolving the path.
@returns {SC.Binding} this
*/
from: function(propertyPath, root) {
// if the propertyPath is null/undefined, return this. This allows the
// method to be called from other methods when the fromPath might be
// optional. (cf single(), multiple())
if (!propertyPath) return this ;
// beget if needed.
var binding = (this === SC.Binding) ? this.beget() : this ;
binding._fromPropertyPath = propertyPath ;
binding._fromRoot = root ;
binding._fromTuple = null ;
return binding ;
},
/**
This will set the "to" property path to the specified value. It will not
attempt to reoslve this property path to an actual object/property tuple
until you connect the binding.
@param {String|Tuple} propertyPath A property path or tuple
@param {Object} [root] root object to use when resolving the path.
@returns {SC.Binding} this
*/
to: function(propertyPath, root) {
// beget if needed.
var binding = (this === SC.Binding) ? this.beget() : this ;
binding._toPropertyPath = propertyPath ;
binding._toRoot = root ;
binding._toTuple = null ; // clear out any existing one.
return binding ;
},
/**
Attempts to connect this binding instance so that it can receive and relay
changes. This method will raise an exception if you have not set the
from/to properties yet.
@returns {SC.Binding} this
*/
connect: function() {
// If the binding is already connected, do nothing.
if (this.isConnected) return this ;
this.isConnected = YES ;
this._connectionPending = YES ; // its connected but not really...
this._syncOnConnect = YES ;
SC.Binding._connectQueue.add(this) ;
if (!SC.RunLoop.isRunLoopInProgress()) {
this._scheduleSync();
}
return this;
},
/** @private
Actually connects the binding. This is done at the end of the runloop
to give you time to setup your entire object graph before the bindings
try to activate.
*/
_connect: function() {
if (!this._connectionPending) return; //nothing to do
this._connectionPending = NO ;
var path, root,
bench = SC.BENCHMARK_BINDING_SETUP;
if (bench) SC.Benchmark.start("SC.Binding.connect()");
// try to connect the from side.
// as a special behavior, if the from property path begins with either a
// . or * and the fromRoot is null, use the toRoot instead. This allows
// for support for the SC.Object shorthand:
//
// contentBinding: "*owner.value"
//
path = this._fromPropertyPath; root = this._fromRoot ;
if (typeof path === "string") {
// if the first character is a '.', this is a static path. make the
// toRoot the default root.
if (path.indexOf('.') === 0) {
path = path.slice(1);
if (!root) root = this._toRoot ;
// if the first character is a '*', then setup a tuple since this is a
// chained path.
} else if (path.indexOf('*') === 0) {
path = [this._fromRoot || this._toRoot, path.slice(1)] ;
root = null ;
}
}
this._fromObserverData = [path, this, this.fromPropertyDidChange, root];
SC.Observers.addObserver.apply(SC.Observers, this._fromObserverData);
// try to connect the to side
if (!this._oneWay) {
path = this._toPropertyPath; root = this._toRoot ;
this._toObserverData = [path, this, this.toPropertyDidChange, root];
SC.Observers.addObserver.apply(SC.Observers, this._toObserverData);
}
if (bench) SC.Benchmark.end("SC.Binding.connect()");
// now try to sync if needed
if (this._syncOnConnect) {
this._syncOnConnect = NO ;
if (bench) SC.Benchmark.start("SC.Binding.connect().sync");
this.sync();
if (bench) SC.Benchmark.end("SC.Binding.connect().sync");
}
},
/**
Disconnects the binding instance. Changes will no longer be relayed. You
will not usually need to call this method.
@returns {SC.Binding} this
*/
disconnect: function() {
if (!this.isConnected) return this; // nothing to do.
// if connection is still pending, just cancel
if (this._connectionPending) {
this._connectionPending = NO ;
// connection is completed, disconnect.
} else {
SC.Observers.removeObserver.apply(SC.Observers, this._fromObserverData);
if (!this._oneWay) {
SC.Observers.removeObserver.apply(SC.Observers, this._toObserverData);
}
}
this.isConnected = NO ;
return this ;
},
/**
Invoked whenever the value of the "from" property changes. This will mark
the binding as dirty if the value has changed.
@param {Object} target The object that contains the key
@param {String} key The name of the property which changed
*/
fromPropertyDidChange: function(target, key) {
var v = target ? target.get(key) : null;
// if the new value is different from the current binding value, then
// schedule to register an update.
if (v !== this._bindingValue || key === '[]') {
this._setBindingValue(target, key) ;
this._changePending = YES ;
SC.Binding._changeQueue.add(this) ; // save for later.
this._scheduleSync();
}
},
/**
Invoked whenever the value of the "to" property changes. This will mark the
binding as dirty only if:
- the binding is not one way
- the value does not match the stored transformedBindingValue
if the value does not match the transformedBindingValue, then it will
become the new bindingValue.
@param {Object} target The object that contains the key
@param {String} key The name of the property which changed
*/
toPropertyDidChange: function(target, key) {
if (this._oneWay) return; // nothing to do
var v = target.get(key) ;
// if the new value is different from the current binding value, then
// schedule to register an update.
if (v !== this._transformedBindingValue) {
this._setBindingValue(target, key) ;
this._changePending = YES ;
SC.Binding._changeQueue.add(this) ; // save for later.
this._scheduleSync();
}
},
_scheduleSync: function() {
if (SC.RunLoop.isRunLoopInProgress() || SC.Binding._syncScheduled) { return; }
SC.Binding._syncScheduled = YES;
setTimeout(function() { SC.run(); SC.Binding._syncScheduled = NO; }, 1);
},
/** @private
Saves the source location for the binding value. This will be used later
to actually update the binding value.
*/
_setBindingValue: function(source, key) {
this._bindingSource = source;
this._bindingKey = key;
},
/** @private
Updates the binding value from the current binding source if needed. This
should be called just before using this._bindingValue.
*/
_computeBindingValue: function() {
var that = this,
source = this._bindingSource,
key = this._bindingKey,
v, idx;
this._bindingValue = v = (source ? source.getPath(key) : null);
// apply any transforms to get the to property value also
var transforms = this._transforms;
if (transforms) {
var len = transforms.length,
isForward = this.isForward(),
transform;
for(idx=0;idx<len;idx++) {
transform = transforms[idx] ;
v = transform(v, isForward, this) ;
}
if (!this._oneWay && this._syncTransformedValue && this._bindingValue !== v) {
setTimeout(function() {
that._fromTarget.setPathIfChanged(that._fromPropertyKey, v) ;
}, 1);
//@if(debug)
var _lastSyncTimestamp = new Date().getTime();
if (this._lastSyncTimestamp - 10 < _lastSyncTimestamp) {
SC.warn("Developer Warning: It looks like a binding is running into an infinite sync loop. Try to set your binding oneWay or set the syncTransformedValue parameter of your transform method to NO. fromPropertyKey: '%@' - bindingValue: '%@' - transformedBindingValue: '%@'.".fmt(that._fromPropertyKey, this._bindingValue, v));
if (!this._warnCount) this._warnCount = 0;
this._warnCount++;
if (this._warnCount > 100) throw 'Maximum call stack size exceeded.';
}
this._lastSyncTimestamp = _lastSyncTimestamp;
//@endif
}
}
// if error objects are not allowed, and the value is an error, then
// change it to null.
if (this._noError && SC.typeOf(v) === SC.T_ERROR) v = null ;
this._transformedBindingValue = v;
},
_connectQueue: SC.CoreSet.create(),
_alternateConnectQueue: SC.CoreSet.create(),
_changeQueue: SC.CoreSet.create(),
_alternateChangeQueue: SC.CoreSet.create(),
_changePending: NO,
/**
Call this method on SC.Binding to flush all bindings with changed pending.
@returns {Boolean} YES if changes were flushed.
*/
flushPendingChanges: function() {
// don't allow flushing more than one at a time
if (this._isFlushing) return NO;
this._isFlushing = YES ;
SC.Observers.suspendPropertyObserving();
var didFlush = NO,
log = SC.LOG_BINDINGS,
// connect any bindings
queue, binding ;
while((queue = this._connectQueue).length >0) {
this._connectQueue = this._alternateConnectQueue ;
this._alternateConnectQueue = queue ;
while(binding = queue.pop()) binding._connect() ;
}
// loop through the changed queue...
while ((queue = this._changeQueue).length > 0) {
if (log) SC.Logger.log("Begin: Trigger changed bindings") ;
didFlush = YES ;
// first, swap the change queues. This way any binding changes that
// happen while we flush the current queue can be queued up.
this._changeQueue = this._alternateChangeQueue ;
this._alternateChangeQueue = queue ;
// next, apply any bindings in the current queue. This may cause
// additional bindings to trigger, which will end up in the new active
// queue.
while(binding = queue.pop()) binding.applyBindingValue() ;
// now loop back and see if there are additional changes pending in the
// active queue. Repeat this until all bindings that need to trigger
// have triggered.
if (log) SC.Logger.log("End: Trigger changed bindings") ;
}
// clean up
this._isFlushing = NO ;
SC.Observers.resumePropertyObserving();
return didFlush ;
},
/**
This method is called at the end of the Run Loop to relay the changed
binding value from one side to the other.
*/
applyBindingValue: function() {
this._changePending = NO ;
// compute the binding targets if needed.
this._computeBindingTargets() ;
this._computeBindingValue();
var v = this._bindingValue,
tv = this._transformedBindingValue,
bench = SC.BENCHMARK_BINDING_NOTIFICATIONS,
log = SC.LOG_BINDINGS ;
// the from property value will always be the binding value, update if
// needed.
if (!this._oneWay && this._fromTarget) {
if (log) SC.Logger.log("%@: %@ -> %@".fmt(this, v, tv)) ;
if (bench) SC.Benchmark.start(this.toString() + "->") ;
if (this.isForward()) {
this._fromTarget.setPathIfChanged(this._fromPropertyKey, v) ;
} else {
this._fromTarget.setPathIfChanged(this._fromPropertyKey, tv) ;
}
if (bench) SC.Benchmark.end(this.toString() + "->") ;
}
// update the to value with the transformed value if needed.
if (this._toTarget) {
if (log) SC.Logger.log("%@: %@ <- %@".fmt(this, v, tv)) ;
if (bench) SC.Benchmark.start(this.toString() + "<-") ;
if (this.isForward()) {
this._toTarget.setPathIfChanged(this._toPropertyKey, tv) ;
} else {
this._toTarget.setPathIfChanged(this._toPropertyKey, v) ;
}
if (bench) SC.Benchmark.start(this.toString() + "<-") ;
}
},
/**
Calling this method on a binding will cause it to check the value of the
from side of the binding matches the current expected value of the
binding. If not, it will relay the change as if the from side's value has
just changed.
This method is useful when you are dynamically connecting bindings to a
network of objects that may have already been initialized.
*/
sync: function() {
// do nothing if not connected
if (!this.isConnected) return this;
// connection is pending, just note that we should sync also
if (this._connectionPending) {
this._syncOnConnect = YES ;
// we are connected, go ahead and sync
} else {
this._computeBindingTargets() ;
var target = this._fromTarget,
key = this._fromPropertyKey ;
if (!target || !key) return this ; // nothing to do
// Let's check for whether target is a valid observable with getPath.
// Common cases might have it be a Window or a DOM object.
//
// If we have a target, it is ready, but if it is invalid, that is WRONG.
if (!target.isObservable) {
SC.Logger.warn("Cannot bind '%@' to property '%@' on non-observable '%@'".fmt(this._toPropertyPath, key, target));
return this;
}
// get the new value
var v = target.getPath(key) ;
// if the new value is different from the current binding value, then
// schedule to register an update.
if (v !== this._bindingValue || key === '[]') {
this._setBindingValue(target, key) ;
this._changePending = YES ;
SC.Binding._changeQueue.add(this) ; // save for later.
}
}
return this ;
},
// set if you call sync() when the binding connection is still pending.
_syncOnConnect: NO,
_computeBindingTargets: function() {
if (!this._fromTarget) {
var path, root, tuple ;
// if the fromPropertyPath begins with a . or * then we may use the
// toRoot as the root object. Similar code exists in connect() so if
// you make a change to one be sure to update the other.
path = this._fromPropertyPath; root = this._fromRoot ;
if (typeof path === "string") {
// static path beginning with the toRoot
if (path.indexOf('.') === 0) {
path = path.slice(1) ; // remove the .
if (!root) root = this._toRoot; // use the toRoot optionally
// chained path beginning with toRoot. Setup a tuple
} else if (path.indexOf('*') === 0) {
path = [root || this._toRoot, path.slice(1)];
root = null ;
}
}
tuple = SC.tupleForPropertyPath(path, root) ;
if (tuple) {
this._fromTarget = tuple[0]; this._fromPropertyKey = tuple[1] ;
}
}
if (!this._toTarget) {
path = this._toPropertyPath; root = this._toRoot ;
tuple = SC.tupleForPropertyPath(path, root) ;
if (tuple) {
this._toTarget = tuple[0]; this._toPropertyKey = tuple[1] ;
}
}
},
/**
Returns the direction of the binding. If isForward is YES, then the value
being passed came from the "from" side of the binding (i.e. the "Binding.path"
you named). If isForward is NO, then the value came from the "to" side (i.e.
the property you named with "propertyBinding"). You can vary your transform
behavior if you are based on the direction of the change.
@returns {Boolean}
*/
isForward: function() {
return this._fromTarget === this._bindingSource;
},
/**
Configures the binding as one way. A one-way binding will relay changes
on the "from" side to the "to" side, but not the other way around. This
means that if you change the "to" side directly, the "from" side may have
a different value.
@param {String} [fromPath] from path to connect.
@param {Boolean} [aFlag] Pass NO to set the binding back to two-way
@returns {SC.Binding} this
*/
oneWay: function(fromPath, aFlag) {
// If fromPath is a bool but aFlag is undefined, swap.
if ((aFlag === undefined) && (SC.typeOf(fromPath) === SC.T_BOOL)) {
aFlag = fromPath; fromPath = null ;
}
// beget if needed.
var binding = this.from(fromPath) ;
if (binding === SC.Binding) binding = binding.beget() ;
binding._oneWay = (aFlag === undefined) ? YES : aFlag ;
return binding ;
},
/**
Adds the specified transform function to the array of transform functions.
The function you pass must have the following signature:
function(value) {} ;
It must return either the transformed value or an error object.
Transform functions are chained, so they are called in order. If you are
extending a binding and want to reset the transforms, you can call
resetTransform() first.
@param {Function} transformFunc the transform function.
@param {Boolean} syncTransformedValue Pass NO if you don't want the transformed value
to be sync with the "from" object.
@returns {SC.Binding} this
*/
transform: function(transformFunc, syncTransformedValue) {
var binding = (this === SC.Binding) ? this.beget() : this ;
var t = binding._transforms ;
// clone the transform array if this comes from the parent
if (t && (t === binding.parentBinding._transform)) {
t = binding._transforms = t.slice() ;
}
// create the transform array if needed.
if (!t) t = binding._transforms = [] ;
// add the transform function
t.push(transformFunc) ;
binding._syncTransformedValue = (syncTransformedValue === undefined) ? YES : syncTransformedValue ;
return binding;
},
/**
Resets the transforms for the binding. After calling this method the
binding will no longer transform values. You can then add new transforms
as needed.
@returns {SC.Binding} this
*/
resetTransforms: function() {
var binding = (this === SC.Binding) ? this.beget() : this ;
binding._transforms = null ; return binding ;
},
/**
Specifies that the binding should not return error objects. If the value
of a binding is an Error object, it will be transformed to a null value
instead.
Note that this is not a transform function since it will be called at the
end of the transform chain.
@param {String} [fromPath] from path to connect.
@param {Boolean} [aFlag] Pass NO to allow error objects again.
@returns {SC.Binding} this
*/
noError: function(fromPath, aFlag) {
// If fromPath is a bool but aFlag is undefined, swap.
if ((aFlag === undefined) && (SC.typeOf(fromPath) === SC.T_BOOL)) {
aFlag = fromPath; fromPath = null ;
}
// beget if needed.
var binding = this.from(fromPath) ;
if (binding === SC.Binding) binding = binding.beget() ;
binding._noError = (aFlag === undefined) ? YES : aFlag ;
return binding ;
},
/**
Adds a transform to the chain that will allow only single values to pass.
This will allow single values, nulls, and error values to pass through. If
you pass an array, it will be mapped as so:
[] => null
[a] => a
[a,b,c] => Multiple Placeholder
You can pass in an optional multiple placeholder or it will use the
default.
Note that this transform will only happen on forwarded valued. Reverse
values are send unchanged.
@param {String} fromPath from path or null
@param {Object} [placeholder] placeholder value.
@returns {SC.Binding} this
*/
single: function(fromPath, placeholder) {
if (placeholder === undefined) {
placeholder = SC.MULTIPLE_PLACEHOLDER ;
}
return this.from(fromPath).transform(function(value, isForward, binding) {
if (value && value.isEnumerable) {
var len = value.get('length');
value = (len>1) ? placeholder : (len<=0) ? null : value.firstObject();
}
return value ;
}) ;
},
/**
Adds a transform that will return the placeholder value if the value is
null, undefined, an empty array or an empty string. See also notNull().
@param {String} fromPath from path or null
@param {Object} [placeholder]
@returns {SC.Binding} this
*/
notEmpty: function(fromPath, placeholder) {
if (placeholder === undefined) placeholder = SC.EMPTY_PLACEHOLDER ;
return this.from(fromPath).transform(function(value, isForward, binding) {
if (SC.none(value) || (value === '') || (SC.isArray(value) && (value.get ? value.get('length') : value.length)=== 0)) {
value = placeholder ;
}
return value ;
}) ;
},
/**
Adds a transform that will return the placeholder value if the value is
null or undefined. Otherwise it will pass through untouched. See also notEmpty().
@param {String} fromPath from path or null
@param {Object} [placeholder]
@returns {SC.Binding} this
*/
notNull: function(fromPath, placeholder) {
if (placeholder === undefined) placeholder = SC.EMPTY_PLACEHOLDER ;
return this.from(fromPath).transform(function(value, isForward, binding) {
if (SC.none(value)) value = placeholder ;
return value ;
}) ;
},
/**
Adds a transform that will convert the passed value to an array. If
the value is null or undefined, it will be converted to an empty array.
@param {String} [fromPath]
@returns {SC.Binding} this
*/
multiple: function(fromPath) {
return this.from(fromPath).transform(function(value, isForward, binding) {
if (!SC.isArray(value)) value = (value == null) ? [] : [value] ;
return value ;
}) ;
},
/**
Adds a transform to convert the value to a bool value. If the value is
an array it will return YES if array is not empty. If the value is a string
it will return YES if the string is not empty.
@param {String} [fromPath]
@returns {SC.Binding} this
*/
bool: function(fromPath) {
return this.from(fromPath).transform(function(value, isForward, binding) {
var t = SC.typeOf(value) ;
if (t === SC.T_ERROR) return value ;
return (t == SC.T_ARRAY) ? (value.length > 0) : (value === '') ? NO : !!value ;
}) ;
},
/**
Adds a transform that forwards the logical 'AND' of values at 'pathA' and
'pathB' whenever either source changes. Note that the transform acts strictly
as a one-way binding, working only in the direction
'pathA' AND 'pathB' --> value (value returned is the result of ('pathA' && 'pathB'))
Usage example where a delete button's 'isEnabled' value is determined by whether
something is selected in a list and whether the current user is allowed to delete:
deleteButton: SC.ButtonView.design({
isEnabledBinding: SC.Binding.and('MyApp.itemsController.hasSelection', 'MyApp.userController.canDelete')
})
@param {String} pathA The first part of the conditional
@param {String} pathB The second part of the conditional
*/
and: function(pathA, pathB) {
// create an object to do the logical computation
var gate = SC.Object.create({
valueABinding: pathA,
valueBBinding: pathB,
and: function() {
return (this.get('valueA') && this.get('valueB'));
}.property('valueA', 'valueB').cacheable()
});
// add a transform that depends on the result of that computation.
return this.from('and', gate).oneWay();
},
/**
Adds a transform that forwards the 'OR' of values at 'pathA' and
'pathB' whenever either source changes. Note that the transform acts strictly
as a one-way binding, working only in the direction
'pathA' AND 'pathB' --> value (value returned is the result of ('pathA' || 'pathB'))
@param {String} pathA The first part of the conditional
@param {String} pathB The second part of the conditional
*/
or: function(pathA, pathB) {
// create an object to the logical computation
var gate = SC.Object.create({
valueABinding: pathA,
valueBBinding: pathB,