Commit 3dd6105b authored by Pascal Hartig's avatar Pascal Hartig

Merge branch 'pr/1103'

Close #1103
parents e5adfb0c 189da5c8
node_modules/angular/*
!node_modules/angular/angular.js
node_modules/angularfire/*
node_modules/angularfire/dist/*
!node_modules/angularfire/dist/angularfire.js
node_modules/todomvc-app-css/*
!node_modules/todomvc-app-css/index.css
node_modules/todomvc-common/*
!node_modules/todomvc-common/base.css
!node_modules/todomvc-common/base.js
{
"name": "todomvc-angular",
"version": "0.0.0",
"dependencies": {
"angular": "1.2.8",
"angularfire": "~0.5.0",
"todomvc-common": "~0.3.0"
},
"devDependencies": {
"angular-mocks": "1.2.8"
},
"resolutions": {
"angular": "1.2.8"
}
}
/**
* @license AngularJS v1.2.8
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
'use strict';
/**
* @ngdoc overview
* @name angular.mock
* @description
*
* Namespace from 'angular-mocks.js' which contains testing related code.
*/
angular.mock = {};
/**
* ! This is a private undocumented service !
*
* @name ngMock.$browser
*
* @description
* This service is a mock implementation of {@link ng.$browser}. It provides fake
* implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr,
* cookies, etc...
*
* The api of this service is the same as that of the real {@link ng.$browser $browser}, except
* that there are several helper methods available which can be used in tests.
*/
angular.mock.$BrowserProvider = function() {
this.$get = function() {
return new angular.mock.$Browser();
};
};
angular.mock.$Browser = function() {
var self = this;
this.isMock = true;
self.$$url = "http://server/";
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
// TODO(vojta): remove this temporary api
self.$$completeOutstandingRequest = angular.noop;
self.$$incOutstandingRequestCount = angular.noop;
// register url polling fn
self.onUrlChange = function(listener) {
self.pollFns.push(
function() {
if (self.$$lastUrl != self.$$url) {
self.$$lastUrl = self.$$url;
listener(self.$$url);
}
}
);
return listener;
};
self.cookieHash = {};
self.lastCookieHash = {};
self.deferredFns = [];
self.deferredNextId = 0;
self.defer = function(fn, delay) {
delay = delay || 0;
self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
self.deferredFns.sort(function(a,b){ return a.time - b.time;});
return self.deferredNextId++;
};
/**
* @name ngMock.$browser#defer.now
* @propertyOf ngMock.$browser
*
* @description
* Current milliseconds mock time.
*/
self.defer.now = 0;
self.defer.cancel = function(deferId) {
var fnIndex;
angular.forEach(self.deferredFns, function(fn, index) {
if (fn.id === deferId) fnIndex = index;
});
if (fnIndex !== undefined) {
self.deferredFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @name ngMock.$browser#defer.flush
* @methodOf ngMock.$browser
*
* @description
* Flushes all pending requests and executes the defer callbacks.
*
* @param {number=} number of milliseconds to flush. See {@link #defer.now}
*/
self.defer.flush = function(delay) {
if (angular.isDefined(delay)) {
self.defer.now += delay;
} else {
if (self.deferredFns.length) {
self.defer.now = self.deferredFns[self.deferredFns.length-1].time;
} else {
throw new Error('No deferred tasks to be flushed');
}
}
while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) {
self.deferredFns.shift().fn();
}
};
self.$$baseHref = '';
self.baseHref = function() {
return this.$$baseHref;
};
};
angular.mock.$Browser.prototype = {
/**
* @name ngMock.$browser#poll
* @methodOf ngMock.$browser
*
* @description
* run all fns in pollFns
*/
poll: function poll() {
angular.forEach(this.pollFns, function(pollFn){
pollFn();
});
},
addPollFn: function(pollFn) {
this.pollFns.push(pollFn);
return pollFn;
},
url: function(url, replace) {
if (url) {
this.$$url = url;
return this;
}
return this.$$url;
},
cookies: function(name, value) {
if (name) {
if (angular.isUndefined(value)) {
delete this.cookieHash[name];
} else {
if (angular.isString(value) && //strings only
value.length <= 4096) { //strict cookie storage limits
this.cookieHash[name] = value;
}
}
} else {
if (!angular.equals(this.cookieHash, this.lastCookieHash)) {
this.lastCookieHash = angular.copy(this.cookieHash);
this.cookieHash = angular.copy(this.cookieHash);
}
return this.cookieHash;
}
},
notifyWhenNoOutstandingRequests: function(fn) {
fn();
}
};
/**
* @ngdoc object
* @name ngMock.$exceptionHandlerProvider
*
* @description
* Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
* passed into the `$exceptionHandler`.
*/
/**
* @ngdoc object
* @name ngMock.$exceptionHandler
*
* @description
* Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
* into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
* information.
*
*
* <pre>
* describe('$exceptionHandlerProvider', function() {
*
* it('should capture log messages and exceptions', function() {
*
* module(function($exceptionHandlerProvider) {
* $exceptionHandlerProvider.mode('log');
* });
*
* inject(function($log, $exceptionHandler, $timeout) {
* $timeout(function() { $log.log(1); });
* $timeout(function() { $log.log(2); throw 'banana peel'; });
* $timeout(function() { $log.log(3); });
* expect($exceptionHandler.errors).toEqual([]);
* expect($log.assertEmpty());
* $timeout.flush();
* expect($exceptionHandler.errors).toEqual(['banana peel']);
* expect($log.log.logs).toEqual([[1], [2], [3]]);
* });
* });
* });
* </pre>
*/
angular.mock.$ExceptionHandlerProvider = function() {
var handler;
/**
* @ngdoc method
* @name ngMock.$exceptionHandlerProvider#mode
* @methodOf ngMock.$exceptionHandlerProvider
*
* @description
* Sets the logging mode.
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
* - `rethrow`: If any errors are passed into the handler in tests, it typically
* means that there is a bug in the application or test, so this mock will
* make these tests fail.
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()}
*/
this.mode = function(mode) {
switch(mode) {
case 'rethrow':
handler = function(e) {
throw e;
};
break;
case 'log':
var errors = [];
handler = function(e) {
if (arguments.length == 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
};
handler.errors = errors;
break;
default:
throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
}
};
this.$get = function() {
return handler;
};
this.mode('rethrow');
};
/**
* @ngdoc service
* @name ngMock.$log
*
* @description
* Mock implementation of {@link ng.$log} that gathers all logged messages in arrays
* (one array per logging level). These arrays are exposed as `logs` property of each of the
* level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`.
*
*/
angular.mock.$LogProvider = function() {
var debug = true;
function concat(array1, array2, index) {
return array1.concat(Array.prototype.slice.call(array2, index));
}
this.debugEnabled = function(flag) {
if (angular.isDefined(flag)) {
debug = flag;
return this;
} else {
return debug;
}
};
this.$get = function () {
var $log = {
log: function() { $log.log.logs.push(concat([], arguments, 0)); },
warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
info: function() { $log.info.logs.push(concat([], arguments, 0)); },
error: function() { $log.error.logs.push(concat([], arguments, 0)); },
debug: function() {
if (debug) {
$log.debug.logs.push(concat([], arguments, 0));
}
}
};
/**
* @ngdoc method
* @name ngMock.$log#reset
* @methodOf ngMock.$log
*
* @description
* Reset all of the logging arrays to empty.
*/
$log.reset = function () {
/**
* @ngdoc property
* @name ngMock.$log#log.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#log}.
*
* @example
* <pre>
* $log.log('Some Log');
* var first = $log.log.logs.unshift();
* </pre>
*/
$log.log.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#info.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#info}.
*
* @example
* <pre>
* $log.info('Some Info');
* var first = $log.info.logs.unshift();
* </pre>
*/
$log.info.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#warn.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#warn}.
*
* @example
* <pre>
* $log.warn('Some Warning');
* var first = $log.warn.logs.unshift();
* </pre>
*/
$log.warn.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#error.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#error}.
*
* @example
* <pre>
* $log.log('Some Error');
* var first = $log.error.logs.unshift();
* </pre>
*/
$log.error.logs = [];
/**
* @ngdoc property
* @name ngMock.$log#debug.logs
* @propertyOf ngMock.$log
*
* @description
* Array of messages logged using {@link ngMock.$log#debug}.
*
* @example
* <pre>
* $log.debug('Some Error');
* var first = $log.debug.logs.unshift();
* </pre>
*/
$log.debug.logs = [];
};
/**
* @ngdoc method
* @name ngMock.$log#assertEmpty
* @methodOf ngMock.$log
*
* @description
* Assert that the all of the logging methods have no logged messages. If messages present, an
* exception is thrown.
*/
$log.assertEmpty = function() {
var errors = [];
angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
angular.forEach($log[logLevel].logs, function(log) {
angular.forEach(log, function (logItem) {
errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
(logItem.stack || ''));
});
});
});
if (errors.length) {
errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+
"an expected log message was not checked and removed:");
errors.push('');
throw new Error(errors.join('\n---------\n'));
}
};
$log.reset();
return $log;
};
};
/**
* @ngdoc service
* @name ngMock.$interval
*
* @description
* Mock implementation of the $interval service.
*
* Use {@link ngMock.$interval#methods_flush `$interval.flush(millis)`} to
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that
* time.
*
* @param {function()} fn A function that should be called repeatedly.
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block.
* @returns {promise} A promise which will be notified on each iteration.
*/
angular.mock.$IntervalProvider = function() {
this.$get = ['$rootScope', '$q',
function($rootScope, $q) {
var repeatFns = [],
nextRepeatId = 0,
now = 0;
var $interval = function(fn, delay, count, invokeApply) {
var deferred = $q.defer(),
promise = deferred.promise,
iteration = 0,
skipApply = (angular.isDefined(invokeApply) && !invokeApply);
count = (angular.isDefined(count)) ? count : 0,
promise.then(null, null, fn);
promise.$$intervalId = nextRepeatId;
function tick() {
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
var fnIndex;
deferred.resolve(iteration);
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns.splice(fnIndex, 1);
}
}
if (!skipApply) $rootScope.$apply();
}
repeatFns.push({
nextTime:(now + delay),
delay: delay,
fn: tick,
id: nextRepeatId,
deferred: deferred
});
repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
nextRepeatId++;
return promise;
};
$interval.cancel = function(promise) {
var fnIndex;
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns[fnIndex].deferred.reject('canceled');
repeatFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @ngdoc method
* @name ngMock.$interval#flush
* @methodOf ngMock.$interval
* @description
*
* Runs interval tasks scheduled to be run in the next `millis` milliseconds.
*
* @param {number=} millis maximum timeout amount to flush up until.
*
* @return {number} The amount of time moved forward.
*/
$interval.flush = function(millis) {
now += millis;
while (repeatFns.length && repeatFns[0].nextTime <= now) {
var task = repeatFns[0];
task.fn();
task.nextTime += task.delay;
repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;});
}
return millis;
};
return $interval;
}];
};
/* jshint -W101 */
/* The R_ISO8061_STR regex is never going to fit into the 100 char limit!
* This directive should go inside the anonymous function but a bug in JSHint means that it would
* not be enacted early enough to prevent the warning.
*/
var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
function jsonStringToDate(string) {
var match;
if (match = string.match(R_ISO8061_STR)) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
if (match[9]) {
tzHour = int(match[9] + match[10]);
tzMin = int(match[9] + match[11]);
}
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
date.setUTCHours(int(match[4]||0) - tzHour,
int(match[5]||0) - tzMin,
int(match[6]||0),
int(match[7]||0));
return date;
}
return string;
}
function int(str) {
return parseInt(str, 10);
}
function padNumber(num, digits, trim) {
var neg = '';
if (num < 0) {
neg = '-';
num = -num;
}
num = '' + num;
while(num.length < digits) num = '0' + num;
if (trim)
num = num.substr(num.length - digits);
return neg + num;
}
/**
* @ngdoc object
* @name angular.mock.TzDate
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
*
* Mock of the Date type which has its timezone specified via constructor arg.
*
* The main purpose is to create Date-like instances with timezone fixed to the specified timezone
* offset, so that we can test code that depends on local timezone settings without dependency on
* the time zone settings of the machine where the code is running.
*
* @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
* @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
*
* @example
* !!!! WARNING !!!!!
* This is not a complete Date object so only methods that were implemented can be called safely.
* To make matters worse, TzDate instances inherit stuff from Date via a prototype.
*
* We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
* incomplete we might be missing some non-standard methods. This can result in errors like:
* "Date.prototype.foo called on incompatible Object".
*
* <pre>
* var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
* newYearInBratislava.getTimezoneOffset() => -60;
* newYearInBratislava.getFullYear() => 2010;
* newYearInBratislava.getMonth() => 0;
* newYearInBratislava.getDate() => 1;
* newYearInBratislava.getHours() => 0;
* newYearInBratislava.getMinutes() => 0;
* newYearInBratislava.getSeconds() => 0;
* </pre>
*
*/
angular.mock.TzDate = function (offset, timestamp) {
var self = new Date(0);
if (angular.isString(timestamp)) {
var tsStr = timestamp;
self.origDate = jsonStringToDate(timestamp);
timestamp = self.origDate.getTime();
if (isNaN(timestamp))
throw {
name: "Illegal Argument",
message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
};
} else {
self.origDate = new Date(timestamp);
}
var localOffset = new Date(timestamp).getTimezoneOffset();
self.offsetDiff = localOffset*60*1000 - offset*1000*60*60;
self.date = new Date(timestamp + self.offsetDiff);
self.getTime = function() {
return self.date.getTime() - self.offsetDiff;
};
self.toLocaleDateString = function() {
return self.date.toLocaleDateString();
};
self.getFullYear = function() {
return self.date.getFullYear();
};
self.getMonth = function() {
return self.date.getMonth();
};
self.getDate = function() {
return self.date.getDate();
};
self.getHours = function() {
return self.date.getHours();
};
self.getMinutes = function() {
return self.date.getMinutes();
};
self.getSeconds = function() {
return self.date.getSeconds();
};
self.getMilliseconds = function() {
return self.date.getMilliseconds();
};
self.getTimezoneOffset = function() {
return offset * 60;
};
self.getUTCFullYear = function() {
return self.origDate.getUTCFullYear();
};
self.getUTCMonth = function() {
return self.origDate.getUTCMonth();
};
self.getUTCDate = function() {
return self.origDate.getUTCDate();
};
self.getUTCHours = function() {
return self.origDate.getUTCHours();
};
self.getUTCMinutes = function() {
return self.origDate.getUTCMinutes();
};
self.getUTCSeconds = function() {
return self.origDate.getUTCSeconds();
};
self.getUTCMilliseconds = function() {
return self.origDate.getUTCMilliseconds();
};
self.getDay = function() {
return self.date.getDay();
};
// provide this method only on browsers that already have it
if (self.toISOString) {
self.toISOString = function() {
return padNumber(self.origDate.getUTCFullYear(), 4) + '-' +
padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' +
padNumber(self.origDate.getUTCDate(), 2) + 'T' +
padNumber(self.origDate.getUTCHours(), 2) + ':' +
padNumber(self.origDate.getUTCMinutes(), 2) + ':' +
padNumber(self.origDate.getUTCSeconds(), 2) + '.' +
padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z';
};
}
//hide all methods not implemented in this mock that the Date prototype exposes
var unimplementedMethods = ['getUTCDay',
'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString',
'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
angular.forEach(unimplementedMethods, function(methodName) {
self[methodName] = function() {
throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock");
};
});
return self;
};
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
/* jshint +W101 */
angular.mock.animate = angular.module('mock.animate', ['ng'])
.config(['$provide', function($provide) {
$provide.decorator('$animate', function($delegate) {
var animate = {
queue : [],
enabled : $delegate.enabled,
flushNext : function(name) {
var tick = animate.queue.shift();
if (!tick) throw new Error('No animation to be flushed');
if(tick.method !== name) {
throw new Error('The next animation is not "' + name +
'", but is "' + tick.method + '"');
}
tick.fn();
return tick;
}
};
angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) {
animate[method] = function() {
var params = arguments;
animate.queue.push({
method : method,
params : params,
element : angular.isElement(params[0]) && params[0],
parent : angular.isElement(params[1]) && params[1],
after : angular.isElement(params[2]) && params[2],
fn : function() {
$delegate[method].apply($delegate, params);
}
});
};
});
return animate;
});
}]);
/**
* @ngdoc function
* @name angular.mock.dump
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available function.
*
* Method for serializing common angular objects (scope, elements, etc..) into strings, useful for
* debugging.
*
* This method is also available on window, where it can be used to display objects on debug
* console.
*
* @param {*} object - any object to turn into string.
* @return {string} a serialized string of the argument
*/
angular.mock.dump = function(object) {
return serialize(object);
function serialize(object) {
var out;
if (angular.isElement(object)) {
object = angular.element(object);
out = angular.element('<div></div>');
angular.forEach(object, function(element) {
out.append(angular.element(element).clone());
});
out = out.html();
} else if (angular.isArray(object)) {
out = [];
angular.forEach(object, function(o) {
out.push(serialize(o));
});
out = '[ ' + out.join(', ') + ' ]';
} else if (angular.isObject(object)) {
if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) {
out = serializeScope(object);
} else if (object instanceof Error) {
out = object.stack || ('' + object.name + ': ' + object.message);
} else {
// TODO(i): this prevents methods being logged,
// we should have a better way to serialize objects
out = angular.toJson(object, true);
}
} else {
out = String(object);
}
return out;
}
function serializeScope(scope, offset) {
offset = offset || ' ';
var log = [offset + 'Scope(' + scope.$id + '): {'];
for ( var key in scope ) {
if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
log.push(' ' + key + ': ' + angular.toJson(scope[key]));
}
}
var child = scope.$$childHead;
while(child) {
log.push(serializeScope(child, offset + ' '));
child = child.$$nextSibling;
}
log.push('}');
return log.join('\n' + offset);
}
};
/**
* @ngdoc object
* @name ngMock.$httpBackend
* @description
* Fake HTTP backend implementation suitable for unit testing applications that use the
* {@link ng.$http $http service}.
*
* *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less
* development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
*
* During unit testing, we want our unit tests to run quickly and have no external dependencies so
* we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or
* {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is
* to verify whether a certain request has been sent or not, or alternatively just let the
* application make requests, respond with pre-trained responses and assert that the end result is
* what we expect it to be.
*
* This mock implementation can be used to respond with static or dynamic responses via the
* `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc).
*
* When an Angular application needs some data from a server, it calls the $http service, which
* sends the request to a real server using $httpBackend service. With dependency injection, it is
* easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify
* the requests and respond with some testing data without sending a request to real server.
*
* There are two ways to specify what test data should be returned as http responses by the mock
* backend when the code under test makes http requests:
*
* - `$httpBackend.expect` - specifies a request expectation
* - `$httpBackend.when` - specifies a backend definition
*
*
* # Request Expectations vs Backend Definitions
*
* Request expectations provide a way to make assertions about requests made by the application and
* to define responses for those requests. The test will fail if the expected requests are not made
* or they are made in the wrong order.
*
* Backend definitions allow you to define a fake backend for your application which doesn't assert
* if a particular request was made or not, it just returns a trained response if a request is made.
* The test will pass whether or not the request gets made during testing.
*
*
* <table class="table">
* <tr><th width="220px"></th><th>Request expectations</th><th>Backend definitions</th></tr>
* <tr>
* <th>Syntax</th>
* <td>.expect(...).respond(...)</td>
* <td>.when(...).respond(...)</td>
* </tr>
* <tr>
* <th>Typical usage</th>
* <td>strict unit tests</td>
* <td>loose (black-box) unit testing</td>
* </tr>
* <tr>
* <th>Fulfills multiple requests</th>
* <td>NO</td>
* <td>YES</td>
* </tr>
* <tr>
* <th>Order of requests matters</th>
* <td>YES</td>
* <td>NO</td>
* </tr>
* <tr>
* <th>Request required</th>
* <td>YES</td>
* <td>NO</td>
* </tr>
* <tr>
* <th>Response required</th>
* <td>optional (see below)</td>
* <td>YES</td>
* </tr>
* </table>
*
* In cases where both backend definitions and request expectations are specified during unit
* testing, the request expectations are evaluated first.
*
* If a request expectation has no response specified, the algorithm will search your backend
* definitions for an appropriate response.
*
* If a request didn't match any expectation or if the expectation doesn't have the response
* defined, the backend definitions are evaluated in sequential order to see if any of them match
* the request. The response from the first matched definition is returned.
*
*
* # Flushing HTTP requests
*
* The $httpBackend used in production, always responds to requests with responses asynchronously.
* If we preserved this behavior in unit testing, we'd have to create async unit tests, which are
* hard to write, follow and maintain. At the same time the testing mock, can't respond
* synchronously because that would change the execution of the code under test. For this reason the
* mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending
* requests and thus preserving the async api of the backend, while allowing the test to execute
* synchronously.
*
*
* # Unit testing with mock $httpBackend
* The following code shows how to setup and use the mock backend in unit testing a controller.
* First we create the controller under test
*
<pre>
// The controller code
function MyController($scope, $http) {
var authToken;
$http.get('/auth.py').success(function(data, status, headers) {
authToken = headers('A-Token');
$scope.user = data;
});
$scope.saveMessage = function(message) {
var headers = { 'Authorization': authToken };
$scope.status = 'Saving...';
$http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
$scope.status = '';
}).error(function() {
$scope.status = 'ERROR!';
});
};
}
</pre>
*
* Now we setup the mock backend and create the test specs.
*
<pre>
// testing controller
describe('MyController', function() {
var $httpBackend, $rootScope, createController;
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
$httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
createController = function() {
return $controller('MyController', {'$scope' : $rootScope });
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch authentication token', function() {
$httpBackend.expectGET('/auth.py');
var controller = createController();
$httpBackend.flush();
});
it('should send msg to server', function() {
var controller = createController();
$httpBackend.flush();
// now you don’t care about the authentication, but
// the controller will still send the request and
// $httpBackend will respond without you having to
// specify the expectation and response for this request
$httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
$rootScope.saveMessage('message content');
expect($rootScope.status).toBe('Saving...');
$httpBackend.flush();
expect($rootScope.status).toBe('');
});
it('should send auth header', function() {
var controller = createController();
$httpBackend.flush();
$httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
// check if the header was send, if it wasn't the expectation won't
// match the request and the test will fail
return headers['Authorization'] == 'xxx';
}).respond(201, '');
$rootScope.saveMessage('whatever');
$httpBackend.flush();
});
});
</pre>
*/
angular.mock.$HttpBackendProvider = function() {
this.$get = ['$rootScope', createHttpBackendMock];
};
/**
* General factory function for $httpBackend mock.
* Returns instance for unit testing (when no arguments specified):
* - passing through is disabled
* - auto flushing is disabled
*
* Returns instance for e2e testing (when `$delegate` and `$browser` specified):
* - passing through (delegating request to real backend) is enabled
* - auto flushing is enabled
*
* @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified)
* @param {Object=} $browser Auto-flushing enabled if specified
* @return {Object} Instance of $httpBackend mock
*/
function createHttpBackendMock($rootScope, $delegate, $browser) {
var definitions = [],
expectations = [],
responses = [],
responsesPush = angular.bind(responses, responses.push),
copy = angular.copy;
function createResponse(status, data, headers) {
if (angular.isFunction(status)) return status;
return function() {
return angular.isNumber(status)
? [status, data, headers]
: [200, status, data];
};
}
// TODO(vojta): change params to: method, url, data, headers, callback
function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) {
var xhr = new MockXhr(),
expectation = expectations[0],
wasExpected = false;
function prettyPrint(data) {
return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
? data
: angular.toJson(data);
}
function wrapResponse(wrapped) {
if (!$browser && timeout && timeout.then) timeout.then(handleTimeout);
return handleResponse;
function handleResponse() {
var response = wrapped.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders());
}
function handleTimeout() {
for (var i = 0, ii = responses.length; i < ii; i++) {
if (responses[i] === handleResponse) {
responses.splice(i, 1);
callback(-1, undefined, '');
break;
}
}
}
}
if (expectation && expectation.match(method, url)) {
if (!expectation.matchData(data))
throw new Error('Expected ' + expectation + ' with different data\n' +
'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
if (!expectation.matchHeaders(headers))
throw new Error('Expected ' + expectation + ' with different headers\n' +
'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
prettyPrint(headers));
expectations.shift();
if (expectation.response) {
responses.push(wrapResponse(expectation));
return;
}
wasExpected = true;
}
var i = -1, definition;
while ((definition = definitions[++i])) {
if (definition.match(method, url, data, headers || {})) {
if (definition.response) {
// if $browser specified, we do auto flush all requests
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
} else if (definition.passThrough) {
$delegate(method, url, data, callback, headers, timeout, withCredentials);
} else throw new Error('No response defined !');
return;
}
}
throw wasExpected ?
new Error('No response defined !') :
new Error('Unexpected request: ' + method + ' ' + url + '\n' +
(expectation ? 'Expected ' + expectation : 'No more request expected'));
}
/**
* @ngdoc method
* @name ngMock.$httpBackend#when
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition.
*
* @param {string} method HTTP method.
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled.
*
* - respond –
* `{function([status,] data[, headers])|function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string) and response headers
* (Object).
*/
$httpBackend.when = function(method, url, data, headers) {
var definition = new MockHttpExpectation(method, url, data, headers),
chain = {
respond: function(status, data, headers) {
definition.response = createResponse(status, data, headers);
}
};
if ($browser) {
chain.passThrough = function() {
definition.passThrough = true;
};
}
definitions.push(definition);
return chain;
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenGET
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenHEAD
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenDELETE
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenPOST
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenPUT
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#whenJSONP
* @methodOf ngMock.$httpBackend
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
createShortMethods('when');
/**
* @ngdoc method
* @name ngMock.$httpBackend#expect
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation.
*
* @param {string} method HTTP method.
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*
* - respond –
* `{function([status,] data[, headers])|function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string) and response headers
* (Object).
*/
$httpBackend.expect = function(method, url, data, headers) {
var expectation = new MockHttpExpectation(method, url, data, headers);
expectations.push(expectation);
return {
respond: function(status, data, headers) {
expectation.response = createResponse(status, data, headers);
}
};
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectGET
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for GET requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled. See #expect for more info.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectHEAD
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectDELETE
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectPOST
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for POST requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectPUT
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for PUT requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectPATCH
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
/**
* @ngdoc method
* @name ngMock.$httpBackend#expectJSONP
* @methodOf ngMock.$httpBackend
* @description
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
*
* @param {string|RegExp} url HTTP url.
* @returns {requestHandler} Returns an object with `respond` method that control how a matched
* request is handled.
*/
createShortMethods('expect');
/**
* @ngdoc method
* @name ngMock.$httpBackend#flush
* @methodOf ngMock.$httpBackend
* @description
* Flushes all pending requests using the trained responses.
*
* @param {number=} count Number of responses to flush (in the order they arrived). If undefined,
* all pending requests will be flushed. If there are no pending requests when the flush method
* is called an exception is thrown (as this typically a sign of programming error).
*/
$httpBackend.flush = function(count) {
$rootScope.$digest();
if (!responses.length) throw new Error('No pending request to flush !');
if (angular.isDefined(count)) {
while (count--) {
if (!responses.length) throw new Error('No more pending request to flush !');
responses.shift()();
}
} else {
while (responses.length) {
responses.shift()();
}
}
$httpBackend.verifyNoOutstandingExpectation();
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#verifyNoOutstandingExpectation
* @methodOf ngMock.$httpBackend
* @description
* Verifies that all of the requests defined via the `expect` api were made. If any of the
* requests were not made, verifyNoOutstandingExpectation throws an exception.
*
* Typically, you would call this method following each test case that asserts requests using an
* "afterEach" clause.
*
* <pre>
* afterEach($httpBackend.verifyNoOutstandingExpectation);
* </pre>
*/
$httpBackend.verifyNoOutstandingExpectation = function() {
$rootScope.$digest();
if (expectations.length) {
throw new Error('Unsatisfied requests: ' + expectations.join(', '));
}
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#verifyNoOutstandingRequest
* @methodOf ngMock.$httpBackend
* @description
* Verifies that there are no outstanding requests that need to be flushed.
*
* Typically, you would call this method following each test case that asserts requests using an
* "afterEach" clause.
*
* <pre>
* afterEach($httpBackend.verifyNoOutstandingRequest);
* </pre>
*/
$httpBackend.verifyNoOutstandingRequest = function() {
if (responses.length) {
throw new Error('Unflushed requests: ' + responses.length);
}
};
/**
* @ngdoc method
* @name ngMock.$httpBackend#resetExpectations
* @methodOf ngMock.$httpBackend
* @description
* Resets all request expectations, but preserves all backend definitions. Typically, you would
* call resetExpectations during a multiple-phase test when you want to reuse the same instance of
* $httpBackend mock.
*/
$httpBackend.resetExpectations = function() {
expectations.length = 0;
responses.length = 0;
};
return $httpBackend;
function createShortMethods(prefix) {
angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) {
$httpBackend[prefix + method] = function(url, headers) {
return $httpBackend[prefix](method, url, undefined, headers);
};
});
angular.forEach(['PUT', 'POST', 'PATCH'], function(method) {
$httpBackend[prefix + method] = function(url, data, headers) {
return $httpBackend[prefix](method, url, data, headers);
};
});
}
}
function MockHttpExpectation(method, url, data, headers) {
this.data = data;
this.headers = headers;
this.match = function(m, u, d, h) {
if (method != m) return false;
if (!this.matchUrl(u)) return false;
if (angular.isDefined(d) && !this.matchData(d)) return false;
if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
return true;
};
this.matchUrl = function(u) {
if (!url) return true;
if (angular.isFunction(url.test)) return url.test(u);
return url == u;
};
this.matchHeaders = function(h) {
if (angular.isUndefined(headers)) return true;
if (angular.isFunction(headers)) return headers(h);
return angular.equals(headers, h);
};
this.matchData = function(d) {
if (angular.isUndefined(data)) return true;
if (data && angular.isFunction(data.test)) return data.test(d);
if (data && angular.isFunction(data)) return data(d);
if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d));
return data == d;
};
this.toString = function() {
return method + ' ' + url;
};
}
function createMockXhr() {
return new MockXhr();
}
function MockXhr() {
// hack for testing $http, $httpBackend
MockXhr.$$lastInstance = this;
this.open = function(method, url, async) {
this.$$method = method;
this.$$url = url;
this.$$async = async;
this.$$reqHeaders = {};
this.$$respHeaders = {};
};
this.send = function(data) {
this.$$data = data;
};
this.setRequestHeader = function(key, value) {
this.$$reqHeaders[key] = value;
};
this.getResponseHeader = function(name) {
// the lookup must be case insensitive,
// that's why we try two quick lookups first and full scan last
var header = this.$$respHeaders[name];
if (header) return header;
name = angular.lowercase(name);
header = this.$$respHeaders[name];
if (header) return header;
header = undefined;
angular.forEach(this.$$respHeaders, function(headerVal, headerName) {
if (!header && angular.lowercase(headerName) == name) header = headerVal;
});
return header;
};
this.getAllResponseHeaders = function() {
var lines = [];
angular.forEach(this.$$respHeaders, function(value, key) {
lines.push(key + ': ' + value);
});
return lines.join('\n');
};
this.abort = angular.noop;
}
/**
* @ngdoc function
* @name ngMock.$timeout
* @description
*
* This service is just a simple decorator for {@link ng.$timeout $timeout} service
* that adds a "flush" and "verifyNoPendingTasks" methods.
*/
angular.mock.$TimeoutDecorator = function($delegate, $browser) {
/**
* @ngdoc method
* @name ngMock.$timeout#flush
* @methodOf ngMock.$timeout
* @description
*
* Flushes the queue of pending tasks.
*
* @param {number=} delay maximum timeout amount to flush up until
*/
$delegate.flush = function(delay) {
$browser.defer.flush(delay);
};
/**
* @ngdoc method
* @name ngMock.$timeout#verifyNoPendingTasks
* @methodOf ngMock.$timeout
* @description
*
* Verifies that there are no pending tasks that need to be flushed.
*/
$delegate.verifyNoPendingTasks = function() {
if ($browser.deferredFns.length) {
throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' +
formatPendingTasksAsString($browser.deferredFns));
}
};
function formatPendingTasksAsString(tasks) {
var result = [];
angular.forEach(tasks, function(task) {
result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}');
});
return result.join(', ');
}
return $delegate;
};
/**
*
*/
angular.mock.$RootElementProvider = function() {
this.$get = function() {
return angular.element('<div ng-app></div>');
};
};
/**
* @ngdoc overview
* @name ngMock
* @description
*
* # ngMock
*
* The `ngMock` module providers support to inject and mock Angular services into unit tests.
* In addition, ngMock also extends various core ng services such that they can be
* inspected and controlled in a synchronous manner within test code.
*
* {@installModule mocks}
*
* <div doc-module-components="ngMock"></div>
*
*/
angular.module('ngMock', ['ng']).provider({
$browser: angular.mock.$BrowserProvider,
$exceptionHandler: angular.mock.$ExceptionHandlerProvider,
$log: angular.mock.$LogProvider,
$interval: angular.mock.$IntervalProvider,
$httpBackend: angular.mock.$HttpBackendProvider,
$rootElement: angular.mock.$RootElementProvider
}).config(['$provide', function($provide) {
$provide.decorator('$timeout', angular.mock.$TimeoutDecorator);
}]);
/**
* @ngdoc overview
* @name ngMockE2E
* @description
*
* The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing.
* Currently there is only one mock present in this module -
* the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock.
*/
angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
$provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
}]);
/**
* @ngdoc object
* @name ngMockE2E.$httpBackend
* @description
* Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of
* applications that use the {@link ng.$http $http service}.
*
* *Note*: For fake http backend implementation suitable for unit testing please see
* {@link ngMock.$httpBackend unit-testing $httpBackend mock}.
*
* This implementation can be used to respond with static or dynamic responses via the `when` api
* and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the
* real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch
* templates from a webserver).
*
* As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application
* is being developed with the real backend api replaced with a mock, it is often desirable for
* certain category of requests to bypass the mock and issue a real http request (e.g. to fetch
* templates or static files from the webserver). To configure the backend with this behavior
* use the `passThrough` request handler of `when` instead of `respond`.
*
* Additionally, we don't want to manually have to flush mocked out requests like we do during unit
* testing. For this reason the e2e $httpBackend automatically flushes mocked out requests
* automatically, closely simulating the behavior of the XMLHttpRequest object.
*
* To setup the application to run with this http backend, you have to create a module that depends
* on the `ngMockE2E` and your application modules and defines the fake backend:
*
* <pre>
* myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
* myAppDev.run(function($httpBackend) {
* phones = [{name: 'phone1'}, {name: 'phone2'}];
*
* // returns the current list of phones
* $httpBackend.whenGET('/phones').respond(phones);
*
* // adds a new phone to the phones array
* $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
* phones.push(angular.fromJson(data));
* });
* $httpBackend.whenGET(/^\/templates\//).passThrough();
* //...
* });
* </pre>
*
* Afterwards, bootstrap your app with this new module.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#when
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition.
*
* @param {string} method HTTP method.
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers])|function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string) and response headers
* (Object).
* - passThrough – `{function()}` – Any request matching a backend definition with `passThrough`
* handler, will be pass through to the real backend (an XHR request will be made to the
* server.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenGET
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenHEAD
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenDELETE
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenPOST
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenPUT
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenPATCH
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for PATCH requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
/**
* @ngdoc method
* @name ngMockE2E.$httpBackend#whenJSONP
* @methodOf ngMockE2E.$httpBackend
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
* @param {string|RegExp} url HTTP url.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled.
*/
angular.mock.e2e = {};
angular.mock.e2e.$httpBackendDecorator =
['$rootScope', '$delegate', '$browser', createHttpBackendMock];
angular.mock.clearDataCache = function() {
var key,
cache = angular.element.cache;
for(key in cache) {
if (Object.prototype.hasOwnProperty.call(cache,key)) {
var handle = cache[key].handle;
handle && angular.element(handle.elem).off();
delete cache[key];
}
}
};
if(window.jasmine || window.mocha) {
var currentSpec = null,
isSpecRunning = function() {
return currentSpec && (window.mocha || currentSpec.queue.running);
};
beforeEach(function() {
currentSpec = this;
});
afterEach(function() {
var injector = currentSpec.$injector;
currentSpec.$injector = null;
currentSpec.$modules = null;
currentSpec = null;
if (injector) {
injector.get('$rootElement').off();
injector.get('$browser').pollFns.length = 0;
}
angular.mock.clearDataCache();
// clean up jquery's fragment cache
angular.forEach(angular.element.fragments, function(val, key) {
delete angular.element.fragments[key];
});
MockXhr.$$lastInstance = null;
angular.forEach(angular.callbacks, function(val, key) {
delete angular.callbacks[key];
});
angular.callbacks.counter = 0;
});
/**
* @ngdoc function
* @name angular.mock.module
* @description
*
* *NOTE*: This function is also published on window for easy access.<br>
*
* This function registers a module configuration code. It collects the configuration information
* which will be used when the injector is created by {@link angular.mock.inject inject}.
*
* See {@link angular.mock.inject inject} for usage example
*
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
* aliases or as anonymous module initialization functions. The modules are used to
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
* object literal is passed they will be register as values in the module, the key being
* the module name and the value being what is returned.
*/
window.module = angular.mock.module = function() {
var moduleFns = Array.prototype.slice.call(arguments, 0);
return isSpecRunning() ? workFn() : workFn;
/////////////////////
function workFn() {
if (currentSpec.$injector) {
throw new Error('Injector already created, can not register a module!');
} else {
var modules = currentSpec.$modules || (currentSpec.$modules = []);
angular.forEach(moduleFns, function(module) {
if (angular.isObject(module) && !angular.isArray(module)) {
modules.push(function($provide) {
angular.forEach(module, function(value, key) {
$provide.value(key, value);
});
});
} else {
modules.push(module);
}
});
}
}
};
/**
* @ngdoc function
* @name angular.mock.inject
* @description
*
* *NOTE*: This function is also published on window for easy access.<br>
*
* The inject function wraps a function into an injectable function. The inject() creates new
* instance of {@link AUTO.$injector $injector} per test, which is then used for
* resolving references.
*
*
* ## Resolving References (Underscore Wrapping)
* Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this
* in multiple `it()` clauses. To be able to do this we must assign the reference to a variable
* that is declared in the scope of the `describe()` block. Since we would, most likely, want
* the variable to have the same name of the reference we have a problem, since the parameter
* to the `inject()` function would hide the outer variable.
*
* To help with this, the injected parameters can, optionally, be enclosed with underscores.
* These are ignored by the injector when the reference name is resolved.
*
* For example, the parameter `_myService_` would be resolved as the reference `myService`.
* Since it is available in the function body as _myService_, we can then assign it to a variable
* defined in an outer scope.
*
* ```
* // Defined out reference variable outside
* var myService;
*
* // Wrap the parameter in underscores
* beforeEach( inject( function(_myService_){
* myService = _myService_;
* }));
*
* // Use myService in a series of tests.
* it('makes use of myService', function() {
* myService.doStuff();
* });
*
* ```
*
* See also {@link angular.mock.module angular.mock.module}
*
* ## Example
* Example of what a typical jasmine tests looks like with the inject method.
* <pre>
*
* angular.module('myApplicationModule', [])
* .value('mode', 'app')
* .value('version', 'v1.0.1');
*
*
* describe('MyApp', function() {
*
* // You need to load modules that you want to test,
* // it loads only the "ng" module by default.
* beforeEach(module('myApplicationModule'));
*
*
* // inject() is used to inject arguments of all given functions
* it('should provide a version', inject(function(mode, version) {
* expect(version).toEqual('v1.0.1');
* expect(mode).toEqual('app');
* }));
*
*
* // The inject and module method can also be used inside of the it or beforeEach
* it('should override a version and test the new version is injected', function() {
* // module() takes functions or strings (module aliases)
* module(function($provide) {
* $provide.value('version', 'overridden'); // override version here
* });
*
* inject(function(version) {
* expect(version).toEqual('overridden');
* });
* });
* });
*
* </pre>
*
* @param {...Function} fns any number of functions which will be injected using the injector.
*/
var ErrorAddingDeclarationLocationStack = function(e, errorForStack) {
this.message = e.message;
this.name = e.name;
if (e.line) this.line = e.line;
if (e.sourceId) this.sourceId = e.sourceId;
if (e.stack && errorForStack)
this.stack = e.stack + '\n' + errorForStack.stack;
if (e.stackArray) this.stackArray = e.stackArray;
};
ErrorAddingDeclarationLocationStack.prototype.toString = Error.prototype.toString;
window.inject = angular.mock.inject = function() {
var blockFns = Array.prototype.slice.call(arguments, 0);
var errorForStack = new Error('Declaration Location');
return isSpecRunning() ? workFn() : workFn;
/////////////////////
function workFn() {
var modules = currentSpec.$modules || [];
modules.unshift('ngMock');
modules.unshift('ng');
var injector = currentSpec.$injector;
if (!injector) {
injector = currentSpec.$injector = angular.injector(modules);
}
for(var i = 0, ii = blockFns.length; i < ii; i++) {
try {
/* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */
injector.invoke(blockFns[i] || angular.noop, this);
/* jshint +W040 */
} catch (e) {
if (e.stack && errorForStack) {
throw new ErrorAddingDeclarationLocationStack(e, errorForStack);
}
throw e;
} finally {
errorForStack = null;
}
}
}
};
}
})(window, window.angular);
{
"name": "angular",
"version": "1.2.8",
"main": "./angular.js",
"dependencies": {
}
}
// AngularFire is an officially supported AngularJS binding for Firebase.
// The bindings let you associate a Firebase URL with a model (or set of
// models), and they will be transparently kept in sync across all clients
// currently using your app. The 2-way data binding offered by AngularJS works
// as normal, except that the changes are also sent to all other clients
// instead of just a server.
//
// AngularFire 0.5.0
// http://angularfire.com
// License: MIT
"use strict";
var AngularFire, AngularFireAuth;
// Define the `firebase` module under which all AngularFire services will live.
angular.module("firebase", []).value("Firebase", Firebase);
// Define the `$firebase` service that provides synchronization methods.
angular.module("firebase").factory("$firebase", ["$q", "$parse", "$timeout",
function($q, $parse, $timeout) {
// The factory returns an object containing the value of the data at
// the Firebase location provided, as well as several methods. It
// takes a single argument:
//
// * `ref`: A Firebase reference. Queries or limits may be applied.
return function(ref) {
var af = new AngularFire($q, $parse, $timeout, ref);
return af.construct();
};
}
]);
// Define the `orderByPriority` filter that sorts objects returned by
// $firebase in the order of priority. Priority is defined by Firebase,
// for more info see: https://www.firebase.com/docs/ordered-data.html
angular.module("firebase").filter("orderByPriority", function() {
return function(input) {
if (!input.$getIndex || typeof input.$getIndex != "function") {
// If input is an object, map it to an array for the time being.
var type = Object.prototype.toString.call(input);
if (typeof input == "object" && type == "[object Object]") {
var ret = [];
for (var prop in input) {
if (input.hasOwnProperty(prop)) {
ret.push(input[prop]);
}
}
return ret;
}
return input;
}
var sorted = [];
var index = input.$getIndex();
if (index.length <= 0) {
return input;
}
for (var i = 0; i < index.length; i++) {
var val = input[index[i]];
if (val) {
val.$id = index[i];
sorted.push(val);
}
}
return sorted;
};
});
// The `AngularFire` object that implements synchronization.
AngularFire = function($q, $parse, $timeout, ref) {
this._q = $q;
this._bound = false;
this._loaded = false;
this._parse = $parse;
this._timeout = $timeout;
this._index = [];
this._onChange = [];
this._onLoaded = [];
if (typeof ref == "string") {
throw new Error("Please provide a Firebase reference instead " +
"of a URL, eg: new Firebase(url)");
}
this._fRef = ref;
};
AngularFire.prototype = {
// This function is called by the factory to create a new explicit sync
// point between a particular model and a Firebase location.
construct: function() {
var self = this;
var object = {};
// Establish a 3-way data binding (implicit sync) with the specified
// Firebase location and a model on $scope. To be used from a controller
// to automatically synchronize *all* local changes. It take two arguments:
//
// * `$scope`: The scope with which the bound model is associated.
// * `name` : The name of the model.
//
// This function also returns a promise, which when resolve will be
// provided an `unbind` method, a function which you can call to stop
// watching the local model for changes.
object.$bind = function(scope, name) {
return self._bind(scope, name);
};
// Add an object to the remote data. Adding an object is the
// equivalent of calling `push()` on a Firebase reference. It takes
// up to two arguments:
//
// * `item`: The object or primitive to add.
// * `cb` : An optional callback function to be invoked when the
// item is added to the Firebase server. It will be called
// with an Error object if one occurred, null otherwise.
//
// This function returns a Firebase reference to the newly added object
// or primitive. The key name can be extracted using `ref.name()`.
object.$add = function(item, cb) {
var ref;
if (typeof item == "object") {
ref = self._fRef.ref().push(self._parseObject(item), cb);
} else {
ref = self._fRef.ref().push(item, cb);
}
return ref;
};
// Save the current state of the object (or a child) to the remote.
// Takes a single optional argument:
//
// * `key`: Specify a child key to save the data for. If no key is
// specified, the entire object's current state will be saved.
object.$save = function(key) {
if (key) {
self._fRef.ref().child(key).set(self._parseObject(self._object[key]));
} else {
self._fRef.ref().set(self._parseObject(self._object));
}
};
// Set the current state of the object to the specified value. Calling
// this is the equivalent of calling `set()` on a Firebase reference.
object.$set = function(newValue) {
self._fRef.ref().set(newValue);
};
// Remove this object from the remote data. Calling this is the equivalent
// of calling `remove()` on a Firebase reference. This function takes a
// single optional argument:
//
// * `key`: Specify a child key to remove. If no key is specified, the
// entire object will be removed from the remote data store.
object.$remove = function(key) {
if (key) {
self._fRef.ref().child(key).remove();
} else {
self._fRef.ref().remove();
}
};
// Get an AngularFire wrapper for a named child.
object.$child = function(key) {
var af = new AngularFire(
self._q, self._parse, self._timeout, self._fRef.ref().child(key)
);
return af.construct();
};
// Attach an event handler for when the object is changed. You can attach
// handlers for the following events:
//
// - "change": The provided function will be called whenever the local
// object is modified because the remote data was updated.
// - "loaded": This function will be called *once*, when the initial
// data has been loaded. 'object' will be an empty object ({})
// until this function is called.
object.$on = function(type, callback) {
switch (type) {
case "change":
self._onChange.push(callback);
break;
case "loaded":
self._onLoaded.push(callback);
break;
default:
throw new Error("Invalid event type " + type + " specified");
}
};
// Return the current index, which is a list of key names in an array,
// ordered by their Firebase priority.
object.$getIndex = function() {
return angular.copy(self._index);
};
self._object = object;
self._getInitialValue();
return self._object;
},
// This function is responsible for fetching the initial data for the
// given reference. If the data returned from the server is an object or
// array, we'll attach appropriate child event handlers. If the value is
// a primitive, we'll continue to watch for value changes.
_getInitialValue: function() {
var self = this;
var gotInitialValue = function(snapshot) {
var value = snapshot.val();
if (value === null) {
// NULLs are handled specially. If there's a 3-way data binding
// on a local primitive, then update that, otherwise switch to object
// binding using child events.
if (self._bound) {
var local = self._parseObject(self._parse(self._name)(self._scope));
switch (typeof local) {
// Primitive defaults.
case "string":
case "undefined":
value = "";
break;
case "number":
value = 0;
break;
case "boolean":
value = false;
break;
}
}
}
switch (typeof value) {
// For primitive values, simply update the object returned.
case "string":
case "number":
case "boolean":
self._updatePrimitive(value);
break;
// For arrays and objects, switch to child methods.
case "object":
self._getChildValues();
self._fRef.off("value", gotInitialValue);
break;
default:
throw new Error("Unexpected type from remote data " + typeof value);
}
// Call handlers for the "loaded" event.
self._loaded = true;
self._broadcastEvent("loaded", value);
};
self._fRef.on("value", gotInitialValue);
},
// This function attaches child events for object and array types.
_getChildValues: function() {
var self = this;
// Store the priority of the current property as "$priority". Changing
// the value of this property will also update the priority of the
// object (see _parseObject).
function _processSnapshot(snapshot, prevChild) {
var key = snapshot.name();
var val = snapshot.val();
// If the item already exists in the index, remove it first.
var curIdx = self._index.indexOf(key);
if (curIdx !== -1) {
self._index.splice(curIdx, 1);
}
// Update index. This is used by $getIndex and orderByPriority.
if (prevChild) {
var prevIdx = self._index.indexOf(prevChild);
self._index.splice(prevIdx + 1, 0, key);
} else {
self._index.unshift(key);
}
// Update local model with priority field, if needed.
if (snapshot.getPriority() !== null) {
val.$priority = snapshot.getPriority();
}
self._updateModel(key, val);
}
self._fRef.on("child_added", _processSnapshot);
self._fRef.on("child_moved", _processSnapshot);
self._fRef.on("child_changed", _processSnapshot);
self._fRef.on("child_removed", function(snapshot) {
// Remove from index.
var key = snapshot.name();
var idx = self._index.indexOf(key);
self._index.splice(idx, 1);
// Remove from local model.
self._updateModel(key, null);
});
},
// Called whenever there is a remote change. Applies them to the local
// model for both explicit and implicit sync modes.
_updateModel: function(key, value) {
var self = this;
self._timeout(function() {
if (value == null) {
delete self._object[key];
} else {
self._object[key] = value;
}
// Call change handlers.
self._broadcastEvent("change");
// If there is an implicit binding, also update the local model.
if (!self._bound) {
return;
}
var current = self._object;
var local = self._parse(self._name)(self._scope);
// If remote value matches local value, don't do anything, otherwise
// apply the change.
if (!angular.equals(current, local)) {
self._parse(self._name).assign(self._scope, angular.copy(current));
}
});
},
// Called whenever there is a remote change for a primitive value.
_updatePrimitive: function(value) {
var self = this;
self._timeout(function() {
// Primitive values are represented as a special object {$value: value}.
// Only update if the remote value is different from the local value.
if (!self._object.$value || !angular.equals(self._object.$value, value)) {
self._object.$value = value;
}
// Call change handlers.
self._broadcastEvent("change");
// If there's an implicit binding, simply update the local scope model.
if (self._bound) {
var local = self._parseObject(self._parse(self._name)(self._scope));
if (!angular.equals(local, value)) {
self._parse(self._name).assign(self._scope, value);
}
}
});
},
// If event handlers for a specified event were attached, call them.
_broadcastEvent: function(evt, param) {
var cbs;
switch (evt) {
case "change":
cbs = this._onChange;
break;
case "loaded":
cbs = this._onLoaded;
break;
default:
cbs = [];
break;
}
if (cbs.length > 0) {
for (var i = 0; i < cbs.length; i++) {
if (typeof cbs[i] == "function") {
cbs[i](param);
}
}
}
},
// This function creates a 3-way binding between the provided scope model
// and Firebase. All changes made to the local model are saved to Firebase
// and changes to the remote data automatically appear on the local model.
_bind: function(scope, name) {
var self = this;
var deferred = self._q.defer();
// _updateModel or _updatePrimitive will take care of updating the local
// model if _bound is set to true.
self._name = name;
self._bound = true;
self._scope = scope;
// If the local model is an object, call an update to set local values.
var local = self._parse(name)(scope);
if (local !== undefined && typeof local == "object") {
self._fRef.update(self._parseObject(local));
}
// We're responsible for setting up scope.$watch to reflect local changes
// on the Firebase data.
var unbind = scope.$watch(name, function() {
// If the new local value matches the current remote value, we don't
// trigger a remote update.
var local = self._parseObject(self._parse(name)(scope));
if (self._object.$value && angular.equals(local, self._object.$value)) {
return;
} else if (angular.equals(local, self._object)) {
return;
}
// If the local model is undefined or the remote data hasn't been
// loaded yet, don't update.
if (local === undefined || !self._loaded) {
return;
}
// Use update if limits are in effect, set if not.
if (self._fRef.set) {
self._fRef.set(local);
} else {
self._fRef.ref().update(local);
}
}, true);
// When the scope is destroyed, unbind automatically.
scope.$on("$destroy", function() {
unbind();
});
// Once we receive the initial value, resolve the promise.
self._fRef.once("value", function() {
deferred.resolve(unbind);
});
return deferred.promise;
},
// Parse a local model, removing all properties beginning with "$" and
// converting $priority to ".priority".
_parseObject: function(obj) {
function _findReplacePriority(item) {
for (var prop in item) {
if (item.hasOwnProperty(prop)) {
if (prop == "$priority") {
item[".priority"] = item.$priority;
delete item.$priority;
} else if (typeof item[prop] == "object") {
_findReplacePriority(item[prop]);
}
}
}
return item;
}
// We use toJson/fromJson to remove $$hashKey and others. Can be replaced
// by angular.copy, but only for later versions of AngularJS.
var newObj = _findReplacePriority(angular.copy(obj));
return angular.fromJson(angular.toJson(newObj));
}
};
// Defines the `$firebaseAuth` service that provides authentication support
// for AngularFire.
angular.module("firebase").factory("$firebaseAuth", [
"$q", "$timeout", "$injector", "$rootScope", "$location",
function($q, $t, $i, $rs, $l) {
// The factory returns an object containing the authentication state
// of the current user. This service takes 2 arguments:
//
// * `ref` : A Firebase reference.
// * `options`: An object that may contain the following options:
//
// * `path` : The path to which the user will be redirected if the
// authRequired property was set to true in the
// $routeProvider, and the user isn't logged in.
// * `simple` : $firebaseAuth requires inclusion of the
// firebase-simple-login.js file by default. If this
// value is set to false, this requirement is waived,
// but only custom login functionality will be enabled.
// * `callback`: A function that will be called when there is a change
// in authentication state.
//
// The returned object has the following properties:
//
// * `user`: Set to "null" if the user is currently logged out. This value
// will be changed to an object when the user successfully logs in. This
// object will contain details of the logged in user. The exact
// properties will vary based on the method used to login, but will at
// a minimum contain the `id` and `provider` properties.
//
// The returned object will also have the following methods available:
// $login(), $logout() and $createUser().
return function(ref, options) {
var auth = new AngularFireAuth($q, $t, $i, $rs, $l, ref, options);
return auth.construct();
};
}
]);
AngularFireAuth = function($q, $t, $i, $rs, $l, ref, options) {
this._q = $q;
this._timeout = $t;
this._injector = $i;
this._location = $l;
this._rootScope = $rs;
// Check if '$route' is present, use if available.
this._route = null;
if (this._injector.has("$route")) {
this._route = this._injector.get("$route");
}
// Setup options and callback.
this._cb = function(){};
this._options = options || {};
if (this._options.callback && typeof this._options.callback === "function") {
this._cb = options.callback;
}
this._deferred = null;
this._redirectTo = null;
this._authenticated = false;
if (typeof ref == "string") {
throw new Error("Please provide a Firebase reference instead " +
"of a URL, eg: new Firebase(url)");
}
this._fRef = ref;
};
AngularFireAuth.prototype = {
construct: function() {
var self = this;
var object = {
user: null,
$login: self.login.bind(self),
$logout: self.logout.bind(self),
$createUser: self.createUser.bind(self)
};
if (self._options.path && self._route !== null) {
// Check if the current page requires authentication.
if (self._route.current) {
self._authRequiredRedirect(self._route.current, self._options.path);
}
// Set up a handler for all future route changes, so we can check
// if authentication is required.
self._rootScope.$on("$routeChangeStart", function(e, next) {
self._authRequiredRedirect(next, self._options.path);
});
}
// If Simple Login is disabled, simply return.
self._object = object;
if (self._options.simple === false) {
return;
}
// Initialize Simple Login.
if (!window.FirebaseSimpleLogin) {
var err = new Error("FirebaseSimpleLogin undefined, " +
"did you include firebase-simple-login.js?");
self._rootScope.$broadcast("$firebaseAuth:error", err);
return;
}
var client = new FirebaseSimpleLogin(self._fRef, function(err, user) {
self._cb(err, user);
if (err) {
if (self._deferred) {
self._deferred.reject(err);
self._deferred = null;
}
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else if (user) {
if (self._deferred) {
self._deferred.resolve(user);
self._deferred = null;
}
self._loggedIn(user);
} else {
self._loggedOut();
}
});
self._authClient = client;
return self._object;
},
// The login method takes a provider (for Simple Login) or a token
// (for Custom Login) and authenticates the Firebase URL with which
// the service was initialized. This method returns a promise, which will
// be resolved when the login succeeds (and rejected when an error occurs).
login: function(tokenOrProvider, options) {
var self = this;
var deferred = self._q.defer();
switch (tokenOrProvider) {
case "github":
case "persona":
case "twitter":
case "facebook":
case "password":
case "anonymous":
if (!self._authClient) {
var err = new Error("Simple Login not initialized");
deferred.reject(err);
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else {
self._deferred = deferred;
self._authClient.login(tokenOrProvider, options);
}
break;
// A token was provided, so initialize custom login.
default:
try {
// Extract claims and update user auth state to include them.
var claims = self._deconstructJWT(tokenOrProvider);
self._fRef.auth(tokenOrProvider, function(err) {
if (err) {
deferred.reject(err);
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else {
self._deferred = deferred;
self._loggedIn(claims);
}
});
} catch(e) {
deferred.reject(e);
self._rootScope.$broadcast("$firebaseAuth:error", e);
}
}
return deferred.promise;
},
// Unauthenticate the Firebase reference.
logout: function() {
if (this._authClient) {
this._authClient.logout();
} else {
this._fRef.unauth();
this._loggedOut();
}
},
// Creates a user for Firebase Simple Login.
// Function 'cb' receives an error as the first argument and a
// Simple Login user object as the second argument. Pass noLogin=true
// if you don't want the newly created user to also be logged in.
createUser: function(email, password, cb, noLogin) {
var self = this;
self._authClient.createUser(email, password, function(err, user) {
try {
if (err) {
self._rootScope.$broadcast("$firebaseAuth:error", err);
} else {
if (!noLogin) {
self.login("password", {email: email, password: password});
}
}
} catch(e) {
self._rootScope.$broadcast("$firebaseAuth:error", e);
}
if (cb) {
self._timeout(function(){
cb(err, user);
});
}
});
},
// Changes the password for a Firebase Simple Login user.
// Take an email, old password and new password as three mandatory arguments.
// An optional callback may be specified to be notified when the password
// has been changed successfully.
changePassword: function(email, old, np, cb) {
var self = this;
self._authClient.changePassword(email, old, np, function(err, user) {
if (err) {
self._rootScope.$broadcast("$firebaseAuth:error", err);
}
if (cb) {
self._timeout(function() {
cb(err, user);
});
}
});
},
// Common function to trigger a login event on the root scope.
_loggedIn: function(user) {
var self = this;
self._timeout(function() {
self._object.user = user;
self._authenticated = true;
self._rootScope.$broadcast("$firebaseAuth:login", user);
if (self._redirectTo) {
self._location.replace();
self._location.path(self._redirectTo);
self._redirectTo = null;
}
});
},
// Common function to trigger a logout event on the root scope.
_loggedOut: function() {
var self = this;
self._timeout(function() {
self._object.user = null;
self._authenticated = false;
self._rootScope.$broadcast("$firebaseAuth:logout");
});
},
// A function to check whether the current path requires authentication,
// and if so, whether a redirect to a login page is needed.
_authRequiredRedirect: function(route, path) {
if (route.authRequired && !this._authenticated){
if (route.pathTo === undefined) {
this._redirectTo = this._location.path();
} else {
this._redirectTo = route.pathTo === path ? "/" : route.pathTo;
}
this._location.replace();
this._location.path(path);
}
},
// Helper function to decode Base64 (polyfill for window.btoa on IE).
// From: https://github.com/mshang/base64-js/blob/master/base64.js
_decodeBase64: function(str) {
var char_set =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var output = ""; // final output
var buf = ""; // binary buffer
var bits = 8;
for (var i = 0; i < str.length; ++i) {
if (str[i] == "=") {
break;
}
var c_num = char_set.indexOf(str.charAt(i));
if (c_num == -1) {
throw new Error("Not base64.");
}
var c_bin = c_num.toString(2);
while (c_bin.length < 6) {
c_bin = "0" + c_bin;
}
buf += c_bin;
while (buf.length >= bits) {
var octet = buf.slice(0, bits);
buf = buf.slice(bits);
output += String.fromCharCode(parseInt(octet, 2));
}
}
return output;
},
// Helper function to extract claims from a JWT. Does *not* verify the
// validity of the token.
_deconstructJWT: function(token) {
var segments = token.split(".");
if (!segments instanceof Array || segments.length !== 3) {
throw new Error("Invalid JWT");
}
var decoded = "";
var claims = segments[1];
if (window.atob) {
decoded = window.atob(claims);
} else {
decoded = this._decodeBase64(claims);
}
return JSON.parse(decodeURIComponent(escape(decoded)));
}
};
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Firebase &amp; AngularJS • TodoMVC</title> <title>Firebase &amp; AngularJS • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css"> <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<style>[ng-cloak] { display: none; }</style> <style>[ng-cloak] { display: none; }</style>
</head> </head>
<body> <body>
...@@ -20,12 +21,12 @@ ...@@ -20,12 +21,12 @@
<ul id="todo-list"> <ul id="todo-list">
<li ng-repeat="(id, todo) in todos | todoFilter" ng-class="{completed: todo.completed, editing: todo == editedTodo}"> <li ng-repeat="(id, todo) in todos | todoFilter" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
<div class="view"> <div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="todos.$save(id)"> <input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="todos.$save(todo)">
<label ng-dblclick="editTodo(id)">{{todo.title}}</label> <label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(id)"></button> <button class="destroy" ng-click="removeTodo(todo)"></button>
</div> </div>
<form ng-submit="doneEditing(id)"> <form ng-submit="doneEditing(todo)">
<input class="edit" ng-model="todo.title" todo-escape="revertEditing(id)" todo-blur="doneEditing(id)" todo-focus="todo == editedTodo"> <input class="edit" ng-model="todo.title" todo-escape="revertEditing(todo)" todo-blur="doneEditing(todo)" todo-focus="todo == editedTodo">
</form> </form>
</li> </li>
</ul> </ul>
...@@ -53,15 +54,16 @@ ...@@ -53,15 +54,16 @@
<p>Credits: <p>Credits:
<a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>, <a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
<a href="http://ericbidelman.com">Eric Bidelman</a>, <a href="http://ericbidelman.com">Eric Bidelman</a>,
<a href="http://twitter.com/_davideast">David East</a>,
<a href="http://jacobmumm.com">Jacob Mumm</a> and <a href="http://jacobmumm.com">Jacob Mumm</a> and
<a href="http://igorminar.com">Igor Minar</a> <a href="http://igorminar.com">Igor Minar</a>
</p> </p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> </footer>
<script src="https://cdn.firebase.com/v0/firebase.js"></script> <script src="//cdn.firebase.com/js/client/2.2.2/firebase.js"></script>
<script src="bower_components/todomvc-common/base.js"></script> <script src="node_modules/todomvc-common/base.js"></script>
<script src="bower_components/angular/angular.js"></script> <script src="node_modules/angular/angular.js"></script>
<script src="bower_components/angularfire/angularfire.js"></script> <script src="node_modules/angularfire/dist/angularfire.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script src="js/controllers/todoCtrl.js"></script> <script src="js/controllers/todoCtrl.js"></script>
<script src="js/directives/todoFocus.js"></script> <script src="js/directives/todoFocus.js"></script>
......
...@@ -3,18 +3,22 @@ ...@@ -3,18 +3,22 @@
/** /**
* The main controller for the app. The controller: * The main controller for the app. The controller:
* - retrieves and persists the model via the $firebase service * - retrieves and persists the model via the $firebaseArray service
* - exposes the model to the template and provides event handlers * - exposes the model to the template and provides event handlers
*/ */
todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebase) { todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebaseArray) {
var url = 'https://todomvc-angular.firebaseio.com/'; var url = 'https://todomvc-angular.firebaseio.com/todos';
var fireRef = new Firebase(url); var fireRef = new Firebase(url);
// Bind the todos to the firebase provider.
$scope.todos = $firebaseArray(fireRef);
$scope.newTodo = '';
$scope.editedTodo = null;
$scope.$watch('todos', function () { $scope.$watch('todos', function () {
var total = 0; var total = 0;
var remaining = 0; var remaining = 0;
$scope.todos.$getIndex().forEach(function (index) { $scope.todos.forEach(function (todo) {
var todo = $scope.todos[index];
// Skip invalid entries so they don't break the entire app. // Skip invalid entries so they don't break the entire app.
if (!todo || !todo.title) { if (!todo || !todo.title) {
return; return;
...@@ -43,59 +47,47 @@ todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebase) { ...@@ -43,59 +47,47 @@ todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, $firebase) {
$scope.newTodo = ''; $scope.newTodo = '';
}; };
$scope.editTodo = function (id) { $scope.editTodo = function (todo) {
$scope.editedTodo = $scope.todos[id]; $scope.editedTodo = todo;
$scope.originalTodo = angular.extend({}, $scope.editedTodo); $scope.originalTodo = angular.extend({}, $scope.editedTodo);
}; };
$scope.doneEditing = function (id) { $scope.doneEditing = function (todo) {
$scope.editedTodo = null; $scope.editedTodo = null;
var title = $scope.todos[id].title.trim(); var title = todo.title.trim();
if (title) { if (title) {
$scope.todos.$save(id); $scope.todos.$save(todo);
} else { } else {
$scope.removeTodo(id); $scope.removeTodo(todo);
} }
}; };
$scope.revertEditing = function (id) { $scope.revertEditing = function (todo) {
$scope.todos[id] = $scope.originalTodo; todo.title = $scope.originalTodo.title;
$scope.doneEditing(id); $scope.doneEditing(todo);
};
$scope.removeTodo = function (id) {
$scope.todos.$remove(id);
}; };
$scope.toggleCompleted = function (id) { $scope.removeTodo = function (todo) {
var todo = $scope.todos[id]; $scope.todos.$remove(todo);
todo.completed = !todo.completed;
$scope.todos.$save(id);
}; };
$scope.clearCompletedTodos = function () { $scope.clearCompletedTodos = function () {
angular.forEach($scope.todos.$getIndex(), function (index) { $scope.todos.forEach(function(todo) {
if ($scope.todos[index].completed) { if(todo.completed) {
$scope.todos.$remove(index); $scope.removeTodo(todo);
} }
}); });
}; };
$scope.markAll = function (allCompleted) { $scope.markAll = function (allCompleted) {
angular.forEach($scope.todos.$getIndex(), function (index) { $scope.todos.forEach(function(todo) {
$scope.todos[index].completed = !allCompleted; todo.completed = allCompleted;
$scope.todos.$save(todo);
}); });
$scope.todos.$save();
}; };
$scope.newTodo = '';
$scope.editedTodo = null;
if ($location.path() === '') { if ($location.path() === '') {
$location.path('/'); $location.path('/');
} }
$scope.location = $location; $scope.location = $location;
// Bind the todos to the firebase provider.
$scope.todos = $firebase(fireRef);
}); });
/*!
* AngularFire is the officially supported AngularJS binding for Firebase. Firebase
* is a full backend so you don't need servers to build your Angular app. AngularFire
* provides you with the $firebase service which allows you to easily keep your $scope
* variables in sync with your Firebase backend.
*
* AngularFire 1.0.0
* https://github.com/firebase/angularfire/
* Date: 03/04/2015
* License: MIT
*/
(function(exports) {
"use strict";
// Define the `firebase` module under which all AngularFire
// services will live.
angular.module("firebase", [])
//todo use $window
.value("Firebase", exports.Firebase)
// used in conjunction with firebaseUtils.debounce function, this is the
// amount of time we will wait for additional records before triggering
// Angular's digest scope to dirty check and re-render DOM elements. A
// larger number here significantly improves performance when working with
// big data sets that are frequently changing in the DOM, but delays the
// speed at which each record is rendered in real-time. A number less than
// 100ms will usually be optimal.
.value('firebaseBatchDelay', 50 /* milliseconds */);
})(window);
(function() {
'use strict';
/**
* Creates and maintains a synchronized list of data. This is a pseudo-read-only array. One should
* not call splice(), push(), pop(), et al directly on this array, but should instead use the
* $remove and $add methods.
*
* It is acceptable to .sort() this array, but it is important to use this in conjunction with
* $watch(), so that it will be re-sorted any time the server data changes. Examples of this are
* included in the $watch documentation.
*
* Internally, the $firebase object depends on this class to provide several $$ (i.e. protected)
* methods, which it invokes to notify the array whenever a change has been made at the server:
* $$added - called whenever a child_added event occurs
* $$updated - called whenever a child_changed event occurs
* $$moved - called whenever a child_moved event occurs
* $$removed - called whenever a child_removed event occurs
* $$error - called when listeners are canceled due to a security error
* $$process - called immediately after $$added/$$updated/$$moved/$$removed
* (assuming that these methods do not abort by returning false or null)
* to splice/manipulate the array and invoke $$notify
*
* Additionally, these methods may be of interest to devs extending this class:
* $$notify - triggers notifications to any $watch listeners, called by $$process
* $$getKey - determines how to look up a record's key (returns $id by default)
*
* Instead of directly modifying this class, one should generally use the $extend
* method to add or change how methods behave. $extend modifies the prototype of
* the array class by returning a clone of $firebaseArray.
*
* <pre><code>
* var ExtendedArray = $firebaseArray.$extend({
* // add a new method to the prototype
* foo: function() { return 'bar'; },
*
* // change how records are created
* $$added: function(snap, prevChild) {
* return new Widget(snap, prevChild);
* },
*
* // change how records are updated
* $$updated: function(snap) {
* return this.$getRecord(snap.key()).update(snap);
* }
* });
*
* var list = new ExtendedArray(ref);
* </code></pre>
*/
angular.module('firebase').factory('$firebaseArray', ["$log", "$firebaseUtils",
function($log, $firebaseUtils) {
/**
* This constructor should probably never be called manually. It is used internally by
* <code>$firebase.$asArray()</code>.
*
* @param {Firebase} ref
* @returns {Array}
* @constructor
*/
function FirebaseArray(ref) {
if( !(this instanceof FirebaseArray) ) {
return new FirebaseArray(ref);
}
var self = this;
this._observers = [];
this.$list = [];
this._ref = ref;
this._sync = new ArraySyncManager(this);
$firebaseUtils.assertValidRef(ref, 'Must pass a valid Firebase reference ' +
'to $firebaseArray (not a string or URL)');
// indexCache is a weak hashmap (a lazy list) of keys to array indices,
// items are not guaranteed to stay up to date in this list (since the data
// array can be manually edited without calling the $ methods) and it should
// always be used with skepticism regarding whether it is accurate
// (see $indexFor() below for proper usage)
this._indexCache = {};
// Array.isArray will not work on objects which extend the Array class.
// So instead of extending the Array class, we just return an actual array.
// However, it's still possible to extend FirebaseArray and have the public methods
// appear on the array object. We do this by iterating the prototype and binding
// any method that is not prefixed with an underscore onto the final array.
$firebaseUtils.getPublicMethods(self, function(fn, key) {
self.$list[key] = fn.bind(self);
});
this._sync.init(this.$list);
return this.$list;
}
FirebaseArray.prototype = {
/**
* Create a new record with a unique ID and add it to the end of the array.
* This should be used instead of Array.prototype.push, since those changes will not be
* synchronized with the server.
*
* Any value, including a primitive, can be added in this way. Note that when the record
* is created, the primitive value would be stored in $value (records are always objects
* by default).
*
* Returns a future which is resolved when the data has successfully saved to the server.
* The resolve callback will be passed a Firebase ref representing the new data element.
*
* @param data
* @returns a promise resolved after data is added
*/
$add: function(data) {
this._assertNotDestroyed('$add');
var def = $firebaseUtils.defer();
var ref = this.$ref().ref().push();
ref.set($firebaseUtils.toJSON(data), $firebaseUtils.makeNodeResolver(def));
return def.promise.then(function() {
return ref;
});
},
/**
* Pass either an item in the array or the index of an item and it will be saved back
* to Firebase. While the array is read-only and its structure should not be changed,
* it is okay to modify properties on the objects it contains and then save those back
* individually.
*
* Returns a future which is resolved when the data has successfully saved to the server.
* The resolve callback will be passed a Firebase ref representing the saved element.
* If passed an invalid index or an object which is not a record in this array,
* the promise will be rejected.
*
* @param {int|object} indexOrItem
* @returns a promise resolved after data is saved
*/
$save: function(indexOrItem) {
this._assertNotDestroyed('$save');
var self = this;
var item = self._resolveItem(indexOrItem);
var key = self.$keyAt(item);
if( key !== null ) {
var ref = self.$ref().ref().child(key);
var data = $firebaseUtils.toJSON(item);
return $firebaseUtils.doSet(ref, data).then(function() {
self.$$notify('child_changed', key);
return ref;
});
}
else {
return $firebaseUtils.reject('Invalid record; could determine key for '+indexOrItem);
}
},
/**
* Pass either an existing item in this array or the index of that item and it will
* be removed both locally and in Firebase. This should be used in place of
* Array.prototype.splice for removing items out of the array, as calling splice
* will not update the value on the server.
*
* Returns a future which is resolved when the data has successfully removed from the
* server. The resolve callback will be passed a Firebase ref representing the deleted
* element. If passed an invalid index or an object which is not a record in this array,
* the promise will be rejected.
*
* @param {int|object} indexOrItem
* @returns a promise which resolves after data is removed
*/
$remove: function(indexOrItem) {
this._assertNotDestroyed('$remove');
var key = this.$keyAt(indexOrItem);
if( key !== null ) {
var ref = this.$ref().ref().child(key);
return $firebaseUtils.doRemove(ref).then(function() {
return ref;
});
}
else {
return $firebaseUtils.reject('Invalid record; could not determine key for '+indexOrItem);
}
},
/**
* Given an item in this array or the index of an item in the array, this returns the
* Firebase key (record.$id) for that record. If passed an invalid key or an item which
* does not exist in this array, it will return null.
*
* @param {int|object} indexOrItem
* @returns {null|string}
*/
$keyAt: function(indexOrItem) {
var item = this._resolveItem(indexOrItem);
return this.$$getKey(item);
},
/**
* The inverse of $keyAt, this method takes a Firebase key (record.$id) and returns the
* index in the array where that record is stored. If the record is not in the array,
* this method returns -1.
*
* @param {String} key
* @returns {int} -1 if not found
*/
$indexFor: function(key) {
var self = this;
var cache = self._indexCache;
// evaluate whether our key is cached and, if so, whether it is up to date
if( !cache.hasOwnProperty(key) || self.$keyAt(cache[key]) !== key ) {
// update the hashmap
var pos = self.$list.findIndex(function(rec) { return self.$$getKey(rec) === key; });
if( pos !== -1 ) {
cache[key] = pos;
}
}
return cache.hasOwnProperty(key)? cache[key] : -1;
},
/**
* The loaded method is invoked after the initial batch of data arrives from the server.
* When this resolves, all data which existed prior to calling $asArray() is now cached
* locally in the array.
*
* As a shortcut is also possible to pass resolve/reject methods directly into this
* method just as they would be passed to .then()
*
* @param {Function} [resolve]
* @param {Function} [reject]
* @returns a promise
*/
$loaded: function(resolve, reject) {
var promise = this._sync.ready();
if( arguments.length ) {
// allow this method to be called just like .then
// by passing any arguments on to .then
promise = promise.then.call(promise, resolve, reject);
}
return promise;
},
/**
* @returns {Firebase} the original Firebase ref used to create this object.
*/
$ref: function() { return this._ref; },
/**
* Listeners passed into this method are notified whenever a new change (add, updated,
* move, remove) is received from the server. Each invocation is sent an object
* containing <code>{ type: 'added|updated|moved|removed', key: 'key_of_item_affected'}</code>
*
* Additionally, added and moved events receive a prevChild parameter, containing the
* key of the item before this one in the array.
*
* This method returns a function which can be invoked to stop observing events.
*
* @param {Function} cb
* @param {Object} [context]
* @returns {Function} used to stop observing
*/
$watch: function(cb, context) {
var list = this._observers;
list.push([cb, context]);
// an off function for cancelling the listener
return function() {
var i = list.findIndex(function(parts) {
return parts[0] === cb && parts[1] === context;
});
if( i > -1 ) {
list.splice(i, 1);
}
};
},
/**
* Informs $firebase to stop sending events and clears memory being used
* by this array (delete's its local content).
*/
$destroy: function(err) {
if( !this._isDestroyed ) {
this._isDestroyed = true;
this._sync.destroy(err);
this.$list.length = 0;
$log.debug('destroy called for FirebaseArray: '+this.$ref().ref().toString());
}
},
/**
* Returns the record for a given Firebase key (record.$id). If the record is not found
* then returns null.
*
* @param {string} key
* @returns {Object|null} a record in this array
*/
$getRecord: function(key) {
var i = this.$indexFor(key);
return i > -1? this.$list[i] : null;
},
/**
* Called to inform the array when a new item has been added at the server.
* This method should return the record (an object) that will be passed into $$process
* along with the add event. Alternately, the record will be skipped if this method returns
* a falsey value.
*
* @param {object} snap a Firebase snapshot
* @param {string} prevChild
* @return {object} the record to be inserted into the array
* @protected
*/
$$added: function(snap/*, prevChild*/) {
// check to make sure record does not exist
var i = this.$indexFor($firebaseUtils.getKey(snap));
if( i === -1 ) {
// parse data and create record
var rec = snap.val();
if( !angular.isObject(rec) ) {
rec = { $value: rec };
}
rec.$id = $firebaseUtils.getKey(snap);
rec.$priority = snap.getPriority();
$firebaseUtils.applyDefaults(rec, this.$$defaults);
return rec;
}
return false;
},
/**
* Called whenever an item is removed at the server.
* This method does not physically remove the objects, but instead
* returns a boolean indicating whether it should be removed (and
* taking any other desired actions before the remove completes).
*
* @param {object} snap a Firebase snapshot
* @return {boolean} true if item should be removed
* @protected
*/
$$removed: function(snap) {
return this.$indexFor($firebaseUtils.getKey(snap)) > -1;
},
/**
* Called whenever an item is changed at the server.
* This method should apply the changes, including changes to data
* and to $priority, and then return true if any changes were made.
*
* If this method returns false, then $$process will not be invoked,
* which means that $$notify will not take place and no $watch events
* will be triggered.
*
* @param {object} snap a Firebase snapshot
* @return {boolean} true if any data changed
* @protected
*/
$$updated: function(snap) {
var changed = false;
var rec = this.$getRecord($firebaseUtils.getKey(snap));
if( angular.isObject(rec) ) {
// apply changes to the record
changed = $firebaseUtils.updateRec(rec, snap);
$firebaseUtils.applyDefaults(rec, this.$$defaults);
}
return changed;
},
/**
* Called whenever an item changes order (moves) on the server.
* This method should set $priority to the updated value and return true if
* the record should actually be moved. It should not actually apply the move
* operation.
*
* If this method returns false, then the record will not be moved in the array
* and no $watch listeners will be notified. (When true, $$process is invoked
* which invokes $$notify)
*
* @param {object} snap a Firebase snapshot
* @param {string} prevChild
* @protected
*/
$$moved: function(snap/*, prevChild*/) {
var rec = this.$getRecord($firebaseUtils.getKey(snap));
if( angular.isObject(rec) ) {
rec.$priority = snap.getPriority();
return true;
}
return false;
},
/**
* Called whenever a security error or other problem causes the listeners to become
* invalid. This is generally an unrecoverable error.
*
* @param {Object} err which will have a `code` property and possibly a `message`
* @protected
*/
$$error: function(err) {
$log.error(err);
this.$destroy(err);
},
/**
* Returns ID for a given record
* @param {object} rec
* @returns {string||null}
* @protected
*/
$$getKey: function(rec) {
return angular.isObject(rec)? rec.$id : null;
},
/**
* Handles placement of recs in the array, sending notifications,
* and other internals. Called by the synchronization process
* after $$added, $$updated, $$moved, and $$removed return a truthy value.
*
* @param {string} event one of child_added, child_removed, child_moved, or child_changed
* @param {object} rec
* @param {string} [prevChild]
* @protected
*/
$$process: function(event, rec, prevChild) {
var key = this.$$getKey(rec);
var changed = false;
var curPos;
switch(event) {
case 'child_added':
curPos = this.$indexFor(key);
break;
case 'child_moved':
curPos = this.$indexFor(key);
this._spliceOut(key);
break;
case 'child_removed':
// remove record from the array
changed = this._spliceOut(key) !== null;
break;
case 'child_changed':
changed = true;
break;
default:
throw new Error('Invalid event type: ' + event);
}
if( angular.isDefined(curPos) ) {
// add it to the array
changed = this._addAfter(rec, prevChild) !== curPos;
}
if( changed ) {
// send notifications to anybody monitoring $watch
this.$$notify(event, key, prevChild);
}
return changed;
},
/**
* Used to trigger notifications for listeners registered using $watch. This method is
* typically invoked internally by the $$process method.
*
* @param {string} event
* @param {string} key
* @param {string} [prevChild]
* @protected
*/
$$notify: function(event, key, prevChild) {
var eventData = {event: event, key: key};
if( angular.isDefined(prevChild) ) {
eventData.prevChild = prevChild;
}
angular.forEach(this._observers, function(parts) {
parts[0].call(parts[1], eventData);
});
},
/**
* Used to insert a new record into the array at a specific position. If prevChild is
* null, is inserted first, if prevChild is not found, it is inserted last, otherwise,
* it goes immediately after prevChild.
*
* @param {object} rec
* @param {string|null} prevChild
* @private
*/
_addAfter: function(rec, prevChild) {
var i;
if( prevChild === null ) {
i = 0;
}
else {
i = this.$indexFor(prevChild)+1;
if( i === 0 ) { i = this.$list.length; }
}
this.$list.splice(i, 0, rec);
this._indexCache[this.$$getKey(rec)] = i;
return i;
},
/**
* Removes a record from the array by calling splice. If the item is found
* this method returns it. Otherwise, this method returns null.
*
* @param {string} key
* @returns {object|null}
* @private
*/
_spliceOut: function(key) {
var i = this.$indexFor(key);
if( i > -1 ) {
delete this._indexCache[key];
return this.$list.splice(i, 1)[0];
}
return null;
},
/**
* Resolves a variable which may contain an integer or an item that exists in this array.
* Returns the item or null if it does not exist.
*
* @param indexOrItem
* @returns {*}
* @private
*/
_resolveItem: function(indexOrItem) {
var list = this.$list;
if( angular.isNumber(indexOrItem) && indexOrItem >= 0 && list.length >= indexOrItem ) {
return list[indexOrItem];
}
else if( angular.isObject(indexOrItem) ) {
// it must be an item in this array; it's not sufficient for it just to have
// a $id or even a $id that is in the array, it must be an actual record
// the fastest way to determine this is to use $getRecord (to avoid iterating all recs)
// and compare the two
var key = this.$$getKey(indexOrItem);
var rec = this.$getRecord(key);
return rec === indexOrItem? rec : null;
}
return null;
},
/**
* Throws an error if $destroy has been called. Should be used for any function
* which tries to write data back to $firebase.
* @param {string} method
* @private
*/
_assertNotDestroyed: function(method) {
if( this._isDestroyed ) {
throw new Error('Cannot call ' + method + ' method on a destroyed $firebaseArray object');
}
}
};
/**
* This method allows FirebaseArray to be inherited by child classes. Methods passed into this
* function will be added onto the array's prototype. They can override existing methods as
* well.
*
* In addition to passing additional methods, it is also possible to pass in a class function.
* The prototype on that class function will be preserved, and it will inherit from
* FirebaseArray. It's also possible to do both, passing a class to inherit and additional
* methods to add onto the prototype.
*
* <pre><code>
* var ExtendedArray = $firebaseArray.$extend({
* // add a method onto the prototype that sums all items in the array
* getSum: function() {
* var ct = 0;
* angular.forEach(this.$list, function(rec) { ct += rec.x; });
* return ct;
* }
* });
*
* // use our new factory in place of $firebaseArray
* var list = new ExtendedArray(ref);
* </code></pre>
*
* @param {Function} [ChildClass] a child class which should inherit FirebaseArray
* @param {Object} [methods] a list of functions to add onto the prototype
* @returns {Function} a child class suitable for use with $firebase (this will be ChildClass if provided)
* @static
*/
FirebaseArray.$extend = function(ChildClass, methods) {
if( arguments.length === 1 && angular.isObject(ChildClass) ) {
methods = ChildClass;
ChildClass = function() { return FirebaseArray.apply(this, arguments); };
}
return $firebaseUtils.inherit(ChildClass, FirebaseArray, methods);
};
function ArraySyncManager(firebaseArray) {
function destroy(err) {
if( !sync.isDestroyed ) {
sync.isDestroyed = true;
var ref = firebaseArray.$ref();
ref.off('child_added', created);
ref.off('child_moved', moved);
ref.off('child_changed', updated);
ref.off('child_removed', removed);
firebaseArray = null;
initComplete(err||'destroyed');
}
}
function init($list) {
var ref = firebaseArray.$ref();
// listen for changes at the Firebase instance
ref.on('child_added', created, error);
ref.on('child_moved', moved, error);
ref.on('child_changed', updated, error);
ref.on('child_removed', removed, error);
// determine when initial load is completed
ref.once('value', function(snap) {
if (angular.isArray(snap.val())) {
$log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://www.firebase.com/docs/web/guide/understanding-data.html#section-arrays-in-firebase for more information.');
}
initComplete(null, $list);
}, initComplete);
}
// call initComplete(), do not call this directly
function _initComplete(err, result) {
if( !isResolved ) {
isResolved = true;
if( err ) { def.reject(err); }
else { def.resolve(result); }
}
}
var def = $firebaseUtils.defer();
var batch = $firebaseUtils.batch();
var created = batch(function(snap, prevChild) {
var rec = firebaseArray.$$added(snap, prevChild);
if( rec ) {
firebaseArray.$$process('child_added', rec, prevChild);
}
});
var updated = batch(function(snap) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var changed = firebaseArray.$$updated(snap);
if( changed ) {
firebaseArray.$$process('child_changed', rec);
}
}
});
var moved = batch(function(snap, prevChild) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var confirmed = firebaseArray.$$moved(snap, prevChild);
if( confirmed ) {
firebaseArray.$$process('child_moved', rec, prevChild);
}
}
});
var removed = batch(function(snap) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var confirmed = firebaseArray.$$removed(snap);
if( confirmed ) {
firebaseArray.$$process('child_removed', rec);
}
}
});
var isResolved = false;
var error = batch(function(err) {
_initComplete(err);
firebaseArray.$$error(err);
});
var initComplete = batch(_initComplete);
var sync = {
destroy: destroy,
isDestroyed: false,
init: init,
ready: function() { return def.promise; }
};
return sync;
}
return FirebaseArray;
}
]);
/** @deprecated */
angular.module('firebase').factory('$FirebaseArray', ['$log', '$firebaseArray',
function($log, $firebaseArray) {
return function() {
$log.warn('$FirebaseArray has been renamed. Use $firebaseArray instead.');
return $firebaseArray.apply(null, arguments);
};
}
]);
})();
(function() {
'use strict';
var FirebaseAuth;
// Define a service which provides user authentication and management.
angular.module('firebase').factory('$firebaseAuth', [
'$q', '$firebaseUtils', '$log', function($q, $firebaseUtils, $log) {
/**
* This factory returns an object allowing you to manage the client's authentication state.
*
* @param {Firebase} ref A Firebase reference to authenticate.
* @return {object} An object containing methods for authenticating clients, retrieving
* authentication state, and managing users.
*/
return function(ref) {
var auth = new FirebaseAuth($q, $firebaseUtils, $log, ref);
return auth.construct();
};
}
]);
FirebaseAuth = function($q, $firebaseUtils, $log, ref) {
this._q = $q;
this._utils = $firebaseUtils;
this._log = $log;
if (typeof ref === 'string') {
throw new Error('Please provide a Firebase reference instead of a URL when creating a `$firebaseAuth` object.');
}
this._ref = ref;
};
FirebaseAuth.prototype = {
construct: function() {
this._object = {
// Authentication methods
$authWithCustomToken: this.authWithCustomToken.bind(this),
$authAnonymously: this.authAnonymously.bind(this),
$authWithPassword: this.authWithPassword.bind(this),
$authWithOAuthPopup: this.authWithOAuthPopup.bind(this),
$authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this),
$authWithOAuthToken: this.authWithOAuthToken.bind(this),
$unauth: this.unauth.bind(this),
// Authentication state methods
$onAuth: this.onAuth.bind(this),
$getAuth: this.getAuth.bind(this),
$requireAuth: this.requireAuth.bind(this),
$waitForAuth: this.waitForAuth.bind(this),
// User management methods
$createUser: this.createUser.bind(this),
$changePassword: this.changePassword.bind(this),
$changeEmail: this.changeEmail.bind(this),
$removeUser: this.removeUser.bind(this),
$resetPassword: this.resetPassword.bind(this)
};
return this._object;
},
/********************/
/* Authentication */
/********************/
/**
* Authenticates the Firebase reference with a custom authentication token.
*
* @param {string} authToken An authentication token or a Firebase Secret. A Firebase Secret
* should only be used for authenticating a server process and provides full read / write
* access to the entire Firebase.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithCustomToken: function(authToken, options) {
var deferred = this._q.defer();
try {
this._ref.authWithCustomToken(authToken, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference anonymously.
*
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authAnonymously: function(options) {
var deferred = this._q.defer();
try {
this._ref.authAnonymously(this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with an email/password user.
*
* @param {Object} credentials An object containing email and password attributes corresponding
* to the user account.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithPassword: function(credentials, options) {
var deferred = this._q.defer();
try {
this._ref.authWithPassword(credentials, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with the OAuth popup flow.
*
* @param {string} provider The unique string identifying the OAuth provider to authenticate
* with, e.g. google.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithOAuthPopup: function(provider, options) {
var deferred = this._q.defer();
try {
this._ref.authWithOAuthPopup(provider, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with the OAuth redirect flow.
*
* @param {string} provider The unique string identifying the OAuth provider to authenticate
* with, e.g. google.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithOAuthRedirect: function(provider, options) {
var deferred = this._q.defer();
try {
this._ref.authWithOAuthRedirect(provider, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Authenticates the Firebase reference with an OAuth token.
*
* @param {string} provider The unique string identifying the OAuth provider to authenticate
* with, e.g. google.
* @param {string|Object} credentials Either a string, such as an OAuth 2.0 access token, or an
* Object of key / value pairs, such as a set of OAuth 1.0a credentials.
* @param {Object} [options] An object containing optional client arguments, such as configuring
* session persistence.
* @return {Promise<Object>} A promise fulfilled with an object containing authentication data.
*/
authWithOAuthToken: function(provider, credentials, options) {
var deferred = this._q.defer();
try {
this._ref.authWithOAuthToken(provider, credentials, this._utils.makeNodeResolver(deferred), options);
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Unauthenticates the Firebase reference.
*/
unauth: function() {
if (this.getAuth() !== null) {
this._ref.unauth();
}
},
/**************************/
/* Authentication State */
/**************************/
/**
* Asynchronously fires the provided callback with the current authentication data every time
* the authentication data changes. It also fires as soon as the authentication data is
* retrieved from the server.
*
* @param {function} callback A callback that fires when the client's authenticate state
* changes. If authenticated, the callback will be passed an object containing authentication
* data according to the provider used to authenticate. Otherwise, it will be passed null.
* @param {string} [context] If provided, this object will be used as this when calling your
* callback.
* @return {function} A function which can be used to deregister the provided callback.
*/
onAuth: function(callback, context) {
var self = this;
var fn = this._utils.debounce(callback, context, 0);
this._ref.onAuth(fn);
// Return a method to detach the `onAuth()` callback.
return function() {
self._ref.offAuth(fn);
};
},
/**
* Synchronously retrieves the current authentication data.
*
* @return {Object} The client's authentication data.
*/
getAuth: function() {
return this._ref.getAuth();
},
/**
* Helper onAuth() callback method for the two router-related methods.
*
* @param {boolean} rejectIfAuthDataIsNull Determines if the returned promise should be
* resolved or rejected upon an unauthenticated client.
* @return {Promise<Object>} A promise fulfilled with the client's authentication state or
* rejected if the client is unauthenticated and rejectIfAuthDataIsNull is true.
*/
_routerMethodOnAuthPromise: function(rejectIfAuthDataIsNull) {
var ref = this._ref;
return this._utils.promise(function(resolve,reject){
function callback(authData) {
// Turn off this onAuth() callback since we just needed to get the authentication data once.
ref.offAuth(callback);
if (authData !== null) {
resolve(authData);
return;
}
else if (rejectIfAuthDataIsNull) {
reject("AUTH_REQUIRED");
return;
}
else {
resolve(null);
return;
}
}
ref.onAuth(callback);
});
},
/**
* Utility method which can be used in a route's resolve() method to require that a route has
* a logged in client.
*
* @returns {Promise<Object>} A promise fulfilled with the client's current authentication
* state or rejected if the client is not authenticated.
*/
requireAuth: function() {
return this._routerMethodOnAuthPromise(true);
},
/**
* Utility method which can be used in a route's resolve() method to grab the current
* authentication data.
*
* @returns {Promise<Object|null>} A promise fulfilled with the client's current authentication
* state, which will be null if the client is not authenticated.
*/
waitForAuth: function() {
return this._routerMethodOnAuthPromise(false);
},
/*********************/
/* User Management */
/*********************/
/**
* Creates a new email/password user. Note that this function only creates the user, if you
* wish to log in as the newly created user, call $authWithPassword() after the promise for
* this method has been resolved.
*
* @param {Object} credentials An object containing the email and password of the user to create.
* @return {Promise<Object>} A promise fulfilled with the user object, which contains the
* uid of the created user.
*/
createUser: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in separate string arguments
if (typeof credentials === "string") {
throw new Error("$createUser() expects an object containing 'email' and 'password', but got a string.");
}
try {
this._ref.createUser(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Changes the password for an email/password user.
*
* @param {Object} credentials An object containing the email, old password, and new password of
* the user whose password is to change.
* @return {Promise<>} An empty promise fulfilled once the password change is complete.
*/
changePassword: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in separate string arguments
if (typeof credentials === "string") {
throw new Error("$changePassword() expects an object containing 'email', 'oldPassword', and 'newPassword', but got a string.");
}
try {
this._ref.changePassword(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Changes the email for an email/password user.
*
* @param {Object} credentials An object containing the old email, new email, and password of
* the user whose email is to change.
* @return {Promise<>} An empty promise fulfilled once the email change is complete.
*/
changeEmail: function(credentials) {
if (typeof this._ref.changeEmail !== 'function') {
throw new Error("$changeEmail() expects an object containing 'oldEmail', 'newEmail', and 'password', but got a string.");
}
var deferred = this._q.defer();
try {
this._ref.changeEmail(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Removes an email/password user.
*
* @param {Object} credentials An object containing the email and password of the user to remove.
* @return {Promise<>} An empty promise fulfilled once the user is removed.
*/
removeUser: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in separate string arguments
if (typeof credentials === "string") {
throw new Error("$removeUser() expects an object containing 'email' and 'password', but got a string.");
}
try {
this._ref.removeUser(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
},
/**
* Sends a password reset email to an email/password user.
*
* @param {Object} credentials An object containing the email of the user to send a reset
* password email to.
* @return {Promise<>} An empty promise fulfilled once the reset password email is sent.
*/
resetPassword: function(credentials) {
var deferred = this._q.defer();
// Throw an error if they are trying to pass in a string argument
if (typeof credentials === "string") {
throw new Error("$resetPassword() expects an object containing 'email', but got a string.");
}
try {
this._ref.resetPassword(credentials, this._utils.makeNodeResolver(deferred));
} catch (error) {
deferred.reject(error);
}
return deferred.promise;
}
};
})();
(function() {
'use strict';
/**
* Creates and maintains a synchronized object, with 2-way bindings between Angular and Firebase.
*
* Implementations of this class are contracted to provide the following internal methods,
* which are used by the synchronization process and 3-way bindings:
* $$updated - called whenever a change occurs (a value event from Firebase)
* $$error - called when listeners are canceled due to a security error
* $$notify - called to update $watch listeners and trigger updates to 3-way bindings
* $ref - called to obtain the underlying Firebase reference
*
* Instead of directly modifying this class, one should generally use the $extend
* method to add or change how methods behave:
*
* <pre><code>
* var ExtendedObject = $firebaseObject.$extend({
* // add a new method to the prototype
* foo: function() { return 'bar'; },
* });
*
* var obj = new ExtendedObject(ref);
* </code></pre>
*/
angular.module('firebase').factory('$firebaseObject', [
'$parse', '$firebaseUtils', '$log',
function($parse, $firebaseUtils, $log) {
/**
* Creates a synchronized object with 2-way bindings between Angular and Firebase.
*
* @param {Firebase} ref
* @returns {FirebaseObject}
* @constructor
*/
function FirebaseObject(ref) {
if( !(this instanceof FirebaseObject) ) {
return new FirebaseObject(ref);
}
// These are private config props and functions used internally
// they are collected here to reduce clutter in console.log and forEach
this.$$conf = {
// synchronizes data to Firebase
sync: new ObjectSyncManager(this, ref),
// stores the Firebase ref
ref: ref,
// synchronizes $scope variables with this object
binding: new ThreeWayBinding(this),
// stores observers registered with $watch
listeners: []
};
// this bit of magic makes $$conf non-enumerable and non-configurable
// and non-writable (its properties are still writable but the ref cannot be replaced)
// we redundantly assign it above so the IDE can relax
Object.defineProperty(this, '$$conf', {
value: this.$$conf
});
this.$id = $firebaseUtils.getKey(ref.ref());
this.$priority = null;
$firebaseUtils.applyDefaults(this, this.$$defaults);
// start synchronizing data with Firebase
this.$$conf.sync.init();
}
FirebaseObject.prototype = {
/**
* Saves all data on the FirebaseObject back to Firebase.
* @returns a promise which will resolve after the save is completed.
*/
$save: function () {
var self = this;
var ref = self.$ref();
var data = $firebaseUtils.toJSON(self);
return $firebaseUtils.doSet(ref, data).then(function() {
self.$$notify();
return self.$ref();
});
},
/**
* Removes all keys from the FirebaseObject and also removes
* the remote data from the server.
*
* @returns a promise which will resolve after the op completes
*/
$remove: function() {
var self = this;
$firebaseUtils.trimKeys(self, {});
self.$value = null;
return $firebaseUtils.doRemove(self.$ref()).then(function() {
self.$$notify();
return self.$ref();
});
},
/**
* The loaded method is invoked after the initial batch of data arrives from the server.
* When this resolves, all data which existed prior to calling $asObject() is now cached
* locally in the object.
*
* As a shortcut is also possible to pass resolve/reject methods directly into this
* method just as they would be passed to .then()
*
* @param {Function} resolve
* @param {Function} reject
* @returns a promise which resolves after initial data is downloaded from Firebase
*/
$loaded: function(resolve, reject) {
var promise = this.$$conf.sync.ready();
if (arguments.length) {
// allow this method to be called just like .then
// by passing any arguments on to .then
promise = promise.then.call(promise, resolve, reject);
}
return promise;
},
/**
* @returns {Firebase} the original Firebase instance used to create this object.
*/
$ref: function () {
return this.$$conf.ref;
},
/**
* Creates a 3-way data sync between this object, the Firebase server, and a
* scope variable. This means that any changes made to the scope variable are
* pushed to Firebase, and vice versa.
*
* If scope emits a $destroy event, the binding is automatically severed. Otherwise,
* it is possible to unbind the scope variable by using the `unbind` function
* passed into the resolve method.
*
* Can only be bound to one scope variable at a time. If a second is attempted,
* the promise will be rejected with an error.
*
* @param {object} scope
* @param {string} varName
* @returns a promise which resolves to an unbind method after data is set in scope
*/
$bindTo: function (scope, varName) {
var self = this;
return self.$loaded().then(function () {
return self.$$conf.binding.bindTo(scope, varName);
});
},
/**
* Listeners passed into this method are notified whenever a new change is received
* from the server. Each invocation is sent an object containing
* <code>{ type: 'updated', key: 'my_firebase_id' }</code>
*
* This method returns an unbind function that can be used to detach the listener.
*
* @param {Function} cb
* @param {Object} [context]
* @returns {Function} invoke to stop observing events
*/
$watch: function (cb, context) {
var list = this.$$conf.listeners;
list.push([cb, context]);
// an off function for cancelling the listener
return function () {
var i = list.findIndex(function (parts) {
return parts[0] === cb && parts[1] === context;
});
if (i > -1) {
list.splice(i, 1);
}
};
},
/**
* Informs $firebase to stop sending events and clears memory being used
* by this object (delete's its local content).
*/
$destroy: function(err) {
var self = this;
if (!self.$isDestroyed) {
self.$isDestroyed = true;
self.$$conf.sync.destroy(err);
self.$$conf.binding.destroy();
$firebaseUtils.each(self, function (v, k) {
delete self[k];
});
}
},
/**
* Called by $firebase whenever an item is changed at the server.
* This method must exist on any objectFactory passed into $firebase.
*
* It should return true if any changes were made, otherwise `$$notify` will
* not be invoked.
*
* @param {object} snap a Firebase snapshot
* @return {boolean} true if any changes were made.
*/
$$updated: function (snap) {
// applies new data to this object
var changed = $firebaseUtils.updateRec(this, snap);
// applies any defaults set using $$defaults
$firebaseUtils.applyDefaults(this, this.$$defaults);
// returning true here causes $$notify to be triggered
return changed;
},
/**
* Called whenever a security error or other problem causes the listeners to become
* invalid. This is generally an unrecoverable error.
* @param {Object} err which will have a `code` property and possibly a `message`
*/
$$error: function (err) {
// prints an error to the console (via Angular's logger)
$log.error(err);
// frees memory and cancels any remaining listeners
this.$destroy(err);
},
/**
* Called internally by $bindTo when data is changed in $scope.
* Should apply updates to this record but should not call
* notify().
*/
$$scopeUpdated: function(newData) {
// we use a one-directional loop to avoid feedback with 3-way bindings
// since set() is applied locally anyway, this is still performant
var def = $firebaseUtils.defer();
this.$ref().set($firebaseUtils.toJSON(newData), $firebaseUtils.makeNodeResolver(def));
return def.promise;
},
/**
* Updates any bound scope variables and
* notifies listeners registered with $watch
*/
$$notify: function() {
var self = this, list = this.$$conf.listeners.slice();
// be sure to do this after setting up data and init state
angular.forEach(list, function (parts) {
parts[0].call(parts[1], {event: 'value', key: self.$id});
});
},
/**
* Overrides how Angular.forEach iterates records on this object so that only
* fields stored in Firebase are part of the iteration. To include meta fields like
* $id and $priority in the iteration, utilize for(key in obj) instead.
*/
forEach: function(iterator, context) {
return $firebaseUtils.each(this, iterator, context);
}
};
/**
* This method allows FirebaseObject to be copied into a new factory. Methods passed into this
* function will be added onto the object's prototype. They can override existing methods as
* well.
*
* In addition to passing additional methods, it is also possible to pass in a class function.
* The prototype on that class function will be preserved, and it will inherit from
* FirebaseObject. It's also possible to do both, passing a class to inherit and additional
* methods to add onto the prototype.
*
* Once a factory is obtained by this method, it can be passed into $firebase as the
* `objectFactory` parameter:
*
* <pre><code>
* var MyFactory = $firebaseObject.$extend({
* // add a method onto the prototype that prints a greeting
* getGreeting: function() {
* return 'Hello ' + this.first_name + ' ' + this.last_name + '!';
* }
* });
*
* // use our new factory in place of $firebaseObject
* var obj = $firebase(ref, {objectFactory: MyFactory}).$asObject();
* </code></pre>
*
* @param {Function} [ChildClass] a child class which should inherit FirebaseObject
* @param {Object} [methods] a list of functions to add onto the prototype
* @returns {Function} a new factory suitable for use with $firebase
*/
FirebaseObject.$extend = function(ChildClass, methods) {
if( arguments.length === 1 && angular.isObject(ChildClass) ) {
methods = ChildClass;
ChildClass = function() { FirebaseObject.apply(this, arguments); };
}
return $firebaseUtils.inherit(ChildClass, FirebaseObject, methods);
};
/**
* Creates a three-way data binding on a scope variable.
*
* @param {FirebaseObject} rec
* @returns {*}
* @constructor
*/
function ThreeWayBinding(rec) {
this.subs = [];
this.scope = null;
this.key = null;
this.rec = rec;
}
ThreeWayBinding.prototype = {
assertNotBound: function(varName) {
if( this.scope ) {
var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' +
this.key + '; one binding per instance ' +
'(call unbind method or create another FirebaseObject instance)';
$log.error(msg);
return $firebaseUtils.reject(msg);
}
},
bindTo: function(scope, varName) {
function _bind(self) {
var sending = false;
var parsed = $parse(varName);
var rec = self.rec;
self.scope = scope;
self.varName = varName;
function equals(scopeValue) {
return angular.equals(scopeValue, rec) &&
scopeValue.$priority === rec.$priority &&
scopeValue.$value === rec.$value;
}
function setScope(rec) {
parsed.assign(scope, $firebaseUtils.scopeData(rec));
}
var send = $firebaseUtils.debounce(function(val) {
var scopeData = $firebaseUtils.scopeData(val);
rec.$$scopeUpdated(scopeData)
['finally'](function() {
sending = false;
if(!scopeData.hasOwnProperty('$value')){
delete rec.$value;
delete parsed(scope).$value;
}
}
);
}, 50, 500);
var scopeUpdated = function(newVal) {
newVal = newVal[0];
if( !equals(newVal) ) {
sending = true;
send(newVal);
}
};
var recUpdated = function() {
if( !sending && !equals(parsed(scope)) ) {
setScope(rec);
}
};
// $watch will not check any vars prefixed with $, so we
// manually check $priority and $value using this method
function watchExp(){
var obj = parsed(scope);
return [obj, obj.$priority, obj.$value];
}
setScope(rec);
self.subs.push(scope.$on('$destroy', self.unbind.bind(self)));
// monitor scope for any changes
self.subs.push(scope.$watch(watchExp, scopeUpdated, true));
// monitor the object for changes
self.subs.push(rec.$watch(recUpdated));
return self.unbind.bind(self);
}
return this.assertNotBound(varName) || _bind(this);
},
unbind: function() {
if( this.scope ) {
angular.forEach(this.subs, function(unbind) {
unbind();
});
this.subs = [];
this.scope = null;
this.key = null;
}
},
destroy: function() {
this.unbind();
this.rec = null;
}
};
function ObjectSyncManager(firebaseObject, ref) {
function destroy(err) {
if( !sync.isDestroyed ) {
sync.isDestroyed = true;
ref.off('value', applyUpdate);
firebaseObject = null;
initComplete(err||'destroyed');
}
}
function init() {
ref.on('value', applyUpdate, error);
ref.once('value', function(snap) {
if (angular.isArray(snap.val())) {
$log.warn('Storing data using array indices in Firebase can result in unexpected behavior. See https://www.firebase.com/docs/web/guide/understanding-data.html#section-arrays-in-firebase for more information. Also note that you probably wanted $firebaseArray and not $firebaseObject.');
}
initComplete(null);
}, initComplete);
}
// call initComplete(); do not call this directly
function _initComplete(err) {
if( !isResolved ) {
isResolved = true;
if( err ) { def.reject(err); }
else { def.resolve(firebaseObject); }
}
}
var isResolved = false;
var def = $firebaseUtils.defer();
var batch = $firebaseUtils.batch();
var applyUpdate = batch(function(snap) {
var changed = firebaseObject.$$updated(snap);
if( changed ) {
// notifies $watch listeners and
// updates $scope if bound to a variable
firebaseObject.$$notify();
}
});
var error = batch(firebaseObject.$$error, firebaseObject);
var initComplete = batch(_initComplete);
var sync = {
isDestroyed: false,
destroy: destroy,
init: init,
ready: function() { return def.promise; }
};
return sync;
}
return FirebaseObject;
}
]);
/** @deprecated */
angular.module('firebase').factory('$FirebaseObject', ['$log', '$firebaseObject',
function($log, $firebaseObject) {
return function() {
$log.warn('$FirebaseObject has been renamed. Use $firebaseObject instead.');
return $firebaseObject.apply(null, arguments);
};
}
]);
})();
(function() {
'use strict';
angular.module("firebase")
/** @deprecated */
.factory("$firebase", function() {
return function() {
throw new Error('$firebase has been removed. You may instantiate $firebaseArray and $firebaseObject ' +
'directly now. For simple write operations, just use the Firebase ref directly. ' +
'See the AngularFire 1.0.0 changelog for details: https://www.firebase.com/docs/web/libraries/angular/changelog.html');
};
});
})();
'use strict';
// Shim Array.indexOf for IE compatibility.
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (searchElement, fromIndex) {
if (this === undefined || this === null) {
throw new TypeError("'this' is null or not defined");
}
// Hack to convert object.length to a UInt32
// jshint -W016
var length = this.length >>> 0;
fromIndex = +fromIndex || 0;
// jshint +W016
if (Math.abs(fromIndex) === Infinity) {
fromIndex = 0;
}
if (fromIndex < 0) {
fromIndex += length;
if (fromIndex < 0) {
fromIndex = 0;
}
}
for (;fromIndex < length; fromIndex++) {
if (this[fromIndex] === searchElement) {
return fromIndex;
}
}
return -1;
};
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, 'findIndex', {
enumerable: false,
configurable: true,
writable: true,
value: function(predicate) {
if (this == null) {
throw new TypeError('Array.prototype.find called on null or undefined');
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;
var thisArg = arguments[1];
var value;
for (var i = 0; i < length; i++) {
if (i in list) {
value = list[i];
if (predicate.call(thisArg, value, i, list)) {
return i;
}
}
}
return -1;
}
});
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
if (typeof Object.create != 'function') {
(function () {
var F = function () {};
Object.create = function (o) {
if (arguments.length > 1) {
throw new Error('Second argument not supported');
}
if (o === null) {
throw new Error('Cannot set a null [[Prototype]]');
}
if (typeof o != 'object') {
throw new TypeError('Argument must be an object');
}
F.prototype = o;
return new F();
};
})();
}
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = (function () {
'use strict';
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function (obj) {
if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) {
throw new TypeError('Object.keys called on non-object');
}
var result = [], prop, i;
for (prop in obj) {
if (hasOwnProperty.call(obj, prop)) {
result.push(prop);
}
}
if (hasDontEnumBug) {
for (i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
};
}());
}
// http://ejohn.org/blog/objectgetprototypeof/
if ( typeof Object.getPrototypeOf !== "function" ) {
if ( typeof "test".__proto__ === "object" ) {
Object.getPrototypeOf = function(object){
return object.__proto__;
};
} else {
Object.getPrototypeOf = function(object){
// May break if the constructor has been tampered with
return object.constructor.prototype;
};
}
}
(function() {
'use strict';
angular.module('firebase')
.factory('$firebaseConfig', ["$firebaseArray", "$firebaseObject", "$injector",
function($firebaseArray, $firebaseObject, $injector) {
return function(configOpts) {
// make a copy we can modify
var opts = angular.extend({}, configOpts);
// look up factories if passed as string names
if( typeof opts.objectFactory === 'string' ) {
opts.objectFactory = $injector.get(opts.objectFactory);
}
if( typeof opts.arrayFactory === 'string' ) {
opts.arrayFactory = $injector.get(opts.arrayFactory);
}
// extend defaults and return
return angular.extend({
arrayFactory: $firebaseArray,
objectFactory: $firebaseObject
}, opts);
};
}
])
.factory('$firebaseUtils', ["$q", "$timeout", "firebaseBatchDelay",
function($q, $timeout, firebaseBatchDelay) {
// ES6 style promises polyfill for angular 1.2.x
// Copied from angular 1.3.x implementation: https://github.com/angular/angular.js/blob/v1.3.5/src/ng/q.js#L539
function Q(resolver) {
if (!angular.isFunction(resolver)) {
throw new Error('missing resolver function');
}
var deferred = $q.defer();
function resolveFn(value) {
deferred.resolve(value);
}
function rejectFn(reason) {
deferred.reject(reason);
}
resolver(resolveFn, rejectFn);
return deferred.promise;
}
var utils = {
/**
* Returns a function which, each time it is invoked, will pause for `wait`
* milliseconds before invoking the original `fn` instance. If another
* request is received in that time, it resets `wait` up until `maxWait` is
* reached.
*
* Unlike a debounce function, once wait is received, all items that have been
* queued will be invoked (not just once per execution). It is acceptable to use 0,
* which means to batch all synchronously queued items.
*
* The batch function actually returns a wrap function that should be called on each
* method that is to be batched.
*
* <pre><code>
* var total = 0;
* var batchWrapper = batch(10, 100);
* var fn1 = batchWrapper(function(x) { return total += x; });
* var fn2 = batchWrapper(function() { console.log(total); });
* fn1(10);
* fn2();
* fn1(10);
* fn2();
* console.log(total); // 0 (nothing invoked yet)
* // after 10ms will log "10" and then "20"
* </code></pre>
*
* @param {int} wait number of milliseconds to pause before sending out after each invocation
* @param {int} maxWait max milliseconds to wait before sending out, defaults to wait * 10 or 100
* @returns {Function}
*/
batch: function(wait, maxWait) {
wait = typeof('wait') === 'number'? wait : firebaseBatchDelay;
if( !maxWait ) { maxWait = wait*10 || 100; }
var queue = [];
var start;
var cancelTimer;
var runScheduledForNextTick;
// returns `fn` wrapped in a function that queues up each call event to be
// invoked later inside fo runNow()
function createBatchFn(fn, context) {
if( typeof(fn) !== 'function' ) {
throw new Error('Must provide a function to be batched. Got '+fn);
}
return function() {
var args = Array.prototype.slice.call(arguments, 0);
queue.push([fn, context, args]);
resetTimer();
};
}
// clears the current wait timer and creates a new one
// however, if maxWait is exceeded, calls runNow() on the next tick.
function resetTimer() {
if( cancelTimer ) {
cancelTimer();
cancelTimer = null;
}
if( start && Date.now() - start > maxWait ) {
if(!runScheduledForNextTick){
runScheduledForNextTick = true;
utils.compile(runNow);
}
}
else {
if( !start ) { start = Date.now(); }
cancelTimer = utils.wait(runNow, wait);
}
}
// Clears the queue and invokes all of the functions awaiting notification
function runNow() {
cancelTimer = null;
start = null;
runScheduledForNextTick = false;
var copyList = queue.slice(0);
queue = [];
angular.forEach(copyList, function(parts) {
parts[0].apply(parts[1], parts[2]);
});
}
return createBatchFn;
},
/**
* A rudimentary debounce method
* @param {function} fn the function to debounce
* @param {object} [ctx] the `this` context to set in fn
* @param {int} wait number of milliseconds to pause before sending out after each invocation
* @param {int} [maxWait] max milliseconds to wait before sending out, defaults to wait * 10 or 100
*/
debounce: function(fn, ctx, wait, maxWait) {
var start, cancelTimer, args, runScheduledForNextTick;
if( typeof(ctx) === 'number' ) {
maxWait = wait;
wait = ctx;
ctx = null;
}
if( typeof wait !== 'number' ) {
throw new Error('Must provide a valid integer for wait. Try 0 for a default');
}
if( typeof(fn) !== 'function' ) {
throw new Error('Must provide a valid function to debounce');
}
if( !maxWait ) { maxWait = wait*10 || 100; }
// clears the current wait timer and creates a new one
// however, if maxWait is exceeded, calls runNow() on the next tick.
function resetTimer() {
if( cancelTimer ) {
cancelTimer();
cancelTimer = null;
}
if( start && Date.now() - start > maxWait ) {
if(!runScheduledForNextTick){
runScheduledForNextTick = true;
utils.compile(runNow);
}
}
else {
if( !start ) { start = Date.now(); }
cancelTimer = utils.wait(runNow, wait);
}
}
// Clears the queue and invokes the debounced function with the most recent arguments
function runNow() {
cancelTimer = null;
start = null;
runScheduledForNextTick = false;
fn.apply(ctx, args);
}
function debounced() {
args = Array.prototype.slice.call(arguments, 0);
resetTimer();
}
debounced.running = function() {
return start > 0;
};
return debounced;
},
assertValidRef: function(ref, msg) {
if( !angular.isObject(ref) ||
typeof(ref.ref) !== 'function' ||
typeof(ref.ref().transaction) !== 'function' ) {
throw new Error(msg || 'Invalid Firebase reference');
}
},
// http://stackoverflow.com/questions/7509831/alternative-for-the-deprecated-proto
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
inherit: function(ChildClass, ParentClass, methods) {
var childMethods = ChildClass.prototype;
ChildClass.prototype = Object.create(ParentClass.prototype);
ChildClass.prototype.constructor = ChildClass; // restoring proper constructor for child class
angular.forEach(Object.keys(childMethods), function(k) {
ChildClass.prototype[k] = childMethods[k];
});
if( angular.isObject(methods) ) {
angular.extend(ChildClass.prototype, methods);
}
return ChildClass;
},
getPrototypeMethods: function(inst, iterator, context) {
var methods = {};
var objProto = Object.getPrototypeOf({});
var proto = angular.isFunction(inst) && angular.isObject(inst.prototype)?
inst.prototype : Object.getPrototypeOf(inst);
while(proto && proto !== objProto) {
for (var key in proto) {
// we only invoke each key once; if a super is overridden it's skipped here
if (proto.hasOwnProperty(key) && !methods.hasOwnProperty(key)) {
methods[key] = true;
iterator.call(context, proto[key], key, proto);
}
}
proto = Object.getPrototypeOf(proto);
}
},
getPublicMethods: function(inst, iterator, context) {
utils.getPrototypeMethods(inst, function(m, k) {
if( typeof(m) === 'function' && k.charAt(0) !== '_' ) {
iterator.call(context, m, k);
}
});
},
defer: $q.defer,
reject: $q.reject,
resolve: $q.when,
//TODO: Remove false branch and use only angular implementation when we drop angular 1.2.x support.
promise: angular.isFunction($q) ? $q : Q,
makeNodeResolver:function(deferred){
return function(err,result){
if(err === null){
if(arguments.length > 2){
result = Array.prototype.slice.call(arguments,1);
}
deferred.resolve(result);
}
else {
deferred.reject(err);
}
};
},
wait: function(fn, wait) {
var to = $timeout(fn, wait||0);
return function() {
if( to ) {
$timeout.cancel(to);
to = null;
}
};
},
compile: function(fn) {
return $timeout(fn||function() {});
},
deepCopy: function(obj) {
if( !angular.isObject(obj) ) { return obj; }
var newCopy = angular.isArray(obj) ? obj.slice() : angular.extend({}, obj);
for (var key in newCopy) {
if (newCopy.hasOwnProperty(key)) {
if (angular.isObject(newCopy[key])) {
newCopy[key] = utils.deepCopy(newCopy[key]);
}
}
}
return newCopy;
},
trimKeys: function(dest, source) {
utils.each(dest, function(v,k) {
if( !source.hasOwnProperty(k) ) {
delete dest[k];
}
});
},
scopeData: function(dataOrRec) {
var data = {
$id: dataOrRec.$id,
$priority: dataOrRec.$priority
};
var hasPublicProp = false;
utils.each(dataOrRec, function(v,k) {
hasPublicProp = true;
data[k] = utils.deepCopy(v);
});
if(!hasPublicProp && dataOrRec.hasOwnProperty('$value')){
data.$value = dataOrRec.$value;
}
return data;
},
updateRec: function(rec, snap) {
var data = snap.val();
var oldData = angular.extend({}, rec);
// deal with primitives
if( !angular.isObject(data) ) {
rec.$value = data;
data = {};
}
else {
delete rec.$value;
}
// apply changes: remove old keys, insert new data, set priority
utils.trimKeys(rec, data);
angular.extend(rec, data);
rec.$priority = snap.getPriority();
return !angular.equals(oldData, rec) ||
oldData.$value !== rec.$value ||
oldData.$priority !== rec.$priority;
},
applyDefaults: function(rec, defaults) {
if( angular.isObject(defaults) ) {
angular.forEach(defaults, function(v,k) {
if( !rec.hasOwnProperty(k) ) {
rec[k] = v;
}
});
}
return rec;
},
dataKeys: function(obj) {
var out = [];
utils.each(obj, function(v,k) {
out.push(k);
});
return out;
},
each: function(obj, iterator, context) {
if(angular.isObject(obj)) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
var c = k.charAt(0);
if( c !== '_' && c !== '$' && c !== '.' ) {
iterator.call(context, obj[k], k, obj);
}
}
}
}
else if(angular.isArray(obj)) {
for(var i = 0, len = obj.length; i < len; i++) {
iterator.call(context, obj[i], i, obj);
}
}
return obj;
},
/**
* A utility for retrieving a Firebase reference or DataSnapshot's
* key name. This is backwards-compatible with `name()` from Firebase
* 1.x.x and `key()` from Firebase 2.0.0+. Once support for Firebase
* 1.x.x is dropped in AngularFire, this helper can be removed.
*/
getKey: function(refOrSnapshot) {
return (typeof refOrSnapshot.key === 'function') ? refOrSnapshot.key() : refOrSnapshot.name();
},
/**
* A utility for converting records to JSON objects
* which we can save into Firebase. It asserts valid
* keys and strips off any items prefixed with $.
*
* If the rec passed into this method has a toJSON()
* method, that will be used in place of the custom
* functionality here.
*
* @param rec
* @returns {*}
*/
toJSON: function(rec) {
var dat;
if( !angular.isObject(rec) ) {
rec = {$value: rec};
}
if (angular.isFunction(rec.toJSON)) {
dat = rec.toJSON();
}
else {
dat = {};
utils.each(rec, function (v, k) {
dat[k] = stripDollarPrefixedKeys(v);
});
}
if( angular.isDefined(rec.$value) && Object.keys(dat).length === 0 && rec.$value !== null ) {
dat['.value'] = rec.$value;
}
if( angular.isDefined(rec.$priority) && Object.keys(dat).length > 0 && rec.$priority !== null ) {
dat['.priority'] = rec.$priority;
}
angular.forEach(dat, function(v,k) {
if (k.match(/[.$\[\]#\/]/) && k !== '.value' && k !== '.priority' ) {
throw new Error('Invalid key ' + k + ' (cannot contain .$[]#)');
}
else if( angular.isUndefined(v) ) {
throw new Error('Key '+k+' was undefined. Cannot pass undefined in JSON. Use null instead.');
}
});
return dat;
},
doSet: function(ref, data) {
var def = utils.defer();
if( angular.isFunction(ref.set) || !angular.isObject(data) ) {
// this is not a query, just do a flat set
ref.set(data, utils.makeNodeResolver(def));
}
else {
var dataCopy = angular.extend({}, data);
// this is a query, so we will replace all the elements
// of this query with the value provided, but not blow away
// the entire Firebase path
ref.once('value', function(snap) {
snap.forEach(function(ss) {
if( !dataCopy.hasOwnProperty(utils.getKey(ss)) ) {
dataCopy[utils.getKey(ss)] = null;
}
});
ref.ref().update(dataCopy, utils.makeNodeResolver(def));
}, function(err) {
def.reject(err);
});
}
return def.promise;
},
doRemove: function(ref) {
var def = utils.defer();
if( angular.isFunction(ref.remove) ) {
// ref is not a query, just do a flat remove
ref.remove(utils.makeNodeResolver(def));
}
else {
// ref is a query so let's only remove the
// items in the query and not the entire path
ref.once('value', function(snap) {
var promises = [];
snap.forEach(function(ss) {
var d = utils.defer();
promises.push(d.promise);
ss.ref().remove(utils.makeNodeResolver(def));
});
utils.allPromises(promises)
.then(function() {
def.resolve(ref);
},
function(err){
def.reject(err);
}
);
}, function(err) {
def.reject(err);
});
}
return def.promise;
},
/**
* AngularFire version number.
*/
VERSION: '1.0.0',
batchDelay: firebaseBatchDelay,
allPromises: $q.all.bind($q)
};
return utils;
}
]);
function stripDollarPrefixedKeys(data) {
if( !angular.isObject(data) ) { return data; }
var out = angular.isArray(data)? [] : {};
angular.forEach(data, function(v,k) {
if(typeof k !== 'string' || k.charAt(0) !== '$') {
out[k] = stripDollarPrefixedKeys(v);
}
});
return out;
}
})();
...@@ -12,25 +12,27 @@ button { ...@@ -12,25 +12,27 @@ button {
font-size: 100%; font-size: 100%;
vertical-align: baseline; vertical-align: baseline;
font-family: inherit; font-family: inherit;
font-weight: inherit;
color: inherit; color: inherit;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
body { body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em; line-height: 1.4em;
background: #eaeaea url('bg.png'); background: #f5f5f5;
color: #4d4d4d; color: #4d4d4d;
width: 550px; min-width: 230px;
max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased; font-smoothing: antialiased;
font-weight: 300;
} }
button, button,
...@@ -38,78 +40,49 @@ input[type="checkbox"] { ...@@ -38,78 +40,49 @@ input[type="checkbox"] {
outline: none; outline: none;
} }
.hidden {
display: none;
}
#todoapp { #todoapp {
background: #fff; background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0; margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative; position: relative;
border-top-left-radius: 2px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
border-top-right-radius: 2px; 0 25px 50px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
} }
#todoapp input::-webkit-input-placeholder { #todoapp input::-webkit-input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300;
color: #e6e6e6;
} }
#todoapp input::-moz-placeholder { #todoapp input::-moz-placeholder {
font-style: italic; font-style: italic;
color: #a9a9a9; font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
} }
#todoapp h1 { #todoapp h1 {
position: absolute; position: absolute;
top: -120px; top: -155px;
width: 100%; width: 100%;
font-size: 70px; font-size: 100px;
font-weight: bold; font-weight: 100;
text-align: center; text-align: center;
color: #b3b3b3; color: rgba(175, 47, 47, 0.15);
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility; -webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo, #new-todo,
.edit { .edit {
position: relative; position: relative;
...@@ -117,6 +90,7 @@ input[type="checkbox"] { ...@@ -117,6 +90,7 @@ input[type="checkbox"] {
width: 100%; width: 100%;
font-size: 24px; font-size: 24px;
font-family: inherit; font-family: inherit;
font-weight: inherit;
line-height: 1.4em; line-height: 1.4em;
border: 0; border: 0;
outline: none; outline: none;
...@@ -124,29 +98,23 @@ input[type="checkbox"] { ...@@ -124,29 +98,23 @@ input[type="checkbox"] {
padding: 6px; padding: 6px;
border: 1px solid #999; border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased; font-smoothing: antialiased;
} }
#new-todo { #new-todo {
padding: 16px 16px 16px 60px; padding: 16px 16px 16px 60px;
border: none; border: none;
background: rgba(0, 0, 0, 0.02); background: rgba(0, 0, 0, 0.003);
z-index: 2; box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
box-shadow: none;
} }
#main { #main {
position: relative; position: relative;
z-index: 2; z-index: 2;
border-top: 1px dotted #adadad; border-top: 1px solid #e6e6e6;
} }
label[for='toggle-all'] { label[for='toggle-all'] {
...@@ -155,19 +123,19 @@ label[for='toggle-all'] { ...@@ -155,19 +123,19 @@ label[for='toggle-all'] {
#toggle-all { #toggle-all {
position: absolute; position: absolute;
top: -42px; top: -55px;
left: -4px; left: -12px;
width: 40px; width: 60px;
height: 34px;
text-align: center; text-align: center;
/* Mobile Safari */ border: none; /* Mobile Safari */
border: none;
} }
#toggle-all:before { #toggle-all:before {
content: '»'; content: '';
font-size: 28px; font-size: 22px;
color: #d9d9d9; color: #e6e6e6;
padding: 0 25px 7px; padding: 10px 27px 10px 27px;
} }
#toggle-all:checked:before { #toggle-all:checked:before {
...@@ -183,7 +151,7 @@ label[for='toggle-all'] { ...@@ -183,7 +151,7 @@ label[for='toggle-all'] {
#todo-list li { #todo-list li {
position: relative; position: relative;
font-size: 24px; font-size: 24px;
border-bottom: 1px dotted #ccc; border-bottom: 1px solid #ededed;
} }
#todo-list li:last-child { #todo-list li:last-child {
...@@ -215,28 +183,17 @@ label[for='toggle-all'] { ...@@ -215,28 +183,17 @@ label[for='toggle-all'] {
top: 0; top: 0;
bottom: 0; bottom: 0;
margin: auto 0; margin: auto 0;
/* Mobile Safari */ border: none; /* Mobile Safari */
border: none;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none; appearance: none;
} }
#todo-list li .toggle:after { #todo-list li .toggle:after {
content: '✔'; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
} }
#todo-list li .toggle:checked:after { #todo-list li .toggle:checked:after {
color: #85ada7; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
} }
#todo-list li label { #todo-list li label {
...@@ -246,12 +203,11 @@ label[for='toggle-all'] { ...@@ -246,12 +203,11 @@ label[for='toggle-all'] {
margin-left: 45px; margin-left: 45px;
display: block; display: block;
line-height: 1.2; line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s; transition: color 0.4s;
} }
#todo-list li.completed label { #todo-list li.completed label {
color: #a9a9a9; color: #d9d9d9;
text-decoration: line-through; text-decoration: line-through;
} }
...@@ -264,21 +220,18 @@ label[for='toggle-all'] { ...@@ -264,21 +220,18 @@ label[for='toggle-all'] {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin: auto 0; margin: auto 0;
font-size: 22px; font-size: 30px;
color: #a88a8a; color: #cc9a9a;
-webkit-transition: all 0.2s; margin-bottom: 11px;
transition: all 0.2s; transition: color 0.2s ease-out;
} }
#todo-list li .destroy:hover { #todo-list li .destroy:hover {
text-shadow: 0 0 1px #000, color: #af5b5e;
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
transform: scale(1.3);
} }
#todo-list li .destroy:after { #todo-list li .destroy:after {
content: ''; content: '×';
} }
#todo-list li:hover .destroy { #todo-list li:hover .destroy {
...@@ -295,29 +248,25 @@ label[for='toggle-all'] { ...@@ -295,29 +248,25 @@ label[for='toggle-all'] {
#footer { #footer {
color: #777; color: #777;
padding: 0 15px; padding: 10px 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px; height: 20px;
z-index: 1;
text-align: center; text-align: center;
border-top: 1px solid #e6e6e6;
} }
#footer:before { #footer:before {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; right: 0;
bottom: 31px; bottom: 0;
left: 0; left: 0;
height: 50px; height: 50px;
z-index: -1; overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 6px 0 -3px rgba(255, 255, 255, 0.8), 0 8px 0 -3px #f6f6f6,
0 7px 1px -3px rgba(0, 0, 0, 0.3), 0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 43px 0 -6px rgba(255, 255, 255, 0.8), 0 16px 0 -6px #f6f6f6,
0 44px 2px -6px rgba(0, 0, 0, 0.2); 0 17px 2px -6px rgba(0, 0, 0, 0.2);
} }
#todo-count { #todo-count {
...@@ -325,6 +274,10 @@ label[for='toggle-all'] { ...@@ -325,6 +274,10 @@ label[for='toggle-all'] {
text-align: left; text-align: left;
} }
#todo-count strong {
font-weight: 300;
}
#filters { #filters {
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -339,49 +292,73 @@ label[for='toggle-all'] { ...@@ -339,49 +292,73 @@ label[for='toggle-all'] {
} }
#filters li a { #filters li a {
color: #83756f; color: inherit;
margin: 2px; margin: 3px;
padding: 3px 7px;
text-decoration: none; text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
#filters li a.selected,
#filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
} }
#filters li a.selected { #filters li a.selected {
font-weight: bold; border-color: rgba(175, 47, 47, 0.2);
} }
#clear-completed { #clear-completed,
html #clear-completed:active {
float: right; float: right;
position: relative; position: relative;
line-height: 20px; line-height: 20px;
text-decoration: none; text-decoration: none;
background: rgba(0, 0, 0, 0.1); cursor: pointer;
font-size: 11px; visibility: hidden;
padding: 0 10px; position: relative;
border-radius: 3px; }
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
#clear-completed::after {
visibility: visible;
content: 'Clear completed';
position: absolute;
top: 0;
right: 0;
white-space: nowrap;
} }
#clear-completed:hover { #clear-completed:hover::after {
background: rgba(0, 0, 0, 0.15); text-decoration: underline;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
} }
#info { #info {
margin: 65px auto 0; margin: 65px auto 0;
color: #a6a6a6; color: #bfbfbf;
font-size: 12px; font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center; text-align: center;
} }
#info p {
line-height: 1;
}
#info a { #info a {
color: inherit; color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
text-decoration: underline;
} }
/* /*
Hack to remove background from Mobile Safari. Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera Can't use it globally since it destroys checkboxes in Firefox
*/ */
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all, #toggle-all,
#todo-list li .toggle { #todo-list li .toggle {
...@@ -393,10 +370,6 @@ label[for='toggle-all'] { ...@@ -393,10 +370,6 @@ label[for='toggle-all'] {
} }
#toggle-all { #toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg); -webkit-transform: rotate(90deg);
transform: rotate(90deg); transform: rotate(90deg);
-webkit-appearance: none; -webkit-appearance: none;
...@@ -404,151 +377,12 @@ label[for='toggle-all'] { ...@@ -404,151 +377,12 @@ label[for='toggle-all'] {
} }
} }
.hidden { @media (max-width: 430px) {
display: none; #footer {
} height: 50px;
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
} }
.learn-bar #todoapp { #filters {
width: 550px; bottom: 10px;
margin: 130px auto 40px auto;
} }
} }
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
/* global _ */
(function () { (function () {
'use strict'; 'use strict';
/* jshint ignore:start */
// Underscore's Template Module // Underscore's Template Module
// Courtesy of underscorejs.org // Courtesy of underscorejs.org
var _ = (function (_) { var _ = (function (_) {
...@@ -114,6 +116,7 @@ ...@@ -114,6 +116,7 @@
if (location.hostname === 'todomvc.com') { if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
} }
/* jshint ignore:end */
function redirect() { function redirect() {
if (location.hostname === 'tastejs.github.io') { if (location.hostname === 'tastejs.github.io') {
...@@ -175,13 +178,17 @@ ...@@ -175,13 +178,17 @@
if (learnJSON.backend) { if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend; this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({ this.append({
backend: true backend: true
}); });
} else if (learnJSON[framework]) { } else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework]; this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append(); this.append();
} }
this.fetchIssueCount();
} }
Learn.prototype.append = function (opts) { Learn.prototype.append = function (opts) {
...@@ -212,6 +219,26 @@ ...@@ -212,6 +219,26 @@
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
}; };
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect(); redirect();
getFile('learn.json', Learn); getFile('learn.json', Learn);
})(); })();
{
"private": true,
"dependencies": {
"angular": "^1.3.15",
"angularfire": "^1.0.0",
"todomvc-app-css": "^1.0.1",
"todomvc-common": "^1.0.1"
}
}
...@@ -13,10 +13,10 @@ Here are some links you may find helpful: ...@@ -13,10 +13,10 @@ Here are some links you may find helpful:
* [Tutorial](https://www.firebase.com/tutorial/) * [Tutorial](https://www.firebase.com/tutorial/)
* [Documentation & Examples](https://www.firebase.com/docs/) * [Documentation & Examples](https://www.firebase.com/docs/)
* [API Reference](https://www.firebase.com/docs/javascript/firebase/) * [API Reference](https://www.firebase.com/docs/web)
* [Blog](https://www.firebase.com/blog/) * [Blog](https://www.firebase.com/blog/)
* [Firebase on Github](http://firebase.github.io) * [Firebase on Github](http://firebase.github.io)
* [AngularJS bindings for Firebase](http://github.com/firebase/angularFire) * [AngularJS bindings for Firebase](https://www.firebase.com/docs/web/libraries/angular/)
Get help from other AngularJS users: Get help from other AngularJS users:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment