todomvc
Version:
> Helping you select an MV\* framework
780 lines (703 loc) • 25.5 kB
JavaScript
// 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)));
}
};