Search code examples
angularjsfirebasebinddatapicker

3-way bind date format with AngularJS and Firebase


In all my projects I'm running into the same problem trying to figure out what the best way is to have a proper way of binding a timestamp (date) with my view and Firebase. I'm currently using the md-datepicker directive from Angular-Material but unfortunately this directive only supports a date object, and because Firebase won't accept a date object the value will always be empty.

In several projects I'm storing epoch timestamps in milliseconds in Firebase. Is there any possibility to have a proper 3-way data binding with epoch timestamps in Firebase and the md-datepicker?

Thanks!


Solution

  • Solved it by creating my own bind function and converting to an ISO date string when saving it and converting it into a date object when fetching it.

    It user $parse to evaluate expressions in the given scope and to update the local variable in that scope. Here is the service:

    angular.module('symApp').service('realtimeService', function($rootScope, $q, $log, $window, $state,
                                                               $firebaseObject, $parse) {
    
    // Utilities
    var regexIso8601 = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
    var convertDateStringsToDates = function (input) {
        // Ignore things that aren't objects.
        if (typeof input !== "object") return input;
        for (var key in input) {
            if (!input.hasOwnProperty(key)) continue;
            var value = input[key];
            var match;
            // Check for string properties which look like dates.
            if (typeof value === "string" && (match = value.match(regexIso8601))) {
                var milliseconds = Date.parse(match[0]);
                if (!isNaN(milliseconds)) {
                    input[key] = new Date(milliseconds);
                }
            } else if (typeof value === "object") {
                // Recurse into object
                convertDateStringsToDates(value);
            }
        }
    };
    
    this.bindVar = function(scope, remote_path, local_var_name) {
    // This function binds a local variable in scope to a remote variable in firebase
    // and handles any dates by converting them into iso formatted strings.
    // Note: Arrays inside the object are not supported
    
    // Parse the local variable name so we can interact with the scope variable
    var parsed = $parse(local_var_name);
    
    // Grab the reference to the realtime database
    var ref = firebase.database().ref().child(remote_path);
    
    // Create the firebase object and set watchers to bind the data
    var remote = $firebaseObject(ref);
    remote.$loaded().then(function() {
    
        // Watch for changes and call $save on firebaseObject
        // Have to do this when loaded otherwise we'll get a change from nothing to null and write null to realtime database...
    
        // Local watcher
        scope.$watch(local_var_name,
            function(value) {
                // This is called when the local variable changes
                $log.debug(local_var_name, 'local value changed');
    
                // Convert to JSON to change dates to strings
                var local = angular.fromJson(angular.toJson(parsed(scope)));
    
                // Check if local has changed with respect to remote (stops us from saving when we don't need to)
                if(!angular.equals(remote.value, local)){
                    remote.value = local;
                    remote.$save();
                    $log.debug(local_var_name, 'saved to remote with value: ', remote.value);
                }
            },
            true
        );
    
        // Remote watcher
        scope.$watch(
            function () {
                return remote.value;
            },
            function(value) {
                // If the firebase value has changed, then update the local value
                $log.debug(local_var_name, 'remote value changed');
    
                // Convert date strings from firebase into date objects before setting scope variable
                var remote_with_date_objects = $.extend(true,{},remote.value);
                convertDateStringsToDates(remote_with_date_objects);
    
                if(!angular.equals(remote_with_date_objects,parsed(scope))){
                    parsed.assign(scope, remote_with_date_objects);
                    $log.debug(local_var_name, 'updated with remote value: ', remote_with_date_objects);
                }
            },
            true
        );
    
        });
    };
    

    });

    And here is how you use it in your controller to bind a scope variable:

    realtimeService.bindVar($scope, 'datum/charter', 'charter');
    

    My $scope.charter object has its own objects and arrays within it and everything seems to sync correctly. When saving data it uses angular.toJson and angular.fromJson to convert all the date objects to strings. When loading remote data it uses a custom function convertDateStringsToDates() to convert any date strings within the object to date objects whenever the remote is updated.