Search code examples
angularjsrestexpressmongooseng-bind

Cannot bind response object from POST to my view


I've been trying to solve this for hours, and have tried to find a working solution on stack overflow and other sites, but none worked so far.

The Issue

I am building a travelogue web app that allows users to log and view their journeys (e.g. a road trip). At the moment I am implementing the feature that lets users view a particular journey in a separate view which they have selected from a list of journeys. I pass down the id of the selected journey and retrieve an Object from MongoDB. I implemented this using POST. It works in that the _id of the selected journey is passed in the request, then used to identify the document with Model.findById - then the response yields the data. The data is bound to $scope.selection.

But while $scope.selection contains the data (when logged to console), I cannot seem to bind it to the view (called view_journey). Meaning, whenever I want to access, e.g. selection.name in my view_journey.html, the expression or ng-bind is left empty.

app.js

$scope.viewJourneybyId = function(id) {
    var selectOne = { _id : id };
    $http.post('http://localhost:8080/view_journey', selectOne).
    success(function(data) {
        $scope.selection = data;
        $scope.$apply();
        console.log("POST found the right Journey");
        console.log($scope.selection);
    }).error(function(data) {
        console.error("POST encountered an error");
    })    
}  

server.js

app.post("/view_journey", function(request, response, next) {
   Journeys.findById(request.body._id, function(error, selection) {
      if (error)
         response.send(error)
      response.json({ message: 'Journey found!', selection });
      });
   });

index.html

<tr ng-repeat="journey in journeys">
   <td>
      <a href="#/view_journey" ng-click="viewJourneybyId(journey._id)">
      {{journey.name}}</a>
   </td>
   <td>...</td>     
</tr>

view_journey.html

<div class="panel panel-default">
   <div class="panel-heading">
     <h2 ng-bind="selection.name"></h2>
     <!-- For Debugging -->
     ID <span ng-bind="selection._id">
   </div>
   <div class="panel-body">
     <table class=table>
        <caption>{{selection.desc}}</caption>
        ...
     </table>
   </div>
</div>

Feedback This is my first question on stack overflow, so please also tell me if I phrased my question in a way that could be misunderstood, and whether or not I should supply more details, e.g. console output. Thank you ;)


Solution

  • After fiddling with your code I can confirm that when triggering the route you are getting a new instance of the controller that has a new, clean scope. This is the expected behavior with AngularJS.

    You can verify this by adding a simple log message as the first line of your controller:

    console.log($scope.selected);
    

    You will notice that it always logs out "undefined" because the variable has never been set (within viewJourneyById). If you leave that logging in and test the code you will see the logging fire in viewJourneyById but then immediately the "undefined" as it loads the view_journey.html template into ng-view and creates the new instance of mainCtrl. The presence of the "undefined" after the new view loads shows that the controller function is being executed again on the route change.

    There are a couple of ways to address this. First you could create a factory or service, inject it into your controller, and have it store the data for you. That is actually one of the reasons they exist, to share data between controllers.

    Factory:

    travelogueApp.factory('myFactory',function() {
        return {
            selected: null,
            journeys: []
        };
    });
    

    Controller:

    travelogueApp.controller('mainCtrl', ['$scope','$http','$location','myFactory', function ($scope, $http, $location, myFactory) {
        // put it into the scope so the template can see it.
        $scope.factory = myFactory; 
    
        // do other stuff
    
        $scope.viewJourneybyId = function(id) {
            var selectOne = { _id : id };
            $http.post('http://localhost:8080/view_journey', selectOne)
                .success(function(data) {
                    $scope.factory.selection = data;
                    console.log("POST found the right Journey");
                    console.log($scope.factory.selection);
                })
                .error(function(data) {
                    console.error("POST encountered an error");
                })    
            }  
        }]); // end controller
    

    Template:

    <div class="panel panel-default">
        <div class="panel-heading">
            <h2>{{factory.selection.name}}</h2>
        </div>
        <div class="panel-body">
            <table class=table>
            <caption>{{factory.selection.desc}}</caption>
            ...
            </table>
        </div>
    </div>
    

    More or less something like that. Another way to do it would be to construct the link with the journey id as part of the query string and then in the controller check for the presence of the journey id and if you find one, look up the journey. This would be a case of firing the route, loading a new instance of the controller and then loading the data once you're on the view_journey page. You can search for query string parameters in the controller like this:

    var journey_id = $location.search().id;
    

    Either way works. The factory/service method allows you to minimize web service calls over time by storing some data. However, then you have to start considering data management so you don't have stale data in your app. The query string way would be your quickest way to solve the problem but means that every route transition is going to be waiting a web service call, even if you are just going back and forth between the same two pages.