Showing posts with label angularjs. Show all posts
Showing posts with label angularjs. Show all posts

Sunday, July 13, 2014

DjangoRESTFramework, AngularJS and working with URLs

I like to put constants, URL's, etc into a django view that returns JSON for my angular apps that use django as a backend. You can write an angular service that calls this view, fetches and initialises this data for other angular services to consume by creating one service for constants, one for URL-mappings, etc. Or you can use a jsonify template tag like the one here and directly embed it in a script tag into your index template. I prefer the latter - I don't think it's worth the extra request just to keep your angular templates and your django templates separate.
<script type="text/javascript">
    var CONF = {
        urls: {{ urls | jsonify }},
        constants: {{ constants | jsonify }}
    };
</script>

With DjangoRESTFramework
Suppose you have endpoints, like the ones below, registered using a router in DRF,
api_router = DefaultRouter()
api_router.register("products", ProductViewSet)
api_router.register("product_notes", ProductNotesViewSet)
you should be able to enumerate all URLs for a given api router and build mappings for them using the following snippet.
def get_urls_by_router(api_router, urlconf=None):
    urls = {}
    list_name = api_router.routes[0].name
    for prefix, _viewset, basename in api_router.registry:
        urls[prefix] = reverse(list_name.format(basename=basename), urlconf=urlconf)
    return urls
print get_urls_by_router(api_router)
# {"products": "/api/v1/product/products", "product_notes": "/api/v1/product/product_notes"}
And in angular, I use a service, like the one below, to refer to these URLs by name instead of hardcoding them all over the place.
module.factory("URL", function(conf){
    var urls = conf.urls;
    return function(name){
        if(urls.hasOwnProperty(name))
            return urls[name];
        else
            throw new Error("Unknown URL identifier: " + name);
    };
});
where conf.urls is a mapping of URL names to URL patterns.

Special note about Multi-tenant systems
Suppose you are using the Sites framework to run multiple sites on a single django instance, you are probably using separate urlconfs for each site (so that Site1's URLs aren't visible from Site2). In cases like this, the URL you are trying to reverse might not be part of the request urlconf (available in request.urlconf). The urlconf argument to get_urls_by_router() is for cases like this where you have to override the request urlconf.

Cross-Domain requests
Sometimes, you might choose to serve your REST endpoints and the rest of your app from different domains (for eg., your actual app is at app.com while API is at api.app.com). For this, I used Django CORS headers to CORS enable my views. I use something like this to refer to API URLs in my code if the API is hosted on a different domain.
module.factory("APIURL", function(URL, constants){
    var api_base_url = constants.api_base_url;
    return function(name){
        return api_base_url + URL(name);
    };
});
where constants.api_base_url can be something like 'http://api.site2.com/v1'.

Cross-Domain requests not setting cookies/getting blocked?
When serving your API from a secure https:// URL and using a self-signed certificate, you may notice weird behaviour in certain browsers. In Chrome, everything was OK, except for the fact that cookies set by my django views, although visible in the response, were being ignored. As for Firefox, POST requests were being blocked by the browser. Using a proper certificate fixed these issues. Just an FYI.

Navigation elements breaking in IE?
Lack of support for the HTML5 history API broke navigation on IE. Use something like the snippet below to render href attributes for <a></a> elements in your angular templates. I also attach the hashURL service to $rootScope so that it's available in all my templates.
module.run(function(..., hashURL , ...){
    $rootScope.hashURL = hashURL;
}).factory("hashURL", function(URL, $location){
    // Used to build hashbanged urls if the HTML5 history.pushState
    // API is unavailable.
    var hashURL;
    if(!$location.$$html5){
        hashURL = URL;
    } else {
        hashURL = function(name){
            return "#" + URL(name);
        };
    }
    return hashURL;
});
and in your templates, use it like this
View more <a href="{{ hashURL('products') }}">products</a>.
Now whether you decide to support IE or not, I still think it's a good idea to refer to URLs by name using a service in your templates.

Serving from localhost:8000 breaking ngResource?
In Angular, the : symbol is used to define replaceable components that are part of URLs in ngResource/djResource for eg., http://example.com/api/products/:id. You need to escape the ":" in URLs passed to these services. I use a URLParse service (borrowed from here) to parse the port number out of the URL to escape the ":" before it. Note, URLs passed to $http don't need this escaping.
module.factory("URLParse", function($window){
    var parser = document.createElement('a');
    return function(url){
        parser.href = url;
        return {
            href: parser.href,
            protocol: parser.protocol,
            host: parser.host,
            hostname: parser.hostname,
            port: parser.port,
            pathname: parser.pathname,
            hash: parser.hash,
            search: parser.search,
        };
    };
}).factory("ResourceURL", function(URL, URLParse){
    return function(name){
        var url = URL(name),
            port = URLParse(url).port;
        return url.replace("\:" + port, "\\:" + port);
    };
});

Sunday, June 22, 2014

$locationChangeSuccess not fired in service

angular.module("app", [
).factory("TrackerUtil", function($rootScope, $location){
    var visited = [];
    $rootScope.$on('$locationChangeSuccess', function (event, toState, fromState){
        visited.push(toState);
    });
    return {
        getVisited: function(){
            return visited;
        }
    };

}).controller("trackedListCtrl", function($scope, ..., TrackerUtil){
    ...
    $scope.visited = TrackerUtil.getVisited();
    ...
})

So you've got code that looks something like this. You're hoping every time the location changes (somebody clicks a link, etc), visited.push() will get called. But for some reason, your service behaves very erratically - fires only sometimes, fires only for certain views, etc. So what went wrong?

When TrackerUtil is registered as an angular service, it doesn't really do much, that is unless you are actually referencing it somewhere in your code like from a controller or from another service. Only if this is the case does angular actually go execute the service body - in our case, registering a listener for the $locationChangeSuccess event. If the service isn't used anywhere, angular doesn't even try checking for it's dependencies.

In my case, I was calling TrackerUtil.getVisited() only from trackedListCtrl and hence had added TrackerUtil as a dependency only for that controller. Because of this, visited was always empty until a view handled by trackedListCtrl was activated, giving the appearance of erratic behaviour.

Solution?
I added it to the app's run() section. Not a great solution, but it works.