Skip to content

Commit dc0eb88

Browse files
committed
fix(viewmodel): View model and scope can both use 'getReactively'
1 parent 384fa70 commit dc0eb88

File tree

6 files changed

+168
-56
lines changed

6 files changed

+168
-56
lines changed

dist/angular-meteor.js

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,6 +1607,9 @@
16071607
.service(Mixer, function () {
16081608
var _this = this;
16091609

1610+
// Used to store method's caller
1611+
var caller = void 0;
1612+
16101613
this._mixins = [];
16111614
// Apply mixins automatically on specified contexts
16121615
this._autoExtend = [];
@@ -1649,11 +1652,64 @@
16491652
};
16501653

16511654
// Extend prototype with the defined mixins
1652-
this._extend = function (obj) {
1655+
this._extend = function (obj, options) {
16531656
var _ref;
16541657

1655-
return (_ref = _).extend.apply(_ref, [obj].concat(_toConsumableArray(_this._mixins)));
1658+
var _$defaults = _.defaults({}, options, {
1659+
pattern: /.*/ });
1660+
1661+
var pattern = _$defaults.pattern;
1662+
var context = _$defaults.context;
1663+
// The patterns of the keys which will be filtered
1664+
1665+
1666+
var mixins = _this._mixins.map(function (mixin) {
1667+
// Filtering the keys by the specified pattern
1668+
var keys = _.keys(mixin).filter(function (k) {
1669+
return k.match(pattern);
1670+
}).filter(function (k) {
1671+
return _.isFunction(mixin[k]);
1672+
});
1673+
1674+
return keys.reduce(function (boundMixin, methodName) {
1675+
var methodHandler = mixin[methodName];
1676+
1677+
// Note that this is not an arrow function so we can conserve the conetxt
1678+
boundMixin[methodName] = function () {
1679+
// Storing original caller so we will know who actually called the
1680+
// method event though it is bound to another context
1681+
var methodContext = context || this;
1682+
var recentCaller = caller;
1683+
caller = this;
1684+
1685+
try {
1686+
for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
1687+
args[_key2] = arguments[_key2];
1688+
}
1689+
1690+
return methodHandler.apply(methodContext, args);
1691+
} finally {
1692+
// No matter what happens, restore variable to the previous one
1693+
caller = recentCaller;
1694+
}
1695+
};
1696+
1697+
return boundMixin;
1698+
}, {});
1699+
});
1700+
1701+
return (_ref = _).extend.apply(_ref, [obj].concat(_toConsumableArray(mixins)));
16561702
};
1703+
1704+
// Caller property can not be set
1705+
Object.defineProperty(this, 'caller', {
1706+
configurable: true,
1707+
enumerable: true,
1708+
1709+
get: function get() {
1710+
return caller;
1711+
}
1712+
});
16571713
});
16581714

16591715
/***/ },
@@ -1870,26 +1926,17 @@
18701926

18711927
// Gets an object, wraps it with scope functions and returns it
18721928
$$ViewModel.viewModel = function (vm) {
1873-
var _this = this;
1874-
18751929
if (!_.isObject(vm)) {
18761930
throw Error('argument 1 must be an object');
18771931
}
18781932

1879-
// Apply mixin functions
1880-
$Mixer._mixins.forEach(function (mixin) {
1881-
// Reject methods which starts with double $
1882-
var keys = _.keys(mixin).filter(function (k) {
1883-
return k.match(/^(?!\$\$).*$/);
1884-
});
1885-
var proto = _.pick(mixin, keys);
1886-
// Bind all the methods to the prototype
1887-
var boundProto = $$utils.bind(proto, _this);
1888-
// Add the methods to the view model
1889-
_.extend(vm, boundProto);
1933+
// Apply extend view model with mixin functions
1934+
$Mixer._extend(vm, {
1935+
pattern: /^(?!\$\$).*$/,
1936+
context: this
18901937
});
18911938

1892-
// Apply mixin constructors on the view model
1939+
// Apply mixin constructors on scope with view model
18931940
$Mixer._construct(this, vm);
18941941
return vm;
18951942
};
@@ -1908,7 +1955,7 @@
19081955
.service(reactive, [_utils.utils, function ($$utils) {
19091956
var Reactive = function () {
19101957
function Reactive(vm) {
1911-
var _this2 = this;
1958+
var _this = this;
19121959

19131960
_classCallCheck(this, Reactive);
19141961

@@ -1917,7 +1964,7 @@
19171964
}
19181965

19191966
_.defer(function () {
1920-
if (!_this2._attached) {
1967+
if (!_this._attached) {
19211968
console.warn('view model was not attached to any scope');
19221969
}
19231970
});
@@ -1980,7 +2027,7 @@
19802027
A mixin which enhance our reactive abilities by providing methods
19812028
that are capable of updating our scope reactively.
19822029
*/
1983-
.factory(Reactive, ['$parse', _utils.utils, function ($parse, $$utils) {
2030+
.factory(Reactive, ['$parse', _utils.utils, _mixer.Mixer, function ($parse, $$utils, $Mixer) {
19842031
function $$Reactive() {
19852032
var vm = arguments.length <= 0 || arguments[0] === undefined ? this : arguments[0];
19862033

@@ -2021,46 +2068,46 @@
20212068
throw Error('argument 2 must be a boolean');
20222069
}
20232070

2024-
return this.$$reactivateEntity(k, this.$watch, isDeep);
2071+
return this.$$reactivateEntity($Mixer.caller, k, this.$watch, isDeep);
20252072
};
20262073

20272074
// Gets a collection reactively
20282075
$$Reactive.getCollectionReactively = function (k) {
2029-
return this.$$reactivateEntity(k, this.$watchCollection);
2076+
return this.$$reactivateEntity($Mixer.caller, k, this.$watchCollection);
20302077
};
20312078

20322079
// Gets an entity reactively, and once it has been changed the computation will be recomputed
2033-
$$Reactive.$$reactivateEntity = function (k, watcher) {
2080+
$$Reactive.$$reactivateEntity = function (context, k, watcher) {
20342081
if (!_.isString(k)) {
20352082
throw Error('argument 1 must be a string');
20362083
}
20372084

20382085
if (!this.$$vm.$$dependencies[k]) {
20392086
this.$$vm.$$dependencies[k] = new Tracker.Dependency();
20402087

2041-
for (var _len = arguments.length, watcherArgs = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
2042-
watcherArgs[_key - 2] = arguments[_key];
2088+
for (var _len = arguments.length, watcherArgs = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
2089+
watcherArgs[_key - 3] = arguments[_key];
20432090
}
20442091

2045-
this.$$watchEntity.apply(this, [k, watcher].concat(watcherArgs));
2092+
this.$$watchEntity.apply(this, [context, k, watcher].concat(watcherArgs));
20462093
}
20472094

20482095
this.$$vm.$$dependencies[k].depend();
2049-
return $parse(k)(this.$$vm);
2096+
return $parse(k)(context);
20502097
};
20512098

20522099
// Watches for changes in the view model, and if so will notify a change
2053-
$$Reactive.$$watchEntity = function (k, watcher) {
2100+
$$Reactive.$$watchEntity = function (context, k, watcher) {
20542101
var _this2 = this;
20552102

2056-
// Gets a deep property from the view model
2057-
var getVal = _.partial($parse(k), this.$$vm);
2103+
// Gets a deep property from the caller
2104+
var getVal = _.partial($parse(k), context);
20582105
var initialVal = getVal();
20592106

20602107
// Watches for changes in the view model
20612108

2062-
for (var _len2 = arguments.length, watcherArgs = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
2063-
watcherArgs[_key2 - 2] = arguments[_key2];
2109+
for (var _len2 = arguments.length, watcherArgs = Array(_len2 > 3 ? _len2 - 3 : 0), _key2 = 3; _key2 < _len2; _key2++) {
2110+
watcherArgs[_key2 - 3] = arguments[_key2];
20642111
}
20652112

20662113
watcher.call.apply(watcher, [this, getVal, function (val, oldVal) {

src/modules/mixer.js

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ angular.module(name, [])
1414
we will just use the `$Mixer` service.
1515
*/
1616
.service(Mixer, function() {
17+
// Used to store method's caller
18+
let caller;
19+
1720
this._mixins = [];
1821
// Apply mixins automatically on specified contexts
1922
this._autoExtend = [];
@@ -48,7 +51,51 @@ angular.module(name, [])
4851
};
4952

5053
// Extend prototype with the defined mixins
51-
this._extend = (obj) => {
52-
return _.extend(obj, ...this._mixins);
54+
this._extend = (obj, options) => {
55+
const { pattern, context } = _.defaults({}, options, {
56+
pattern: /.*/, // The patterns of the keys which will be filtered
57+
});
58+
59+
const mixins = this._mixins.map((mixin) => {
60+
// Filtering the keys by the specified pattern
61+
const keys = _.keys(mixin)
62+
.filter(k => k.match(pattern))
63+
.filter(k => _.isFunction(mixin[k]));
64+
65+
return keys.reduce((boundMixin, methodName) => {
66+
const methodHandler = mixin[methodName];
67+
68+
// Note that this is not an arrow function so we can conserve the conetxt
69+
boundMixin[methodName] = function(...args) {
70+
// Storing original caller so we will know who actually called the
71+
// method event though it is bound to another context
72+
const methodContext = context || this;
73+
const recentCaller = caller;
74+
caller = this;
75+
76+
try {
77+
return methodHandler.apply(methodContext, args);
78+
}
79+
finally {
80+
// No matter what happens, restore variable to the previous one
81+
caller = recentCaller;
82+
}
83+
};
84+
85+
return boundMixin;
86+
}, {});
87+
});
88+
89+
return _.extend(obj, ...mixins);
5390
};
91+
92+
// Caller property can not be set
93+
Object.defineProperty(this, 'caller', {
94+
configurable: true,
95+
enumerable: true,
96+
97+
get: () => {
98+
return caller;
99+
}
100+
});
54101
});

src/modules/reactive.js

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { name as utilsName, utils } from './utils';
2-
import { name as mixerName } from './mixer';
2+
import { name as mixerName, Mixer } from './mixer';
33
import { name as coreName } from './core';
44
import { name as viewModelName } from './view-model';
55

@@ -21,8 +21,9 @@ angular.module(name, [
2121
.factory(Reactive, [
2222
'$parse',
2323
utils,
24+
Mixer,
2425

25-
function($parse, $$utils) {
26+
function($parse, $$utils, $Mixer) {
2627
function $$Reactive(vm = this) {
2728
// Helps us track changes made in the view model
2829
vm.$$dependencies = {};
@@ -55,33 +56,33 @@ angular.module(name, [
5556
throw Error('argument 2 must be a boolean');
5657
}
5758

58-
return this.$$reactivateEntity(k, this.$watch, isDeep);
59+
return this.$$reactivateEntity($Mixer.caller, k, this.$watch, isDeep);
5960
};
6061

6162
// Gets a collection reactively
6263
$$Reactive.getCollectionReactively = function(k) {
63-
return this.$$reactivateEntity(k, this.$watchCollection);
64+
return this.$$reactivateEntity($Mixer.caller, k, this.$watchCollection);
6465
};
6566

6667
// Gets an entity reactively, and once it has been changed the computation will be recomputed
67-
$$Reactive.$$reactivateEntity = function(k, watcher, ...watcherArgs) {
68+
$$Reactive.$$reactivateEntity = function(context, k, watcher, ...watcherArgs) {
6869
if (!_.isString(k)) {
6970
throw Error('argument 1 must be a string');
7071
}
7172

7273
if (!this.$$vm.$$dependencies[k]) {
7374
this.$$vm.$$dependencies[k] = new Tracker.Dependency();
74-
this.$$watchEntity(k, watcher, ...watcherArgs);
75+
this.$$watchEntity(context, k, watcher, ...watcherArgs);
7576
}
7677

7778
this.$$vm.$$dependencies[k].depend();
78-
return $parse(k)(this.$$vm);
79+
return $parse(k)(context);
7980
};
8081

8182
// Watches for changes in the view model, and if so will notify a change
82-
$$Reactive.$$watchEntity = function(k, watcher, ...watcherArgs) {
83-
// Gets a deep property from the view model
84-
const getVal = _.partial($parse(k), this.$$vm);
83+
$$Reactive.$$watchEntity = function(context, k, watcher, ...watcherArgs) {
84+
// Gets a deep property from the caller
85+
const getVal = _.partial($parse(k), context);
8586
const initialVal = getVal();
8687

8788
// Watches for changes in the view model

src/modules/view-model.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,13 @@ angular.module(name, [
3535
throw Error('argument 1 must be an object');
3636
}
3737

38-
// Apply mixin functions
39-
$Mixer._mixins.forEach((mixin) => {
40-
// Reject methods which starts with double $
41-
const keys = _.keys(mixin).filter(k => k.match(/^(?!\$\$).*$/));
42-
const proto = _.pick(mixin, keys);
43-
// Bind all the methods to the prototype
44-
const boundProto = $$utils.bind(proto, this);
45-
// Add the methods to the view model
46-
_.extend(vm, boundProto);
38+
// Extend view model with mixin functions
39+
$Mixer._extend(vm, {
40+
pattern: /^(?!\$\$).*$/, // Omitting methods which start with a $$ notation
41+
context: this // Binding methods to scope
4742
});
4843

49-
// Apply mixin constructors on the view model
44+
// Apply mixin constructors on scope with view model
5045
$Mixer._construct(this, vm);
5146
return vm;
5247
};

tests/integration/reactive.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,23 @@ describe('angular-meteor.reactive', function() {
483483

484484
expect(changedSpy).toHaveBeenCalled();
485485
});
486+
487+
it('should be able to get properties reactively of both view model and scope at the same time', function() {
488+
var actualValue;
489+
scope.myProp = 'initial';
490+
491+
vm.autorun(() => {
492+
actualValue = scope.getReactively('myProp');
493+
});
494+
495+
expect(actualValue).toEqual('initial');
496+
497+
scope.myProp = 'changed';
498+
scope.$$throttledDigest();
499+
Tracker.flush();
500+
501+
expect(actualValue).toEqual('changed');
502+
});
486503
});
487504

488505
describe('getCollectionReactively()', function() {

tests/integration/view-model.spec.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ describe('angular-meteor.view-model', function() {
2121
this.scopeProp = 'scopeProp';
2222
};
2323

24-
Mixin.$method = jasmine.createSpy();
24+
Mixin.$method = jasmine.createSpy(function(fn) {
25+
if (_.isFunction(fn)) return fn();
26+
});
27+
2528
Mixin.$$hidden = jasmine.createSpy();
2629

2730
$Mixer.mixin(Mixin);
@@ -48,9 +51,11 @@ describe('angular-meteor.view-model', function() {
4851

4952
it('should bind methods to scope', function() {
5053
var vm = scope.viewModel({});
51-
vm.$method();
5254

53-
expect(Mixin.$method.calls.mostRecent().object).toEqual(scope);
55+
vm.$method(function() {
56+
expect(this).toEqual(scope);
57+
expect($Mixer.caller).toEqual(vm);
58+
});
5459
});
5560
});
5661

0 commit comments

Comments
 (0)