Intercept route and modify it before ui-router responds in AngularJS - angularjs

I'm using HTML5 pushstate on a mobile AngularJS website. When someone hits the desktop version, they get redirected to the mobile site. Sometimes the URL has a hash that the desktop site uses. The extra # is causing me digest loop problems.
I want to modify the route string before UI router even has a chance to act on it. Given:
http://www2.health.com.au/product/basic/#?blah I only want UI router to read the path /product/basic/.
I've tried adding a rule to the route provider (docs) that checks for '#?' and splits the string - but this seems to have no effect:
$urlRouterProvider.rule(function ($injector, $location){
var path = $location.url();
debugger;
if(path[path.length - 1].indexOf('#?')){
var splitpath = path.split('#?');
$location.replace().path(splitpath[0]);
return splitpath[0];
}
});
And the route ends up like this: http://www2.health.com.au/product/basic/#%252525252525252525252525252525252525252525252525252525252525252525252.......
With the error 10 $digest() iterations reached
I was under the impression that rules intercept the route, change it if possible and then pass the fixed route for ui-router to handle. Can anybody shed any light?

I am not particularly happy with this solution, but I ended up doing a manual bootstrap WITH A FREAKIN SETTIMEOUT!!! without it, angular.bootstrap() races the window.location = ... and wins unbelievably.
angular.element(document).ready(function(){
var loc = window.location.href,
index = loc.indexOf('#?');
if(index > 0){
alert('needsfix');
window.location = loc.substring(0, index);
setTimeout(function() {
angular.bootstrap(document, ['health3App']);
}, 500);
} else {
angular.bootstrap(document, ['health3App']);
}
})

Related

AngularJs ui-router reload template

I have a angularjs project that's using ui-router for routing.
I am using $state.reload() to reload the current state and it works just fine except that in my development system i want the $state.reload() to also reload the html template to reflect the new changes without a full page reload.
Is there a hack or option to do this ?
Update :
Chris T's solution almost worked, but i have templateFactoryDecorator witch adds a cache buster to the template url.
function configureTemplateFactory($provide) {
// Set a suffix outside the decorator function
var cacheBuster = Date.now().toString();
function templateFactoryDecorator($delegate) {
var fromUrl = angular.bind($delegate, $delegate.fromUrl);
$delegate.fromUrl = function (url, params) {
if (url !== null && angular.isDefined(url) && angular.isString(url)) {
url += (url.indexOf("?") === -1 ? "?" : "&");
url += "v=" + cacheBuster;
}
return fromUrl(url, params);
};
return $delegate;
}
$provide.decorator('$templateFactory', ['$delegate', templateFactoryDecorator]);
}
so
$templateCache.remove($state.current.templateUrl);
didn't work and i had to use
$templateCache.removeAll();
Its not ideal but for development environment its ok.
Internally, UI-Router leverages the $templateCache service to avoid re-fetching templates. Before calling $state.reload(), clear out the template you want to refetch from the server.
Assuming you want to do this from a controller:
function MyController($scope, $templateCache, $state) {
$scope.reload = function() {
$templateCache.remove("/path/to/template.html");
$state.reload();
}
}
Docs:
$templateCache
$cacheFactory
On a side note, I find it odd that you would want to refetch the template from the server.
I may have missed your point but When you use $state.reload() it reloads the state that means all the partials in side that template and the controllers in side that controller scope. Thus the state is reloaded not the full page.
So I see no errors in your side unless you want to do other staff ..
And if you are dynamically generating the DOM, It will do it for you .

Get previous URL on Angular request interceptor

I'm implementing an angular page-wide authentication app.
It is an event machine that uses an interceptor, so if any request results in a 401 response, it pops a login modal. When the login succeeds, it performs the request again (which could as well result in a 403 if the logged user do not have enough privileges to access the requested resource).
So long, things are working as expected except that when the user cancels the login process, the $location.path() still points to the requested resource. I want to revert this to the previous path. So:
What I expected
I expected to be able to get the "callee" URL like so:
.factory('authInterceptor', ...
return {
// Append the authentication headers to every request
request: function(config) {
previousLocation = $location.path(); // <--- HERE
if(Session.exist) {
config.headers.Authorization = Session.token;
}
return config || $q.when(config);
},
I expected this line to give me the "callee" path, but it actually gives me the requested one (angular has changed it before performing the request).
The route provider
.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/admin', {
templateUrl: "/admin"
})
.otherwise({ redirectTo: '/' });
}])
This naughty little boy is changing my path prior to the request. I guess I can do something here on resolve, but I don't find this useful, since it will completely break encapsulation, forcing me to resolve each time I call any protected resource, so I can pass the current path to some service in my auth app.
So, how can I get the previous path from the interceptor, or some service within the auth app, without explicitly sending it from the route provider configuration?
Why don't you watch $on route change events and bind your current/previous paths to a $window or global $scope variable?
I haven't read all your code above, but this is how I handle location things within my app:
$scope.$on('$routeChangeSuccess', function(evt, current, previous){
var routeData = {};
//prop: uriSegment
//desc: get all uri segments for current location
routeData.uriSegment = [];
routeData.uriSegment = $location.path().substr(1).split('/');
//prop: urls
//desc: get current & previous urls
routeData.urls = {};
routeData.urls.curr = $location.url().substr(1);
//...previous url
if(previous && previous.originalPath){
//remove matching structure & slashes from url & add params as plain values
var prevUrl = previous.originalPath.substr(1).replace('/:', '/').replace(':', '');
if(previous.pathParams){
angular.forEach(previous.pathParams, function(val, key){
prevUrl = prevUrl.replace(key, val);
});
}
routeData.urls.prev = prevUrl;
}
//bind routeData to scope/window...
$scope.uriSegment = routeData.uriSegment;
$scope.urls = routeData.urls;
});
How does it work:
1. Get current & previous url:
$scope.urls.curr //shows current url
$scope.urls.prev //shows previous url
... if your route is defined like /admin/:userID and you are on that page, the url will contain that specific userID, it won't return the param name. /admin/53 will be /admin/53.
2. Get a specific url segment from current url:
$scope.uriSegment[1]
... for current url like /admin/53 will print 53
There were a lot more things in the above code, but I think that's what you need - to remember the previous url. You can play with route events and build your own.
I see that you inject $routeScope in your factories, so you can add my code in your main controller and update the $routeScope with routeData on every route change and then you can go like this:
...
previousLocation = $rootScope.urls.prev;
...
More about route events here.

Case insensitivity with angularjs ui-router

I'm building a new angularJS app, based from the AngularJS SPA Visual studio template (http://visualstudiogallery.msdn.microsoft.com/5af151b2-9ed2-4809-bfe8-27566bfe7d83)
this uses ui-router (https://github.com/angular-ui/ui-router) for its routing.
however, it seems to be case sensitive.
Any idea how I would instruct angular/ui-router to ignore the case of the url parameter?
case sensitivity doesn't matter while in the app, though should a user type a url to enter the application at a specific page, we need to ensure that about is also the same as aBouT
Cheers
You can now configure ui-router to be case insensitive directly. Here is how you can use it:
angular.module('main', ['ui.router']);
angular.module('main').config(['$urlMatcherFactoryProvider', '$stateProvider', '$urlRouterProvider',
function($urlMatcherFactory, $stateProvider, $urlRouter) {
$urlMatcherFactory.caseInsensitive(true);
$urlMatcherFactory.strictMode(false);
$stateProvider.state('foo', {
url: '/foo',
template: '<b>The Foo View</b>'
});
$stateProvider.state('bar', {
url: '/bar',
template: '<b>The Bar View</b>'
});
$stateProvider.state('nomatch', {
url: '/nomatch',
template: '<b>No match found View</b>'
});
$urlRouter.otherwise('/nomatch');
}
]);
In the latest release (0.2.11), this is broken. A fix has been pushed already that can be seen at Github. So, currently, the best solution is to clone ui-router and build the head of master manually. Alternatively, you can just alter the source manually until the next release comes.
UPDATE (11/18/2014):
A release has now been made that incorporates the fix from above so that you no longer have to pull source and build manually. You can view the release on Github or just get the latest build.
Following the link in the comments to the original question, i was able to get the answer I needed.
Before my $stateProvider.state(......) routes I now have this piece of code:
$urlRouterProvider.rule(function ($injector, $location) {
//what this function returns will be set as the $location.url
var path = $location.path(), normalized = path.toLowerCase();
if (path != normalized) {
//instead of returning a new url string, I'll just change the $location.path directly so I don't have to worry about constructing a new url string and so a new state change is not triggered
$location.replace().path(normalized);
}
// because we've returned nothing, no state change occurs
});
Essentially it will toLowerCase() a url that isn't all lowercase already.
Once done, it replaces the url rather than redirects. Then carries on with matching a state.
You shouldn't change how ui-route handles URL matching to accept case insensitive URLs (that will have unexpected problems), but you can attempt to correct URLs for the user automatically when the routes fail.
When ui-route can not match a URL to a route it triggers the otherWise() callback. I'll show you have to redirect using this callback.
The following makes the assumption that all URLs for your app should be in lower case.
var stateHandler = function($urlRouterProvider)
{
$urlRouterProvider.otherwise(function($injector, $location)
{
var url = $location.absUrl();
var redirect = url.toLowerCase();
if(url == redirect)
{
return;
}
$window.location = redirect;
});
};
YourAngularApp.config(['$urlRouterProvider',stateHandler]);
If you need more control, then use a regex to select which URLs need rewriting.
According to official wiki,
https://github.com/angular-ui/ui-router/wiki/URL-Routing
Darren's answer looks right:
app.config(function ($urlRouterProvider) {
// Here's an example of how you might allow case insensitive urls
$urlRouterProvider.rule(function ($injector, $location) {
//what this function returns will be set as the $location.url
var path = $location.path(), normalized = path.toLowerCase();
if (path != normalized) {
//instead of returning a new url string, I'll just change the $location.path directly so I don't have to worry about constructing a new url string and so a new state change is not triggered
$location.replace().path(normalized);
}
// because we've returned nothing, no state change occurs
});}

Use Angular routing alongside roundtrip routing

I'm working on a Django app which makes heavy use of Angular in some pages, e.g. at domain.com/myAngularApp
Within the angular page I'm using Angular routing for navigating between different views/states within that page. However across the whole website there are navigation links which need to result in round trip requests to Django. However all the pages include the same compiled javascript file which includes the Angular route declarations.
So my question is: how to I get Angular to mange its own routes and get out of the way when the location is changed (primarily by clicking a link on the page) to a path that it hasn't explicitly been told to own, i.e. to different subdirectories off the domain.
My routing declaration looks something like:
myApp.config( function($locationProvider, $routeProvider) {
$locationProvider.html5Mode(true);
$routeProvider.when('/myAngularApp/', {
templateURL: 'template1.html'
});
$routeProvider.when('/myAngularApp/stuff', {
templateURL: 'template12.html'
});
$routeProvider.otherwise({redirectTo: <not sure what to do here...> });
})
I've tried something like:
$routeProvider.otherwise({redirectTo: function(a1,r,a3){ window.location.href = r }})
But this makes the page refresh endlessly on any non-matched route.
Leaving out the otherwise statement seems to make it impossible to leave a page with a non-matched route when accessed directly... don't really understand why?
It must be possible to do what I want no?
I think I may have found a solution. I'm doing a similar thing, a multi-page angular site that uses angular for some of it's pages. Here's what I'm doing
var app = angular.module('appname', ['ui.bootstrap', 'ui.autocomplete'])
.config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) {
$locationProvider.html5Mode(true);
$locationProvider.hashPrefix('!');
}])
.run(function($rootScope, $location) {
var redirected = false;
$rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) {
if(!redirected && $location.path() !== '/current-url') {
redirected = true;
event.preventDefault();
window.location = $location.path();
}
});
});
So what I have to work out next is how to pass in the current-url path. One way I'm thinking is to us ng-init to set that data in the view (I'm using express.js so I'd use Jade). Or possibly in the run function grab the initial path and test against that.
The event.preventDefault() is there to stop an extra item being added to the browsers history. Before I did that I had to hit back twice to get back to the angular page.
Note This hasn't been tested with IE8 yet. I'm going to do that now and see how it fairs.
Update Ok I just tested this in IE8 and I got stuck in a redirect loop. I've updated the code to have a simple variable to check if we've redirected. Seems to work. I'd love to know a prettier way.

Backbone Router not working using boilerplate

Ok, I think this is something simple, however I am being to stupid to see it. Here is my code in backbone using the backbone boilerplate method
require([
"app",
// Libs
"jquery",
"backbone",
// Modules
"modules/example"
],
function(app, $, Backbone, Example) {
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"": "index",
"item" : 'item'
},
index: function()
{
console.info('Index Function');
var tutorial = new Example.Views.Tutorial();
// Attach the tutorial to the DOM
tutorial.$el.appendTo("#main");
// Render the tutorial.
tutorial.render();
},
item: function()
{
console.info('Item View');
}
});
// Treat the jQuery ready function as the entry point to the application.
// Inside this function, kick-off all initialization, everything up to this
// point should be definitions.
$(function() {
// Define your master router on the application namespace and trigger all
// navigation from this instance.
app.router = new Router();
// Trigger the initial route and enable HTML5 History API support
Backbone.history.start({ pushState: true, root: '/reel' });
});
// All navigation that is relative should be passed through the navigate
// method, to be processed by the router. If the link has a data-bypass
// attribute, bypass the delegation completely.
$(document).on("click", "a:not([data-bypass])", function(evt) {
// Get the anchor href and protcol
var href = $(this).attr("href");
var protocol = this.protocol + "//";
// Ensure the protocol is not part of URL, meaning its relative.
if (href && href.slice(0, protocol.length) !== protocol &&
href.indexOf("javascript:") !== 0) {
// Stop the default event to ensure the link will not cause a page
// refresh.
evt.preventDefault();
// `Backbone.history.navigate` is sufficient for all Routers and will
// trigger the correct events. The Router's internal `navigate` method
// calls this anyways.
Backbone.history.navigate(href, true);
}
});
});
I am running this of a MAMP server and when i type Localhost:8888/reel , I get the example index page that comes with boilerplate. However when I type Localhost:8888/reel/item or Localhost:8888/reel/#item I either get, page can not be found or directed back to my index page.
My question is what am i doing wrong. Do I need to use htaccess? This doesnt seem right. Is there a way using backbone to sort this. Sorry if this is really simple, just cant get my head around it.
the problem may lie with the pushState flag.
With that on the request goes all the way to the server and it sees the full url and responds to it with whatever it would do ...
does it work if you have a
$(function (){
setTimeout(navMe, 2000);
});
function navMe() {
backbone.navigate("item");
}
that way 2 seconds after load it will navigate to item view and you know that its because of the request going to the server and not to backbone.

Resources