Skip to content

Commit

Permalink
Merge pull request #12908 from mmun/fix-12475
Browse files Browse the repository at this point in the history
[BUGFIX release] Fix #12475
  • Loading branch information
rwjblue committed Feb 4, 2016
2 parents 1fda8f3 + 944e61d commit d5b6c8a
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 26 deletions.
73 changes: 73 additions & 0 deletions packages/ember-metal/lib/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { Binding } from 'ember-metal/binding';
import {
addObserver,
removeObserver,
_addBeforeObserver,
_removeBeforeObserver,
_suspendObserver
} from 'ember-metal/observer';
import {
Expand Down Expand Up @@ -352,11 +354,13 @@ function replaceObserversAndListeners(obj, key, observerOrListener) {
var prev = obj[key];

if ('function' === typeof prev) {
updateObserversAndListeners(obj, key, prev, '__ember_observesBefore__', _removeBeforeObserver);
updateObserversAndListeners(obj, key, prev, '__ember_observes__', removeObserver);
updateObserversAndListeners(obj, key, prev, '__ember_listens__', removeListener);
}

if ('function' === typeof observerOrListener) {
updateObserversAndListeners(obj, key, observerOrListener, '__ember_observesBefore__', _addBeforeObserver);
updateObserversAndListeners(obj, key, observerOrListener, '__ember_observes__', addObserver);
updateObserversAndListeners(obj, key, observerOrListener, '__ember_listens__', addListener);
}
Expand Down Expand Up @@ -815,6 +819,75 @@ export function _immediateObserver() {
return observer.apply(this, arguments);
}

/**
When observers fire, they are called with the arguments `obj`, `keyName`.
Note, `@each.property` observer is called per each add or replace of an element
and it's not called with a specific enumeration item.
A `_beforeObserver` fires before a property changes.
A `_beforeObserver` is an alternative form of `.observesBefore()`.
```javascript
App.PersonView = Ember.View.extend({
friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }],
valueDidChange: Ember.observer('content.value', function(obj, keyName) {
// only run if updating a value already in the DOM
if (this.get('state') === 'inDOM') {
var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red';
// logic
}
}),
friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) {
// some logic
// obj.get(keyName) returns friends array
})
});
```
Also available as `Function.prototype.observesBefore` if prototype extensions are
enabled.
@method beforeObserver
@for Ember
@param {String} propertyNames*
@param {Function} func
@return func
@deprecated
@private
*/
export function _beforeObserver(...args) {
var func = args.slice(-1)[0];
var paths;

var addWatchedProperty = function(path) { paths.push(path); };

var _paths = args.slice(0, -1);

if (typeof func !== 'function') {
// revert to old, soft-deprecated argument ordering

func = args[0];
_paths = args.slice(1);
}

paths = [];

for (var i = 0; i < _paths.length; ++i) {
expandProperties(_paths[i], addWatchedProperty);
}

if (typeof func !== 'function') {
throw new Ember.Error('Ember.beforeObserver called without a function');
}

func.__ember_observesBefore__ = paths;
return func;
}

export {
IS_BINDING,
Mixin,
Expand Down
1 change: 1 addition & 0 deletions packages/ember-metal/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ function _wrap(func, superFunc) {

superWrapper.wrappedFunction = func;
superWrapper.__ember_observes__ = func.__ember_observes__;
superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__;
superWrapper.__ember_listens__ = func.__ember_listens__;

return superWrapper;
Expand Down
5 changes: 5 additions & 0 deletions packages/ember-metal/lib/watching.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export function isWatching(obj, key) {
return (meta && meta.peekWatching(key)) > 0;
}

export function watcherCount(obj, key) {
var meta = peekMeta(obj);
return (meta && meta.peekWatching(key)) || 0;
}

watch.flushPending = flushPendingChains;

export function unwatch(obj, _keyPath, m) {
Expand Down
48 changes: 48 additions & 0 deletions packages/ember-metal/tests/observer_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Mixin,
mixin,
observer,
_beforeObserver,
_immediateObserver
} from 'ember-metal/mixin';
import run from 'ember-metal/run_loop';
Expand Down Expand Up @@ -678,6 +679,53 @@ testBoth('observer should fire before dependent property is modified', function(
equal(count, 1, 'should have invoked observer');
});

testBoth('before observer watching multiple properties via brace expansion should fire when properties change', function (get, set) {
var obj = {};
var count = 0;

mixin(obj, {
fooAndBarWatcher: _beforeObserver('{foo,bar}', function () {
count++;
})
});

set(obj, 'foo', 'foo');
equal(count, 1, 'observer specified via brace expansion invoked on property change');

set(obj, 'bar', 'bar');
equal(count, 2, 'observer specified via brace expansion invoked on property change');

set(obj, 'baz', 'baz');
equal(count, 2, 'observer not invoked on unspecified property');
});

testBoth('before observer watching multiple properties via brace expansion should fire when dependent property changes', function (get, set) {
var obj = { baz: 'Initial' };
var count = 0;

defineProperty(obj, 'foo', computed(function() {
return get(this, 'bar').toLowerCase();
}).property('bar'));

defineProperty(obj, 'bar', computed(function() {
return get(this, 'baz').toUpperCase();
}).property('baz'));

mixin(obj, {
fooAndBarWatcher: _beforeObserver('{foo,bar}', function () {
count++;
})
});

get(obj, 'foo');
set(obj, 'baz', 'Baz');
// fire once for foo, once for bar
equal(count, 2, 'observer specified via brace expansion invoked on dependent property change');

set(obj, 'quux', 'Quux');
equal(count, 2, 'observer not fired on unspecified property');
});

testBoth('_addBeforeObserver should propagate through prototype', function(get, set) {
var obj = { foo: 'foo', count: 0 };
var obj2;
Expand Down
57 changes: 31 additions & 26 deletions packages/ember-runtime/lib/system/array_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import {
isArray
} from 'ember-runtime/utils';
import { computed } from 'ember-metal/computed';
import { observer } from 'ember-metal/mixin';
import {
_beforeObserver,
observer
} from 'ember-metal/mixin';
import {
beginPropertyChanges,
endPropertyChanges
Expand Down Expand Up @@ -74,23 +77,7 @@ var ArrayProxy = EmberObject.extend(MutableArray, {
@type Ember.Array
@private
*/
content: computed({
get() {
return this._content;
},
set(k, v) {
if (this._didInitArrayProxy) {
var oldContent = this._content;
var len = oldContent ? get(oldContent, 'length') : 0;
this.arrangedContentArrayWillChange(this, 0, len, undefined);
this.arrangedContentWillChange(this);
}
this._content = v;
return v;
}
}),


content: null,

/**
The array that the proxy pretends to be. In the default `ArrayProxy`
Expand All @@ -99,7 +86,7 @@ var ArrayProxy = EmberObject.extend(MutableArray, {
@property arrangedContent
@private
*/
*/
arrangedContent: alias('content'),

/**
Expand Down Expand Up @@ -137,7 +124,20 @@ var ArrayProxy = EmberObject.extend(MutableArray, {
get(this, 'content').replace(idx, amt, objects);
},

_teardownContent(content) {
/**
Invoked when the content property is about to change. Notifies observers that the
entire array content will change.
@private
@method _contentWillChange
*/
_contentWillChange: _beforeObserver('content', function() {
this._teardownContent();
}),

_teardownContent() {
var content = get(this, 'content');

if (content) {
content.removeArrayObserver(this, {
willChange: 'contentArrayWillChange',
Expand Down Expand Up @@ -180,7 +180,6 @@ var ArrayProxy = EmberObject.extend(MutableArray, {
*/
_contentDidChange: observer('content', function() {
var content = get(this, 'content');
this._teardownContent(this._prevContent);

assert('Can\'t set ArrayProxy\'s content to itself', content !== this);

Expand All @@ -189,7 +188,6 @@ var ArrayProxy = EmberObject.extend(MutableArray, {

_setupContent() {
var content = get(this, 'content');
this._prevContent = content;

if (content) {
assert(`ArrayProxy expects an Array or Ember.ArrayProxy, but you passed ${typeof content}`, isArray(content) || content.isDestroyed);
Expand All @@ -201,8 +199,17 @@ var ArrayProxy = EmberObject.extend(MutableArray, {
}
},

_arrangedContentWillChange: _beforeObserver('arrangedContent', function() {
var arrangedContent = get(this, 'arrangedContent');
var len = arrangedContent ? get(arrangedContent, 'length') : 0;

this.arrangedContentArrayWillChange(this, 0, len, undefined);
this.arrangedContentWillChange(this);

this._teardownArrangedContent(arrangedContent);
}),

_arrangedContentDidChange: observer('arrangedContent', function() {
this._teardownArrangedContent(this._prevArrangedContent);
var arrangedContent = get(this, 'arrangedContent');
var len = arrangedContent ? get(arrangedContent, 'length') : 0;

Expand All @@ -216,7 +223,6 @@ var ArrayProxy = EmberObject.extend(MutableArray, {

_setupArrangedContent() {
var arrangedContent = get(this, 'arrangedContent');
this._prevArrangedContent = arrangedContent;

if (arrangedContent) {
assert(`ArrayProxy expects an Array or Ember.ArrayProxy, but you passed ${typeof arrangedContent}`,
Expand Down Expand Up @@ -369,15 +375,14 @@ var ArrayProxy = EmberObject.extend(MutableArray, {
},

init() {
this._didInitArrayProxy = true;
this._super(...arguments);
this._setupContent();
this._setupArrangedContent();
},

willDestroy() {
this._teardownArrangedContent();
this._teardownContent(this.get('content'));
this._teardownContent();
}
});

Expand Down
Loading

0 comments on commit d5b6c8a

Please sign in to comment.