Defer rendering of template in AngularJS

1.4k Views Asked by At

BACKGROUND INFO

I'm new to AngularJS and am working on a simple app that's displaying photos feed. I have 2 views:

  • List - main view that displays a list of all photos
  • Detail - when you click one of the photos you're redirected here to see the details of that photo

I'm fetching the photos feed from remote URL using JSONP request. To avoid fetching it multiple times when changing views, I created a provider "feedFactory" which controllers attached to both views use to provide feed objects into the scope.

The problem I have is I see JavaScript errors (error shown on the bottom of the post), because of undefined values being passed to the filters when initially rendering the views. That is because views are rendered instantly - before the promise is completed and the feed fetched - and the values are still undefined then. After all everything displays properly, but of course I need to get rid of those errors in the JavaScript console.

QUESTION

How do I defer the rendering of the view in template, until the promise is completed and feed is inserted into the scope.

CODE

providers.js

var module = angular.module("flickrFeedProviders", []);

/* Factory providing a function that returns a promise object. Promise provides
   a feed after it is fetched. */
module.factory("feedFactory", ["$http", "$q",
    function($http, $q) {
        /* Adds unique "id" key to every object in the array */
        function indexList(photos) {
            for (var i = 0; i < photos.length; i++) {
                photos[i].id = i;
            }
        };

        /* URL from which the feed is fetched */
        var FEED_URL = "https://api.flickr.com/services/feeds/photos_public.gne?tags=potato&tagmode=all&format=json&jsoncallback=JSON_CALLBACK";

        /* Create a deffered object */
        var deferred = $q.defer();

        $http.jsonp(FEED_URL)
            .success(function(response) {
                indexList(response.items);
                /* Pass data on success */
                deferred.resolve(response)
            })
            .error(function(response) {
                /* Send friendly error message on failure */
                deferred.reject("Error occured while fetching feed");
            });

        /* Return promise object */
        return deferred.promise;
    }]);

controllers.js

var module = angular.module("flickrFeedControllers", [
    "flickrFeedProviders"
]);


/* Loads the whole feed - list of photos */
module.controller("photoListController", ["feedFactory", "$scope",
    function(feedFactory, $scope) {
        feedFactory.then(function(feed) {
            $scope.feed = feed;
        });
    }]);


/* Load only 1 photo */
module.controller("photoDetailController",
                  ["feedFactory", "$scope", "$routeParams",
    function(feedFactory, $scope, $routeParams) {
        var photoID = parseInt($routeParams.photoID);

        feedFactory.then(function(feed) {
            $scope.photo = feed.items[photoID];
        });
    }]);

filters.js

var module = angular.module("flickrFeedFilters", []);


/* Given author_id from Flickr feed, return the URL to his page */
module.filter("flickrAuthorURL", function() {
    var FLICKR_URL = "https://www.flickr.com/";

    return function(author_id) {
        return FLICKR_URL + "photos/" + author_id;
    };
})


/* Given author field from Flickr feed, return hid nickname only */
module.filter("flickrAuthorName", function() {
    /* Regular expression for author field from feed, that groups the name
       part of the string, so that it can be later extracted */
    var nameExtractionRegExp = /.* \((.*)\)/;

    return function(author) {
        return author.match(nameExtractionRegExp)[1];
    }
})


/* Given date ISO string return day number with added suffix st/nd/rd/th */
module.filter("dayNumber", function () {
    return function(dateISO) {
        var suffix;
        var date       = new Date(dateISO);
        var dayOfMonth = date.getDate();

        switch(dayOfMonth % 10) {
        case 1:
            suffix = "st";
            break;
        case 2:
            suffix = "nd";
            break;
        case 3:
            suffix = "rd";
            break;
        default:
            suffix = "th";
            break;
        }

        return dayOfMonth + suffix;
    };
});


/* Splits string using delimiter and returns array of results strings.*/
module.filter("split", function() {
    return function(string, delimiter) {
        return string.split(delimiter);
    };
});

photo-detail.html template

<!-- Title -->
<a href="{{ photo.link }}" title="Go to photo's details"
   class="title-container">
  <h2 class="title">{{ photo.title }}</h2>
</a>

<!-- Photo author -->
<a href="{{ photo.author_id | flickrAuthorURL }}"
   title="Go to author's page"
   class="author-link">{{ photo.author | flickrAuthorName }}</a>

<!-- Publication date information -->
<div class="publication-date">
  Published:
  {{ photo.published | dayNumber }}
  {{ photo.published | date : "MMM yyyy 'at' h:mm" }}
</div>

<!-- Photo image -->
<img alt="{{ photo.title }}" ng-src="{{ photo.media['m'] }}" class="photo" />

<!-- Description -->
<p class="description">{{ description }}</p>

<!-- Tag list -->
<ul class="tag-list">
  <li ng-repeat="tag in photo.tags | split : ' '" class="tag">
    <a href="#/tag/{{ tag }}" title="Filter photos by this tag">{{ tag }}</a>
  </li>
</ul>

<!-- Back button -->
<a href="#/photos" title="Go back" class="back" />

one of the console errors

Error: author is undefined
@http://localhost:8000/app/js/filters.js:24:9
anonymous/fn@http://localhost:8000/app/bower_components/angular/angular.js line 13145 > Function:2:211
regularInterceptedExpression@http://localhost:8000/app/bower_components/angular/angular.js:14227:21
expressionInputWatch@http://localhost:8000/app/bower_components/angular/angular.js:14129:26
$RootScopeProvider/this.$get</Scope.prototype.$digest@http://localhost:8000/app/bower_components/angular/angular.js:15675:34
$RootScopeProvider/this.$get</Scope.prototype.$apply@http://localhost:8000/app/bower_components/angular/angular.js:15951:13
done@http://localhost:8000/app/bower_components/angular/angular.js:10364:36
completeRequest@http://localhost:8000/app/bower_components/angular/angular.js:10536:7
requestLoaded@http://localhost:8000/app/bower_components/angular/angular.js:10477:1

http://localhost:8000/app/bower_components/angular/angular.js
Line 12330

other error

Error: string is undefined
@http://localhost:8000/app/js/filters.js:59:9
anonymous/fn@http://localhost:8000/app/bower_components/angular/angular.js line 13145 > Function:2:208
regularInterceptedExpression@http://localhost:8000/app/bower_components/angular/angular.js:14227:21
$RootScopeProvider/this.$get</Scope.prototype.$digest@http://localhost:8000/app/bower_components/angular/angular.js:15675:34
$RootScopeProvider/this.$get</Scope.prototype.$apply@http://localhost:8000/app/bower_components/angular/angular.js:15951:13
done@http://localhost:8000/app/bower_components/angular/angular.js:10364:36
completeRequest@http://localhost:8000/app/bower_components/angular/angular.js:10536:7
requestLoaded@http://localhost:8000/app/bower_components/angular/angular.js:10477:1

http://localhost:8000/app/bower_components/angular/angular.js
Line 12330
1

There are 1 best solutions below

0
On BEST ANSWER

OK, so to summarize the comments and the subsequent chat, here's the answer:

The cause of the "author is undefined" error

It turned out it was the custom filter (flickrAuthorName) that is raising it. From the template:

<a ...>{{ photo.author | flickrAuthorName }}</a>

When the template is loaded, the data has not been fetched from the server yet and photo.author is undefined, which is passed to the filter. The filter should be made more robust to check for this edge case, simply returning undefined itself, too.

"How do I defer the rendering of the view in template, until the promise is completed and feed is inserted into the scope?"

This has been answered here.

The idea is to configure Angular's $route service (via $routeProvider) to wait with the rendering of a template until some promise has been resolved, e.g. until some data fetched from the server arrives.

Copy/paste from the accepted answer:

$routeProvider.when("path", {
    controller: ["$scope", "mydata", MyPathCtrl], // NOTE THE NAME: mydata
    templateUrl: "...",
    resolve: {
        mydata: ["$http", function($http) { // NOTE THE NAME: mydata
            // $http.get() returns a promise, so it is OK for this usage
            return $http.get(...your code...);
        }]
        // You can also use a service name instead of a function, see docs
    },
    ...
});

This mechanism is also described in the Angular's documentation for $routeProvider (under the description of the when()'s function route parameter).