API Docs for: v2.11.0-beta.7
Show:

File: packages/ember-metal/lib/binding.js

import { guidFor } from 'ember-utils';
import Logger from 'ember-console';
import { context, ENV } from 'ember-environment';
import run from './run_loop';
import { assert, deprecate } from './debug';
import { get } from './property_get';
import { trySet } from './property_set';
import { addListener } from './events';
import {
  addObserver,
  removeObserver,
  _suspendObserver
} from './observer';
import {
  isGlobalPath,
  getFirstKey,
  getTailPath
} from './path_cache';

/**
@module ember
@submodule ember-metal
*/

// ..........................................................
// BINDING
//

function Binding(toPath, fromPath) {
  // Configuration
  this._from = fromPath;
  this._to = toPath;
  this._oneWay = undefined;

  // State
  this._direction = undefined;
  this._readyToSync = undefined;
  this._fromObj = undefined;
  this._fromPath = undefined;
  this._toObj = undefined;
}

/**
  @class Binding
  @namespace Ember
  @deprecated See http://emberjs.com/deprecations/v2.x#toc_ember-binding
  @public
*/

Binding.prototype = {
  /**
    This copies the Binding so it can be connected to another object.

    @method copy
    @return {Ember.Binding} `this`
    @public
  */
  copy() {
    var copy = new Binding(this._to, this._from);
    if (this._oneWay) { copy._oneWay = true; }
    return copy;
  },

  // ..........................................................
  // CONFIG
  //

  /**
    This will set `from` property path to the specified value. It will not
    attempt to resolve this property path to an actual object until you
    connect the binding.

    The binding will search for the property path starting at the root object
    you pass when you `connect()` the binding. It follows the same rules as
    `get()` - see that method for more information.

    @method from
    @param {String} path The property path to connect to.
    @return {Ember.Binding} `this`
    @public
  */
  from(path) {
    this._from = path;
    return this;
  },

  /**
    This will set the `to` property path to the specified value. It will not
    attempt to resolve this property path to an actual object until you
    connect the binding.

    The binding will search for the property path starting at the root object
    you pass when you `connect()` the binding. It follows the same rules as
    `get()` - see that method for more information.

    @method to
    @param {String|Tuple} path A property path or tuple.
    @return {Ember.Binding} `this`
    @public
  */
  to(path) {
    this._to = path;
    return this;
  },

  /**
    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.

    @method oneWay
    @return {Ember.Binding} `this`
    @public
  */
  oneWay() {
    this._oneWay = true;
    return this;
  },

  /**
    @method toString
    @return {String} string representation of binding
    @public
  */
  toString() {
    var oneWay = this._oneWay ? '[oneWay]' : '';
    return `Ember.Binding<${guidFor(this)}>(${this._from} -> ${this._to})${oneWay}`;
  },

  // ..........................................................
  // CONNECT AND SYNC
  //

  /**
    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.

    @method connect
    @param {Object} obj The root object for this binding.
    @return {Ember.Binding} `this`
    @public
  */
  connect(obj) {
    assert('Must pass a valid object to Ember.Binding.connect()', !!obj);

    let fromObj, fromPath, possibleGlobal;

    // If the binding's "from" path could be interpreted as a global, verify
    // whether the path refers to a global or not by consulting `Ember.lookup`.
    if (isGlobalPath(this._from)) {
      let name = getFirstKey(this._from);
      possibleGlobal = context.lookup[name];

      if (possibleGlobal) {
        fromObj = possibleGlobal;
        fromPath = getTailPath(this._from);
      }
    }

    if (fromObj === undefined) {
      fromObj = obj;
      fromPath = this._from;
    }

    trySet(obj, this._to, get(fromObj, fromPath));

    // Add an observer on the object to be notified when the binding should be updated.
    addObserver(fromObj, fromPath, this, 'fromDidChange');

    // If the binding is a two-way binding, also set up an observer on the target.
    if (!this._oneWay) {
      addObserver(obj, this._to, this, 'toDidChange');
    }

    addListener(obj, 'willDestroy', this, 'disconnect');

    fireDeprecations(
      obj,
      this._to,
      this._from,
      possibleGlobal,
      this._oneWay,
      (!possibleGlobal && !this._oneWay)
    );

    this._readyToSync = true;
    this._fromObj = fromObj;
    this._fromPath = fromPath;
    this._toObj = obj;

    return this;
  },

  /**
    Disconnects the binding instance. Changes will no longer be relayed. You
    will not usually need to call this method.

    @method disconnect
    @return {Ember.Binding} `this`
    @public
  */
  disconnect() {
    assert('Must pass a valid object to Ember.Binding.disconnect()', !!this._toObj);

    // Remove an observer on the object so we're no longer notified of
    // changes that should update bindings.
    removeObserver(this._fromObj, this._fromPath, this, 'fromDidChange');

    // If the binding is two-way, remove the observer from the target as well.
    if (!this._oneWay) {
      removeObserver(this._toObj, this._to, this, 'toDidChange');
    }

    this._readyToSync = false; // Disable scheduled syncs...
    return this;
  },

  // ..........................................................
  // PRIVATE
  //

  /* Called when the from side changes. */
  fromDidChange(target) {
    this._scheduleSync('fwd');
  },

  /* Called when the to side changes. */
  toDidChange(target) {
    this._scheduleSync('back');
  },

  _scheduleSync(dir) {
    var existingDir = this._direction;

    // If we haven't scheduled the binding yet, schedule it.
    if (existingDir === undefined) {
      run.schedule('sync', this, '_sync');
      this._direction  = dir;
    }

    // If both a 'back' and 'fwd' sync have been scheduled on the same object,
    // default to a 'fwd' sync so that it remains deterministic.
    if (existingDir === 'back' && dir === 'fwd') {
      this._direction = 'fwd';
    }
  },

  _sync() {
    var log = ENV.LOG_BINDINGS;

    let toObj = this._toObj;

    // Don't synchronize destroyed objects or disconnected bindings.
    if (toObj.isDestroyed || !this._readyToSync) { return; }

    // Get the direction of the binding for the object we are
    // synchronizing from.
    var direction = this._direction;

    var fromObj = this._fromObj;
    var fromPath = this._fromPath;

    this._direction = undefined;

    // If we're synchronizing from the remote object...
    if (direction === 'fwd') {
      var fromValue = get(fromObj, fromPath);
      if (log) {
        Logger.log(' ', this.toString(), '->', fromValue, fromObj);
      }
      if (this._oneWay) {
        trySet(toObj, this._to, fromValue);
      } else {
        _suspendObserver(toObj, this._to, this, 'toDidChange', function() {
          trySet(toObj, this._to, fromValue);
        });
      }
    // If we're synchronizing *to* the remote object.
    } else if (direction === 'back') {
      var toValue = get(toObj, this._to);
      if (log) {
        Logger.log(' ', this.toString(), '<-', toValue, toObj);
      }
      _suspendObserver(fromObj, fromPath, this, 'fromDidChange', function() {
        trySet(fromObj, fromPath, toValue);
      });
    }
  }

};

function fireDeprecations(obj, toPath, fromPath, deprecateGlobal, deprecateOneWay, deprecateAlias) {
  let deprecateGlobalMessage = '`Ember.Binding` is deprecated. Since you' +
    ' are binding to a global consider using a service instead.';
  let deprecateOneWayMessage = '`Ember.Binding` is deprecated. Since you' +
    ' are using a `oneWay` binding consider using a `readOnly` computed' +
    ' property instead.';
  let deprecateAliasMessage = '`Ember.Binding` is deprecated. Consider' +
    ' using an `alias` computed property instead.';

  let objectInfo = `The \`${toPath}\` property of \`${obj}\` is an \`Ember.Binding\` connected to \`${fromPath}\`, but `;
  deprecate(objectInfo + deprecateGlobalMessage, !deprecateGlobal, {
    id: 'ember-metal.binding',
    until: '3.0.0',
    url: 'http://emberjs.com/deprecations/v2.x#toc_ember-binding'
  });
  deprecate(objectInfo + deprecateOneWayMessage, !deprecateOneWay, {
    id: 'ember-metal.binding',
    until: '3.0.0',
    url: 'http://emberjs.com/deprecations/v2.x#toc_ember-binding'
  });
  deprecate(objectInfo + deprecateAliasMessage, !deprecateAlias, {
    id: 'ember-metal.binding',
    until: '3.0.0',
    url: 'http://emberjs.com/deprecations/v2.x#toc_ember-binding'
  });
}

function mixinProperties(to, from) {
  for (var key in from) {
    if (from.hasOwnProperty(key)) {
      to[key] = from[key];
    }
  }
}

mixinProperties(Binding, {

  /*
    See `Ember.Binding.from`.

    @method from
    @static
  */
  from(from) {
    var C = this;
    return new C(undefined, from);
  },

  /*
    See `Ember.Binding.to`.

    @method to
    @static
  */
  to(to) {
    var C = this;
    return new C(to, undefined);
  }
});
/**
  An `Ember.Binding` connects the properties of two objects so that whenever
  the value of one property changes, the other property will be changed also.

  ## Automatic Creation of Bindings with `/^*Binding/`-named Properties.

  You do not usually create Binding objects directly but instead describe
  bindings in your class or object definition using automatic binding
  detection.

  Properties ending in a `Binding` suffix will be converted to `Ember.Binding`
  instances. The value of this property should be a string representing a path
  to another object or a custom binding instance created using Binding helpers
  (see "One Way Bindings"):

  ```
  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.

  ## One Way Bindings

  One especially useful binding customization you can use is the `oneWay()`
  helper. This helper tells Ember 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: Ember.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 Bindings Manually

  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 instance. 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, Ember objects will effectively call
  something like this on your binding:

  ```javascript
  binding = Ember.Binding.from("valueBinding").to("value");
  ```

  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:

  ```javascript
  binding.connect(this);
  ```

  Note that when you connect a binding you pass the object you want it to be
  connected to. This object will be used as the root for both the from and
  to side of the binding when inspecting relative paths. This allows the
  binding to be automatically inherited by subclassed objects as well.

  This also allows you to bind between objects using the paths you declare in
  `from` and `to`:

  ```javascript
  // Example 1
  binding = Ember.Binding.from("App.someObject.value").to("value");
  binding.connect(this);

  // Example 2
  binding = Ember.Binding.from("parentView.value").to("App.someObject.value");
  binding.connect(this);
  ```

  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
  using the `Ember.bind()` helper method. (This is the same method used by
  to setup your bindings on objects):

  ```javascript
  Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value");
  ```

  Both of these code fragments have the same effect as doing the most friendly
  form of binding creation like so:

  ```javascript
  MyApp.anotherObject = Ember.Object.create({
    valueBinding: "MyApp.someController.value",

    // OTHER CODE FOR THIS OBJECT...
  });
  ```

  Ember's built in binding creation method makes it easy to automatically
  create bindings for you. You should always use the highest-level APIs
  available, even if you understand how it works underneath.

  @class Binding
  @namespace Ember
  @since Ember 0.9
  @public
*/
// Ember.Binding = Binding; ES6TODO: where to put this?


/**
  Global helper method to create a new binding. Just pass the root object
  along with a `to` and `from` path to create and connect the binding.

  @method bind
  @for Ember
  @param {Object} obj The root object of the transform.
  @param {String} to The path to the 'to' side of the binding.
    Must be relative to obj.
  @param {String} from The path to the 'from' side of the binding.
    Must be relative to obj or a global path.
  @return {Ember.Binding} binding instance
  @public
*/
export function bind(obj, to, from) {
  return new Binding(to, from).connect(obj);
}

export {
  Binding
};