Firebase, AngularJS and GeoFire - wait for response from factory
Asked Answered
E

1

1

I'm starting to learn a lot of things related to the hybrid apps coding. I decided to go on and use Ionic framework, based on AngularJS (this gives me the occasion to learn it), and use Firebase as my backend solution.

I want to setup a map in my app, so I would locate the user and some points of interests around him. I found out GeoFire existed, so I started working with it.

My problem : I fail in displaying a user's location saved into Firebase. I query it using GeoFire, and get the response (console.log it) but it won't update the scope variable.

Here is the JS code :

myApp.factory("MyLoc", function(){
    var firebaseRef = new Firebase("https://myfirebase.firebaseio.com/geofire/");
    var geoFire = new GeoFire(firebaseRef);

    /*    geoFire.set("user_loc", [37.785326, -122.405696]).then(function() {
     console.log("Provided key has been added to GeoFire");
     }, function(error) {
     console.log("Error: " + error);
     });*/

     return geoFire.get("user_loc").then(function(location) {
        if (location === null) {
            console.log("Provided key is not in GeoFire");
            return 0;
        }
        else {
            console.log("Provided key has a location of " + location);
            return location;
        }
    }, function(error) {
        console.log("Error: " + error);
        return 0;
    });
})

myApp.controller("firstCtrl", function($scope, MyLoc) {
    $scope.data = {};

    $scope.data.myLoc = MyLoc;
});

The HTML just displays it :

{{data.myLoc}}

Since I'm an Angular N00b, I guess I'm missing something obvious. I guess I should use a promise or something similar, just can't figure out how ! Can someone help me, please ? :)

Thank you very much bros !

[UPDATE]

Okay so, here is the answer to the first question !

MyLoc.then(function(data){
  $scope.data.myLoc = data;

  $scope.$apply();
});

However I had to use

$scope.$apply();

for the HTML to display it, apparently it doesn't updates right away. Do you have any idea why ? I understand Angular has to be notified of a change for it to display, as explained very nicely here : http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

What I don't understand, is why it's the case here ? Why is Angular unaware of the change ?

Thanks again !

Edom answered 12/11, 2014 at 22:25 Comment(3)
To learn more about why you need to call $apply, see jimhoskins.com/2012/12/17/angularjs-and-apply.htmlAlleged
I just posted that link ^^ The question is why is Angular unaware of this change, since it's wrapped in Angular event type ?Edom
It is not wrapped in an Angular event. The callback that you pass into then can and will fire at any moment. So Angular has no knowledge of it, unless you tell it. Don't take my or anyone's word for that though: just set a breakpoint on the line $scope.data.myLoc and check the stack trace, it is very likely to be different than what you expect it to be.Alleged
A
4

You're being bitten here by the fact that Firebase's API is asynchronous. You cannot return value from an asynchronous call in the way you're trying:

This is the pertinent fragment of your code:

myApp.factory("MyLoc", function(){
    var firebaseRef = new Firebase("https://myfirebase.firebaseio.com/geofire/");
    var geoFire = new GeoFire(firebaseRef);

     return geoFire.get("user_loc").then(function(location) {
        if (location === null) {
            console.log("Provided key is not in GeoFire");
            return 0;
        }
        else {
            console.log("Provided key has a location of " + location);
            return location;
        }
    }, function(error) {
        console.log("Error: " + error);
        return 0;
    });
})

Notice those functions that you're passing into then? Those are called callback functions and are key to Firebase's (and the modern web's in general) asynchronous operation. It's easier to see why your construct won't work if you extract those callbacks into regular, global, named functions:

myApp.factory("MyLoc", function(){
    var firebaseRef = new Firebase("https://myfirebase.firebaseio.com/geofire/");
    var geoFire = new GeoFire(firebaseRef);

     return geoFire.get("user_loc").then(onGeoFireResult, onGeoFireError);
})

function onGeoFireResult(location) {
    if (location === null) {
        console.log("Provided key is not in GeoFire");
        return 0;
    }
    else {
        console.log("Provided key has a location of " + location);
        return location;
    }
}

function onGeoFireError(error) {
    console.log("Error: " + error);
    return 0;
}

When you call geoFire.get("user_loc") it performs a request to the server. That request may take some time and we don't want the browser to be blocked. So instead of waiting for the request to complete, we pass in a function to be called when the server responds with the requested data. Since things may go wrong, we also pass in a function to be called in case of an error.

So it is important to realize that the onGeoFireResult and onGeoFireError run at a different time than when you call geoFire.get("user_loc"). So you cannot return their results from the encapsulating function. Instead you have to deal with the concept of a promise: a structure that at some point will have the data you requested.

If you want to continue using Firebase and Angular without AngularFire, I highly recommend that you follow this video tutorial: https://www.youtube.com/watch?v=3YVlcFyxBr4 . Spending an hour or so on that will save you dozens of question here.

Alternatively set two breakpoints: one on onGeoFireResult and one on the line after geoFire.get("user_loc") and see which breakpoint triggers first.

Alleged answered 13/11, 2014 at 14:28 Comment(6)
Hello Frank and thank you very much for the answer ! First, I should have written that I do understand the theory : I followed Thinkster.io tutorial and viewed Egghead.io videos. So everything you say makes perfect sense to me. I also know very well about callback functions. Congratulations for the level of details, anyway :) Now, I may understand this, but still don't know how to handle my controller and how to wait for the answer. I do use AngularFire, so if it contains an answer it's fine for me ! Basically I need to understand how a controller can wait for the answer from a factory...Edom
David explains the asynchronous nature of Firebase in one of his earlier videos.Alleged
Ok, I will take a look at them, anyway, I already created a free trial account :) Thank you for your help.Edom
If you have a suggestion on how to use AngularFire to accomplish what I'm targeting WITHOUT getting the call to GeoFire in the controller, I would gladly take it anyway !Edom
The videos are on youtube too and don't require payment there.Alleged
Hey Frank, I updated the question, see if you would like to help me understand this :) Thanks again !Edom

© 2022 - 2024 — McMap. All rights reserved.