├── .bowerrc ├── www ├── css │ └── style.css ├── img │ ├── License.pdf │ ├── ionic.png │ └── food-drink-17.jpg ├── js │ ├── services.js │ ├── app.js │ ├── controller.js │ └── angularfire.js └── index.html ├── bower.json ├── .gitignore ├── ionic.project ├── scss ├── _custom.scss └── ionic.app.scss ├── package.json ├── config.xml ├── LICENSE ├── README.md ├── gulpfile.js └── hooks ├── after_prepare └── 010_add_platform_class.js └── README.md /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "www/lib" 3 | } 4 | -------------------------------------------------------------------------------- /www/css/style.css: -------------------------------------------------------------------------------- 1 | /* Empty. Add your own CSS if you like */ 2 | -------------------------------------------------------------------------------- /www/img/License.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/mobile-apps-with-ionic-and-firebase/HEAD/www/img/License.pdf -------------------------------------------------------------------------------- /www/img/ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/mobile-apps-with-ionic-and-firebase/HEAD/www/img/ionic.png -------------------------------------------------------------------------------- /www/img/food-drink-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tutsplus/mobile-apps-with-ionic-and-firebase/HEAD/www/img/food-drink-17.jpg -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HelloIonic", 3 | "private": "true", 4 | "devDependencies": { 5 | "ionic": "driftyco/ionic-bower#1.0.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | node_modules/ 5 | www/lib/ 6 | platforms/ 7 | plugins/ 8 | -------------------------------------------------------------------------------- /ionic.project: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foodbook", 3 | "app_id": "", 4 | "gulpStartupTasks": [ 5 | "sass", 6 | "watch" 7 | ], 8 | "watchPatterns": [ 9 | "www/**/*", 10 | "!www/lib/**/*" 11 | ] 12 | } -------------------------------------------------------------------------------- /www/js/services.js: -------------------------------------------------------------------------------- 1 | fbook.factory('recipeService',function($firebaseArray) { 2 | var fb = new Firebase("https://firebase reference"); 3 | var recs = $firebaseArray(fb); 4 | var recipeService = { 5 | all: recs, 6 | get: function(recId) { 7 | return recs.$getRecord(recId); 8 | } 9 | }; 10 | return recipeService; 11 | }); 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /scss/_custom.scss: -------------------------------------------------------------------------------- 1 | //background for homepage content 2 | .myContent { 3 | background: $dark; 4 | } 5 | 6 | //Remove border from header 7 | .bar-dark { 8 | border: none; 9 | } 10 | 11 | //border for text inputs on add page 12 | .myBorder { 13 | border: 2px solid black; 14 | } 15 | 16 | //recipe listing 17 | .recListing { 18 | text-align: center; 19 | a{ 20 | text-decoration: none; 21 | } 22 | } 23 | 24 | //wrapping for ion-item 25 | .item { 26 | white-space: normal; 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foodbook", 3 | "version": "1.0.0", 4 | "description": "foodbook: An Ionic project", 5 | "dependencies": { 6 | "angularfire": "^1.1.2", 7 | "gulp": "^3.5.6", 8 | "gulp-concat": "^2.2.0", 9 | "gulp-minify-css": "^0.3.0", 10 | "gulp-rename": "^1.2.0", 11 | "gulp-sass": "^1.3.3" 12 | }, 13 | "devDependencies": { 14 | "bower": "^1.3.3", 15 | "gulp-util": "^2.2.14", 16 | "shelljs": "^0.3.0" 17 | }, 18 | "cordovaPlugins": [ 19 | "cordova-plugin-device", 20 | "cordova-plugin-console", 21 | "cordova-plugin-whitelist", 22 | "cordova-plugin-splashscreen", 23 | "com.ionic.keyboard" 24 | ], 25 | "cordovaPlatforms": [] 26 | } 27 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | foodbook 4 | 5 | An Ionic Framework and Cordova project. 6 | 7 | 8 | Ionic Framework Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /scss/ionic.app.scss: -------------------------------------------------------------------------------- 1 | /* 2 | To customize the look and feel of Ionic, you can override the variables 3 | in ionic's _variables.scss file. 4 | 5 | For example, you might change some of the default colors: 6 | 7 | $light: #fff !default; 8 | $stable: #f8f8f8 !default; 9 | $positive: #387ef5 !default; 10 | $calm: #11c1f3 !default; 11 | $balanced: #33cd5f !default; 12 | $energized: #ffc900 !default; 13 | $assertive: #ef473a !default; 14 | $royal: #886aea !default; 15 | $dark: #444 !default; 16 | */ 17 | 18 | // The path for our ionicons font files, relative to the built CSS in www/css 19 | $ionicons-font-path: "../lib/ionic/fonts" !default; 20 | 21 | // Include all of Ionic 22 | @import "www/lib/ionic/scss/ionic"; 23 | @import "custom"; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tuts+ 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Mobile Apps With Ionic and Firebase][published url] 2 | ## Instructor: [Reginald Dawson][instructor url] 3 | 4 | 5 | Ionic is a framework for building cross-platform mobile apps with HTML, CSS and JavaScript. Not only does Ionic come with numerous mobile-optimized UI components, but it is also built on top of AngularJS for powerful templating and easy two-way data binding. 6 | 7 | Firebase is a perfect complement to Ionic. While Ionic is a great tool for building the front-end, Firebase can power the back-end. With Firebase, we don't need to worry about provisioning servers or building REST APIs - with a little bit of configuration, we can let Firebase do the work. 8 | 9 | In this course, we're going to build a recipe app called "Foodbook". We'll start by getting familiar with the Ionic components, then we'll take it further as we create controllers and services from AngularJS. 10 | 11 | ## Source Files Description 12 | 13 | This source repository contains the completed course project: the Foodbook recipe app that uses Firebase for storing and retrieving data. 14 | 15 | 16 | ------ 17 | 18 | These are source files for the Tuts+ course: [Mobile Apps With Ionic and Firebase][published url] 19 | 20 | Available on [Tuts+](https://tutsplus.com). Teaching skills to millions worldwide. 21 | 22 | [published url]: https://code.tutsplus.com/courses/mobile-apps-with-ionic-and-firebase 23 | [instructor url]: https://tutsplus.com/authors/reginald-dawson 24 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var bower = require('bower'); 4 | var concat = require('gulp-concat'); 5 | var sass = require('gulp-sass'); 6 | var minifyCss = require('gulp-minify-css'); 7 | var rename = require('gulp-rename'); 8 | var sh = require('shelljs'); 9 | 10 | var paths = { 11 | sass: ['./scss/**/*.scss'] 12 | }; 13 | 14 | gulp.task('default', ['sass']); 15 | 16 | gulp.task('sass', function(done) { 17 | gulp.src('./scss/ionic.app.scss') 18 | .pipe(sass({ 19 | errLogToConsole: true 20 | })) 21 | .pipe(gulp.dest('./www/css/')) 22 | .pipe(minifyCss({ 23 | keepSpecialComments: 0 24 | })) 25 | .pipe(rename({ extname: '.min.css' })) 26 | .pipe(gulp.dest('./www/css/')) 27 | .on('end', done); 28 | }); 29 | 30 | gulp.task('watch', function() { 31 | gulp.watch(paths.sass, ['sass']); 32 | }); 33 | 34 | gulp.task('install', ['git-check'], function() { 35 | return bower.commands.install() 36 | .on('log', function(data) { 37 | gutil.log('bower', gutil.colors.cyan(data.id), data.message); 38 | }); 39 | }); 40 | 41 | gulp.task('git-check', function(done) { 42 | if (!sh.which('git')) { 43 | console.log( 44 | ' ' + gutil.colors.red('Git is not installed.'), 45 | '\n Git, the version control system, is required to download Ionic.', 46 | '\n Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.', 47 | '\n Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.' 48 | ); 49 | process.exit(1); 50 | } 51 | done(); 52 | }); 53 | -------------------------------------------------------------------------------- /www/js/app.js: -------------------------------------------------------------------------------- 1 | // Ionic Starter App 2 | 3 | // angular.module is a global place for creating, registering and retrieving Angular modules 4 | // 'starter' is the name of this angular module example (also set in a attribute in index.html) 5 | // the 2nd parameter is an array of 'requires' 6 | var fbook = angular.module('foodbook', ['ionic','firebase']); 7 | 8 | fbook.run(function($ionicPlatform) { 9 | $ionicPlatform.ready(function() { 10 | // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard 11 | // for form inputs) 12 | if(window.cordova && window.cordova.plugins.Keyboard) { 13 | cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); 14 | } 15 | if(window.StatusBar) { 16 | StatusBar.styleDefault(); 17 | } 18 | }); 19 | }); 20 | 21 | 22 | fbook.config(function($stateProvider,$urlRouterProvider) { 23 | 24 | $stateProvider.state("home", { 25 | url: "/", 26 | templateUrl: "home.html" 27 | }); 28 | $stateProvider.state("recList", { 29 | url: "/recList", 30 | templateUrl: "recList.html", 31 | controller: "listController" 32 | }); 33 | $stateProvider.state("singleRecipe", { 34 | url: "/:id", 35 | templateUrl: "singleRec.html", 36 | controller: "recipeController" 37 | }); 38 | $stateProvider.state("add", { 39 | url: "/add", 40 | templateUrl: "add.html", 41 | controller: "addController" 42 | }); 43 | $stateProvider.state("del", { 44 | url: "/del", 45 | templateUrl: "delRec.html", 46 | controller: "deleteController" 47 | }); 48 | $stateProvider.state("edit", { 49 | url: "/edit", 50 | templateUrl: "edit.html", 51 | controller: "editController" 52 | }); 53 | $stateProvider.state("one", { 54 | url: "/edit/:id", 55 | templateUrl: "editOne.html", 56 | controller: "recipeEditController" 57 | }); 58 | 59 | $urlRouterProvider.otherwise("/"); 60 | 61 | }); -------------------------------------------------------------------------------- /www/js/controller.js: -------------------------------------------------------------------------------- 1 | //add controller 2 | fbook.controller('addController',function($scope,$firebaseArray,$state,recipeService){ 3 | $scope.submitRecipe = function(){ 4 | $scope.newRec = recipeService.all; 5 | $scope.newRec.$add({ 6 | recipeName: $scope.recName, 7 | recipeIngredients: $scope.recIngredients, 8 | recipeDirections: $scope.recDirections 9 | }); 10 | $state.go('home'); 11 | }; 12 | }); 13 | 14 | 15 | 16 | fbook.controller('listController',function($scope,recipeService){ 17 | $scope.recipes = recipeService.all; 18 | }); 19 | 20 | 21 | fbook.controller('recipeController',function($scope,recipeService,$stateParams,$state){ 22 | $scope.singleRecipe = recipeService.get($stateParams.id); 23 | $scope.ingList = $scope.singleRecipe.recipeIngredients.split(';'); 24 | $scope.prepList = $scope.singleRecipe.recipeDirections.split(';'); 25 | }); 26 | 27 | 28 | 29 | 30 | 31 | fbook.controller('deleteController',function($scope,recipeService,$state,$firebaseArray,$ionicActionSheet){ 32 | $scope.recs = recipeService.all; 33 | 34 | $scope.showDetails = function(id) { 35 | $ionicActionSheet.show({ 36 | destructiveText: 'Delete', 37 | titleText: 'Sure you want to delete?', 38 | cancelText: 'Cancel', 39 | destructiveButtonClicked: function() { 40 | var rem = $scope.recs.$getRecord(id); 41 | $scope.recs.$remove(rem); 42 | return true; 43 | } 44 | }); 45 | }; 46 | }); 47 | 48 | 49 | fbook.controller('editController',function($scope,recipeService){ 50 | $scope.editRecipes = recipeService.all; 51 | }); 52 | 53 | 54 | fbook.controller('recipeEditController',function($scope,recipeService,$stateParams,$state){ 55 | $scope.allRecs = recipeService.all; 56 | $scope.singleRecipe = recipeService.get($stateParams.id); 57 | $scope.title = $scope.singleRecipe.recipeName; 58 | $scope.ingredients = $scope.singleRecipe.recipeIngredients; 59 | $scope.directions = $scope.singleRecipe.recipeDirections; 60 | $scope.myid = $scope.singleRecipe.$id; 61 | $scope.updateRecipe = function(id) { 62 | var ed = $scope.allRecs.$getRecord(id); 63 | ed.recipeName = $scope.title; 64 | ed.recipeIngredients = $scope.ingredients; 65 | ed.recipeDirections = $scope.directions; 66 | $scope.allRecs.$save(ed); 67 | $state.go('edit'); 68 | }; 69 | }); 70 | -------------------------------------------------------------------------------- /hooks/after_prepare/010_add_platform_class.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Add Platform Class 4 | // v1.0 5 | // Automatically adds the platform class to the body tag 6 | // after the `prepare` command. By placing the platform CSS classes 7 | // directly in the HTML built for the platform, it speeds up 8 | // rendering the correct layout/style for the specific platform 9 | // instead of waiting for the JS to figure out the correct classes. 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | 14 | var rootdir = process.argv[2]; 15 | 16 | function addPlatformBodyTag(indexPath, platform) { 17 | // add the platform class to the body tag 18 | try { 19 | var platformClass = 'platform-' + platform; 20 | var cordovaClass = 'platform-cordova platform-webview'; 21 | 22 | var html = fs.readFileSync(indexPath, 'utf8'); 23 | 24 | var bodyTag = findBodyTag(html); 25 | if(!bodyTag) return; // no opening body tag, something's wrong 26 | 27 | if(bodyTag.indexOf(platformClass) > -1) return; // already added 28 | 29 | var newBodyTag = bodyTag; 30 | 31 | var classAttr = findClassAttr(bodyTag); 32 | if(classAttr) { 33 | // body tag has existing class attribute, add the classname 34 | var endingQuote = classAttr.substring(classAttr.length-1); 35 | var newClassAttr = classAttr.substring(0, classAttr.length-1); 36 | newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; 37 | newBodyTag = bodyTag.replace(classAttr, newClassAttr); 38 | 39 | } else { 40 | // add class attribute to the body tag 41 | newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); 42 | } 43 | 44 | html = html.replace(bodyTag, newBodyTag); 45 | 46 | fs.writeFileSync(indexPath, html, 'utf8'); 47 | 48 | process.stdout.write('add to body class: ' + platformClass + '\n'); 49 | } catch(e) { 50 | process.stdout.write(e); 51 | } 52 | } 53 | 54 | function findBodyTag(html) { 55 | // get the body tag 56 | try{ 57 | return html.match(/])(.*?)>/gi)[0]; 58 | }catch(e){} 59 | } 60 | 61 | function findClassAttr(bodyTag) { 62 | // get the body tag's class attribute 63 | try{ 64 | return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; 65 | }catch(e){} 66 | } 67 | 68 | if (rootdir) { 69 | 70 | // go through each of the platform directories that have been prepared 71 | var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); 72 | 73 | for(var x=0; x 21 | # Cordova Hooks 22 | 23 | This directory may contain scripts used to customize cordova commands. This 24 | directory used to exist at `.cordova/hooks`, but has now been moved to the 25 | project root. Any scripts you add to these directories will be executed before 26 | and after the commands corresponding to the directory name. Useful for 27 | integrating your own build systems or integrating with version control systems. 28 | 29 | __Remember__: Make your scripts executable. 30 | 31 | ## Hook Directories 32 | The following subdirectories will be used for hooks: 33 | 34 | after_build/ 35 | after_compile/ 36 | after_docs/ 37 | after_emulate/ 38 | after_platform_add/ 39 | after_platform_rm/ 40 | after_platform_ls/ 41 | after_plugin_add/ 42 | after_plugin_ls/ 43 | after_plugin_rm/ 44 | after_plugin_search/ 45 | after_prepare/ 46 | after_run/ 47 | after_serve/ 48 | before_build/ 49 | before_compile/ 50 | before_docs/ 51 | before_emulate/ 52 | before_platform_add/ 53 | before_platform_rm/ 54 | before_platform_ls/ 55 | before_plugin_add/ 56 | before_plugin_ls/ 57 | before_plugin_rm/ 58 | before_plugin_search/ 59 | before_prepare/ 60 | before_run/ 61 | before_serve/ 62 | pre_package/ <-- Windows 8 and Windows Phone only. 63 | 64 | ## Script Interface 65 | 66 | All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: 67 | 68 | * CORDOVA_VERSION - The version of the Cordova-CLI. 69 | * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). 70 | * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) 71 | * CORDOVA_HOOK - Path to the hook that is being executed. 72 | * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) 73 | 74 | If a script returns a non-zero exit code, then the parent cordova command will be aborted. 75 | 76 | 77 | ## Writing hooks 78 | 79 | We highly recommend writting your hooks using Node.js so that they are 80 | cross-platform. Some good examples are shown here: 81 | 82 | [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) 83 | 84 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 58 | 59 | 60 | 83 | 84 | 85 | 86 | 97 | 98 | 99 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /www/js/angularfire.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * AngularFire is the officially supported AngularJS binding for Firebase. Firebase 3 | * is a full backend so you don't need servers to build your Angular app. AngularFire 4 | * provides you with the $firebase service which allows you to easily keep your $scope 5 | * variables in sync with your Firebase backend. 6 | * 7 | * AngularFire 1.1.2 8 | * https://github.com/firebase/angularfire/ 9 | * Date: 06/25/2015 10 | * License: MIT 11 | */ 12 | (function(exports) { 13 | "use strict"; 14 | 15 | // Define the `firebase` module under which all AngularFire 16 | // services will live. 17 | angular.module("firebase", []) 18 | //todo use $window 19 | .value("Firebase", exports.Firebase); 20 | 21 | })(window); 22 | (function() { 23 | 'use strict'; 24 | /** 25 | * Creates and maintains a synchronized list of data. This is a pseudo-read-only array. One should 26 | * not call splice(), push(), pop(), et al directly on this array, but should instead use the 27 | * $remove and $add methods. 28 | * 29 | * It is acceptable to .sort() this array, but it is important to use this in conjunction with 30 | * $watch(), so that it will be re-sorted any time the server data changes. Examples of this are 31 | * included in the $watch documentation. 32 | * 33 | * Internally, the $firebase object depends on this class to provide several $$ (i.e. protected) 34 | * methods, which it invokes to notify the array whenever a change has been made at the server: 35 | * $$added - called whenever a child_added event occurs 36 | * $$updated - called whenever a child_changed event occurs 37 | * $$moved - called whenever a child_moved event occurs 38 | * $$removed - called whenever a child_removed event occurs 39 | * $$error - called when listeners are canceled due to a security error 40 | * $$process - called immediately after $$added/$$updated/$$moved/$$removed 41 | * (assuming that these methods do not abort by returning false or null) 42 | * to splice/manipulate the array and invoke $$notify 43 | * 44 | * Additionally, these methods may be of interest to devs extending this class: 45 | * $$notify - triggers notifications to any $watch listeners, called by $$process 46 | * $$getKey - determines how to look up a record's key (returns $id by default) 47 | * 48 | * Instead of directly modifying this class, one should generally use the $extend 49 | * method to add or change how methods behave. $extend modifies the prototype of 50 | * the array class by returning a clone of $firebaseArray. 51 | * 52 | *

  53 |    * var ExtendedArray = $firebaseArray.$extend({
  54 |    *    // add a new method to the prototype
  55 |    *    foo: function() { return 'bar'; },
  56 |    *
  57 |    *    // change how records are created
  58 |    *    $$added: function(snap, prevChild) {
  59 |    *       return new Widget(snap, prevChild);
  60 |    *    },
  61 |    *
  62 |    *    // change how records are updated
  63 |    *    $$updated: function(snap) {
  64 |    *      return this.$getRecord(snap.key()).update(snap);
  65 |    *    }
  66 |    * });
  67 |    *
  68 |    * var list = new ExtendedArray(ref);
  69 |    * 
70 | */ 71 | angular.module('firebase').factory('$firebaseArray', ["$log", "$firebaseUtils", "$q", 72 | function($log, $firebaseUtils, $q) { 73 | /** 74 | * This constructor should probably never be called manually. It is used internally by 75 | * $firebase.$asArray(). 76 | * 77 | * @param {Firebase} ref 78 | * @returns {Array} 79 | * @constructor 80 | */ 81 | function FirebaseArray(ref) { 82 | if( !(this instanceof FirebaseArray) ) { 83 | return new FirebaseArray(ref); 84 | } 85 | var self = this; 86 | this._observers = []; 87 | this.$list = []; 88 | this._ref = ref; 89 | this._sync = new ArraySyncManager(this); 90 | 91 | $firebaseUtils.assertValidRef(ref, 'Must pass a valid Firebase reference ' + 92 | 'to $firebaseArray (not a string or URL)'); 93 | 94 | // indexCache is a weak hashmap (a lazy list) of keys to array indices, 95 | // items are not guaranteed to stay up to date in this list (since the data 96 | // array can be manually edited without calling the $ methods) and it should 97 | // always be used with skepticism regarding whether it is accurate 98 | // (see $indexFor() below for proper usage) 99 | this._indexCache = {}; 100 | 101 | // Array.isArray will not work on objects which extend the Array class. 102 | // So instead of extending the Array class, we just return an actual array. 103 | // However, it's still possible to extend FirebaseArray and have the public methods 104 | // appear on the array object. We do this by iterating the prototype and binding 105 | // any method that is not prefixed with an underscore onto the final array. 106 | $firebaseUtils.getPublicMethods(self, function(fn, key) { 107 | self.$list[key] = fn.bind(self); 108 | }); 109 | 110 | this._sync.init(this.$list); 111 | 112 | return this.$list; 113 | } 114 | 115 | FirebaseArray.prototype = { 116 | /** 117 | * Create a new record with a unique ID and add it to the end of the array. 118 | * This should be used instead of Array.prototype.push, since those changes will not be 119 | * synchronized with the server. 120 | * 121 | * Any value, including a primitive, can be added in this way. Note that when the record 122 | * is created, the primitive value would be stored in $value (records are always objects 123 | * by default). 124 | * 125 | * Returns a future which is resolved when the data has successfully saved to the server. 126 | * The resolve callback will be passed a Firebase ref representing the new data element. 127 | * 128 | * @param data 129 | * @returns a promise resolved after data is added 130 | */ 131 | $add: function(data) { 132 | this._assertNotDestroyed('$add'); 133 | var def = $firebaseUtils.defer(); 134 | var ref = this.$ref().ref().push(); 135 | ref.set($firebaseUtils.toJSON(data), $firebaseUtils.makeNodeResolver(def)); 136 | return def.promise.then(function() { 137 | return ref; 138 | }); 139 | }, 140 | 141 | /** 142 | * Pass either an item in the array or the index of an item and it will be saved back 143 | * to Firebase. While the array is read-only and its structure should not be changed, 144 | * it is okay to modify properties on the objects it contains and then save those back 145 | * individually. 146 | * 147 | * Returns a future which is resolved when the data has successfully saved to the server. 148 | * The resolve callback will be passed a Firebase ref representing the saved element. 149 | * If passed an invalid index or an object which is not a record in this array, 150 | * the promise will be rejected. 151 | * 152 | * @param {int|object} indexOrItem 153 | * @returns a promise resolved after data is saved 154 | */ 155 | $save: function(indexOrItem) { 156 | this._assertNotDestroyed('$save'); 157 | var self = this; 158 | var item = self._resolveItem(indexOrItem); 159 | var key = self.$keyAt(item); 160 | if( key !== null ) { 161 | var ref = self.$ref().ref().child(key); 162 | var data = $firebaseUtils.toJSON(item); 163 | return $firebaseUtils.doSet(ref, data).then(function() { 164 | self.$$notify('child_changed', key); 165 | return ref; 166 | }); 167 | } 168 | else { 169 | return $firebaseUtils.reject('Invalid record; could determine key for '+indexOrItem); 170 | } 171 | }, 172 | 173 | /** 174 | * Pass either an existing item in this array or the index of that item and it will 175 | * be removed both locally and in Firebase. This should be used in place of 176 | * Array.prototype.splice for removing items out of the array, as calling splice 177 | * will not update the value on the server. 178 | * 179 | * Returns a future which is resolved when the data has successfully removed from the 180 | * server. The resolve callback will be passed a Firebase ref representing the deleted 181 | * element. If passed an invalid index or an object which is not a record in this array, 182 | * the promise will be rejected. 183 | * 184 | * @param {int|object} indexOrItem 185 | * @returns a promise which resolves after data is removed 186 | */ 187 | $remove: function(indexOrItem) { 188 | this._assertNotDestroyed('$remove'); 189 | var key = this.$keyAt(indexOrItem); 190 | if( key !== null ) { 191 | var ref = this.$ref().ref().child(key); 192 | return $firebaseUtils.doRemove(ref).then(function() { 193 | return ref; 194 | }); 195 | } 196 | else { 197 | return $firebaseUtils.reject('Invalid record; could not determine key for '+indexOrItem); 198 | } 199 | }, 200 | 201 | /** 202 | * Given an item in this array or the index of an item in the array, this returns the 203 | * Firebase key (record.$id) for that record. If passed an invalid key or an item which 204 | * does not exist in this array, it will return null. 205 | * 206 | * @param {int|object} indexOrItem 207 | * @returns {null|string} 208 | */ 209 | $keyAt: function(indexOrItem) { 210 | var item = this._resolveItem(indexOrItem); 211 | return this.$$getKey(item); 212 | }, 213 | 214 | /** 215 | * The inverse of $keyAt, this method takes a Firebase key (record.$id) and returns the 216 | * index in the array where that record is stored. If the record is not in the array, 217 | * this method returns -1. 218 | * 219 | * @param {String} key 220 | * @returns {int} -1 if not found 221 | */ 222 | $indexFor: function(key) { 223 | var self = this; 224 | var cache = self._indexCache; 225 | // evaluate whether our key is cached and, if so, whether it is up to date 226 | if( !cache.hasOwnProperty(key) || self.$keyAt(cache[key]) !== key ) { 227 | // update the hashmap 228 | var pos = self.$list.findIndex(function(rec) { return self.$$getKey(rec) === key; }); 229 | if( pos !== -1 ) { 230 | cache[key] = pos; 231 | } 232 | } 233 | return cache.hasOwnProperty(key)? cache[key] : -1; 234 | }, 235 | 236 | /** 237 | * The loaded method is invoked after the initial batch of data arrives from the server. 238 | * When this resolves, all data which existed prior to calling $asArray() is now cached 239 | * locally in the array. 240 | * 241 | * As a shortcut is also possible to pass resolve/reject methods directly into this 242 | * method just as they would be passed to .then() 243 | * 244 | * @param {Function} [resolve] 245 | * @param {Function} [reject] 246 | * @returns a promise 247 | */ 248 | $loaded: function(resolve, reject) { 249 | var promise = this._sync.ready(); 250 | if( arguments.length ) { 251 | // allow this method to be called just like .then 252 | // by passing any arguments on to .then 253 | promise = promise.then.call(promise, resolve, reject); 254 | } 255 | return promise; 256 | }, 257 | 258 | /** 259 | * @returns {Firebase} the original Firebase ref used to create this object. 260 | */ 261 | $ref: function() { return this._ref; }, 262 | 263 | /** 264 | * Listeners passed into this method are notified whenever a new change (add, updated, 265 | * move, remove) is received from the server. Each invocation is sent an object 266 | * containing { type: 'child_added|child_updated|child_moved|child_removed', 267 | * key: 'key_of_item_affected'} 268 | * 269 | * Additionally, added and moved events receive a prevChild parameter, containing the 270 | * key of the item before this one in the array. 271 | * 272 | * This method returns a function which can be invoked to stop observing events. 273 | * 274 | * @param {Function} cb 275 | * @param {Object} [context] 276 | * @returns {Function} used to stop observing 277 | */ 278 | $watch: function(cb, context) { 279 | var list = this._observers; 280 | list.push([cb, context]); 281 | // an off function for cancelling the listener 282 | return function() { 283 | var i = list.findIndex(function(parts) { 284 | return parts[0] === cb && parts[1] === context; 285 | }); 286 | if( i > -1 ) { 287 | list.splice(i, 1); 288 | } 289 | }; 290 | }, 291 | 292 | /** 293 | * Informs $firebase to stop sending events and clears memory being used 294 | * by this array (delete's its local content). 295 | */ 296 | $destroy: function(err) { 297 | if( !this._isDestroyed ) { 298 | this._isDestroyed = true; 299 | this._sync.destroy(err); 300 | this.$list.length = 0; 301 | } 302 | }, 303 | 304 | /** 305 | * Returns the record for a given Firebase key (record.$id). If the record is not found 306 | * then returns null. 307 | * 308 | * @param {string} key 309 | * @returns {Object|null} a record in this array 310 | */ 311 | $getRecord: function(key) { 312 | var i = this.$indexFor(key); 313 | return i > -1? this.$list[i] : null; 314 | }, 315 | 316 | /** 317 | * Called to inform the array when a new item has been added at the server. 318 | * This method should return the record (an object) that will be passed into $$process 319 | * along with the add event. Alternately, the record will be skipped if this method returns 320 | * a falsey value. 321 | * 322 | * @param {object} snap a Firebase snapshot 323 | * @param {string} prevChild 324 | * @return {object} the record to be inserted into the array 325 | * @protected 326 | */ 327 | $$added: function(snap/*, prevChild*/) { 328 | // check to make sure record does not exist 329 | var i = this.$indexFor($firebaseUtils.getKey(snap)); 330 | if( i === -1 ) { 331 | // parse data and create record 332 | var rec = snap.val(); 333 | if( !angular.isObject(rec) ) { 334 | rec = { $value: rec }; 335 | } 336 | rec.$id = $firebaseUtils.getKey(snap); 337 | rec.$priority = snap.getPriority(); 338 | $firebaseUtils.applyDefaults(rec, this.$$defaults); 339 | 340 | return rec; 341 | } 342 | return false; 343 | }, 344 | 345 | /** 346 | * Called whenever an item is removed at the server. 347 | * This method does not physically remove the objects, but instead 348 | * returns a boolean indicating whether it should be removed (and 349 | * taking any other desired actions before the remove completes). 350 | * 351 | * @param {object} snap a Firebase snapshot 352 | * @return {boolean} true if item should be removed 353 | * @protected 354 | */ 355 | $$removed: function(snap) { 356 | return this.$indexFor($firebaseUtils.getKey(snap)) > -1; 357 | }, 358 | 359 | /** 360 | * Called whenever an item is changed at the server. 361 | * This method should apply the changes, including changes to data 362 | * and to $priority, and then return true if any changes were made. 363 | * 364 | * If this method returns false, then $$process will not be invoked, 365 | * which means that $$notify will not take place and no $watch events 366 | * will be triggered. 367 | * 368 | * @param {object} snap a Firebase snapshot 369 | * @return {boolean} true if any data changed 370 | * @protected 371 | */ 372 | $$updated: function(snap) { 373 | var changed = false; 374 | var rec = this.$getRecord($firebaseUtils.getKey(snap)); 375 | if( angular.isObject(rec) ) { 376 | // apply changes to the record 377 | changed = $firebaseUtils.updateRec(rec, snap); 378 | $firebaseUtils.applyDefaults(rec, this.$$defaults); 379 | } 380 | return changed; 381 | }, 382 | 383 | /** 384 | * Called whenever an item changes order (moves) on the server. 385 | * This method should set $priority to the updated value and return true if 386 | * the record should actually be moved. It should not actually apply the move 387 | * operation. 388 | * 389 | * If this method returns false, then the record will not be moved in the array 390 | * and no $watch listeners will be notified. (When true, $$process is invoked 391 | * which invokes $$notify) 392 | * 393 | * @param {object} snap a Firebase snapshot 394 | * @param {string} prevChild 395 | * @protected 396 | */ 397 | $$moved: function(snap/*, prevChild*/) { 398 | var rec = this.$getRecord($firebaseUtils.getKey(snap)); 399 | if( angular.isObject(rec) ) { 400 | rec.$priority = snap.getPriority(); 401 | return true; 402 | } 403 | return false; 404 | }, 405 | 406 | /** 407 | * Called whenever a security error or other problem causes the listeners to become 408 | * invalid. This is generally an unrecoverable error. 409 | * 410 | * @param {Object} err which will have a `code` property and possibly a `message` 411 | * @protected 412 | */ 413 | $$error: function(err) { 414 | $log.error(err); 415 | this.$destroy(err); 416 | }, 417 | 418 | /** 419 | * Returns ID for a given record 420 | * @param {object} rec 421 | * @returns {string||null} 422 | * @protected 423 | */ 424 | $$getKey: function(rec) { 425 | return angular.isObject(rec)? rec.$id : null; 426 | }, 427 | 428 | /** 429 | * Handles placement of recs in the array, sending notifications, 430 | * and other internals. Called by the synchronization process 431 | * after $$added, $$updated, $$moved, and $$removed return a truthy value. 432 | * 433 | * @param {string} event one of child_added, child_removed, child_moved, or child_changed 434 | * @param {object} rec 435 | * @param {string} [prevChild] 436 | * @protected 437 | */ 438 | $$process: function(event, rec, prevChild) { 439 | var key = this.$$getKey(rec); 440 | var changed = false; 441 | var curPos; 442 | switch(event) { 443 | case 'child_added': 444 | curPos = this.$indexFor(key); 445 | break; 446 | case 'child_moved': 447 | curPos = this.$indexFor(key); 448 | this._spliceOut(key); 449 | break; 450 | case 'child_removed': 451 | // remove record from the array 452 | changed = this._spliceOut(key) !== null; 453 | break; 454 | case 'child_changed': 455 | changed = true; 456 | break; 457 | default: 458 | throw new Error('Invalid event type: ' + event); 459 | } 460 | if( angular.isDefined(curPos) ) { 461 | // add it to the array 462 | changed = this._addAfter(rec, prevChild) !== curPos; 463 | } 464 | if( changed ) { 465 | // send notifications to anybody monitoring $watch 466 | this.$$notify(event, key, prevChild); 467 | } 468 | return changed; 469 | }, 470 | 471 | /** 472 | * Used to trigger notifications for listeners registered using $watch. This method is 473 | * typically invoked internally by the $$process method. 474 | * 475 | * @param {string} event 476 | * @param {string} key 477 | * @param {string} [prevChild] 478 | * @protected 479 | */ 480 | $$notify: function(event, key, prevChild) { 481 | var eventData = {event: event, key: key}; 482 | if( angular.isDefined(prevChild) ) { 483 | eventData.prevChild = prevChild; 484 | } 485 | angular.forEach(this._observers, function(parts) { 486 | parts[0].call(parts[1], eventData); 487 | }); 488 | }, 489 | 490 | /** 491 | * Used to insert a new record into the array at a specific position. If prevChild is 492 | * null, is inserted first, if prevChild is not found, it is inserted last, otherwise, 493 | * it goes immediately after prevChild. 494 | * 495 | * @param {object} rec 496 | * @param {string|null} prevChild 497 | * @private 498 | */ 499 | _addAfter: function(rec, prevChild) { 500 | var i; 501 | if( prevChild === null ) { 502 | i = 0; 503 | } 504 | else { 505 | i = this.$indexFor(prevChild)+1; 506 | if( i === 0 ) { i = this.$list.length; } 507 | } 508 | this.$list.splice(i, 0, rec); 509 | this._indexCache[this.$$getKey(rec)] = i; 510 | return i; 511 | }, 512 | 513 | /** 514 | * Removes a record from the array by calling splice. If the item is found 515 | * this method returns it. Otherwise, this method returns null. 516 | * 517 | * @param {string} key 518 | * @returns {object|null} 519 | * @private 520 | */ 521 | _spliceOut: function(key) { 522 | var i = this.$indexFor(key); 523 | if( i > -1 ) { 524 | delete this._indexCache[key]; 525 | return this.$list.splice(i, 1)[0]; 526 | } 527 | return null; 528 | }, 529 | 530 | /** 531 | * Resolves a variable which may contain an integer or an item that exists in this array. 532 | * Returns the item or null if it does not exist. 533 | * 534 | * @param indexOrItem 535 | * @returns {*} 536 | * @private 537 | */ 538 | _resolveItem: function(indexOrItem) { 539 | var list = this.$list; 540 | if( angular.isNumber(indexOrItem) && indexOrItem >= 0 && list.length >= indexOrItem ) { 541 | return list[indexOrItem]; 542 | } 543 | else if( angular.isObject(indexOrItem) ) { 544 | // it must be an item in this array; it's not sufficient for it just to have 545 | // a $id or even a $id that is in the array, it must be an actual record 546 | // the fastest way to determine this is to use $getRecord (to avoid iterating all recs) 547 | // and compare the two 548 | var key = this.$$getKey(indexOrItem); 549 | var rec = this.$getRecord(key); 550 | return rec === indexOrItem? rec : null; 551 | } 552 | return null; 553 | }, 554 | 555 | /** 556 | * Throws an error if $destroy has been called. Should be used for any function 557 | * which tries to write data back to $firebase. 558 | * @param {string} method 559 | * @private 560 | */ 561 | _assertNotDestroyed: function(method) { 562 | if( this._isDestroyed ) { 563 | throw new Error('Cannot call ' + method + ' method on a destroyed $firebaseArray object'); 564 | } 565 | } 566 | }; 567 | 568 | /** 569 | * This method allows FirebaseArray to be inherited by child classes. Methods passed into this 570 | * function will be added onto the array's prototype. They can override existing methods as 571 | * well. 572 | * 573 | * In addition to passing additional methods, it is also possible to pass in a class function. 574 | * The prototype on that class function will be preserved, and it will inherit from 575 | * FirebaseArray. It's also possible to do both, passing a class to inherit and additional 576 | * methods to add onto the prototype. 577 | * 578 | *

 579 |        * var ExtendedArray = $firebaseArray.$extend({
 580 |        *    // add a method onto the prototype that sums all items in the array
 581 |        *    getSum: function() {
 582 |        *       var ct = 0;
 583 |        *       angular.forEach(this.$list, function(rec) { ct += rec.x; });
 584 |         *      return ct;
 585 |        *    }
 586 |        * });
 587 |        *
 588 |        * // use our new factory in place of $firebaseArray
 589 |        * var list = new ExtendedArray(ref);
 590 |        * 
591 | * 592 | * @param {Function} [ChildClass] a child class which should inherit FirebaseArray 593 | * @param {Object} [methods] a list of functions to add onto the prototype 594 | * @returns {Function} a child class suitable for use with $firebase (this will be ChildClass if provided) 595 | * @static 596 | */ 597 | FirebaseArray.$extend = function(ChildClass, methods) { 598 | if( arguments.length === 1 && angular.isObject(ChildClass) ) { 599 | methods = ChildClass; 600 | ChildClass = function(ref) { 601 | if( !(this instanceof ChildClass) ) { 602 | return new ChildClass(ref); 603 | } 604 | FirebaseArray.apply(this, arguments); 605 | return this.$list; 606 | }; 607 | } 608 | return $firebaseUtils.inherit(ChildClass, FirebaseArray, methods); 609 | }; 610 | 611 | function ArraySyncManager(firebaseArray) { 612 | function destroy(err) { 613 | if( !sync.isDestroyed ) { 614 | sync.isDestroyed = true; 615 | var ref = firebaseArray.$ref(); 616 | ref.off('child_added', created); 617 | ref.off('child_moved', moved); 618 | ref.off('child_changed', updated); 619 | ref.off('child_removed', removed); 620 | firebaseArray = null; 621 | initComplete(err||'destroyed'); 622 | } 623 | } 624 | 625 | function init($list) { 626 | var ref = firebaseArray.$ref(); 627 | 628 | // listen for changes at the Firebase instance 629 | ref.on('child_added', created, error); 630 | ref.on('child_moved', moved, error); 631 | ref.on('child_changed', updated, error); 632 | ref.on('child_removed', removed, error); 633 | 634 | // determine when initial load is completed 635 | ref.once('value', function(snap) { 636 | if (angular.isArray(snap.val())) { 637 | $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.'); 638 | } 639 | 640 | initComplete(null, $list); 641 | }, initComplete); 642 | } 643 | 644 | // call initComplete(), do not call this directly 645 | function _initComplete(err, result) { 646 | if( !isResolved ) { 647 | isResolved = true; 648 | if( err ) { def.reject(err); } 649 | else { def.resolve(result); } 650 | } 651 | } 652 | 653 | var def = $firebaseUtils.defer(); 654 | var created = function(snap, prevChild) { 655 | waitForResolution(firebaseArray.$$added(snap, prevChild), function(rec) { 656 | firebaseArray.$$process('child_added', rec, prevChild); 657 | }); 658 | }; 659 | var updated = function(snap) { 660 | var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap)); 661 | if( rec ) { 662 | waitForResolution(firebaseArray.$$updated(snap), function() { 663 | firebaseArray.$$process('child_changed', rec); 664 | }); 665 | } 666 | }; 667 | var moved = function(snap, prevChild) { 668 | var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap)); 669 | if( rec ) { 670 | waitForResolution(firebaseArray.$$moved(snap, prevChild), function() { 671 | firebaseArray.$$process('child_moved', rec, prevChild); 672 | }); 673 | } 674 | }; 675 | var removed = function(snap) { 676 | var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap)); 677 | if( rec ) { 678 | waitForResolution(firebaseArray.$$removed(snap), function() { 679 | firebaseArray.$$process('child_removed', rec); 680 | }); 681 | } 682 | }; 683 | 684 | function waitForResolution(maybePromise, callback) { 685 | var promise = $q.when(maybePromise); 686 | promise.then(function(result){ 687 | if (result) { 688 | callback(result); 689 | } 690 | }); 691 | if (!isResolved) { 692 | resolutionPromises.push(promise); 693 | } 694 | } 695 | 696 | var resolutionPromises = []; 697 | var isResolved = false; 698 | var error = $firebaseUtils.batch(function(err) { 699 | _initComplete(err); 700 | if( firebaseArray ) { 701 | firebaseArray.$$error(err); 702 | } 703 | }); 704 | var initComplete = $firebaseUtils.batch(_initComplete); 705 | 706 | var sync = { 707 | destroy: destroy, 708 | isDestroyed: false, 709 | init: init, 710 | ready: function() { return def.promise.then(function(result){ 711 | return $q.all(resolutionPromises).then(function(){ 712 | return result; 713 | }); 714 | }); } 715 | }; 716 | 717 | return sync; 718 | } 719 | 720 | return FirebaseArray; 721 | } 722 | ]); 723 | 724 | /** @deprecated */ 725 | angular.module('firebase').factory('$FirebaseArray', ['$log', '$firebaseArray', 726 | function($log, $firebaseArray) { 727 | return function() { 728 | $log.warn('$FirebaseArray has been renamed. Use $firebaseArray instead.'); 729 | return $firebaseArray.apply(null, arguments); 730 | }; 731 | } 732 | ]); 733 | })(); 734 | 735 | (function() { 736 | 'use strict'; 737 | var FirebaseAuth; 738 | 739 | // Define a service which provides user authentication and management. 740 | angular.module('firebase').factory('$firebaseAuth', [ 741 | '$q', '$firebaseUtils', function($q, $firebaseUtils) { 742 | /** 743 | * This factory returns an object allowing you to manage the client's authentication state. 744 | * 745 | * @param {Firebase} ref A Firebase reference to authenticate. 746 | * @return {object} An object containing methods for authenticating clients, retrieving 747 | * authentication state, and managing users. 748 | */ 749 | return function(ref) { 750 | var auth = new FirebaseAuth($q, $firebaseUtils, ref); 751 | return auth.construct(); 752 | }; 753 | } 754 | ]); 755 | 756 | FirebaseAuth = function($q, $firebaseUtils, ref) { 757 | this._q = $q; 758 | this._utils = $firebaseUtils; 759 | if (typeof ref === 'string') { 760 | throw new Error('Please provide a Firebase reference instead of a URL when creating a `$firebaseAuth` object.'); 761 | } 762 | this._ref = ref; 763 | this._initialAuthResolver = this._initAuthResolver(); 764 | }; 765 | 766 | FirebaseAuth.prototype = { 767 | construct: function() { 768 | this._object = { 769 | // Authentication methods 770 | $authWithCustomToken: this.authWithCustomToken.bind(this), 771 | $authAnonymously: this.authAnonymously.bind(this), 772 | $authWithPassword: this.authWithPassword.bind(this), 773 | $authWithOAuthPopup: this.authWithOAuthPopup.bind(this), 774 | $authWithOAuthRedirect: this.authWithOAuthRedirect.bind(this), 775 | $authWithOAuthToken: this.authWithOAuthToken.bind(this), 776 | $unauth: this.unauth.bind(this), 777 | 778 | // Authentication state methods 779 | $onAuth: this.onAuth.bind(this), 780 | $getAuth: this.getAuth.bind(this), 781 | $requireAuth: this.requireAuth.bind(this), 782 | $waitForAuth: this.waitForAuth.bind(this), 783 | 784 | // User management methods 785 | $createUser: this.createUser.bind(this), 786 | $changePassword: this.changePassword.bind(this), 787 | $changeEmail: this.changeEmail.bind(this), 788 | $removeUser: this.removeUser.bind(this), 789 | $resetPassword: this.resetPassword.bind(this) 790 | }; 791 | 792 | return this._object; 793 | }, 794 | 795 | 796 | /********************/ 797 | /* Authentication */ 798 | /********************/ 799 | 800 | /** 801 | * Authenticates the Firebase reference with a custom authentication token. 802 | * 803 | * @param {string} authToken An authentication token or a Firebase Secret. A Firebase Secret 804 | * should only be used for authenticating a server process and provides full read / write 805 | * access to the entire Firebase. 806 | * @param {Object} [options] An object containing optional client arguments, such as configuring 807 | * session persistence. 808 | * @return {Promise} A promise fulfilled with an object containing authentication data. 809 | */ 810 | authWithCustomToken: function(authToken, options) { 811 | var deferred = this._q.defer(); 812 | 813 | try { 814 | this._ref.authWithCustomToken(authToken, this._utils.makeNodeResolver(deferred), options); 815 | } catch (error) { 816 | deferred.reject(error); 817 | } 818 | 819 | return deferred.promise; 820 | }, 821 | 822 | /** 823 | * Authenticates the Firebase reference anonymously. 824 | * 825 | * @param {Object} [options] An object containing optional client arguments, such as configuring 826 | * session persistence. 827 | * @return {Promise} A promise fulfilled with an object containing authentication data. 828 | */ 829 | authAnonymously: function(options) { 830 | var deferred = this._q.defer(); 831 | 832 | try { 833 | this._ref.authAnonymously(this._utils.makeNodeResolver(deferred), options); 834 | } catch (error) { 835 | deferred.reject(error); 836 | } 837 | 838 | return deferred.promise; 839 | }, 840 | 841 | /** 842 | * Authenticates the Firebase reference with an email/password user. 843 | * 844 | * @param {Object} credentials An object containing email and password attributes corresponding 845 | * to the user account. 846 | * @param {Object} [options] An object containing optional client arguments, such as configuring 847 | * session persistence. 848 | * @return {Promise} A promise fulfilled with an object containing authentication data. 849 | */ 850 | authWithPassword: function(credentials, options) { 851 | var deferred = this._q.defer(); 852 | 853 | try { 854 | this._ref.authWithPassword(credentials, this._utils.makeNodeResolver(deferred), options); 855 | } catch (error) { 856 | deferred.reject(error); 857 | } 858 | 859 | return deferred.promise; 860 | }, 861 | 862 | /** 863 | * Authenticates the Firebase reference with the OAuth popup flow. 864 | * 865 | * @param {string} provider The unique string identifying the OAuth provider to authenticate 866 | * with, e.g. google. 867 | * @param {Object} [options] An object containing optional client arguments, such as configuring 868 | * session persistence. 869 | * @return {Promise} A promise fulfilled with an object containing authentication data. 870 | */ 871 | authWithOAuthPopup: function(provider, options) { 872 | var deferred = this._q.defer(); 873 | 874 | try { 875 | this._ref.authWithOAuthPopup(provider, this._utils.makeNodeResolver(deferred), options); 876 | } catch (error) { 877 | deferred.reject(error); 878 | } 879 | 880 | return deferred.promise; 881 | }, 882 | 883 | /** 884 | * Authenticates the Firebase reference with the OAuth redirect flow. 885 | * 886 | * @param {string} provider The unique string identifying the OAuth provider to authenticate 887 | * with, e.g. google. 888 | * @param {Object} [options] An object containing optional client arguments, such as configuring 889 | * session persistence. 890 | * @return {Promise} A promise fulfilled with an object containing authentication data. 891 | */ 892 | authWithOAuthRedirect: function(provider, options) { 893 | var deferred = this._q.defer(); 894 | 895 | try { 896 | this._ref.authWithOAuthRedirect(provider, this._utils.makeNodeResolver(deferred), options); 897 | } catch (error) { 898 | deferred.reject(error); 899 | } 900 | 901 | return deferred.promise; 902 | }, 903 | 904 | /** 905 | * Authenticates the Firebase reference with an OAuth token. 906 | * 907 | * @param {string} provider The unique string identifying the OAuth provider to authenticate 908 | * with, e.g. google. 909 | * @param {string|Object} credentials Either a string, such as an OAuth 2.0 access token, or an 910 | * Object of key / value pairs, such as a set of OAuth 1.0a credentials. 911 | * @param {Object} [options] An object containing optional client arguments, such as configuring 912 | * session persistence. 913 | * @return {Promise} A promise fulfilled with an object containing authentication data. 914 | */ 915 | authWithOAuthToken: function(provider, credentials, options) { 916 | var deferred = this._q.defer(); 917 | 918 | try { 919 | this._ref.authWithOAuthToken(provider, credentials, this._utils.makeNodeResolver(deferred), options); 920 | } catch (error) { 921 | deferred.reject(error); 922 | } 923 | 924 | return deferred.promise; 925 | }, 926 | 927 | /** 928 | * Unauthenticates the Firebase reference. 929 | */ 930 | unauth: function() { 931 | if (this.getAuth() !== null) { 932 | this._ref.unauth(); 933 | } 934 | }, 935 | 936 | 937 | /**************************/ 938 | /* Authentication State */ 939 | /**************************/ 940 | /** 941 | * Asynchronously fires the provided callback with the current authentication data every time 942 | * the authentication data changes. It also fires as soon as the authentication data is 943 | * retrieved from the server. 944 | * 945 | * @param {function} callback A callback that fires when the client's authenticate state 946 | * changes. If authenticated, the callback will be passed an object containing authentication 947 | * data according to the provider used to authenticate. Otherwise, it will be passed null. 948 | * @param {string} [context] If provided, this object will be used as this when calling your 949 | * callback. 950 | * @return {function} A function which can be used to deregister the provided callback. 951 | */ 952 | onAuth: function(callback, context) { 953 | var self = this; 954 | 955 | var fn = this._utils.debounce(callback, context, 0); 956 | this._ref.onAuth(fn); 957 | 958 | // Return a method to detach the `onAuth()` callback. 959 | return function() { 960 | self._ref.offAuth(fn); 961 | }; 962 | }, 963 | 964 | /** 965 | * Synchronously retrieves the current authentication data. 966 | * 967 | * @return {Object} The client's authentication data. 968 | */ 969 | getAuth: function() { 970 | return this._ref.getAuth(); 971 | }, 972 | 973 | /** 974 | * Helper onAuth() callback method for the two router-related methods. 975 | * 976 | * @param {boolean} rejectIfAuthDataIsNull Determines if the returned promise should be 977 | * resolved or rejected upon an unauthenticated client. 978 | * @return {Promise} A promise fulfilled with the client's authentication state or 979 | * rejected if the client is unauthenticated and rejectIfAuthDataIsNull is true. 980 | */ 981 | _routerMethodOnAuthPromise: function(rejectIfAuthDataIsNull) { 982 | var ref = this._ref, utils = this._utils; 983 | // wait for the initial auth state to resolve; on page load we have to request auth state 984 | // asynchronously so we don't want to resolve router methods or flash the wrong state 985 | return this._initialAuthResolver.then(function() { 986 | // auth state may change in the future so rather than depend on the initially resolved state 987 | // we also check the auth data (synchronously) if a new promise is requested, ensuring we resolve 988 | // to the current auth state and not a stale/initial state 989 | var authData = ref.getAuth(), res = null; 990 | if (rejectIfAuthDataIsNull && authData === null) { 991 | res = utils.reject("AUTH_REQUIRED"); 992 | } 993 | else { 994 | res = utils.resolve(authData); 995 | } 996 | return res; 997 | }); 998 | }, 999 | 1000 | /** 1001 | * Helper that returns a promise which resolves when the initial auth state has been 1002 | * fetched from the Firebase server. This never rejects and resolves to undefined. 1003 | * 1004 | * @return {Promise} A promise fulfilled when the server returns initial auth state. 1005 | */ 1006 | _initAuthResolver: function() { 1007 | var ref = this._ref; 1008 | return this._utils.promise(function(resolve) { 1009 | function callback() { 1010 | // Turn off this onAuth() callback since we just needed to get the authentication data once. 1011 | ref.offAuth(callback); 1012 | resolve(); 1013 | } 1014 | ref.onAuth(callback); 1015 | }); 1016 | }, 1017 | 1018 | /** 1019 | * Utility method which can be used in a route's resolve() method to require that a route has 1020 | * a logged in client. 1021 | * 1022 | * @returns {Promise} A promise fulfilled with the client's current authentication 1023 | * state or rejected if the client is not authenticated. 1024 | */ 1025 | requireAuth: function() { 1026 | return this._routerMethodOnAuthPromise(true); 1027 | }, 1028 | 1029 | /** 1030 | * Utility method which can be used in a route's resolve() method to grab the current 1031 | * authentication data. 1032 | * 1033 | * @returns {Promise} A promise fulfilled with the client's current authentication 1034 | * state, which will be null if the client is not authenticated. 1035 | */ 1036 | waitForAuth: function() { 1037 | return this._routerMethodOnAuthPromise(false); 1038 | }, 1039 | 1040 | 1041 | /*********************/ 1042 | /* User Management */ 1043 | /*********************/ 1044 | /** 1045 | * Creates a new email/password user. Note that this function only creates the user, if you 1046 | * wish to log in as the newly created user, call $authWithPassword() after the promise for 1047 | * this method has been resolved. 1048 | * 1049 | * @param {Object} credentials An object containing the email and password of the user to create. 1050 | * @return {Promise} A promise fulfilled with the user object, which contains the 1051 | * uid of the created user. 1052 | */ 1053 | createUser: function(credentials) { 1054 | var deferred = this._q.defer(); 1055 | 1056 | // Throw an error if they are trying to pass in separate string arguments 1057 | if (typeof credentials === "string") { 1058 | throw new Error("$createUser() expects an object containing 'email' and 'password', but got a string."); 1059 | } 1060 | 1061 | try { 1062 | this._ref.createUser(credentials, this._utils.makeNodeResolver(deferred)); 1063 | } catch (error) { 1064 | deferred.reject(error); 1065 | } 1066 | 1067 | return deferred.promise; 1068 | }, 1069 | 1070 | /** 1071 | * Changes the password for an email/password user. 1072 | * 1073 | * @param {Object} credentials An object containing the email, old password, and new password of 1074 | * the user whose password is to change. 1075 | * @return {Promise<>} An empty promise fulfilled once the password change is complete. 1076 | */ 1077 | changePassword: function(credentials) { 1078 | var deferred = this._q.defer(); 1079 | 1080 | // Throw an error if they are trying to pass in separate string arguments 1081 | if (typeof credentials === "string") { 1082 | throw new Error("$changePassword() expects an object containing 'email', 'oldPassword', and 'newPassword', but got a string."); 1083 | } 1084 | 1085 | try { 1086 | this._ref.changePassword(credentials, this._utils.makeNodeResolver(deferred)); 1087 | } catch (error) { 1088 | deferred.reject(error); 1089 | } 1090 | 1091 | return deferred.promise; 1092 | }, 1093 | 1094 | /** 1095 | * Changes the email for an email/password user. 1096 | * 1097 | * @param {Object} credentials An object containing the old email, new email, and password of 1098 | * the user whose email is to change. 1099 | * @return {Promise<>} An empty promise fulfilled once the email change is complete. 1100 | */ 1101 | changeEmail: function(credentials) { 1102 | var deferred = this._q.defer(); 1103 | 1104 | if (typeof this._ref.changeEmail !== 'function') { 1105 | throw new Error("$firebaseAuth.$changeEmail() requires Firebase version 2.1.0 or greater."); 1106 | } else if (typeof credentials === 'string') { 1107 | throw new Error("$changeEmail() expects an object containing 'oldEmail', 'newEmail', and 'password', but got a string."); 1108 | } 1109 | 1110 | try { 1111 | this._ref.changeEmail(credentials, this._utils.makeNodeResolver(deferred)); 1112 | } catch (error) { 1113 | deferred.reject(error); 1114 | } 1115 | 1116 | return deferred.promise; 1117 | }, 1118 | 1119 | /** 1120 | * Removes an email/password user. 1121 | * 1122 | * @param {Object} credentials An object containing the email and password of the user to remove. 1123 | * @return {Promise<>} An empty promise fulfilled once the user is removed. 1124 | */ 1125 | removeUser: function(credentials) { 1126 | var deferred = this._q.defer(); 1127 | 1128 | // Throw an error if they are trying to pass in separate string arguments 1129 | if (typeof credentials === "string") { 1130 | throw new Error("$removeUser() expects an object containing 'email' and 'password', but got a string."); 1131 | } 1132 | 1133 | try { 1134 | this._ref.removeUser(credentials, this._utils.makeNodeResolver(deferred)); 1135 | } catch (error) { 1136 | deferred.reject(error); 1137 | } 1138 | 1139 | return deferred.promise; 1140 | }, 1141 | 1142 | 1143 | /** 1144 | * Sends a password reset email to an email/password user. 1145 | * 1146 | * @param {Object} credentials An object containing the email of the user to send a reset 1147 | * password email to. 1148 | * @return {Promise<>} An empty promise fulfilled once the reset password email is sent. 1149 | */ 1150 | resetPassword: function(credentials) { 1151 | var deferred = this._q.defer(); 1152 | 1153 | // Throw an error if they are trying to pass in a string argument 1154 | if (typeof credentials === "string") { 1155 | throw new Error("$resetPassword() expects an object containing 'email', but got a string."); 1156 | } 1157 | 1158 | try { 1159 | this._ref.resetPassword(credentials, this._utils.makeNodeResolver(deferred)); 1160 | } catch (error) { 1161 | deferred.reject(error); 1162 | } 1163 | 1164 | return deferred.promise; 1165 | } 1166 | }; 1167 | })(); 1168 | 1169 | (function() { 1170 | 'use strict'; 1171 | /** 1172 | * Creates and maintains a synchronized object, with 2-way bindings between Angular and Firebase. 1173 | * 1174 | * Implementations of this class are contracted to provide the following internal methods, 1175 | * which are used by the synchronization process and 3-way bindings: 1176 | * $$updated - called whenever a change occurs (a value event from Firebase) 1177 | * $$error - called when listeners are canceled due to a security error 1178 | * $$notify - called to update $watch listeners and trigger updates to 3-way bindings 1179 | * $ref - called to obtain the underlying Firebase reference 1180 | * 1181 | * Instead of directly modifying this class, one should generally use the $extend 1182 | * method to add or change how methods behave: 1183 | * 1184 | *

1185 |    * var ExtendedObject = $firebaseObject.$extend({
1186 |    *    // add a new method to the prototype
1187 |    *    foo: function() { return 'bar'; },
1188 |    * });
1189 |    *
1190 |    * var obj = new ExtendedObject(ref);
1191 |    * 
1192 | */ 1193 | angular.module('firebase').factory('$firebaseObject', [ 1194 | '$parse', '$firebaseUtils', '$log', 1195 | function($parse, $firebaseUtils, $log) { 1196 | /** 1197 | * Creates a synchronized object with 2-way bindings between Angular and Firebase. 1198 | * 1199 | * @param {Firebase} ref 1200 | * @returns {FirebaseObject} 1201 | * @constructor 1202 | */ 1203 | function FirebaseObject(ref) { 1204 | if( !(this instanceof FirebaseObject) ) { 1205 | return new FirebaseObject(ref); 1206 | } 1207 | // These are private config props and functions used internally 1208 | // they are collected here to reduce clutter in console.log and forEach 1209 | this.$$conf = { 1210 | // synchronizes data to Firebase 1211 | sync: new ObjectSyncManager(this, ref), 1212 | // stores the Firebase ref 1213 | ref: ref, 1214 | // synchronizes $scope variables with this object 1215 | binding: new ThreeWayBinding(this), 1216 | // stores observers registered with $watch 1217 | listeners: [] 1218 | }; 1219 | 1220 | // this bit of magic makes $$conf non-enumerable and non-configurable 1221 | // and non-writable (its properties are still writable but the ref cannot be replaced) 1222 | // we redundantly assign it above so the IDE can relax 1223 | Object.defineProperty(this, '$$conf', { 1224 | value: this.$$conf 1225 | }); 1226 | 1227 | this.$id = $firebaseUtils.getKey(ref.ref()); 1228 | this.$priority = null; 1229 | 1230 | $firebaseUtils.applyDefaults(this, this.$$defaults); 1231 | 1232 | // start synchronizing data with Firebase 1233 | this.$$conf.sync.init(); 1234 | } 1235 | 1236 | FirebaseObject.prototype = { 1237 | /** 1238 | * Saves all data on the FirebaseObject back to Firebase. 1239 | * @returns a promise which will resolve after the save is completed. 1240 | */ 1241 | $save: function () { 1242 | var self = this; 1243 | var ref = self.$ref(); 1244 | var data = $firebaseUtils.toJSON(self); 1245 | return $firebaseUtils.doSet(ref, data).then(function() { 1246 | self.$$notify(); 1247 | return self.$ref(); 1248 | }); 1249 | }, 1250 | 1251 | /** 1252 | * Removes all keys from the FirebaseObject and also removes 1253 | * the remote data from the server. 1254 | * 1255 | * @returns a promise which will resolve after the op completes 1256 | */ 1257 | $remove: function() { 1258 | var self = this; 1259 | $firebaseUtils.trimKeys(self, {}); 1260 | self.$value = null; 1261 | return $firebaseUtils.doRemove(self.$ref()).then(function() { 1262 | self.$$notify(); 1263 | return self.$ref(); 1264 | }); 1265 | }, 1266 | 1267 | /** 1268 | * The loaded method is invoked after the initial batch of data arrives from the server. 1269 | * When this resolves, all data which existed prior to calling $asObject() is now cached 1270 | * locally in the object. 1271 | * 1272 | * As a shortcut is also possible to pass resolve/reject methods directly into this 1273 | * method just as they would be passed to .then() 1274 | * 1275 | * @param {Function} resolve 1276 | * @param {Function} reject 1277 | * @returns a promise which resolves after initial data is downloaded from Firebase 1278 | */ 1279 | $loaded: function(resolve, reject) { 1280 | var promise = this.$$conf.sync.ready(); 1281 | if (arguments.length) { 1282 | // allow this method to be called just like .then 1283 | // by passing any arguments on to .then 1284 | promise = promise.then.call(promise, resolve, reject); 1285 | } 1286 | return promise; 1287 | }, 1288 | 1289 | /** 1290 | * @returns {Firebase} the original Firebase instance used to create this object. 1291 | */ 1292 | $ref: function () { 1293 | return this.$$conf.ref; 1294 | }, 1295 | 1296 | /** 1297 | * Creates a 3-way data sync between this object, the Firebase server, and a 1298 | * scope variable. This means that any changes made to the scope variable are 1299 | * pushed to Firebase, and vice versa. 1300 | * 1301 | * If scope emits a $destroy event, the binding is automatically severed. Otherwise, 1302 | * it is possible to unbind the scope variable by using the `unbind` function 1303 | * passed into the resolve method. 1304 | * 1305 | * Can only be bound to one scope variable at a time. If a second is attempted, 1306 | * the promise will be rejected with an error. 1307 | * 1308 | * @param {object} scope 1309 | * @param {string} varName 1310 | * @returns a promise which resolves to an unbind method after data is set in scope 1311 | */ 1312 | $bindTo: function (scope, varName) { 1313 | var self = this; 1314 | return self.$loaded().then(function () { 1315 | return self.$$conf.binding.bindTo(scope, varName); 1316 | }); 1317 | }, 1318 | 1319 | /** 1320 | * Listeners passed into this method are notified whenever a new change is received 1321 | * from the server. Each invocation is sent an object containing 1322 | * { type: 'value', key: 'my_firebase_id' } 1323 | * 1324 | * This method returns an unbind function that can be used to detach the listener. 1325 | * 1326 | * @param {Function} cb 1327 | * @param {Object} [context] 1328 | * @returns {Function} invoke to stop observing events 1329 | */ 1330 | $watch: function (cb, context) { 1331 | var list = this.$$conf.listeners; 1332 | list.push([cb, context]); 1333 | // an off function for cancelling the listener 1334 | return function () { 1335 | var i = list.findIndex(function (parts) { 1336 | return parts[0] === cb && parts[1] === context; 1337 | }); 1338 | if (i > -1) { 1339 | list.splice(i, 1); 1340 | } 1341 | }; 1342 | }, 1343 | 1344 | /** 1345 | * Informs $firebase to stop sending events and clears memory being used 1346 | * by this object (delete's its local content). 1347 | */ 1348 | $destroy: function(err) { 1349 | var self = this; 1350 | if (!self.$isDestroyed) { 1351 | self.$isDestroyed = true; 1352 | self.$$conf.sync.destroy(err); 1353 | self.$$conf.binding.destroy(); 1354 | $firebaseUtils.each(self, function (v, k) { 1355 | delete self[k]; 1356 | }); 1357 | } 1358 | }, 1359 | 1360 | /** 1361 | * Called by $firebase whenever an item is changed at the server. 1362 | * This method must exist on any objectFactory passed into $firebase. 1363 | * 1364 | * It should return true if any changes were made, otherwise `$$notify` will 1365 | * not be invoked. 1366 | * 1367 | * @param {object} snap a Firebase snapshot 1368 | * @return {boolean} true if any changes were made. 1369 | */ 1370 | $$updated: function (snap) { 1371 | // applies new data to this object 1372 | var changed = $firebaseUtils.updateRec(this, snap); 1373 | // applies any defaults set using $$defaults 1374 | $firebaseUtils.applyDefaults(this, this.$$defaults); 1375 | // returning true here causes $$notify to be triggered 1376 | return changed; 1377 | }, 1378 | 1379 | /** 1380 | * Called whenever a security error or other problem causes the listeners to become 1381 | * invalid. This is generally an unrecoverable error. 1382 | * @param {Object} err which will have a `code` property and possibly a `message` 1383 | */ 1384 | $$error: function (err) { 1385 | // prints an error to the console (via Angular's logger) 1386 | $log.error(err); 1387 | // frees memory and cancels any remaining listeners 1388 | this.$destroy(err); 1389 | }, 1390 | 1391 | /** 1392 | * Called internally by $bindTo when data is changed in $scope. 1393 | * Should apply updates to this record but should not call 1394 | * notify(). 1395 | */ 1396 | $$scopeUpdated: function(newData) { 1397 | // we use a one-directional loop to avoid feedback with 3-way bindings 1398 | // since set() is applied locally anyway, this is still performant 1399 | var def = $firebaseUtils.defer(); 1400 | this.$ref().set($firebaseUtils.toJSON(newData), $firebaseUtils.makeNodeResolver(def)); 1401 | return def.promise; 1402 | }, 1403 | 1404 | /** 1405 | * Updates any bound scope variables and 1406 | * notifies listeners registered with $watch 1407 | */ 1408 | $$notify: function() { 1409 | var self = this, list = this.$$conf.listeners.slice(); 1410 | // be sure to do this after setting up data and init state 1411 | angular.forEach(list, function (parts) { 1412 | parts[0].call(parts[1], {event: 'value', key: self.$id}); 1413 | }); 1414 | }, 1415 | 1416 | /** 1417 | * Overrides how Angular.forEach iterates records on this object so that only 1418 | * fields stored in Firebase are part of the iteration. To include meta fields like 1419 | * $id and $priority in the iteration, utilize for(key in obj) instead. 1420 | */ 1421 | forEach: function(iterator, context) { 1422 | return $firebaseUtils.each(this, iterator, context); 1423 | } 1424 | }; 1425 | 1426 | /** 1427 | * This method allows FirebaseObject to be copied into a new factory. Methods passed into this 1428 | * function will be added onto the object's prototype. They can override existing methods as 1429 | * well. 1430 | * 1431 | * In addition to passing additional methods, it is also possible to pass in a class function. 1432 | * The prototype on that class function will be preserved, and it will inherit from 1433 | * FirebaseObject. It's also possible to do both, passing a class to inherit and additional 1434 | * methods to add onto the prototype. 1435 | * 1436 | * Once a factory is obtained by this method, it can be passed into $firebase as the 1437 | * `objectFactory` parameter: 1438 | * 1439 | *

1440 |        * var MyFactory = $firebaseObject.$extend({
1441 |        *    // add a method onto the prototype that prints a greeting
1442 |        *    getGreeting: function() {
1443 |        *       return 'Hello ' + this.first_name + ' ' + this.last_name + '!';
1444 |        *    }
1445 |        * });
1446 |        *
1447 |        * // use our new factory in place of $firebaseObject
1448 |        * var obj = $firebase(ref, {objectFactory: MyFactory}).$asObject();
1449 |        * 
1450 | * 1451 | * @param {Function} [ChildClass] a child class which should inherit FirebaseObject 1452 | * @param {Object} [methods] a list of functions to add onto the prototype 1453 | * @returns {Function} a new factory suitable for use with $firebase 1454 | */ 1455 | FirebaseObject.$extend = function(ChildClass, methods) { 1456 | if( arguments.length === 1 && angular.isObject(ChildClass) ) { 1457 | methods = ChildClass; 1458 | ChildClass = function(ref) { 1459 | if( !(this instanceof ChildClass) ) { 1460 | return new ChildClass(ref); 1461 | } 1462 | FirebaseObject.apply(this, arguments); 1463 | }; 1464 | } 1465 | return $firebaseUtils.inherit(ChildClass, FirebaseObject, methods); 1466 | }; 1467 | 1468 | /** 1469 | * Creates a three-way data binding on a scope variable. 1470 | * 1471 | * @param {FirebaseObject} rec 1472 | * @returns {*} 1473 | * @constructor 1474 | */ 1475 | function ThreeWayBinding(rec) { 1476 | this.subs = []; 1477 | this.scope = null; 1478 | this.key = null; 1479 | this.rec = rec; 1480 | } 1481 | 1482 | ThreeWayBinding.prototype = { 1483 | assertNotBound: function(varName) { 1484 | if( this.scope ) { 1485 | var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' + 1486 | this.key + '; one binding per instance ' + 1487 | '(call unbind method or create another FirebaseObject instance)'; 1488 | $log.error(msg); 1489 | return $firebaseUtils.reject(msg); 1490 | } 1491 | }, 1492 | 1493 | bindTo: function(scope, varName) { 1494 | function _bind(self) { 1495 | var sending = false; 1496 | var parsed = $parse(varName); 1497 | var rec = self.rec; 1498 | self.scope = scope; 1499 | self.varName = varName; 1500 | 1501 | function equals(scopeValue) { 1502 | return angular.equals(scopeValue, rec) && 1503 | scopeValue.$priority === rec.$priority && 1504 | scopeValue.$value === rec.$value; 1505 | } 1506 | 1507 | function setScope(rec) { 1508 | parsed.assign(scope, $firebaseUtils.scopeData(rec)); 1509 | } 1510 | 1511 | var send = $firebaseUtils.debounce(function(val) { 1512 | var scopeData = $firebaseUtils.scopeData(val); 1513 | rec.$$scopeUpdated(scopeData) 1514 | ['finally'](function() { 1515 | sending = false; 1516 | if(!scopeData.hasOwnProperty('$value')){ 1517 | delete rec.$value; 1518 | delete parsed(scope).$value; 1519 | } 1520 | } 1521 | ); 1522 | }, 50, 500); 1523 | 1524 | var scopeUpdated = function(newVal) { 1525 | newVal = newVal[0]; 1526 | if( !equals(newVal) ) { 1527 | sending = true; 1528 | send(newVal); 1529 | } 1530 | }; 1531 | 1532 | var recUpdated = function() { 1533 | if( !sending && !equals(parsed(scope)) ) { 1534 | setScope(rec); 1535 | } 1536 | }; 1537 | 1538 | // $watch will not check any vars prefixed with $, so we 1539 | // manually check $priority and $value using this method 1540 | function watchExp(){ 1541 | var obj = parsed(scope); 1542 | return [obj, obj.$priority, obj.$value]; 1543 | } 1544 | 1545 | setScope(rec); 1546 | self.subs.push(scope.$on('$destroy', self.unbind.bind(self))); 1547 | 1548 | // monitor scope for any changes 1549 | self.subs.push(scope.$watch(watchExp, scopeUpdated, true)); 1550 | 1551 | // monitor the object for changes 1552 | self.subs.push(rec.$watch(recUpdated)); 1553 | 1554 | return self.unbind.bind(self); 1555 | } 1556 | 1557 | return this.assertNotBound(varName) || _bind(this); 1558 | }, 1559 | 1560 | unbind: function() { 1561 | if( this.scope ) { 1562 | angular.forEach(this.subs, function(unbind) { 1563 | unbind(); 1564 | }); 1565 | this.subs = []; 1566 | this.scope = null; 1567 | this.key = null; 1568 | } 1569 | }, 1570 | 1571 | destroy: function() { 1572 | this.unbind(); 1573 | this.rec = null; 1574 | } 1575 | }; 1576 | 1577 | function ObjectSyncManager(firebaseObject, ref) { 1578 | function destroy(err) { 1579 | if( !sync.isDestroyed ) { 1580 | sync.isDestroyed = true; 1581 | ref.off('value', applyUpdate); 1582 | firebaseObject = null; 1583 | initComplete(err||'destroyed'); 1584 | } 1585 | } 1586 | 1587 | function init() { 1588 | ref.on('value', applyUpdate, error); 1589 | ref.once('value', function(snap) { 1590 | if (angular.isArray(snap.val())) { 1591 | $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.'); 1592 | } 1593 | 1594 | initComplete(null); 1595 | }, initComplete); 1596 | } 1597 | 1598 | // call initComplete(); do not call this directly 1599 | function _initComplete(err) { 1600 | if( !isResolved ) { 1601 | isResolved = true; 1602 | if( err ) { def.reject(err); } 1603 | else { def.resolve(firebaseObject); } 1604 | } 1605 | } 1606 | 1607 | var isResolved = false; 1608 | var def = $firebaseUtils.defer(); 1609 | var applyUpdate = $firebaseUtils.batch(function(snap) { 1610 | var changed = firebaseObject.$$updated(snap); 1611 | if( changed ) { 1612 | // notifies $watch listeners and 1613 | // updates $scope if bound to a variable 1614 | firebaseObject.$$notify(); 1615 | } 1616 | }); 1617 | var error = $firebaseUtils.batch(function(err) { 1618 | _initComplete(err); 1619 | if( firebaseObject ) { 1620 | firebaseObject.$$error(err); 1621 | } 1622 | }); 1623 | var initComplete = $firebaseUtils.batch(_initComplete); 1624 | 1625 | var sync = { 1626 | isDestroyed: false, 1627 | destroy: destroy, 1628 | init: init, 1629 | ready: function() { return def.promise; } 1630 | }; 1631 | return sync; 1632 | } 1633 | 1634 | return FirebaseObject; 1635 | } 1636 | ]); 1637 | 1638 | /** @deprecated */ 1639 | angular.module('firebase').factory('$FirebaseObject', ['$log', '$firebaseObject', 1640 | function($log, $firebaseObject) { 1641 | return function() { 1642 | $log.warn('$FirebaseObject has been renamed. Use $firebaseObject instead.'); 1643 | return $firebaseObject.apply(null, arguments); 1644 | }; 1645 | } 1646 | ]); 1647 | })(); 1648 | 1649 | (function() { 1650 | 'use strict'; 1651 | 1652 | angular.module("firebase") 1653 | 1654 | /** @deprecated */ 1655 | .factory("$firebase", function() { 1656 | return function() { 1657 | throw new Error('$firebase has been removed. You may instantiate $firebaseArray and $firebaseObject ' + 1658 | 'directly now. For simple write operations, just use the Firebase ref directly. ' + 1659 | 'See the AngularFire 1.0.0 changelog for details: https://www.firebase.com/docs/web/libraries/angular/changelog.html'); 1660 | }; 1661 | }); 1662 | 1663 | })(); 1664 | 1665 | 'use strict'; 1666 | 1667 | // Shim Array.indexOf for IE compatibility. 1668 | if (!Array.prototype.indexOf) { 1669 | Array.prototype.indexOf = function (searchElement, fromIndex) { 1670 | if (this === undefined || this === null) { 1671 | throw new TypeError("'this' is null or not defined"); 1672 | } 1673 | // Hack to convert object.length to a UInt32 1674 | // jshint -W016 1675 | var length = this.length >>> 0; 1676 | fromIndex = +fromIndex || 0; 1677 | // jshint +W016 1678 | 1679 | if (Math.abs(fromIndex) === Infinity) { 1680 | fromIndex = 0; 1681 | } 1682 | 1683 | if (fromIndex < 0) { 1684 | fromIndex += length; 1685 | if (fromIndex < 0) { 1686 | fromIndex = 0; 1687 | } 1688 | } 1689 | 1690 | for (;fromIndex < length; fromIndex++) { 1691 | if (this[fromIndex] === searchElement) { 1692 | return fromIndex; 1693 | } 1694 | } 1695 | 1696 | return -1; 1697 | }; 1698 | } 1699 | 1700 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind 1701 | if (!Function.prototype.bind) { 1702 | Function.prototype.bind = function (oThis) { 1703 | if (typeof this !== "function") { 1704 | // closest thing possible to the ECMAScript 5 1705 | // internal IsCallable function 1706 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 1707 | } 1708 | 1709 | var aArgs = Array.prototype.slice.call(arguments, 1), 1710 | fToBind = this, 1711 | fNOP = function () {}, 1712 | fBound = function () { 1713 | return fToBind.apply(this instanceof fNOP && oThis 1714 | ? this 1715 | : oThis, 1716 | aArgs.concat(Array.prototype.slice.call(arguments))); 1717 | }; 1718 | 1719 | fNOP.prototype = this.prototype; 1720 | fBound.prototype = new fNOP(); 1721 | 1722 | return fBound; 1723 | }; 1724 | } 1725 | 1726 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex 1727 | if (!Array.prototype.findIndex) { 1728 | Object.defineProperty(Array.prototype, 'findIndex', { 1729 | enumerable: false, 1730 | configurable: true, 1731 | writable: true, 1732 | value: function(predicate) { 1733 | if (this == null) { 1734 | throw new TypeError('Array.prototype.find called on null or undefined'); 1735 | } 1736 | if (typeof predicate !== 'function') { 1737 | throw new TypeError('predicate must be a function'); 1738 | } 1739 | var list = Object(this); 1740 | var length = list.length >>> 0; 1741 | var thisArg = arguments[1]; 1742 | var value; 1743 | 1744 | for (var i = 0; i < length; i++) { 1745 | if (i in list) { 1746 | value = list[i]; 1747 | if (predicate.call(thisArg, value, i, list)) { 1748 | return i; 1749 | } 1750 | } 1751 | } 1752 | return -1; 1753 | } 1754 | }); 1755 | } 1756 | 1757 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create 1758 | if (typeof Object.create != 'function') { 1759 | (function () { 1760 | var F = function () {}; 1761 | Object.create = function (o) { 1762 | if (arguments.length > 1) { 1763 | throw new Error('Second argument not supported'); 1764 | } 1765 | if (o === null) { 1766 | throw new Error('Cannot set a null [[Prototype]]'); 1767 | } 1768 | if (typeof o != 'object') { 1769 | throw new TypeError('Argument must be an object'); 1770 | } 1771 | F.prototype = o; 1772 | return new F(); 1773 | }; 1774 | })(); 1775 | } 1776 | 1777 | // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys 1778 | if (!Object.keys) { 1779 | Object.keys = (function () { 1780 | 'use strict'; 1781 | var hasOwnProperty = Object.prototype.hasOwnProperty, 1782 | hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), 1783 | dontEnums = [ 1784 | 'toString', 1785 | 'toLocaleString', 1786 | 'valueOf', 1787 | 'hasOwnProperty', 1788 | 'isPrototypeOf', 1789 | 'propertyIsEnumerable', 1790 | 'constructor' 1791 | ], 1792 | dontEnumsLength = dontEnums.length; 1793 | 1794 | return function (obj) { 1795 | if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { 1796 | throw new TypeError('Object.keys called on non-object'); 1797 | } 1798 | 1799 | var result = [], prop, i; 1800 | 1801 | for (prop in obj) { 1802 | if (hasOwnProperty.call(obj, prop)) { 1803 | result.push(prop); 1804 | } 1805 | } 1806 | 1807 | if (hasDontEnumBug) { 1808 | for (i = 0; i < dontEnumsLength; i++) { 1809 | if (hasOwnProperty.call(obj, dontEnums[i])) { 1810 | result.push(dontEnums[i]); 1811 | } 1812 | } 1813 | } 1814 | return result; 1815 | }; 1816 | }()); 1817 | } 1818 | 1819 | // http://ejohn.org/blog/objectgetprototypeof/ 1820 | if ( typeof Object.getPrototypeOf !== "function" ) { 1821 | if ( typeof "test".__proto__ === "object" ) { 1822 | Object.getPrototypeOf = function(object){ 1823 | return object.__proto__; 1824 | }; 1825 | } else { 1826 | Object.getPrototypeOf = function(object){ 1827 | // May break if the constructor has been tampered with 1828 | return object.constructor.prototype; 1829 | }; 1830 | } 1831 | } 1832 | 1833 | (function() { 1834 | 'use strict'; 1835 | 1836 | angular.module('firebase') 1837 | .factory('$firebaseConfig', ["$firebaseArray", "$firebaseObject", "$injector", 1838 | function($firebaseArray, $firebaseObject, $injector) { 1839 | return function(configOpts) { 1840 | // make a copy we can modify 1841 | var opts = angular.extend({}, configOpts); 1842 | // look up factories if passed as string names 1843 | if( typeof opts.objectFactory === 'string' ) { 1844 | opts.objectFactory = $injector.get(opts.objectFactory); 1845 | } 1846 | if( typeof opts.arrayFactory === 'string' ) { 1847 | opts.arrayFactory = $injector.get(opts.arrayFactory); 1848 | } 1849 | // extend defaults and return 1850 | return angular.extend({ 1851 | arrayFactory: $firebaseArray, 1852 | objectFactory: $firebaseObject 1853 | }, opts); 1854 | }; 1855 | } 1856 | ]) 1857 | 1858 | .factory('$firebaseUtils', ["$q", "$timeout", "$rootScope", 1859 | function($q, $timeout, $rootScope) { 1860 | 1861 | // ES6 style promises polyfill for angular 1.2.x 1862 | // Copied from angular 1.3.x implementation: https://github.com/angular/angular.js/blob/v1.3.5/src/ng/q.js#L539 1863 | function Q(resolver) { 1864 | if (!angular.isFunction(resolver)) { 1865 | throw new Error('missing resolver function'); 1866 | } 1867 | 1868 | var deferred = $q.defer(); 1869 | 1870 | function resolveFn(value) { 1871 | deferred.resolve(value); 1872 | } 1873 | 1874 | function rejectFn(reason) { 1875 | deferred.reject(reason); 1876 | } 1877 | 1878 | resolver(resolveFn, rejectFn); 1879 | 1880 | return deferred.promise; 1881 | } 1882 | 1883 | var utils = { 1884 | /** 1885 | * Returns a function which, each time it is invoked, will gather up the values until 1886 | * the next "tick" in the Angular compiler process. Then they are all run at the same 1887 | * time to avoid multiple cycles of the digest loop. Internally, this is done using $evalAsync() 1888 | * 1889 | * @param {Function} action 1890 | * @param {Object} [context] 1891 | * @returns {Function} 1892 | */ 1893 | batch: function(action, context) { 1894 | return function() { 1895 | var args = Array.prototype.slice.call(arguments, 0); 1896 | utils.compile(function() { 1897 | action.apply(context, args); 1898 | }); 1899 | }; 1900 | }, 1901 | 1902 | /** 1903 | * A rudimentary debounce method 1904 | * @param {function} fn the function to debounce 1905 | * @param {object} [ctx] the `this` context to set in fn 1906 | * @param {int} wait number of milliseconds to pause before sending out after each invocation 1907 | * @param {int} [maxWait] max milliseconds to wait before sending out, defaults to wait * 10 or 100 1908 | */ 1909 | debounce: function(fn, ctx, wait, maxWait) { 1910 | var start, cancelTimer, args, runScheduledForNextTick; 1911 | if( typeof(ctx) === 'number' ) { 1912 | maxWait = wait; 1913 | wait = ctx; 1914 | ctx = null; 1915 | } 1916 | 1917 | if( typeof wait !== 'number' ) { 1918 | throw new Error('Must provide a valid integer for wait. Try 0 for a default'); 1919 | } 1920 | if( typeof(fn) !== 'function' ) { 1921 | throw new Error('Must provide a valid function to debounce'); 1922 | } 1923 | if( !maxWait ) { maxWait = wait*10 || 100; } 1924 | 1925 | // clears the current wait timer and creates a new one 1926 | // however, if maxWait is exceeded, calls runNow() on the next tick. 1927 | function resetTimer() { 1928 | if( cancelTimer ) { 1929 | cancelTimer(); 1930 | cancelTimer = null; 1931 | } 1932 | if( start && Date.now() - start > maxWait ) { 1933 | if(!runScheduledForNextTick){ 1934 | runScheduledForNextTick = true; 1935 | utils.compile(runNow); 1936 | } 1937 | } 1938 | else { 1939 | if( !start ) { start = Date.now(); } 1940 | cancelTimer = utils.wait(runNow, wait); 1941 | } 1942 | } 1943 | 1944 | // Clears the queue and invokes the debounced function with the most recent arguments 1945 | function runNow() { 1946 | cancelTimer = null; 1947 | start = null; 1948 | runScheduledForNextTick = false; 1949 | fn.apply(ctx, args); 1950 | } 1951 | 1952 | function debounced() { 1953 | args = Array.prototype.slice.call(arguments, 0); 1954 | resetTimer(); 1955 | } 1956 | debounced.running = function() { 1957 | return start > 0; 1958 | }; 1959 | 1960 | return debounced; 1961 | }, 1962 | 1963 | assertValidRef: function(ref, msg) { 1964 | if( !angular.isObject(ref) || 1965 | typeof(ref.ref) !== 'function' || 1966 | typeof(ref.ref().transaction) !== 'function' ) { 1967 | throw new Error(msg || 'Invalid Firebase reference'); 1968 | } 1969 | }, 1970 | 1971 | // http://stackoverflow.com/questions/7509831/alternative-for-the-deprecated-proto 1972 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create 1973 | inherit: function(ChildClass, ParentClass, methods) { 1974 | var childMethods = ChildClass.prototype; 1975 | ChildClass.prototype = Object.create(ParentClass.prototype); 1976 | ChildClass.prototype.constructor = ChildClass; // restoring proper constructor for child class 1977 | angular.forEach(Object.keys(childMethods), function(k) { 1978 | ChildClass.prototype[k] = childMethods[k]; 1979 | }); 1980 | if( angular.isObject(methods) ) { 1981 | angular.extend(ChildClass.prototype, methods); 1982 | } 1983 | return ChildClass; 1984 | }, 1985 | 1986 | getPrototypeMethods: function(inst, iterator, context) { 1987 | var methods = {}; 1988 | var objProto = Object.getPrototypeOf({}); 1989 | var proto = angular.isFunction(inst) && angular.isObject(inst.prototype)? 1990 | inst.prototype : Object.getPrototypeOf(inst); 1991 | while(proto && proto !== objProto) { 1992 | for (var key in proto) { 1993 | // we only invoke each key once; if a super is overridden it's skipped here 1994 | if (proto.hasOwnProperty(key) && !methods.hasOwnProperty(key)) { 1995 | methods[key] = true; 1996 | iterator.call(context, proto[key], key, proto); 1997 | } 1998 | } 1999 | proto = Object.getPrototypeOf(proto); 2000 | } 2001 | }, 2002 | 2003 | getPublicMethods: function(inst, iterator, context) { 2004 | utils.getPrototypeMethods(inst, function(m, k) { 2005 | if( typeof(m) === 'function' && k.charAt(0) !== '_' ) { 2006 | iterator.call(context, m, k); 2007 | } 2008 | }); 2009 | }, 2010 | 2011 | defer: $q.defer, 2012 | 2013 | reject: $q.reject, 2014 | 2015 | resolve: $q.when, 2016 | 2017 | //TODO: Remove false branch and use only angular implementation when we drop angular 1.2.x support. 2018 | promise: angular.isFunction($q) ? $q : Q, 2019 | 2020 | makeNodeResolver:function(deferred){ 2021 | return function(err,result){ 2022 | if(err === null){ 2023 | if(arguments.length > 2){ 2024 | result = Array.prototype.slice.call(arguments,1); 2025 | } 2026 | deferred.resolve(result); 2027 | } 2028 | else { 2029 | deferred.reject(err); 2030 | } 2031 | }; 2032 | }, 2033 | 2034 | wait: function(fn, wait) { 2035 | var to = $timeout(fn, wait||0); 2036 | return function() { 2037 | if( to ) { 2038 | $timeout.cancel(to); 2039 | to = null; 2040 | } 2041 | }; 2042 | }, 2043 | 2044 | compile: function(fn) { 2045 | return $rootScope.$evalAsync(fn||function() {}); 2046 | }, 2047 | 2048 | deepCopy: function(obj) { 2049 | if( !angular.isObject(obj) ) { return obj; } 2050 | var newCopy = angular.isArray(obj) ? obj.slice() : angular.extend({}, obj); 2051 | for (var key in newCopy) { 2052 | if (newCopy.hasOwnProperty(key)) { 2053 | if (angular.isObject(newCopy[key])) { 2054 | newCopy[key] = utils.deepCopy(newCopy[key]); 2055 | } 2056 | } 2057 | } 2058 | return newCopy; 2059 | }, 2060 | 2061 | trimKeys: function(dest, source) { 2062 | utils.each(dest, function(v,k) { 2063 | if( !source.hasOwnProperty(k) ) { 2064 | delete dest[k]; 2065 | } 2066 | }); 2067 | }, 2068 | 2069 | scopeData: function(dataOrRec) { 2070 | var data = { 2071 | $id: dataOrRec.$id, 2072 | $priority: dataOrRec.$priority 2073 | }; 2074 | var hasPublicProp = false; 2075 | utils.each(dataOrRec, function(v,k) { 2076 | hasPublicProp = true; 2077 | data[k] = utils.deepCopy(v); 2078 | }); 2079 | if(!hasPublicProp && dataOrRec.hasOwnProperty('$value')){ 2080 | data.$value = dataOrRec.$value; 2081 | } 2082 | return data; 2083 | }, 2084 | 2085 | updateRec: function(rec, snap) { 2086 | var data = snap.val(); 2087 | var oldData = angular.extend({}, rec); 2088 | 2089 | // deal with primitives 2090 | if( !angular.isObject(data) ) { 2091 | rec.$value = data; 2092 | data = {}; 2093 | } 2094 | else { 2095 | delete rec.$value; 2096 | } 2097 | 2098 | // apply changes: remove old keys, insert new data, set priority 2099 | utils.trimKeys(rec, data); 2100 | angular.extend(rec, data); 2101 | rec.$priority = snap.getPriority(); 2102 | 2103 | return !angular.equals(oldData, rec) || 2104 | oldData.$value !== rec.$value || 2105 | oldData.$priority !== rec.$priority; 2106 | }, 2107 | 2108 | applyDefaults: function(rec, defaults) { 2109 | if( angular.isObject(defaults) ) { 2110 | angular.forEach(defaults, function(v,k) { 2111 | if( !rec.hasOwnProperty(k) ) { 2112 | rec[k] = v; 2113 | } 2114 | }); 2115 | } 2116 | return rec; 2117 | }, 2118 | 2119 | dataKeys: function(obj) { 2120 | var out = []; 2121 | utils.each(obj, function(v,k) { 2122 | out.push(k); 2123 | }); 2124 | return out; 2125 | }, 2126 | 2127 | each: function(obj, iterator, context) { 2128 | if(angular.isObject(obj)) { 2129 | for (var k in obj) { 2130 | if (obj.hasOwnProperty(k)) { 2131 | var c = k.charAt(0); 2132 | if( c !== '_' && c !== '$' && c !== '.' ) { 2133 | iterator.call(context, obj[k], k, obj); 2134 | } 2135 | } 2136 | } 2137 | } 2138 | else if(angular.isArray(obj)) { 2139 | for(var i = 0, len = obj.length; i < len; i++) { 2140 | iterator.call(context, obj[i], i, obj); 2141 | } 2142 | } 2143 | return obj; 2144 | }, 2145 | 2146 | /** 2147 | * A utility for retrieving a Firebase reference or DataSnapshot's 2148 | * key name. This is backwards-compatible with `name()` from Firebase 2149 | * 1.x.x and `key()` from Firebase 2.0.0+. Once support for Firebase 2150 | * 1.x.x is dropped in AngularFire, this helper can be removed. 2151 | */ 2152 | getKey: function(refOrSnapshot) { 2153 | return (typeof refOrSnapshot.key === 'function') ? refOrSnapshot.key() : refOrSnapshot.name(); 2154 | }, 2155 | 2156 | /** 2157 | * A utility for converting records to JSON objects 2158 | * which we can save into Firebase. It asserts valid 2159 | * keys and strips off any items prefixed with $. 2160 | * 2161 | * If the rec passed into this method has a toJSON() 2162 | * method, that will be used in place of the custom 2163 | * functionality here. 2164 | * 2165 | * @param rec 2166 | * @returns {*} 2167 | */ 2168 | toJSON: function(rec) { 2169 | var dat; 2170 | if( !angular.isObject(rec) ) { 2171 | rec = {$value: rec}; 2172 | } 2173 | if (angular.isFunction(rec.toJSON)) { 2174 | dat = rec.toJSON(); 2175 | } 2176 | else { 2177 | dat = {}; 2178 | utils.each(rec, function (v, k) { 2179 | dat[k] = stripDollarPrefixedKeys(v); 2180 | }); 2181 | } 2182 | if( angular.isDefined(rec.$value) && Object.keys(dat).length === 0 && rec.$value !== null ) { 2183 | dat['.value'] = rec.$value; 2184 | } 2185 | if( angular.isDefined(rec.$priority) && Object.keys(dat).length > 0 && rec.$priority !== null ) { 2186 | dat['.priority'] = rec.$priority; 2187 | } 2188 | angular.forEach(dat, function(v,k) { 2189 | if (k.match(/[.$\[\]#\/]/) && k !== '.value' && k !== '.priority' ) { 2190 | throw new Error('Invalid key ' + k + ' (cannot contain .$[]#)'); 2191 | } 2192 | else if( angular.isUndefined(v) ) { 2193 | throw new Error('Key '+k+' was undefined. Cannot pass undefined in JSON. Use null instead.'); 2194 | } 2195 | }); 2196 | return dat; 2197 | }, 2198 | 2199 | doSet: function(ref, data) { 2200 | var def = utils.defer(); 2201 | if( angular.isFunction(ref.set) || !angular.isObject(data) ) { 2202 | // this is not a query, just do a flat set 2203 | ref.set(data, utils.makeNodeResolver(def)); 2204 | } 2205 | else { 2206 | var dataCopy = angular.extend({}, data); 2207 | // this is a query, so we will replace all the elements 2208 | // of this query with the value provided, but not blow away 2209 | // the entire Firebase path 2210 | ref.once('value', function(snap) { 2211 | snap.forEach(function(ss) { 2212 | if( !dataCopy.hasOwnProperty(utils.getKey(ss)) ) { 2213 | dataCopy[utils.getKey(ss)] = null; 2214 | } 2215 | }); 2216 | ref.ref().update(dataCopy, utils.makeNodeResolver(def)); 2217 | }, function(err) { 2218 | def.reject(err); 2219 | }); 2220 | } 2221 | return def.promise; 2222 | }, 2223 | 2224 | doRemove: function(ref) { 2225 | var def = utils.defer(); 2226 | if( angular.isFunction(ref.remove) ) { 2227 | // ref is not a query, just do a flat remove 2228 | ref.remove(utils.makeNodeResolver(def)); 2229 | } 2230 | else { 2231 | // ref is a query so let's only remove the 2232 | // items in the query and not the entire path 2233 | ref.once('value', function(snap) { 2234 | var promises = []; 2235 | snap.forEach(function(ss) { 2236 | var d = utils.defer(); 2237 | promises.push(d.promise); 2238 | ss.ref().remove(utils.makeNodeResolver(def)); 2239 | }); 2240 | utils.allPromises(promises) 2241 | .then(function() { 2242 | def.resolve(ref); 2243 | }, 2244 | function(err){ 2245 | def.reject(err); 2246 | } 2247 | ); 2248 | }, function(err) { 2249 | def.reject(err); 2250 | }); 2251 | } 2252 | return def.promise; 2253 | }, 2254 | 2255 | /** 2256 | * AngularFire version number. 2257 | */ 2258 | VERSION: '1.1.2', 2259 | 2260 | allPromises: $q.all.bind($q) 2261 | }; 2262 | 2263 | return utils; 2264 | } 2265 | ]); 2266 | 2267 | function stripDollarPrefixedKeys(data) { 2268 | if( !angular.isObject(data) ) { return data; } 2269 | var out = angular.isArray(data)? [] : {}; 2270 | angular.forEach(data, function(v,k) { 2271 | if(typeof k !== 'string' || k.charAt(0) !== '$') { 2272 | out[k] = stripDollarPrefixedKeys(v); 2273 | } 2274 | }); 2275 | return out; 2276 | } 2277 | })(); 2278 | --------------------------------------------------------------------------------