Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Commit 38e7a36

Browse files
committed
Simplify $$ methods in $FirebaseArray
src/FirebaseArray.js - _notify renamed to $$notify - _process renamed to $$process - $$process no longer called internally by $$added/$$updated/$$moved/$$removed - $$added now returns a record - $$updated now returns a boolean indicating whether anything changed - $$moved now returns a boolean indicating whether record should be moved - $$removed now returns a boolean indicating whether record should be removed src/firebase.js: SyncArray to support new $$process usage - $$process now called after $$added, $$updated, $$removed, and $$moved (instead of coupled to those methods) - $$updated, $$moved, and $$removed only called if rec exists in the array test/jasmineMatchers.js: added toHaveCallCount(), toBeEmpty(), and toHaveLength()
1 parent b587e18 commit 38e7a36

File tree

5 files changed

+386
-220
lines changed

5 files changed

+386
-220
lines changed

src/FirebaseArray.js

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,34 @@
1212
* $$moved - called whenever a child_moved event occurs
1313
* $$removed - called whenever a child_removed event occurs
1414
* $$error - called when listeners are canceled due to a security error
15+
* $$process - called immediately after $$added/$$updated/$$moved/$$removed
16+
* to splice/manipulate the array and invokes $$notify
17+
*
18+
* Additionally, there is one more method of interest to devs extending this class:
19+
* $$notify - triggers notifications to any $watch listeners, called by $$process
1520
*
1621
* Instead of directly modifying this class, one should generally use the $extendFactory
17-
* method to add or change how methods behave:
22+
* method to add or change how methods behave. $extendFactory modifies the prototype of
23+
* the array class by returning a clone of $FirebaseArray.
1824
*
1925
* <pre><code>
2026
* var NewFactory = $FirebaseArray.$extendFactory({
2127
* // add a new method to the prototype
2228
* foo: function() { return 'bar'; },
2329
*
2430
* // change how records are created
25-
* $$added: function(snap) {
26-
* var rec = new Widget(snap);
27-
* this._process('child_added', rec);
31+
* $$added: function(snap, prevChild) {
32+
* return new Widget(snap, prevChild);
33+
* },
34+
*
35+
* // change how records are updated
36+
* $$updated: function(snap) {
37+
* return this.$getRecord(snap.name()).update(snap);
2838
* }
2939
* });
3040
* </code></pre>
3141
*
32-
* And then the new factory can be used by passing it as an argument:
42+
* And then the new factory can be passed as an argument:
3343
* <code>$firebase( firebaseRef, {arrayFactory: NewFactory}).$asArray();</code>
3444
*/
3545
angular.module('firebase').factory('$FirebaseArray', ["$log", "$firebaseUtils",
@@ -108,7 +118,7 @@
108118
if( key !== null ) {
109119
return self.$inst().$set(key, $firebaseUtils.toJSON(item))
110120
.then(function(ref) {
111-
self._notify('child_changed', key);
121+
self.$$notify('child_changed', key);
112122
return ref;
113123
});
114124
}
@@ -251,10 +261,11 @@
251261
* Called by $firebase to inform the array when a new item has been added at the server.
252262
* This method must exist on any array factory used by $firebase.
253263
*
254-
* @param snap
264+
* @param {object} snap a Firebase snapshot
255265
* @param {string} prevChild
266+
* @return {object} the record to be inserted into the array
256267
*/
257-
$$added: function(snap, prevChild) {
268+
$$added: function(snap/*, prevChild*/) {
258269
// check to make sure record does not exist
259270
var i = this.$indexFor($firebaseUtils.getKey(snap));
260271
if( i === -1 ) {
@@ -267,55 +278,59 @@
267278
rec.$priority = snap.getPriority();
268279
$firebaseUtils.applyDefaults(rec, this.$$defaults);
269280

270-
// add it to array and send notifications
271-
this._process('child_added', rec, prevChild);
281+
return rec;
272282
}
283+
return false;
273284
},
274285

275286
/**
276287
* Called by $firebase whenever an item is removed at the server.
277-
* This method must exist on any arrayFactory passed into $firebase
288+
* This method does not physically remove the objects, but instead
289+
* returns a boolean indicating whether it should be removed (and
290+
* taking any other desired actions before the remove completes).
278291
*
279-
* @param snap
292+
* @param {object} snap a Firebase snapshot
293+
* @return {boolean} true if item should be removed
280294
*/
281295
$$removed: function(snap) {
282-
var rec = this.$getRecord($firebaseUtils.getKey(snap));
283-
if( angular.isObject(rec) ) {
284-
this._process('child_removed', rec);
285-
}
296+
return this.$indexFor($firebaseUtils.getKey(snap)) > -1;
286297
},
287298

288299
/**
289300
* Called by $firebase whenever an item is changed at the server.
290-
* This method must exist on any arrayFactory passed into $firebase
301+
* This method should apply the changes, including changes to data
302+
* and to $priority, and then return true if any changes were made.
291303
*
292-
* @param snap
304+
* @param {object} snap a Firebase snapshot
305+
* @return {boolean} true if any data changed
293306
*/
294307
$$updated: function(snap) {
308+
var changed = false;
295309
var rec = this.$getRecord($firebaseUtils.getKey(snap));
296310
if( angular.isObject(rec) ) {
297311
// apply changes to the record
298-
var changed = $firebaseUtils.updateRec(rec, snap);
312+
changed = $firebaseUtils.updateRec(rec, snap);
299313
$firebaseUtils.applyDefaults(rec, this.$$defaults);
300-
if( changed ) {
301-
this._process('child_changed', rec);
302-
}
303314
}
315+
return changed;
304316
},
305317

306318
/**
307319
* Called by $firebase whenever an item changes order (moves) on the server.
308-
* This method must exist on any arrayFactory passed into $firebase
320+
* This method should set $priority to the updated value and return true if
321+
* the record should actually be moved. It should not actually apply the move
322+
* operation.
309323
*
310-
* @param snap
324+
* @param {object} snap a Firebase snapshot
311325
* @param {string} prevChild
312326
*/
313-
$$moved: function(snap, prevChild) {
327+
$$moved: function(snap/*, prevChild*/) {
314328
var rec = this.$getRecord($firebaseUtils.getKey(snap));
315329
if( angular.isObject(rec) ) {
316330
rec.$priority = snap.getPriority();
317-
this._process('child_moved', rec, prevChild);
331+
return true;
318332
}
333+
return false;
319334
},
320335

321336
/**
@@ -340,14 +355,15 @@
340355

341356
/**
342357
* Handles placement of recs in the array, sending notifications,
343-
* and other internals.
358+
* and other internals. Called by the $firebase synchronization process
359+
* after $$added, $$updated, $$moved, and $$removed
344360
*
345361
* @param {string} event one of child_added, child_removed, child_moved, or child_changed
346362
* @param {object} rec
347363
* @param {string} [prevChild]
348364
* @private
349365
*/
350-
_process: function(event, rec, prevChild) {
366+
$$process: function(event, rec, prevChild) {
351367
var key = this._getKey(rec);
352368
var changed = false;
353369
var pos;
@@ -367,27 +383,28 @@
367383
changed = true;
368384
break;
369385
default:
370-
// nothing to do
386+
throw new Error('Invalid event type ' + event);
371387
}
372388
if( angular.isDefined(pos) ) {
373389
// add it to the array
374390
changed = this._addAfter(rec, prevChild) !== pos;
375391
}
376392
if( changed ) {
377393
// send notifications to anybody monitoring $watch
378-
this._notify(event, key, prevChild);
394+
this.$$notify(event, key, prevChild);
379395
}
380396
return changed;
381397
},
382398

383399
/**
384400
* Used to trigger notifications for listeners registered using $watch
401+
*
385402
* @param {string} event
386403
* @param {string} key
387404
* @param {string} [prevChild]
388405
* @private
389406
*/
390-
_notify: function(event, key, prevChild) {
407+
$$notify: function(event, key, prevChild) {
391408
var eventData = {event: event, key: key};
392409
if( angular.isDefined(prevChild) ) {
393410
eventData.prevChild = prevChild;

src/firebase.js

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,39 @@
220220
var def = $firebaseUtils.defer();
221221
var array = new ArrayFactory($inst, destroy, def.promise);
222222
var batch = $firebaseUtils.batch();
223-
var created = batch(array.$$added, array);
224-
var updated = batch(array.$$updated, array);
225-
var moved = batch(array.$$moved, array);
226-
var removed = batch(array.$$removed, array);
223+
var created = batch(function(snap, prevChild) {
224+
var rec = array.$$added(snap, prevChild);
225+
if( rec ) {
226+
array.$$process('child_added', rec, prevChild);
227+
}
228+
});
229+
var updated = batch(function(snap) {
230+
var rec = array.$getRecord(snap.name());
231+
if( rec ) {
232+
var changed = array.$$updated(snap);
233+
if( changed ) {
234+
array.$$process('child_changed', rec);
235+
}
236+
}
237+
});
238+
var moved = batch(function(snap, prevChild) {
239+
var rec = array.$getRecord(snap.name());
240+
if( rec ) {
241+
var confirmed = array.$$moved(snap, prevChild);
242+
if( confirmed ) {
243+
array.$$process('child_moved', rec, prevChild);
244+
}
245+
}
246+
});
247+
var removed = batch(function(snap) {
248+
var rec = array.$getRecord(snap.name());
249+
if( rec ) {
250+
var confirmed = array.$$removed(snap);
251+
if( confirmed ) {
252+
array.$$process('child_removed', rec);
253+
}
254+
}
255+
});
227256
var error = batch(array.$$error, array);
228257
var resolve = batch(_resolveFn);
229258

tests/lib/jasmineMatchers.js

Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,6 @@
66
beforeEach(function() {
77
'use strict';
88

9-
// taken from Angular.js 2.0
10-
var isArray = (function() {
11-
if (typeof Array.isArray !== 'function') {
12-
return function(value) {
13-
return toString.call(value) === '[object Array]';
14-
};
15-
}
16-
return Array.isArray;
17-
})();
18-
199
function extendedTypeOf(x) {
2010
var actual;
2111
if( isArray(x) ) {
@@ -78,12 +68,12 @@ beforeEach(function() {
7868
// inspired by: https://gist.github.com/prantlf/8631877
7969
toBeInstanceOf: function() {
8070
return {
81-
compare: function (actual, expected) {
71+
compare: function (actual, expected, name) {
8272
var result = {
8373
pass: actual instanceof expected
8474
};
8575
var notText = result.pass? ' not' : '';
86-
result.message = 'Expected ' + actual + notText + ' to be an instance of ' + expected;
76+
result.message = 'Expected ' + actual + notText + ' to be an instance of ' + (name||expected.constructor.name);
8777
return result;
8878
}
8979
};
@@ -97,30 +87,104 @@ beforeEach(function() {
9787
*/
9888
toBeA: function() {
9989
return {
100-
compare: compare.bind(null, 'a')
90+
compare: function() {
91+
var args = Array.prototype.slice.apply(arguments);
92+
return compare.apply(null, ['a'].concat(args));
93+
}
10194
};
10295
},
10396

10497
toBeAn: function() {
10598
return {
106-
compare: compare.bind(null, 'an')
99+
compare: function(actual) {
100+
var args = Array.prototype.slice.apply(arguments);
101+
return compare.apply(null, ['an'].concat(args));
102+
}
107103
}
108104
},
109105

110106
toHaveKey: function() {
111107
return {
112108
compare: function(actual, key) {
113-
var pass = actual.hasOwnProperty(key);
109+
var pass =
110+
actual &&
111+
typeof(actual) === 'object' &&
112+
actual.hasOwnProperty(key);
113+
var notText = pass? ' not' : '';
114+
return {
115+
pass: pass,
116+
message: 'Expected key ' + key + notText + ' to exist in ' + extendedTypeOf(actual)
117+
}
118+
}
119+
}
120+
},
121+
122+
toHaveLength: function() {
123+
return {
124+
compare: function(actual, len) {
125+
var actLen = isArray(actual)? actual.length : 'not an array';
126+
var pass = actLen === len;
127+
var notText = pass? ' not' : '';
128+
return {
129+
pass: pass,
130+
message: 'Expected array ' + notText + ' to have length ' + len + ', but it was ' + actLen
131+
}
132+
}
133+
}
134+
},
135+
136+
toBeEmpty: function() {
137+
return {
138+
compare: function(actual) {
139+
var pass, contents;
140+
if( isObject(actual) ) {
141+
actual = Object.keys(actual);
142+
}
143+
if( isArray(actual) ) {
144+
pass = actual.length === 0;
145+
contents = 'had ' + actual.length + ' items';
146+
}
147+
else {
148+
pass = false;
149+
contents = 'was not an array or object';
150+
}
114151
var notText = pass? ' not' : '';
115152
return {
116153
pass: pass,
117-
message: 'Expected ' + key + notText + ' to exist in ' + extendedTypeOf(actual)
154+
message: 'Expected collection ' + notText + ' to be empty, but it ' + contents
155+
}
156+
}
157+
}
158+
},
159+
160+
toHaveCallCount: function() {
161+
return {
162+
compare: function(spy, expCount) {
163+
var pass, not, count;
164+
count = spy.calls.count();
165+
pass = count === expCount;
166+
not = pass? '" not' : '"';
167+
return {
168+
pass: pass,
169+
message: 'Expected spy "' + spy.and.identity() + not + ' to have been called ' + expCount + ' times'
170+
+ (pass? '' : ', but it was called ' + count)
118171
}
119172
}
120173
}
121174
}
122175
});
123176

177+
function isObject(x) {
178+
return x && typeof(x) === 'object' && !isArray(x);
179+
}
180+
181+
function isArray(x) {
182+
if (typeof Array.isArray !== 'function') {
183+
return x && typeof x === 'object' && Object.prototype.toString.call(x) === '[object Array]';
184+
}
185+
return Array.isArray(x);
186+
}
187+
124188
function isFirebaseRef(obj) {
125189
return extendedTypeOf(obj) === 'object' &&
126190
typeof obj.ref === 'function' &&

0 commit comments

Comments
 (0)