flashpoint
Version:
Angular bindings for Fireproof. Replaces AngularFire.
928 lines (710 loc) • 22.5 kB
JavaScript
function FirebaseCtl(
$scope,
$q,
Firebase,
Fireproof,
fpValidatePath,
FPListenerSet) {
/**
* @ngdoc type
* @name FirebaseCtl
* @module flashpoint
* @description The core controller responsible for binding
* Firebase data into Angular.
*
* @property {Firebase} root The root of the instantiated Firebase store.
*
* @property {Boolean} connected The state of the network connection to Firebase.
* This will be:
* - `true`, if there is a good network connection to Firebase
* - `false`, if the connection to Firebase is interrupted or not available
* - `undefined` if the connection state is not known
*
* @property {Object} auth The authentication data from Firebase. This will be:
* - `null`, if the user is not authenticated
* - `undefined`, if the authentication state is not yet known
* - an `Object`, containing information about the currently-authenticated user
*
* @property {Error} authError The error reported by the most recent attempt to
* authenticate to Firebase, or `null` otherwise.
*
* @property {Error} accountError The error reported by the most recent attempt
* to perform an account-related action on Firebase, or `null` otherwise.
*
* @property {Boolean} accountChanging True if an account-changing action
* (password reset, user delete, etc.) is in progress, false otherwise.
*
* @property {Boolean} authenticating True if an authentication attempt is
* in progress, false otherwise.
*/
var self = this;
var _attachListeners = [],
_detachListeners = [];
self.auth = null;
self.authError = null;
self.accountError = null;
self.authenticating = false;
self.accountChanging = false;
function authHandler(authData) {
if (self.listenerSet) {
self.listenerSet.clear();
}
self.auth = authData;
$scope.$evalAsync();
}
function connectedListener(snap) {
self.connected = snap.val();
$scope.$evalAsync();
}
function authPassHandler(auth) {
self.authenticating = false;
self.authError = null;
return auth;
}
function authErrorHandler(err) {
self.authenticating = true;
self.authError = err;
return $q.reject(err);
}
function accountPassHandler() {
self.accountChanging = false;
self.accountError = null;
}
function accountErrorHandler(err) {
self.accountChanging = false;
self.accountError = err;
return $q.reject(err);
}
/**
* @ngdoc method
* @name FirebaseCtl#detachFirebase
* @description Removes and detaches all connections to Firebase used by
* this controller.
*/
self.detachFirebase = function() {
// detach all watchers
if (self.listenerSet) {
self.listenerSet.clear();
delete self.listenerSet;
}
delete self.connected;
self.auth = null;
self.authError = null;
self.accountError = null;
self.authenticating = false;
self.accountChanging = false;
if (self.root) {
// detach any remaining listeners here.
self.root.offAuth(authHandler);
self.root.child('.info/connected').off('value', connectedListener);
self.root.off();
_detachListeners.forEach(function(listener) {
listener(self.root);
});
// remove the actual root object itself, as it's now invalid.
delete self.root;
}
$scope.$evalAsync();
};
self.onDetach = function(fn) {
_detachListeners.push(fn);
if (!self.root) {
fn();
}
return fn;
};
self.offDetach = function(fn) {
_detachListeners.splice(_detachListeners.indexOf(fn), 1);
};
/**
* @ngdoc method
* @name FirebaseCtl#attachFirebase
* @description Connects to the specified Firebase.
* @param {string} url The full URL of the Firebase to connect to.
*/
self.attachFirebase = function(url) {
// if we already have a root, make sure to clean it up first
if (self.root) {
self.detachFirebase();
}
self.root = new Fireproof(new Firebase(url));
self.listenerSet = new FPListenerSet(self.root, $scope);
self.root.onAuth(authHandler);
// maintain knowledge of connection status
// we assume, optimistically, that we're connected initially
self.connected = true;
self.root.child('.info/connected')
.on('value', connectedListener);
_attachListeners.forEach(function(listener) {
listener(self.root);
});
};
self.onAttach = function(fn) {
_attachListeners.push(fn);
if (self.root) {
fn(self.root);
}
return fn;
};
self.offAttach = function(fn) {
_attachListeners.splice(_attachListeners.indexOf(fn), 1);
};
/**
* @ngdoc method
* @name FirebaseCtl#goOffline
* @description Disables the connection to the remote Firebase server. NOTE:
* this method affects _all_ FirebaseCtl instances on the page.
* @see Firebase.goOffline
*/
self.goOffline = function() {
Firebase.goOffline();
};
/**
* @ngdoc method
* @name FirebaseCtl#goOnline
* @description Enables the connection to the remote Firebase server. NOTE:
* this method affects _all_ FirebaseCtl instances on the page.
* @see Firebase.goOnline
*/
self.goOnline = function() {
Firebase.goOnline();
};
/**
* @ngdoc method
* @name FirebaseCtl#unauth
* @description Unauthenticates (i.e., logs out) the Firebase connection.
* @see Fireproof#unauth
*/
self.unauth = function() {
self.authError = null;
self.accountError = null;
self.root.unauth();
};
/**
* @ngdoc method
* @name FirebaseCtl#authWithCustomToken
* @description Authenticates using a custom token or Firebase secret.
* @param {String} token The token to authenticate with.
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#authWithCustomToken
*/
self.authWithCustomToken = function(token) {
self.authenticating = true;
return self.root.authWithCustomToken(token)
.then(authPassHandler, authErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#authAnonymously
* @description Authenticates using a new, temporary guest account.
* @param {Object} options
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#authAnonymously
*/
self.authAnonymously = function(options) {
self.authenticating = true;
return self.root.authAnonymously(null, options)
.then(authPassHandler, authErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#authWithPassword
* @description Authenticates using an email / password combination.
* @param {String} email
* @param {String} password
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#authWithPassword
*/
self.authWithPassword = function(email, password) {
self.authenticating = true;
return self.root.authWithPassword({ email: email, password: password })
.then(authPassHandler, authErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#authWithOAuthPopup
* @description Authenticates using a popup-based OAuth flow.
* @param {String} provider
* @param {Object} options
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#authWithOAuthPopup
*/
self.authWithOAuthPopup = function(provider, options) {
self.authenticating = true;
return self.root.authWithOAuthPopup(provider, null, options)
.then(authPassHandler, authErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#authWithOAuthToken
* @description Authenticates using OAuth access tokens or credentials.
* @param {String} provider
* @param {Object} credentials
* @param {Object} options
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#authWithOAuthToken
*/
self.authWithOAuthToken = function(provider, credentials, options) {
self.authenticating = true;
return self.root.authWithOAuthToken(provider, credentials, null, options)
.then(authPassHandler, authErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#createUser
* @description Creates a new user account using an email / password combination.
* @param {String} email
* @param {String} password
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#createUser
*/
self.createUser = function(email, password) {
self.accountChanging = true;
return self.root.createUser({ email: email, password: password })
.then(accountPassHandler, accountErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#removeUser
* @description Removes an existing user account using an email / password combination.
* @param {String} email
* @param {String} password
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#removeUser
*/
self.removeUser = function(email, password) {
self.accountChanging = true;
return self.root.removeUser({ email: email, password: password })
.then(accountPassHandler, accountErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#changeEmail
* @description Updates the email associated with an email / password user account.
* @param {String} oldEmail
* @param {String} newEmail
* @param {String} password
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#changeEmail
*/
self.changeEmail = function(oldEmail, newEmail, password) {
self.accountChanging = true;
return self.root.changeEmail({
oldEmail: oldEmail,
newEmail: newEmail,
password: password
})
.then(accountPassHandler, accountErrorHandler);
};
/**
* @ngdoc method
* @name changePassword
* @description Changes the password of an existing user using an email / password combination.
* @param {String} email
* @param {String} oldPassword
* @param {String} newPassword
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#changePassword
*/
self.changePassword = function(email, oldPassword, newPassword) {
self.accountChanging = true;
return self.root.changePassword({
email: email,
oldPassword: oldPassword,
newPassword: newPassword
})
.then(accountPassHandler, accountErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#resetPassword
* @description Sends a password-reset email to the owner of the account,
* containing a token that may be used to authenticate and change the user's password.
* @param {String} email
* @returns {Promise} that resolves on success and rejects on error.
* @see Fireproof#resetPassword
*/
self.resetPassword = function(email) {
self.accountChanging = true;
return self.root.resetPassword({ email: email })
.then(accountPassHandler, accountErrorHandler);
};
/**
* @ngdoc method
* @name FirebaseCtl#set
* @description Set a Firebase path to a given value.
* @param {...string} pathPart Path components to be joined.
* @param {(Object|String|Number|Boolean|Array|null)} value The value to set the path to.
* @returns {Promise}
* @see Fireproof#set
*
* @example
* ```js
* fp.set('users', 'fritz', { hometown: 'Metropolis'})
* ```
*
* ```html
* <button ng-click="fp.set('users', user, 'activated', true)">Activate!</button>
* ```
*/
self.set = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
value = args.pop(),
path = fpValidatePath(args);
return self.root.child(path).set(value);
};
/**
* @ngdoc method
* @name FirebaseCtl#setPriority
* @description Set a Firebase path to a given priority.
* @param {...string} pathPart Path components to be joined.
* @param {(String|Number|null)} priority The priority to set the path to.
* @returns {Promise}
* @see Fireproof#setPriority
*
* @example
* ```js
* fp.setPriority('users', 'fritz', Date)
* ```
*
* ```html
* <button ng-click="fp.setPriority('users', user, 0)">To teh top!</button>
* ```
* @see Firebase#setPriority
*/
self.setPriority = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
priority = args.pop(),
path = fpValidatePath(args);
return self.root.child(path).setPriority(priority);
};
/**
* @ngdoc method
* @name FirebaseCtl#setWithPriority
* @description Set a Firebase path to a given value and priority.
* @param {...string} pathPart Path components to be joined.
* @param {(Object|String|Number|Boolean|Array|null)} value The value to set the path to.
* @param {(String|Number|null)} priority The priority to set the path to.
* @returns {Promise}
* @see Fireproof#setWithPriority
*
* @example
* ```js
* fp.setWithPriority('users', 'fritz', { hometown: 'Metropolis' }, Date)
* ```
*
* ```html
* <button ng-click="fp.setWithPriority('status', event, 'pending', 0)">Reset to pending</button>
* ```
* @see Firebase#setWithPriority
*/
self.setWithPriority = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
priority = args.pop(),
value = args.pop(),
path = fpValidatePath(args);
return self.root.child(path).setWithPriority(value, priority);
};
/**
* @ngdoc method
* @name FirebaseCtl#push
* @description Add a child to a Firebase path.
* @param {...string} pathPart Path components to be joined.
* @param {(Object|String|Number|Boolean|Array|null)} value The value to append to the path.
* @param {(String|Number|null)} priority The priority to set the path to.
* @returns {Promise}
* @see Fireproof#push
*
* @example
* ```js
* fp.push('users', { name: 'Fritz', hometown: 'Metropolis' })
* ```
*
* ```html
* <button ng-click="fp.push('comments', commentText)">Add your comment!</button>
* ```
* @see Firebase#push
*/
self.push = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
value = args.pop(),
path = fpValidatePath(args);
return self.root.child(path).push(value);
};
/**
* @ngdoc method
* @name FirebaseCtl#update
* @description Update a Firebase path with a given object.
* @param {...string} pathPart Path components to be joined.
* @param {(Object|String|Number|Boolean|Array|null)} value The value to update the path with.
* @returns {Promise}
* @see Fireproof#update
*
* @example
* ```js
* fp.update('users', 'fritz', { hometown: 'Metropolis' })
* ```
*
* ```html
* <button ng-click="fp.update('users', user, { disabled: true } )">Disable user</button>
* ```
* @see Firebase#update
*/
self.update = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
value = args.pop(),
path = fpValidatePath(args);
return self.root.child(path).update(value);
};
/**
* @ngdoc method
* @name FirebaseCtl#remove
* @description Remove a Firebase path.
* @param {...string} pathPart Path components to be joined.
* @returns {Promise}
* @see Fireproof#remove
*
* @example
* ```js
* fp.remove('users', 'fritz')
* ```
*
* ```html
* <button ng-click="fp.remove('users', user)">Remove user</button>
* ```
* @see Firebase#update
*/
self.remove = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
path = fpValidatePath(args);
return self.root.child(path).remove();
};
/**
* @ngdoc method
* @name FirebaseCtl#increment
* @description Atomically increments a numeric value in Firebase.
* @param {...string} pathPart Path components to be joined.
* @returns {Promise}
*
* @example
* ```js
* fp.increment('users/fritz/votes')
* ```
*
* ```html
* <button ng-click="fp.increment('users', user, 'votes')">Vote for this user!</button>
* ```
*/
self.increment = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
path = fpValidatePath(args);
return self.root.child(path)
.transaction(function(val) {
if (angular.isNumber(val)) {
return val + 1;
} else if (val === null) {
return 1;
} else {
return; // abort transaction
}
})
.then(function(result) {
if (!result.committed) {
return $q.reject(new Error('Cannot increment the object at ' + path));
}
});
};
/**
* @ngdoc method
* @name FirebaseCtl#decrement
* @description Atomically decrements a numeric value in Firebase.
* @param {...string} pathPart Path components to be joined.
* @returns {Promise}
*
* @example
* ```js
* fp.decrement('users', 'fritz', 'votes')
* ```
*
* ```html
* <button ng-click="fp.decrement('users', user, 'votes')">Vote against this user!</button>
* ```
*/
self.decrement = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
path = fpValidatePath(args);
return self.root.child(path)
.transaction(function(val) {
if (angular.isNumber(val)) {
return val - 1;
} else if (val === null) {
return 0;
} else {
return; // abort transaction
}
})
.then(function(result) {
if (!result.committed) {
return $q.reject(new Error('Cannot decrement the object at ' + path));
}
});
};
/**
* @ngdoc method
* @name FirebaseCtl#transaction
* @description Performs a transaction in Firebase.
* @param {...string} pathPart Path components to be joined.
* @param {Function} fn The function that describes the transaction. Takes one
* argument, the existing value in Firebase. See the Firebase docs on transactions.
* @returns {Promise}
* @see Fireproof#transaction
*
* @example
* ```js
* fp.decrement('users', 'fritz', 'votes')
* ```
*
* ```html
* <button ng-click="fp.decrement('users', user, 'votes')">Vote against this user!</button>
* ```
* @see Firebase#transaction
*/
self.transaction = function() {
// check the arguments
var args = Array.prototype.slice.call(arguments, 0),
fn = args.pop(),
path = fpValidatePath(args);
return self.root.child(path)
.transaction(function(val) {
return fn(val);
})
.then(function(result) {
if (!result.committed) {
return $q.reject(new Error('Aborted'));
}
});
};
/**
* @ngdoc method
* @name FirebaseCtl#val
* @description Gets a value from Firebase and triggers scope refresh when that value changes.
* @param {...string} pathPart Path components to be joined.
* @returns {*} `null` on the first scope digest, and the actual value subsequently.
*
* @example
* ```html
* <span>Welcome, {{ fp.val('users', userId, 'firstName') }}!</button>
* ```
*/
self.val = function() {
var path = fpValidatePath(Array.prototype.slice.call(arguments, 0));
if (!path || !self.listenerSet) {
return;
}
self.listenerSet.add(path);
if (self.listenerSet.values.hasOwnProperty(path)) {
return self.listenerSet.values[path];
} else {
return null;
}
};
/**
* @ngdoc method
* @name FirebaseCtl#model
* @description Use a Firebase path with ng-model.
* @param {...string} pathPart Path components to be joined.
* @returns {Function} a function that can be used in an ng-model expression
* if ng-model-options has getterSetter: true.
*
* @example
* ```html
* <input name="firstname" ng-model="fp.model('users', user.id, 'firstName')" ng-model-options="{ getterSetter: true }">
* ```
*/
self.model = function() {
var path = fpValidatePath(Array.prototype.slice.call(arguments, 0));
return function(val) {
// do nothing if we have no path or we aren't attached.
if (!path || !self.listenerSet) {
return;
}
if (angular.isDefined(val)) {
// setter.
return self.set(path, val);
} else {
// getter.
self.listenerSet.add(path);
if (self.listenerSet.values.hasOwnProperty(path)) {
return self.listenerSet.values[path];
} else {
return null;
}
}
};
};
/**
* @ngdoc method
* @name FirebaseCtl#priority
* @description Gets a priority from Firebase and triggers scope refresh when that priority changes.
* @param {...string} pathPart Path components to be joined.
* @returns {*} `null` on the first scope digest, and the actual priority subsequently.
*/
self.priority = function() {
var path = fpValidatePath(Array.prototype.slice.call(arguments, 0));
if (!path || !self.listenerSet) {
return;
}
self.listenerSet.add(path);
if (self.listenerSet.priorities.hasOwnProperty(path)) {
return self.listenerSet.priorities[path];
} else {
return null;
}
};
/**
* @ngdoc method
* @name FirebaseCtl#error
* @description Gets the error associated with trying to read a specific path in Firebase.
* @param {...string} pathPart Path components to be joined.
* @returns {*} The error on trying to read the path, or `null` if there wasn't one.
*
* @example
* ```html
* <span>Welcome, {{ fp.val('users', userId, 'firstName') }}!</button>
* ```
*/
self.error = function() {
var path = fpValidatePath(Array.prototype.slice.call(arguments, 0));
if (path && self.listenerSet && self.listenerSet.errors.hasOwnProperty(path)) {
return self.listenerSet.errors[path];
} else {
return null;
}
};
/**
* @ngdoc method
* @name FirebaseCtl#path
*/
self.path = function() {
var path = fpValidatePath(Array.prototype.slice.call(arguments, 0));
if (path) {
return path;
} else {
return null;
}
};
$scope.$on('$destroy', function() {
// shut down controller
self.detachFirebase();
});
}
angular.module('flashpoint')
.controller('FirebaseCtl', FirebaseCtl);