1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('ext/function'); 9 sc_require('system/object'); 10 11 //@if(debug) 12 /** 13 Debug parameter you can turn on. This will log all bindings that fire to 14 the console. This should be disabled in production code. Note that you 15 can also enable this from the console or temporarily. 16 17 @type Boolean 18 */ 19 SC.LOG_BINDINGS = NO; 20 21 /** 22 Performance parameter. This will benchmark the time spent firing each 23 binding. 24 25 @type Boolean 26 */ 27 SC.BENCHMARK_BINDING_NOTIFICATIONS = NO; 28 29 /** 30 Performance parameter. This will benchmark the time spend configuring each 31 binding. 32 33 @type Boolean 34 */ 35 SC.BENCHMARK_BINDING_SETUP = NO; 36 //@endif 37 38 /** 39 Default placeholder for multiple values in bindings. 40 41 @type String 42 */ 43 SC.MULTIPLE_PLACEHOLDER = '@@MULT@@'; 44 45 /** 46 Default placeholder for null values in bindings. 47 48 @type String 49 */ 50 SC.NULL_PLACEHOLDER = '@@NULL@@'; 51 52 /** 53 Default placeholder for empty values in bindings. 54 55 @type String 56 */ 57 SC.EMPTY_PLACEHOLDER = '@@EMPTY@@'; 58 59 60 /** 61 @class 62 63 A binding simply connects the properties of two objects so that whenever the 64 value of one property changes, the other property will be changed also. You 65 do not usually work with Binding objects directly but instead describe 66 bindings in your class definition using something like: 67 68 valueBinding: "MyApp.someController.title" 69 70 This will create a binding from "MyApp.someController.title" to the "value" 71 property of your object instance automatically. Now the two values will be 72 kept in sync. 73 74 One-Way Bindings 75 === 76 77 By default, bindings are set up as two-way. In cases where you only need the 78 binding to function in one direction, for example if a value from a controller 79 is bound into a read-only LabelView, then for performance reasons you should 80 use a one-way binding. To do this, call the very useful `oneWay` helper: 81 82 valueBinding: SC.Binding.oneWay('MyApp.someController.title') 83 84 or: 85 86 valueBinding: SC.Binding.from('MyApp.someController.title').oneWay() 87 88 This way if the value of MyApp.someController.title changes, your object's 89 `value` will also update. Since `value` will never update on its own, this will 90 avoid the setup time required to plumb the binding in the other direction, 91 nearly doubling performance for this binding. 92 93 Transforms 94 === 95 96 In addition to synchronizing values, bindings can also perform some basic 97 transforms on values. These transforms can help to make sure the data fed 98 into one object always meets the expectations of that object, regardless of 99 what the other object outputs. 100 101 To customize a binding, you can use one of the many helper methods defined 102 on SC.Binding. For example: 103 104 valueBinding: SC.Binding.single("MyApp.someController.title") 105 106 This will create a binding just like the example above, except that now the 107 binding will convert the value of MyApp.someController.title to a single 108 object (removing any arrays) before applying it to the "value" property of 109 your object. 110 111 You can also chain helper methods to build custom bindings like so: 112 113 valueBinding: SC.Binding.single("MyApp.someController.title").notEmpty(null,"(EMPTY)") 114 115 This will force the value of MyApp.someController.title to be a single value 116 and then check to see if the value is "empty" (null, undefined, empty array, 117 or an empty string). If it is empty, the value will be set to the string 118 "(EMPTY)". 119 120 The following transform helper methods are included: `noError`, `single`, `notEmpty`, 121 `notNull`, `multiple`, `bool`, `not`, `isNull`, `and` (two values only), `or` (two 122 values only), and `equalTo`. See each method's documentation for a full description. 123 124 (Note that transforms are only applied in the forward direction (the 'to' side); values 125 are applied untransformed to the 'from' side. If the 'from' object has validation 126 needs, it should own and apply them itself, for example via a read/write calculated 127 property.) 128 129 Adding Custom Transforms 130 === 131 132 In addition to using the standard helpers provided by SproutCore, you can 133 also defined your own custom transform functions which will be used to 134 convert the value. To do this, just define your transform function and add 135 it to the binding with the transform() helper. The following example will 136 not allow Integers less than ten. Note that it checks the value of the 137 bindings and allows all other values to pass: 138 139 valueBinding: SC.Binding.transform(function (value, binding) { 140 return ((SC.typeOf(value) === SC.T_NUMBER) && (value < 10)) ? 10 : value; 141 }).from("MyApp.someController.value") 142 143 If you would like to instead use this transform on a number of bindings, 144 you can also optionally add your own helper method to SC.Binding. This 145 method should simply return the value of this.transform(). The example 146 below adds a new helper called notLessThan() which will limit the value to 147 be not less than the passed minimum: 148 149 SC.Binding.notLessThan = function (minValue) { 150 return this.transform(function (value, binding) { 151 return ((SC.typeOf(value) === SC.T_NUMBER) && (value < minValue)) ? minValue : value; 152 }); 153 }; 154 155 You could specify this in your core.js file, for example. Then anywhere in 156 your application you can use it to define bindings like so: 157 158 valueBinding: SC.Binding.from("MyApp.someController.value").notLessThan(10) 159 160 Also, remember that helpers are chained so you can use your helper along with 161 any other helpers. The example below will create a one way binding that 162 does not allow empty values or values less than 10: 163 164 valueBinding: SC.Binding.oneWay("MyApp.someController.value").notEmpty().notLessThan(10) 165 166 Note that the built in helper methods all allow you to pass a "from" 167 property path so you don't have to use the from() helper to set the path. 168 You can do the same thing with your own helper methods if you like, but it 169 is not required. 170 171 Creating Custom Binding Templates 172 === 173 174 Another way you can customize bindings is to create a binding template. A 175 template is simply a binding that is already partially or completely 176 configured. You can specify this template anywhere in your app and then use 177 it instead of designating your own custom bindings. This is a bit faster on 178 app startup but it is mostly useful in making your code less verbose. 179 180 For example, let's say you will be frequently creating one way, not empty 181 bindings that allow values greater than 10 throughout your app. You could 182 create a binding template in your core.js like this: 183 184 MyApp.LimitBinding = SC.Binding.oneWay().notEmpty().notLessThan(10); 185 186 Then anywhere you want to use this binding, just refer to the template like 187 so: 188 189 valueBinding: MyApp.LimitBinding.beget("MyApp.someController.value") 190 191 Note that when you use binding templates, it is very important that you 192 always start by using beget() to extend the template. If you do not do 193 this, you will end up using the same binding instance throughout your app 194 which will lead to erratic behavior. 195 196 How to Manually Activate a Binding 197 === 198 199 All of the examples above show you how to configure a custom binding, but 200 the result of these customizations will be a binding template, not a fully 201 active binding. The binding will actually become active only when you 202 instantiate the object the binding belongs to. It is useful however, to 203 understand what actually happens when the binding is activated. (Of course 204 you should always use the highest-level APIs available, even if you understand 205 how it works underneath; unless you have specific needs, you should rely on 206 the convenience `fooBinding` format.) 207 208 For a binding to function it must have at least a "from" property and a "to" 209 property. The from property path points to the object/key that you want to 210 bind from while the to path points to the object/key you want to bind to. 211 212 When you define a custom binding, you are usually describing the property 213 you want to bind from (such as "MyApp.someController.value" in the examples 214 above). When your object is created, it will automatically assign the value 215 you want to bind "to" based on the name of your binding key. In the 216 examples above, during init, SproutCore objects will effectively call 217 something like this on your binding: 218 219 binding = this.valueBinding.beget().to("value", this); 220 221 This creates a new binding instance based on the template you provide, and 222 sets the to path to the "value" property of the new object. Now that the 223 binding is fully configured with a "from" and a "to", it simply needs to be 224 connected to become active. This is done through the connect() method: 225 226 binding.connect(); 227 228 Now that the binding is connected, it will observe both the from and to side 229 and relay changes. 230 231 If you ever needed to do so (you almost never will, but it is useful to 232 understand this anyway), you could manually create an active binding by 233 doing the following: 234 235 SC.Binding.from("MyApp.someController.value") 236 .to("MyApp.anotherObject.value") 237 .connect(); 238 239 You could also use the bind() helper method provided by SC.Object. (This is 240 the same method used by SC.Object.init() to setup your bindings): 241 242 MyApp.anotherObject.bind("value", "MyApp.someController.value"); 243 244 Both of these code fragments have the same effect as doing the most friendly 245 form of binding creation like so: 246 247 248 MyApp.anotherObject = SC.Object.create({ 249 valueBinding: "MyApp.someController.value", 250 251 // OTHER CODE FOR THIS OBJECT... 252 253 }); 254 255 SproutCore's built in binding creation method make it easy to automatically 256 create bindings for you. If you need further documentation on SC.Binding's inner 257 workings, see the private method documentation in the source code. 258 259 @since SproutCore 1.0 260 */ 261 SC.Binding = /** @scope SC.Binding.prototype */{ 262 263 /** 264 Quack. 265 */ 266 isBinding: YES, 267 268 /** @private 269 This is the core method you use to create a new binding instance. The 270 binding instance will have the receiver instance as its parent which means 271 any configuration you have there will be inherited. 272 273 The returned instance will also have its parentBinding property set to the 274 receiver. 275 276 @param {String} [fromPath] 277 @returns {SC.Binding} new binding instance 278 */ 279 beget: function (fromPath) { 280 var ret = SC.beget(this); 281 ret.parentBinding = this; 282 283 // Mix adapters must be recreated on beget. 284 if (ret._MixAdapter) { 285 ret._mixAdapter = ret._MixAdapter.create(ret._mixAdapterHash); 286 ret = ret.from('aggregateProperty', ret._mixAdapter).oneWay(); 287 } 288 289 // Enables duplicate API calls for SC.Binding.beget and SC.Binding.from 290 if (fromPath !== undefined) ret = ret.from(fromPath); 291 return ret; 292 }, 293 294 /** @private 295 Returns a builder function for compatibility. 296 */ 297 builder: function () { 298 var binding = this, 299 ret = function (fromProperty) { return binding.beget().from(fromProperty); }; 300 ret.beget = function () { return binding.beget(); }; 301 return ret; 302 }, 303 304 /** 305 This will set "from" property path to the specified value. It will not 306 attempt to resolve this property path to an actual object/property tuple 307 until you connect the binding. 308 309 The binding will search for the property path starting at the root level 310 unless you specify an alternate root object as the second parameter to this 311 method. Alternatively, you can begin your property path with either "." or 312 "*", which will use the root object of the to side be default. This special 313 behavior is used to support the high-level API provided by SC.Object. 314 315 @param {String|Tuple} propertyPath A property path or tuple 316 @param {Object} [root] root object to use when resolving the path. 317 @returns {SC.Binding} this 318 */ 319 from: function (propertyPath, root) { 320 321 // if the propertyPath is null/undefined, return this. This allows the 322 // method to be called from other methods when the fromPath might be 323 // optional. (cf single(), multiple()) 324 if (!propertyPath) return this; 325 326 // beget if needed. 327 var binding = (this === SC.Binding) ? this.beget() : this; 328 binding._fromPropertyPath = propertyPath; 329 binding._fromRoot = root; 330 binding._fromTuple = null; 331 return binding; 332 }, 333 334 /** 335 This will set the "to" property path to the specified value. It will not 336 attempt to reoslve this property path to an actual object/property tuple 337 until you connect the binding. 338 339 If you are using the convenience format `fooBinding`, for example 340 `isVisibleBinding`, you do not need to call this method, as the `to` property 341 path will be generated for you when its object is created. 342 343 @param {String|Tuple} propertyPath A property path or tuple 344 @param {Object} [root] root object to use when resolving the path. 345 @returns {SC.Binding} this 346 */ 347 to: function (propertyPath, root) { 348 // beget if needed. 349 var binding = (this === SC.Binding) ? this.beget() : this; 350 binding._toPropertyPath = propertyPath; 351 binding._toRoot = root; 352 binding._toTuple = null; // clear out any existing one. 353 return binding; 354 }, 355 356 /** 357 Attempts to connect this binding instance so that it can receive and relay 358 changes. This method will raise an exception if you have not set the 359 from/to properties yet. 360 361 @returns {SC.Binding} this 362 */ 363 connect: function () { 364 // If the binding is already connected, do nothing. 365 if (this.isConnected) return this; 366 this.isConnected = YES; 367 this._connectionPending = YES; // its connected but not really... 368 this._syncOnConnect = YES; 369 370 SC.Binding._connectQueue.add(this); 371 372 if (!SC.RunLoop.isRunLoopInProgress()) { 373 this._scheduleSync(); 374 } 375 376 return this; 377 }, 378 379 /** @private 380 Actually connects the binding. This is done at the end of the runloop 381 to give you time to setup your entire object graph before the bindings 382 try to activate. 383 */ 384 _connect: function () { 385 if (!this._connectionPending) return; //nothing to do 386 this._connectionPending = NO; 387 388 var path, root; 389 390 //@if(debug) 391 var bench = SC.BENCHMARK_BINDING_SETUP; 392 if (bench) SC.Benchmark.start("SC.Binding.connect()"); 393 //@endif 394 395 // try to connect the from side. 396 // as a special behavior, if the from property path begins with either a 397 // . or * and the fromRoot is null, use the toRoot instead. This allows 398 // for support for the SC.Object shorthand: 399 // 400 // contentBinding: "*owner.value" 401 // 402 path = this._fromPropertyPath; 403 root = this._fromRoot; 404 405 if (typeof path === "string") { 406 407 // if the first character is a '.', this is a static path. make the 408 // toRoot the default root. 409 if (path.indexOf('.') === 0) { 410 path = path.slice(1); 411 if (!root) root = this._toRoot; 412 413 // if the first character is a '*', then setup a tuple since this is a 414 // chained path. 415 } else if (path.indexOf('*') === 0) { 416 path = [this._fromRoot || this._toRoot, path.slice(1)]; 417 root = null; 418 } 419 } 420 this._fromObserverData = [path, this, this.fromPropertyDidChange, root]; 421 SC.Observers.addObserver.apply(SC.Observers, this._fromObserverData); 422 423 // try to connect the to side 424 if (!this._oneWay) { 425 path = this._toPropertyPath; 426 root = this._toRoot; 427 this._toObserverData = [path, this, this.toPropertyDidChange, root]; 428 SC.Observers.addObserver.apply(SC.Observers, this._toObserverData); 429 } 430 431 //@if(debug) 432 if (bench) SC.Benchmark.end("SC.Binding.connect()"); 433 //@endif 434 435 // now try to sync if needed 436 if (this._syncOnConnect) { 437 this._syncOnConnect = NO; 438 //@if(debug) 439 if (bench) SC.Benchmark.start("SC.Binding.connect().sync"); 440 //@endif 441 this.sync(); 442 //@if(debug) 443 if (bench) SC.Benchmark.end("SC.Binding.connect().sync"); 444 //@endif 445 } 446 }, 447 448 /** 449 Disconnects the binding instance. Changes will no longer be relayed. You 450 will not usually need to call this method. 451 452 @returns {SC.Binding} this 453 */ 454 disconnect: function () { 455 if (!this.isConnected) return this; // nothing to do. 456 457 // if connection is still pending, just cancel 458 if (this._connectionPending) { 459 this._connectionPending = NO; 460 461 SC.Binding._connectQueue.remove(this); 462 // connection is completed, disconnect. 463 } else { 464 SC.Observers.removeObserver.apply(SC.Observers, this._fromObserverData); 465 if (!this._oneWay) { 466 SC.Observers.removeObserver.apply(SC.Observers, this._toObserverData); 467 } 468 469 // Remove ourselves from the change queue (if we are in it). 470 SC.Binding._changeQueue.remove(this); 471 } 472 473 this.isConnected = NO; 474 return this; 475 }, 476 477 /** @private 478 Indicates when the binding has been destroyed. 479 480 @type Boolean 481 @default NO 482 */ 483 isDestroyed: NO, 484 485 /** @private 486 Disconnects the binding and removes all properties and external references. Called by 487 either binding target object when destroyed. 488 489 @private 490 */ 491 destroy: function () { 492 // If we're already destroyed, there's nothing to do. 493 if (this.isDestroyed) return; 494 495 // Mark it destroyed. 496 this.isDestroyed = YES; 497 498 // Clean up the mix adapter, if any. (See adapter methods.) 499 if (this._mixAdapter) { 500 this._mixAdapter.destroy(); 501 this._mixAdapter = null; 502 this._MixAdapter = null; 503 this._mixAdapterHash = null; 504 } 505 506 // Disconnect the binding. 507 this.disconnect(); 508 509 // Aggressively null out internal properties. 510 this._bindingSource = null; 511 this._toRoot = this._toTarget = null; 512 this._fromRoot = this._fromTarget = null; 513 this._toObserverData = this._fromObserverData = null; 514 }, 515 516 /** @private 517 Invoked whenever the value of the "from" property changes. This will mark 518 the binding as dirty if the value has changed. 519 520 @param {Object} target The object that contains the key 521 @param {String} key The name of the property which changed 522 */ 523 fromPropertyDidChange: function (target, key) { 524 var v = target ? target.get(key) : null; 525 526 // In rare circumstances, getting a property can result in observers firing, 527 // which may in turn run code that disconnects the binding. The cause of 528 // this pattern has been difficult to determine and so until a concrete test 529 // scenario and a lower level fix can be found, show a warning and ignore 530 // the update. 531 if (!this.isConnected) { 532 //@if(debug) 533 SC.Logger.warn("Developer Warning: A binding attempted to update after it was disconnected. The update will be ignored for binding: %@".fmt(this._fromPropertyPath, this._fromTarget, this)); 534 //@endif 535 536 // Break early. 537 return; 538 } 539 540 // if the new value is different from the current binding value, then 541 // schedule to register an update. 542 if (v !== this._bindingValue || key === '[]') { 543 544 this._setBindingValue(target, key); 545 SC.Binding._changeQueue.add(this); // save for later. 546 547 this._scheduleSync(); 548 } 549 }, 550 551 /** @private 552 Invoked whenever the value of the "to" property changes. This will mark the 553 binding as dirty only if: 554 555 - the binding is not one way 556 - the value does not match the stored transformedBindingValue 557 558 if the value does not match the transformedBindingValue, then it will 559 become the new bindingValue. 560 561 @param {Object} target The object that contains the key 562 @param {String} key The name of the property which changed 563 */ 564 toPropertyDidChange: function (target, key) { 565 if (this._oneWay) return; // nothing to do 566 567 var v = target.get(key); 568 569 // In rare circumstances, getting a property can result in observers firing, 570 // which may in turn run code that disconnects the binding. The cause of 571 // this pattern has been difficult to determine and so until a concrete test 572 // scenario and a lower level fix can be found, show a warning and ignore 573 // the update. 574 if (!this.isConnected) { 575 //@if(debug) 576 SC.Logger.warn("Developer Warning: A binding attempted to update after it was disconnected. The update will be ignored for binding: %@".fmt(this)); 577 //@endif 578 579 // Break early. 580 return; 581 } 582 583 // if the new value is different from the current binding value, then 584 // schedule to register an update. 585 if (v !== this._transformedBindingValue) { 586 this._setBindingValue(target, key); 587 SC.Binding._changeQueue.add(this); // save for later. 588 589 this._scheduleSync(); 590 } 591 }, 592 593 /** @private */ 594 _scheduleSync: function () { 595 if (SC.RunLoop.isRunLoopInProgress() || SC.Binding._syncScheduled) { return; } 596 SC.Binding._syncScheduled = YES; 597 setTimeout(function () { SC.run(); SC.Binding._syncScheduled = NO; }, 1); 598 }, 599 600 /** @private 601 Saves the source location for the binding value. This will be used later 602 to actually update the binding value. 603 */ 604 _setBindingValue: function (source, key) { 605 this._bindingSource = source; 606 this._bindingKey = key; 607 }, 608 609 /** @private 610 Updates the binding value from the current binding source if needed. This 611 should be called just before using this._bindingValue. 612 */ 613 _computeBindingValue: function () { 614 var source = this._bindingSource, 615 key = this._bindingKey, 616 v; 617 618 this._bindingValue = v = (source ? source.getPath(key) : null); 619 this._transformedBindingValue = this._computeTransformedValue(v); 620 }, 621 622 /** @private 623 Applies transforms to the value and returns the transfomed value. 624 @param {*} value Binding value to transform 625 @returns {*} Transformed value 626 */ 627 _computeTransformedValue: function (value) { 628 var transforms = this._transforms, 629 idx, 630 len, 631 transform; 632 633 if (transforms) { 634 len = transforms.length; 635 for (idx = 0; idx < len; idx++) { 636 transform = transforms[idx]; 637 value = transform(value, this); 638 } 639 } 640 641 // if error objects are not allowed, and the value is an error, then 642 // change it to null. 643 if (this._noError && SC.typeOf(value) === SC.T_ERROR) { value = null; } 644 645 return value; 646 }, 647 648 _connectQueue: SC.CoreSet.create(), 649 _alternateConnectQueue: SC.CoreSet.create(), 650 _changeQueue: SC.CoreSet.create(), 651 _alternateChangeQueue: SC.CoreSet.create(), 652 653 /** @private 654 Call this method on SC.Binding to flush all bindings with changes pending. 655 656 @returns {Boolean} YES if changes were flushed. 657 */ 658 flushPendingChanges: function () { 659 660 // don't allow flushing more than one at a time 661 if (this._isFlushing) return NO; 662 this._isFlushing = YES; 663 SC.Observers.suspendPropertyObserving(); 664 665 var didFlush = NO, 666 // connect any bindings 667 queue, binding; 668 669 while ((queue = this._connectQueue).length > 0) { 670 this._connectQueue = this._alternateConnectQueue; 671 this._alternateConnectQueue = queue; 672 while ((binding = queue.pop())) { binding._connect(); } 673 } 674 675 // loop through the changed queue... 676 while ((queue = this._changeQueue).length > 0) { 677 //@if(debug) 678 if (SC.LOG_BINDINGS) SC.Logger.log("Begin: Trigger changed bindings"); 679 //@endif 680 681 didFlush = YES; 682 683 // first, swap the change queues. This way any binding changes that 684 // happen while we flush the current queue can be queued up. 685 this._changeQueue = this._alternateChangeQueue; 686 this._alternateChangeQueue = queue; 687 688 // next, apply any bindings in the current queue. This may cause 689 // additional bindings to trigger, which will end up in the new active 690 // queue. 691 while ((binding = queue.pop())) { binding.applyBindingValue(); } 692 693 // now loop back and see if there are additional changes pending in the 694 // active queue. Repeat this until all bindings that need to trigger 695 // have triggered. 696 //@if(debug) 697 if (SC.LOG_BINDINGS) SC.Logger.log("End: Trigger changed bindings"); 698 //@endif 699 } 700 701 // clean up 702 this._isFlushing = NO; 703 SC.Observers.resumePropertyObserving(); 704 705 return didFlush; 706 }, 707 708 /** @private 709 This method is called at the end of the Run Loop to relay the changed 710 binding value from one side to the other. 711 */ 712 applyBindingValue: function () { 713 // compute the binding targets if needed. 714 this._computeBindingTargets(); 715 this._computeBindingValue(); 716 717 var v = this._bindingValue, 718 tv = this._transformedBindingValue; 719 720 //@if(debug) 721 var bench = SC.BENCHMARK_BINDING_NOTIFICATIONS, 722 log = SC.LOG_BINDINGS; 723 //@endif 724 725 // the from property value will always be the binding value, update if 726 // needed. 727 if (!this._oneWay && this._fromTarget) { 728 //@if(debug) 729 if (log) SC.Logger.log("%@: %@ -> %@".fmt(this, v, tv)); 730 if (bench) SC.Benchmark.start(this.toString() + "->"); 731 //@endif 732 733 this._fromTarget.setPathIfChanged(this._fromPropertyKey, v); 734 735 //@if(debug) 736 if (bench) SC.Benchmark.end(this.toString() + "->"); 737 //@endif 738 } 739 740 // update the to value with the transformed value if needed. 741 if (this._toTarget) { 742 743 //@if(debug) 744 if (log) SC.Logger.log("%@: %@ <- %@".fmt(this, v, tv)); 745 if (bench) SC.Benchmark.start(this.toString() + "<-"); 746 //@endif 747 748 this._toTarget.setPathIfChanged(this._toPropertyKey, tv); 749 750 //@if(debug) 751 if (bench) SC.Benchmark.start(this.toString() + "<-"); 752 //@endif 753 } 754 }, 755 756 /** 757 Calling this method on a binding will cause it to check the value of the 758 from side of the binding matches the current expected value of the 759 binding. If not, it will relay the change as if the from side's value has 760 just changed. 761 762 This method is useful when you are dynamically connecting bindings to a 763 network of objects that may have already been initialized. Otherwise you 764 should not need to call this method. 765 */ 766 sync: function () { 767 var target, 768 key, 769 v, 770 tv; 771 772 // do nothing if not connected 773 if (!this.isConnected) return this; 774 775 // connection is pending, just note that we should sync also 776 if (this._connectionPending) { 777 this._syncOnConnect = YES; 778 779 // we are connected, go ahead and sync 780 } else { 781 this._computeBindingTargets(); 782 target = this._fromTarget; 783 key = this._fromPropertyKey; 784 if (!target || !key) return this; // nothing to do 785 786 // Let's check for whether target is a valid observable with getPath. 787 // Common cases might have it be a Window or a DOM object. 788 // 789 // If we have a target, it is ready, but if it is invalid, that is WRONG. 790 if (!target.isObservable) { 791 //@if(debug) 792 // Provide some developer support. 793 if (target === window) { 794 var msg = "Developer Warning: You are attempting to bind \"%{to_root}\"'s '%{to_property}' property to the non-observable 'window.%{key}'. It's likely that you've specified a local binding path without prepending a period. For example, you may have `%{to_property}Binding: '%{key}'` instead of `%{to_property}Binding: '.%{key}'`."; 795 msg = msg.fmt({ 796 to_root: (this._toRoot || 'object').toString(), 797 to_property: this._toPropertyPath, 798 key: key 799 }); 800 SC.Logger.warn(msg); 801 } else { 802 SC.Logger.warn("Developer Warning: Cannot bind \"%@\"'s '%@' property to property '%@' on non-observable '%@'".fmt((this._toRoot || 'object').toString(), this._toPropertyPath, key, target)); 803 } 804 //@endif 805 return this; 806 } 807 808 // get the new value 809 v = target.getPath(key); 810 tv = this._computeTransformedValue(v); 811 812 // if the new value is different from the current binding value, then 813 // schedule to register an update. 814 if (v !== this._bindingValue || tv !== this._transformedBindingValue || key === '[]') { 815 this._setBindingValue(target, key); 816 SC.Binding._changeQueue.add(this); // save for later. 817 } 818 } 819 820 return this; 821 }, 822 823 /** @private 824 set if you call sync() when the binding connection is still pending. 825 */ 826 _syncOnConnect: NO, 827 828 /** @private */ 829 _computeBindingTargets: function () { 830 var path, root, tuple; 831 832 if (!this._fromTarget) { 833 // if the fromPropertyPath begins with a . or * then we may use the 834 // toRoot as the root object. Similar code exists in connect() so if 835 // you make a change to one be sure to update the other. 836 path = this._fromPropertyPath; 837 root = this._fromRoot; 838 if (typeof path === "string") { 839 840 // static path beginning with the toRoot 841 if (path.indexOf('.') === 0) { 842 path = path.slice(1); // remove the . 843 if (!root) root = this._toRoot; // use the toRoot optionally 844 845 // chained path beginning with toRoot. Setup a tuple 846 } else if (path.indexOf('*') === 0) { 847 path = [root || this._toRoot, path.slice(1)]; 848 root = null; 849 } 850 } 851 852 tuple = SC.tupleForPropertyPath(path, root); 853 if (tuple) { 854 this._fromTarget = tuple[0]; 855 this._fromPropertyKey = tuple[1]; 856 } 857 } 858 859 if (!this._toTarget) { 860 path = this._toPropertyPath; 861 root = this._toRoot; 862 tuple = SC.tupleForPropertyPath(path, root); 863 if (tuple) { 864 this._toTarget = tuple[0]; 865 this._toPropertyKey = tuple[1]; 866 // Hook up _mixAdapter if needed (see adapter methods). 867 if (this._mixAdapter) { 868 this._mixAdapter.set('localObject', this._toTarget); 869 } 870 } 871 } 872 }, 873 874 // ------------------------------- 875 // Helper Methods 876 // 877 878 /** 879 Configures the binding as one way. A one-way binding will relay changes 880 on the "from" side to the "to" side, but not the other way around. This 881 means that if you change the "to" side directly, the "from" side will not 882 be updated, and may have a different value. 883 884 @param {String} [fromPath] from path to connect. 885 @param {Boolean} [aFlag] Pass NO to set the binding back to two-way 886 @returns {SC.Binding} this 887 */ 888 oneWay: function (fromPath, aFlag) { 889 890 // If fromPath is a bool but aFlag is undefined, swap. 891 if ((aFlag === undefined) && (SC.typeOf(fromPath) === SC.T_BOOL)) { 892 aFlag = fromPath; 893 fromPath = null; 894 } 895 896 // beget if needed. 897 var binding = this.from(fromPath); 898 if (binding === SC.Binding) binding = binding.beget(); 899 binding._oneWay = (aFlag === undefined) ? YES : aFlag; 900 901 return binding; 902 }, 903 904 /** 905 Adds the specified transform function to the array of transform functions. 906 907 The function you pass must have the following signature: 908 909 function (value) {}; 910 911 or: 912 913 function (value, binding) {}; 914 915 It must return either the transformed value or an error object. 916 917 Transform functions are chained, so they are called in order. If you are 918 extending a binding and want to reset its transforms, you can call 919 resetTransform() first. 920 921 @param {Function} transformFunc the transform function. 922 @returns {SC.Binding} this 923 */ 924 transform: function (transformFunc) { 925 var binding = (this === SC.Binding) ? this.beget() : this; 926 var t = binding._transforms; 927 928 // clone the transform array if this comes from the parent 929 if (t && (t === binding.parentBinding._transforms)) { 930 t = binding._transforms = t.slice(); 931 } 932 933 // create the transform array if needed. 934 if (!t) t = binding._transforms = []; 935 936 // add the transform function 937 t.push(transformFunc); 938 return binding; 939 }, 940 941 /** 942 Resets the transforms for the binding. After calling this method the 943 binding will no longer transform values. You can then add new transforms 944 as needed. 945 946 @returns {SC.Binding} this 947 */ 948 resetTransforms: function () { 949 var binding = (this === SC.Binding) ? this.beget() : this; 950 binding._transforms = null; 951 return binding; 952 }, 953 954 /** 955 Adds a transform to convert the value to a bool value. If the value is 956 an array it will return YES if array is not empty. If the value is a string 957 it will return YES if the string is not empty. 958 959 @param {String} [fromPath] 960 @returns {SC.Binding} this 961 */ 962 bool: function (fromPath) { 963 return this.from(fromPath).transform(function (v) { 964 var t = SC.typeOf(v); 965 if (t === SC.T_ERROR) return v; 966 return (t == SC.T_ARRAY) ? (v.length > 0) : (v === '') ? NO : !!v; 967 }); 968 }, 969 970 /** 971 Adds a transform that will *always* return an integer Number value. Null and undefined values will 972 return 0 while String values will be transformed using the parseInt method (according to the 973 radix) and Boolean values will be 1 or 0 if true or false accordingly. Other edge cases like NaN 974 or other non-Numbers will also return 0. 975 976 Example results, 977 978 - null => 0 979 - undefined => 0 980 - '123' => 123 981 - true => 1 982 - {} => 0 983 984 @param {String} fromPathOrRadix from path or the radix for the parsing or null for 10 985 @param {String} radix the radix for the parsing or null for 10 986 @returns {SC.Binding} this 987 */ 988 integer: function (fromPathOrRadix, radix) { 989 // Normalize arguments. 990 if (radix === undefined) { 991 radix = fromPathOrRadix; 992 fromPathOrRadix = null; 993 } 994 995 // Use base 10 by default. 996 if (radix === undefined) radix = 10; 997 998 return this.from(fromPathOrRadix).transform(function (value) { 999 1000 // Null or undefined will be converted to 0. 1001 if (SC.none(value)) { 1002 value = 0; 1003 1004 // String values will be converted to integer Numbers using parseInt with the given radix. 1005 } else if (typeof value === SC.T_STRING) { 1006 value = window.parseInt(value, radix); 1007 1008 // Boolean values will be converted to 0 or 1 accordingly. 1009 } else if (typeof value === SC.T_BOOL) { 1010 value = value ? 1 : 0; 1011 } 1012 1013 // All other non-Number values will be converted to 0 (this includes bad String parses above). 1014 if (typeof value !== SC.T_NUMBER || isNaN(value)) { 1015 value = 0; 1016 } 1017 1018 return value; 1019 }); 1020 }, 1021 1022 /** 1023 Adds a transform that will return YES if the value is null or undefined, NO otherwise. 1024 1025 @param {String} [fromPath] 1026 @returns {SC.Binding} this 1027 */ 1028 isNull: function (fromPath) { 1029 return this.from(fromPath).transform(function (v) { 1030 var t = SC.typeOf(v); 1031 return (t === SC.T_ERROR) ? v : SC.none(v); 1032 }); 1033 }, 1034 1035 /** 1036 Specifies that the binding should not return error objects. If the value 1037 of a binding is an Error object, it will be transformed to a null value 1038 instead. 1039 1040 Note that this is not a transform function since it will be called at the 1041 end of the transform chain. 1042 1043 @param {String} [fromPath] from path to connect. 1044 @param {Boolean} [aFlag] Pass NO to allow error objects again. 1045 @returns {SC.Binding} this 1046 */ 1047 noError: function (fromPath, aFlag) { 1048 // If fromPath is a bool but aFlag is undefined, swap. 1049 if ((aFlag === undefined) && (SC.typeOf(fromPath) === SC.T_BOOL)) { 1050 aFlag = fromPath; 1051 fromPath = null; 1052 } 1053 1054 // beget if needed. 1055 var binding = this.from(fromPath); 1056 if (binding === SC.Binding) binding = binding.beget(); 1057 binding._noError = (aFlag === undefined) ? YES : aFlag; 1058 1059 return binding; 1060 }, 1061 1062 /** 1063 Adds a transform to convert the value to the inverse of a bool value. This 1064 uses the same transform as bool() but inverts it. 1065 1066 @param {String} [fromPath] 1067 @returns {SC.Binding} this 1068 */ 1069 not: function (fromPath) { 1070 return this.from(fromPath).transform(function (v) { 1071 var t = SC.typeOf(v); 1072 if (t === SC.T_ERROR) return v; 1073 return !((t == SC.T_ARRAY) ? (v.length > 0) : (v === '') ? NO : !!v); 1074 }); 1075 }, 1076 1077 /** 1078 Adds a transform that will convert the passed value to an array. If 1079 the value is null or undefined, it will be converted to an empty array. 1080 1081 @param {String} [fromPath] 1082 @returns {SC.Binding} this 1083 */ 1084 multiple: function (fromPath) { 1085 return this.from(fromPath).transform(function (value) { 1086 /*jshint eqnull:true*/ 1087 if (!SC.isArray(value)) value = (value == null) ? [] : [value]; 1088 return value; 1089 }); 1090 }, 1091 1092 /** 1093 Adds a transform that will return the placeholder value if the value is 1094 null, undefined, an empty array or an empty string. See also notNull(). 1095 1096 @param {String} fromPath from path or null 1097 @param {Object} [placeholder] 1098 @returns {SC.Binding} this 1099 */ 1100 notEmpty: function (fromPath, placeholder) { 1101 if (placeholder === undefined) placeholder = SC.EMPTY_PLACEHOLDER; 1102 return this.from(fromPath).transform(function (value, isForward) { 1103 if (SC.none(value) || (value === '') || (SC.isArray(value) && (value.get ? value.get('length') : value.length) === 0)) { 1104 value = placeholder; 1105 } 1106 return value; 1107 }); 1108 }, 1109 1110 /** 1111 Adds a transform that will return the placeholder value if the value is 1112 null or undefined. Otherwise it will pass through untouched. See also notEmpty(). 1113 1114 @param {String} fromPath from path or null 1115 @param {Object} [placeholder] 1116 @returns {SC.Binding} this 1117 */ 1118 notNull: function (fromPath, placeholder) { 1119 if (placeholder === undefined) placeholder = SC.EMPTY_PLACEHOLDER; 1120 return this.from(fromPath).transform(function (value, isForward) { 1121 if (SC.none(value)) value = placeholder; 1122 return value; 1123 }); 1124 }, 1125 1126 /** 1127 Adds a transform to the chain that will allow only single values to pass. 1128 This will allow single values, nulls, and error values to pass through. If 1129 you pass an array, it will be mapped as so: 1130 1131 [] => null 1132 [a] => a 1133 [a,b,c] => Multiple Placeholder 1134 1135 You can pass in an optional multiple placeholder or it will use the 1136 default. 1137 1138 Note that this transform will only happen on forwarded valued. Reverse 1139 values are send unchanged. 1140 1141 @param {String} fromPath from path or null 1142 @param {Object} [placeholder] placeholder value. 1143 @returns {SC.Binding} this 1144 */ 1145 single: function (fromPath, placeholder) { 1146 if (placeholder === undefined) { 1147 placeholder = SC.MULTIPLE_PLACEHOLDER; 1148 } 1149 return this.from(fromPath).transform(function (value, isForward) { 1150 if (value && value.isEnumerable) { 1151 var len = value.get('length'); 1152 value = (len > 1) ? placeholder : (len <= 0) ? null : value.firstObject(); 1153 } 1154 return value; 1155 }); 1156 }, 1157 1158 /** 1159 Adds a transform that will *always* return a String value. Null and undefined values will return 1160 an empty string while all other non-String values will be transformed using the toString method. 1161 1162 Example results, 1163 1164 - null => '' 1165 - undefined => '' 1166 - 123 => '123' 1167 - true => 'true' 1168 - {} => '[object Object]' (i.e. x = {}; return x.toString()) 1169 1170 @param {String} fromPath from path or null 1171 @returns {SC.Binding} this 1172 */ 1173 string: function (fromPath) { 1174 return this.from(fromPath).transform(function (value) { 1175 1176 // Null or undefined will be converted to an empty string. 1177 if (SC.none(value)) { 1178 value = ''; 1179 1180 // Non-string values will be converted to strings using `toString`. 1181 } else if (typeof value !== SC.T_STRING && value.toString) { 1182 value = value.toString(); 1183 } 1184 1185 return value; 1186 }); 1187 }, 1188 1189 /* @private Used by mix adapter bindings. */ 1190 _sc_mixAdapterBinding: function (adapterClass) { 1191 var paths = []; 1192 1193 //@if(debug) 1194 // Add some developer support to prevent improper use. 1195 if (arguments.length < 3 ) { 1196 SC.Logger.warn('Developer Warning: Invalid mix binding, it should have at least two target paths'); 1197 } 1198 //@endif 1199 1200 // If either path is local, remove any * chains and append the localObject path to it. 1201 for (var i = 1; i < arguments.length; i++) { 1202 var path = arguments[i]; 1203 1204 if (path.indexOf('*') === 0 || path.indexOf('.') === 0) { 1205 path = path.slice(1).replace(/\*/g, '.'); 1206 path = '*localObject.' + path; 1207 } 1208 paths.push( path ); 1209 } 1210 1211 // Gets the adapter class and instantiates a nice copy. 1212 var adapterHash = { 1213 localObject: null, 1214 }; 1215 1216 // create the oneWay bindings pointing to the real data sources. 1217 // for naming use a hardcoded convention 'value' + index of the property/path. 1218 // of course, these properties are internal so we are not concerned by the naming convention 1219 for (i = 0; i < paths.length; ++i) { 1220 var key = 'value' + i; 1221 adapterHash[key + 'Binding'] = SC.Binding.oneWay(paths[i]); 1222 } 1223 1224 var adapter = adapterClass.create(adapterHash); 1225 1226 // Creates and populates the return binding. 1227 var ret = this.from('aggregateProperty', adapter).oneWay(); 1228 1229 // This is all needed later on by beget, which must create a new adapter instance 1230 // or risk bad behavior. 1231 ret._MixAdapter = adapterClass; 1232 ret._mixAdapterHash = adapterHash; 1233 ret._mixAdapter = adapter; 1234 1235 // On our way. 1236 return ret; 1237 }, 1238 1239 /** @private */ 1240 _sc_mixImpl: function(paths, mixFunction) { 1241 var len = paths.length, 1242 properties = []; 1243 1244 //@if(debug) 1245 // Add some developer support to prevent improper use. 1246 if (SC.none(mixFunction) || SC.typeOf(mixFunction) !== SC.T_FUNCTION ) { 1247 SC.Logger.error('Developer Error: Invalid mix binding, the last argument must be a function.'); 1248 } 1249 //@endif 1250 1251 // Create the adapter class that eventually will contain bindings pointing to all values that will be processed 1252 // by mixFunction. The effective aggregation is done by another property that depends on all these local properties 1253 // and is invalidated whenever they change. 1254 // First of all, create the list of the property names that the aggregate property depends on. 1255 // The names of these dynamically created properties are matching the pattern 1256 // mentioned above (into _sc_mixAdapterBinding): 'value' + index of the property/path 1257 for (var i = 0; i < len; ++i) { 1258 properties.push('value' + i); 1259 } 1260 1261 // Create a proxy SC.Object which will be bound to the each of the paths and contain a computed 1262 // property that will be dependent on all of the bound properties. The computed property will 1263 // return the result of the mix function. 1264 var adapter = SC.Object.extend({ 1265 // Use SC.Function.property to be able to pass an array as arguments to .property 1266 aggregateProperty: SC.Function.property(function() { 1267 // Get an array of current values that will be passed to the mix function. 1268 var values = properties.map(function (name) { 1269 return this.get(name); 1270 }, this); 1271 1272 // Call the mixFunction providing an array containing all current source property values. 1273 return mixFunction.apply(null, values); 1274 }, properties).cacheable() 1275 }); 1276 1277 return this._sc_mixAdapterBinding.apply(this, [adapter].concat(paths)); 1278 }, 1279 1280 /** 1281 Adds a transform that returns the logical 'AND' of all the values at the provided paths. This is 1282 a quick and useful way to bind a `Boolean` property to two or more other `Boolean` properties. 1283 1284 For example, imagine that we wanted to only enable a deletion button when an item in a list 1285 is selected *and* the current user is allowed to delete items. If these two values are set 1286 on controllers respectively at `MyApp.itemsController.hasSelection` and 1287 `MyApp.userController.canDelete`. We could do the following, 1288 1289 deleteButton: SC.ButtonView.design({ 1290 1291 // Action & target for the button. 1292 action: 'deleteSelectedItem', 1293 target: MyApp.statechart, 1294 1295 // Whether the list has a selection or not. 1296 listHasSelectionBinding: SC.Binding.oneWay('MyApp.itemsController.hasSelection'), 1297 1298 // Whether the user can delete items or not. 1299 userCanDeleteBinding: SC.Binding.oneWay('MyApp.userController.canDelete'), 1300 1301 // Note: Only enable when the list has a selection and the user is allowed! 1302 isEnabled: function () { 1303 return this.get('listHasSelection') && this.get('userCanDelete'); 1304 }.property('listHasSelection', 'userCanDelete').cacheable() 1305 1306 }) 1307 1308 However, this would be much simpler to write by using the `and` binding transform like so, 1309 1310 deleteButton: SC.ButtonView.design({ 1311 1312 // Action & target for the button. 1313 action: 'deleteSelectedItem', 1314 target: MyApp.statechart, 1315 1316 // Note: Only enable when the list has a selection and the user is allowed! 1317 isEnabledBinding: SC.Binding.and('MyApp.itemsController.hasSelection', 'MyApp.userController.canDelete') 1318 1319 }) 1320 1321 *Note:* the transform acts strictly as a one-way binding, working only in the one direction. 1322 1323 @param {String...} the property paths of source values that will be provided to the AND transform. 1324 */ 1325 and: function () { 1326 // Fast copy. 1327 var len = arguments.length, 1328 paths = new Array(len); 1329 for (var i = 0; i < len; i++) { paths[i] = arguments[i]; } 1330 1331 // Create a new mix implementation for the AND function. 1332 return this._sc_mixImpl(paths, function() { 1333 var result = true; 1334 1335 for (i = 0; result && (i < arguments.length); i++) { // Bails early if any value is false. 1336 result = result && arguments[i]; 1337 } 1338 1339 return result; 1340 }); 1341 }, 1342 1343 /** 1344 Adds a transform that returns the logical 'OR' of all the values at the provided paths. This is 1345 a quick and useful way to bind a `Boolean` property to two or more other `Boolean` properties. 1346 1347 For example, imagine that we wanted to show a button when one or both of two values are present. 1348 If these two values are set on controllers respectively at `MyApp.profileController.hasDisplayName` and 1349 `MyApp.profileController.hasFullName`. We could do the following, 1350 1351 saveButton: SC.ButtonView.design({ 1352 1353 // Action & target for the button. 1354 action: 'saveProfile', 1355 target: MyApp.statechart, 1356 1357 // Whether the profile has a displayName or not. 1358 profileHasDisplayNameBinding: SC.Binding.oneWay('MyApp.profileController.hasDisplayName'), 1359 1360 // Whether the profile has a fullName or not. 1361 profileHasFullNameBinding: SC.Binding.oneWay('MyApp.profileController.hasFullName'), 1362 1363 // Note: Only show when the profile has a displayName or a fullName or both! 1364 isVisible: function () { 1365 return this.get('profileHasDisplayName') || this.get('profileHasFullName'); 1366 }.property('profileHasDisplayName', 'profileHasFullName').cacheable() 1367 1368 }) 1369 1370 However, this would be much simpler to write by using the `or` binding transform like so, 1371 1372 saveButton: SC.ButtonView.design({ 1373 1374 // Action & target for the button. 1375 action: 'saveProfile', 1376 target: MyApp.statechart, 1377 1378 // Note: Only show when the profile has a displayName or a fullName or both! 1379 isVisibleBinding: SC.Binding.or('MyApp.profileController.hasDisplayName', 'MyApp.profileController.hasFullName') 1380 1381 }) 1382 1383 *Note:* the transform acts strictly as a one-way binding, working only in the one direction. 1384 1385 @param {String...} the paths of source values that will be provided to the OR sequence. 1386 */ 1387 or: function () { 1388 // Fast copy. 1389 var len = arguments.length, 1390 paths = new Array(len); 1391 for (var i = 0; i < len; i++) { paths[i] = arguments[i]; } 1392 1393 // Create a new mix implementation for the OR function. 1394 return this._sc_mixImpl( paths, function() { 1395 var result = false; 1396 for (i = 0; !result && (i < arguments.length); i++) { // Bails early if any value is true. 1397 result = result || arguments[i]; 1398 } 1399 1400 return result; 1401 }); 1402 }, 1403 1404 /** 1405 Adds a transform that aggregates through a given function the values at the provided paths. The 1406 given function is called whenever any of the values are updated. This is a quick way to 1407 aggregate multiple properties into a single property value. 1408 1409 For example, to concatenate two properties 'MyApp.groupController.name' and 1410 'MyApp.userController.fullName', we could do the following, 1411 1412 currentGroupUserLabel: SC.LabelView.extend({ 1413 1414 // The group name (may be null). 1415 groupNameBinding: SC.Binding.oneWay('MyApp.groupController.name'), 1416 1417 // The user full name (may be null). 1418 userFullNameBinding: SC.Binding.oneWay('MyApp.userController.fullName'), 1419 1420 // Ex. Returns one of "", "Selected Group", or "Selected Group: Selected User" 1421 value: function () { 1422 var groupName = this.get('groupName'), 1423 userFullName = this.get('userFullName'); 1424 1425 if (SC.none(userFullName)) { 1426 if (SC.none(groupName)) { 1427 return ''; // No group and no user. 1428 } else { 1429 return groupName; // Just a group. 1430 } 1431 } else { 1432 return '%@: %@'.fmt(groupName, userFullName); // Group and user. 1433 } 1434 }.property('groupName', 'userFullName').cacheable() 1435 1436 }) 1437 1438 However, this is simpler (ex. 86 fewer characters) to write by using the `mix` binding transform like so, 1439 1440 currentGroupUserLabel: SC.LabelView.extend({ 1441 1442 // Ex. Returns one of "", "Selected Group", or "Selected Group: Selected User" 1443 valueBinding: SC.Binding.mix( 1444 'MyApp.groupController.name', // The group name (may be null). 1445 'MyApp.userController.fullName', // The user full name (may be null). 1446 1447 // Aggregate function. The arguments match the bound property values above. 1448 function (groupName, userFullName) { 1449 if (SC.none(userFullName)) { 1450 if (SC.none(groupName)) { 1451 return ''; // No group and no user. 1452 } else { 1453 return groupName; // Just a group. 1454 } 1455 } else { 1456 return '%@: %@'.fmt(groupName, userFullName); // Group and user. 1457 } 1458 }) 1459 1460 }) 1461 1462 *Note:* the number of parameters of `mixFunction` should match the number of paths provided. 1463 *Note:* the transform acts strictly as a one-way binding, working only in the one direction. 1464 1465 @param {String...} the paths of source values that will be provided to the aggregate function. 1466 @param {Function} mixFunction the function that aggregates the values 1467 */ 1468 mix: function() { 1469 var len = arguments.length - 1, 1470 paths = new Array(len); 1471 1472 // Fast copy. The function is the last argument. 1473 for (var i = 0; i < len; i++) { paths[i] = arguments[i]; } 1474 1475 return this._sc_mixImpl(paths, arguments[len]); 1476 }, 1477 1478 /** 1479 Adds a transform that will return YES if the value is equal to equalValue, NO otherwise. 1480 1481 isVisibleBinding: SC.Binding.oneWay("MyApp.someController.title").equalTo(comparisonValue) 1482 1483 Or: 1484 1485 isVisibleBinding: SC.Binding.equalTo("MyApp.someController.title", comparisonValue) 1486 1487 @param {String} fromPath from path or null 1488 @param {Object} equalValue the value to compare with 1489 @returns {SC.Binding} this 1490 */ 1491 equalTo: function(fromPath, equalValue) { 1492 // Normalize arguments. 1493 if (equalValue === undefined) { 1494 equalValue = fromPath; 1495 fromPath = null; 1496 } 1497 1498 return this.from(fromPath).transform(function(value, binding) { 1499 return value === equalValue; 1500 }); 1501 }, 1502 1503 /** @private */ 1504 toString: function () { 1505 var from = this._fromRoot ? "<%@>:%@".fmt(this._fromRoot, this._fromPropertyPath) : this._fromPropertyPath; 1506 1507 var to = this._toRoot ? "<%@>:%@".fmt(this._toRoot, this._toPropertyPath) : this._toPropertyPath; 1508 1509 var oneWay = this._oneWay ? '[oneWay]' : ''; 1510 return "SC.Binding%@(%@ -> %@)%@".fmt(SC.guidFor(this), from, to, oneWay); 1511 } 1512 }; 1513 1514 /** 1515 Shorthand method to define a binding. This is the same as calling: 1516 1517 SC.binding(path) = SC.Binding.from(path) 1518 */ 1519 SC.binding = function (path, root) { return SC.Binding.from(path, root); }; 1520