diff --git a/.eslintrc.js b/.eslintrc.js index 1c65e6d..25828ee 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -51,6 +51,17 @@ module.exports = { env: { embertest: true } + }, + + // vendor files + { + files: ['vendor/**/*.js'], + globals: { + Ember: false + }, + rules: { + 'ember/new-module-imports': 'off' + } } ] }; diff --git a/config/environment.js b/config/environment.js index 8b5cb77..3aeb5af 100644 --- a/config/environment.js +++ b/config/environment.js @@ -24,7 +24,8 @@ module.exports = function(/* environment, appConfig */) { _ENABLE_FREEZABLE_SUPPORT: true, // https://emberjs.com/deprecations/v1.x/#toc_ember-freezable _ENABLE_COMPONENT_DEFAULTLAYOUT_SUPPORT: true, // https://www.emberjs.com/deprecations/v2.x/#toc_ember-component-defaultlayout _ENABLE_CONTROLLER_WRAPPED_SUPPORT: true, // https://www.emberjs.com/deprecations/v1.x/#toc_objectcontroller - _ENABLE_PROPERTY_REQUIRED_SUPPORT: true // N/A + _ENABLE_PROPERTY_REQUIRED_SUPPORT: true, // N/A + _ENABLE_BINDING_SUPPORT: true, // https://www.emberjs.com/deprecations/v2.x/#toc_ember-binding } }; }; diff --git a/index.js b/index.js index 8b2370a..45d9610 100644 --- a/index.js +++ b/index.js @@ -43,6 +43,7 @@ module.exports = { this.import('vendor/string-fmt.js'); this.import('vendor/freezable.js'); this.import('vendor/component-defaultlayout.js'); + this.import('vendor/binding.js'); }, treeForVendor(rawVendorTree) { diff --git a/tests/dummy/config/targets.js b/tests/dummy/config/targets.js index 082e68b..426ccad 100644 --- a/tests/dummy/config/targets.js +++ b/tests/dummy/config/targets.js @@ -1,6 +1,5 @@ module.exports = { browsers: [ - 'ie 9', 'last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions' diff --git a/tests/unit/binding-test.js b/tests/unit/binding-test.js new file mode 100644 index 0000000..cd24623 --- /dev/null +++ b/tests/unit/binding-test.js @@ -0,0 +1,277 @@ +import Ember from 'ember'; +import EmberObject, { observer } from '@ember/object'; +import Component from '@ember/component'; +import { run } from '@ember/runloop'; +import { set, get } from '@ember/object'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Ember.Binding', function(hooks) { + + hooks.afterEach(function() { + // prevent test leakage + run.cancelTimers(); + }); + + module('Ember.bind', function(hooks) { + let fromObject, toObject, binding, root; + + hooks.beforeEach(function(assert) { + fromObject = EmberObject.create({ value: 'start' }); + toObject = EmberObject.create({ value: 'end' }); + root = { fromObject: fromObject, toObject: toObject }; + run(() => { + assert.expectDeprecation(() => { + binding = Ember.bind(root, 'toObject.value', 'fromObject.value'); + }, /`Ember\.Binding` is deprecated./); + }); + }); + + test('binding should have synced on connect', function(assert) { + assert.equal(get(toObject, 'value'), 'start', 'toObject.value should match fromObject.value'); + }); + + test('fromObject change should propagate to toObject only after flush', function(assert) { + run(() => { + set(fromObject, 'value', 'change'); + assert.equal(get(toObject, 'value'), 'start'); + }); + assert.equal(get(toObject, 'value'), 'change'); + }); + + test('toObject change should propagate to fromObject only after flush', function(assert) { + run(() => { + set(toObject, 'value', 'change'); + assert.equal(get(fromObject, 'value'), 'start'); + }); + assert.equal(get(fromObject, 'value'), 'change'); + }); + + test('deferred observing during bindings', function(assert) { + // setup special binding + fromObject = EmberObject.create({ + value1: 'value1', + value2: 'value2' + }); + + toObject = EmberObject.extend({ + observer: observer('value1', 'value2', function() { + assert.equal(get(this, 'value1'), 'CHANGED', 'value1 when observer fires'); + assert.equal(get(this, 'value2'), 'CHANGED', 'value2 when observer fires'); + this.callCount++; + }) + }).create({ + value1: 'value1', + value2: 'value2', + + callCount: 0 + }); + + let root = { fromObject: fromObject, toObject: toObject }; + run(function () { + assert.expectDeprecation(() => { + Ember.bind(root, 'toObject.value1', 'fromObject.value1'); + }, /`Ember\.Binding` is deprecated./); + + assert.expectDeprecation(() => { + Ember.bind(root, 'toObject.value2', 'fromObject.value2'); + }, /`Ember\.Binding` is deprecated./); + + // change both value1 + value2, then flush bindings. observer should only + // fire after bindings are done flushing. + set(fromObject, 'value1', 'CHANGED'); + set(fromObject, 'value2', 'CHANGED'); + }); + + assert.equal(toObject.callCount, 2, 'should call observer twice'); + }); + + test('binding disconnection actually works', function(assert) { + binding.disconnect(root); + run(function () { + set(fromObject, 'value', 'change'); + }); + assert.equal(get(toObject, 'value'), 'start'); + }); + + test('chained binding', function(assert) { + let first, second, third; + run(function() { + first = EmberObject.create({ output: 'first' }); + + second = EmberObject.extend({ + inputDidChange: observer('input', function() { + set(this, 'output', get(this, 'input')); + }) + }).create({ + input: 'second', + output: 'second' + }); + + third = EmberObject.create({ input: 'third' }); + + root = { first: first, second: second, third: third }; + + assert.expectDeprecation(() => { + Ember.bind(root, 'second.input', 'first.output'); + }, /`Ember\.Binding` is deprecated./); + + assert.expectDeprecation(() => { + Ember.bind(root, 'second.output', 'third.input'); + }, /`Ember\.Binding` is deprecated./); + }); + + run(function() { + set(first, 'output', 'change'); + assert.equal('change', get(first, 'output'), 'first.output'); + assert.ok('change' !== get(third, 'input'), 'third.input'); + }); + + assert.equal('change', get(first, 'output'), 'first.output'); + assert.equal('change', get(second, 'input'), 'second.input'); + assert.equal('change', get(second, 'output'), 'second.output'); + assert.equal('change', get(third, 'input'), 'third.input'); + }); + }); + + module('Ember.Object.prototype.bind', function(hooks) { + let testObject, fromObject, TestObject; + + hooks.beforeEach(function() { + TestObject = EmberObject.extend({ + foo: 'bar', + bar: 'foo', + extraObject: null + }); + + testObject = EmberObject.create({ + foo: 'bar', + bar: 'foo', + extraObject: null + }); + + fromObject = EmberObject.create({ + bar: 'foo', + extraObject: null + }); + + Ember.lookup['TestNamespace'] = { + fromObject: fromObject, + testObject: testObject + }; + }); + + test('bind(.bar) should bind to relative path', function(assert) { + run(() => { + assert.expectDeprecation(() => { + // create binding + testObject.bind('foo', 'bar'); + }, /`Ember.Binding` is deprecated/); + + // now make a change to see if the binding triggers. + set(testObject, 'bar', 'changedValue'); + }); + + assert.equal('changedValue', get(testObject, 'foo'), 'testObject.foo'); + }); + + test('bind(TestNamespace.fromObject.bar) should follow absolute path', function(assert) { + run(() => { + assert.expectDeprecation(() => { + // create binding + testObject.bind('foo', 'TestNamespace.fromObject.bar'); + }, /`Ember.Binding` is deprecated/); + + // now make a change to see if the binding triggers. + set(fromObject, 'bar', 'changedValue'); + }); + + assert.equal('changedValue', get(testObject, 'foo'), 'testObject.foo'); + }); + + test('fooBinding: .bar should bind to relative path', function(assert) { + run(() => { + assert.expectDeprecation(() => { + // create binding + testObject = TestObject.extend({ + fooBinding: 'bar' + }).create(); + }, /`Ember.Binding` is deprecated/); + + // now make a change to see if the binding triggers. + set(testObject, 'bar', 'changedValue'); + }); + + assert.equal('changedValue', get(testObject, 'foo'), 'testObject.foo'); + }); + + test('fooBinding: TestNamespace.fromObject.bar should follow absolute path', function(assert) { + run(() => { + assert.expectDeprecation(() => { + // create binding + testObject = TestObject.extend({ + fooBinding: 'TestNamespace.fromObject.bar' + }).create(); + }, /`Ember.Binding` is deprecated/); + + // now make a change to see if the binding triggers. + set(fromObject, 'bar', 'changedValue'); + }); + + assert.equal('changedValue', get(testObject, 'foo'), 'testObject.foo'); + }); + }); + + module('rendering', function(hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function(assert) { + this.assertText = function(expectedText) { + assert.strictEqual(this.element.textContent, expectedText, `content should be: \`${expectedText}\``); + }; + }); + + test('should accept bindings as a string or an Ember.Binding', async function() { + let FooBarComponent = Component.extend({ + twoWayTestBinding: Ember.Binding.from('direction'), + stringTestBinding: 'direction', + twoWayObjectTestBinding: Ember.Binding.from('displacement.distance'), + stringObjectTestBinding: 'displacement.distance' + }); + + this.owner.register('component:foo-bar', FooBarComponent); + this.owner.register('template:components/foo-bar', hbs`two way: {{twoWayTest}}, string: {{stringTest}}, object: {{twoWayObjectTest}}, string object: {{stringObjectTest}}`); + + this.set('direction', 'down'); + this.set('displacement', { distance: 10 }); + + await render(hbs`{{foo-bar direction=direction displacement=displacement}}`); + + this.assertText('two way: down, string: down, object: 10, string object: 10'); + + this.set('direction', 'up'); + + this.assertText('two way: up, string: up, object: 10, string object: 10'); + + this.set('displacement.distance', 20); + + this.assertText('two way: up, string: up, object: 20, string object: 20'); + + run(() => { + set(this, 'direction', 'right'); + set(this, 'displacement.distance', 30); + }); + + this.assertText('two way: right, string: right, object: 30, string object: 30'); + + run(() => { + set(this, 'direction', 'down'); + set(this, 'displacement', { distance: 10 }); + }); + + this.assertText('two way: down, string: down, object: 10, string object: 10'); + }); + }); +}); diff --git a/vendor/binding.js b/vendor/binding.js new file mode 100644 index 0000000..5546724 --- /dev/null +++ b/vendor/binding.js @@ -0,0 +1,574 @@ +(() => { + +const { + Logger, + ENV, + lookup, + guidFor, + run, + assert, + deprecate, + get, + trySet, + addListener, + addObserver, + removeObserver, + _suspendObserver, + _Cache: Cache, + meta: metaFor, +} = Ember; + +const IS_GLOBAL_PATH = /^[A-Z$].*[.]/; + +const isGlobalPathCache = new Cache(1000, key => IS_GLOBAL_PATH.test(key)); +const firstDotIndexCache = new Cache(1000, key => key.indexOf('.')); + +const firstKeyCache = new Cache(1000, (path) => { + let index = firstDotIndexCache.get(path); + return index === -1 ? path : path.slice(0, index); +}); + +const tailPathCache = new Cache(1000, (path) => { + let index = firstDotIndexCache.get(path); + return index === -1 ? undefined : path.slice(index + 1); +}); + +function isGlobalPath(path) { + return isGlobalPathCache.get(path); +} + +function getFirstKey(path) { + return firstKeyCache.get(path); +} + +function getTailPath(path) { + return tailPathCache.get(path); +} + +// .......................................................... +// BINDING +// + +class Binding { + constructor(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 https://emberjs.com/deprecations/v2.x#toc_ember-binding + @public + */ + + /** + This copies the Binding so it can be connected to another object. + + @method copy + @return {Ember.Binding} `this` + @public + */ + copy() { + let 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() { + let 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 = 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) { // eslint-disable-line no-unused-vars + this._scheduleSync('fwd'); + } + + /* Called when the to side changes. */ + toDidChange(target) { // eslint-disable-line no-unused-vars + this._scheduleSync('back'); + } + + _scheduleSync(dir) { + let 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() { + let 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. + let direction = this._direction; + + let fromObj = this._fromObj; + let fromPath = this._fromPath; + + this._direction = undefined; + + // If we're synchronizing from the remote object... + if (direction === 'fwd') { + let 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') { + let toValue = get(toObj, this._to); + if (log) { + Logger.log(' ', this.toString(), '<-', toValue, toObj); + } + _suspendObserver(fromObj, fromPath, this, 'fromDidChange', () => { + 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: 'https://emberjs.com/deprecations/v2.x#toc_ember-binding' + }); + deprecate(objectInfo + deprecateOneWayMessage, !deprecateOneWay, { + id: 'ember-metal.binding', + until: '3.0.0', + url: 'https://emberjs.com/deprecations/v2.x#toc_ember-binding' + }); + deprecate(objectInfo + deprecateAliasMessage, !deprecateAlias, { + id: 'ember-metal.binding', + until: '3.0.0', + url: 'https://emberjs.com/deprecations/v2.x#toc_ember-binding' + }); +} + +function mixinProperties(to, from) { + for (let key in from) { + if (from.hasOwnProperty(key)) { + to[key] = from[key]; + } + } +} + +mixinProperties(Binding, { + + /* + See `Ember.Binding.from`. + + @method from + @static + */ + from(from) { + let C = this; + return new C(undefined, from); + }, + + /* + See `Ember.Binding.to`. + + @method to + @static + */ + to(to) { + let 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 +*/ +function bind(obj, to, from) { + return new Binding(to, from).connect(obj); +} + +Ember.bind = bind; +Ember.Binding = Binding; + + +function detectBinding(key) { + let length = key.length; + + return length > 7 && key.charCodeAt(length - 7) === 66 && key.indexOf('inding', length - 6) !== -1; +} +// warm both paths of above function +detectBinding('notbound'); +detectBinding('fooBinding'); + +function connectBindings(obj, meta) { + // TODO Mixin.apply(instance) should disconnect binding if exists + meta.forEachBindings((key, binding) => { + if (binding) { + let to = key.slice(0, -7); // strip Binding off end + if (binding instanceof Binding) { + binding = binding.copy(); // copy prototypes' instance + binding.to(to); + } else { // binding is string path + binding = new Binding(to, binding); + } + binding.connect(obj); + obj[key] = binding; + } + }); + // mark as applied + meta.clearBindings(); +} + +function finishPartial(obj, meta) { + connectBindings(obj, meta === undefined ? metaFor(obj) : meta); + return obj; +} + +Ember.CoreObject.reopen({ + bind(to, from) { + if (!(from instanceof Binding)) { from = Binding.from(from); } + from.to(to).connect(this); + return from; + }, +}); + +Ember.Mixin.finishPartial = finishPartial; +Ember.Mixin.detectBinding = detectBinding; + +})();