feat(angular_1_router): add ngRouteShim module
This module attempts to provide a shim for `$routeProvider` to aid in migrating from ngRoute to Component Router. Closes #4266
This commit is contained in:
parent
5205a9e65f
commit
aed34e1f82
|
@ -14,6 +14,7 @@ module.exports = function (config) {
|
|||
'../../node_modules/angular-mocks/angular-mocks.js',
|
||||
|
||||
'../../dist/angular_1_router.js',
|
||||
'src/ng_route_shim.js',
|
||||
|
||||
'test/*.es5.js',
|
||||
'test/**/*_spec.js'
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
/** @license Copyright 2014-2015 Google, Inc. http://github.com/angular/angular/LICENSE */
|
||||
(function () {
|
||||
|
||||
'use strict';
|
||||
|
||||
// keep a reference to compileProvider so we can register new component-directives
|
||||
// on-the-fly based on $routeProvider configuration
|
||||
// TODO: remove this– right now you can only bootstrap one Angular app with this hack
|
||||
var $compileProvider, $q, $injector;
|
||||
|
||||
/**
|
||||
* This module loads services that mimic ngRoute's configuration, and includes
|
||||
* an anchor link directive that intercepts clicks to routing.
|
||||
*
|
||||
* This module is intended to be used as a stop-gap solution for projects upgrading from ngRoute.
|
||||
* It intentionally does not implement all features of ngRoute.
|
||||
*/
|
||||
angular.module('ngRouteShim', [])
|
||||
.provider('$route', $RouteProvider)
|
||||
.config(['$compileProvider', function (compileProvider) {
|
||||
$compileProvider = compileProvider;
|
||||
}])
|
||||
.factory('$routeParams', $routeParamsFactory)
|
||||
.directive('a', anchorLinkDirective)
|
||||
|
||||
// Connects the legacy $routeProvider config shim to Component Router's config.
|
||||
.run(['$route', '$router', function ($route, $router) {
|
||||
$route.$$subscribe(function (routeDefinition) {
|
||||
if (!angular.isArray(routeDefinition)) {
|
||||
routeDefinition = [routeDefinition];
|
||||
}
|
||||
$router.config(routeDefinition);
|
||||
});
|
||||
}]);
|
||||
|
||||
|
||||
/**
|
||||
* A shimmed out provider that provides the same API as ngRoute's $routeProvider, but uses these calls
|
||||
* to configure Component Router.
|
||||
*/
|
||||
function $RouteProvider() {
|
||||
|
||||
var routes = [];
|
||||
var subscriptionFn = null;
|
||||
|
||||
var routeMap = {};
|
||||
|
||||
// Stats for which routes are skipped
|
||||
var skipCount = 0;
|
||||
var successCount = 0;
|
||||
var allCount = 0;
|
||||
|
||||
function consoleMetrics() {
|
||||
return '(' + skipCount + ' skipped / ' + successCount + ' success / ' + allCount + ' total)';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $routeProvider#when
|
||||
*
|
||||
* @param {string} path Route path (matched against `$location.path`). If `$location.path`
|
||||
* contains redundant trailing slash or is missing one, the route will still match and the
|
||||
* `$location.path` will be updated to add or drop the trailing slash to exactly match the
|
||||
* route definition.
|
||||
*
|
||||
* @param {Object} route Mapping information to be assigned to `$route.current` on route
|
||||
* match.
|
||||
*/
|
||||
this.when = function(path, route) {
|
||||
//copy original route object to preserve params inherited from proto chain
|
||||
var routeCopy = angular.copy(route);
|
||||
|
||||
allCount++;
|
||||
|
||||
if (angular.isDefined(routeCopy.reloadOnSearch)) {
|
||||
console.warn('Route for "' + path + '" uses "reloadOnSearch" which is not implemented.');
|
||||
}
|
||||
if (angular.isDefined(routeCopy.caseInsensitiveMatch)) {
|
||||
console.warn('Route for "' + path + '" uses "caseInsensitiveMatch" which is not implemented.');
|
||||
}
|
||||
|
||||
// use new wildcard format
|
||||
path = reformatWildcardParams(path);
|
||||
|
||||
if (path[path.length - 1] == '*') {
|
||||
skipCount++;
|
||||
console.warn('Route for "' + path + '" ignored because it ends with *. Skipping.', consoleMetrics());
|
||||
return this;
|
||||
}
|
||||
|
||||
if (path.indexOf('?') > -1) {
|
||||
skipCount++;
|
||||
console.warn('Route for "' + path + '" ignored because it has optional parameters. Skipping.', consoleMetrics());
|
||||
return this;
|
||||
}
|
||||
|
||||
if (typeof route.redirectTo == 'function') {
|
||||
skipCount++;
|
||||
console.warn('Route for "' + path + '" ignored because lazy redirecting to a function is not yet implemented. Skipping.', consoleMetrics());
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
var routeDefinition = {
|
||||
path: path,
|
||||
data: routeCopy
|
||||
};
|
||||
|
||||
routeMap[path] = routeCopy;
|
||||
|
||||
if (route.redirectTo) {
|
||||
routeDefinition.redirectTo = route.redirectTo;
|
||||
} else {
|
||||
if (routeCopy.controller && !routeCopy.controllerAs) {
|
||||
console.warn('Route for "' + path + '" should use "controllerAs".');
|
||||
}
|
||||
|
||||
var directiveName = routeObjToRouteName(routeCopy, path);
|
||||
|
||||
if (!directiveName) {
|
||||
throw new Error('Could not determine a name for route "' + path + '".');
|
||||
}
|
||||
|
||||
routeDefinition.component = directiveName;
|
||||
routeDefinition.as = upperCase(directiveName);
|
||||
|
||||
var directiveController = routeCopy.controller;
|
||||
|
||||
var directiveDefinition = {
|
||||
scope: false,
|
||||
controller: directiveController,
|
||||
controllerAs: routeCopy.controllerAs,
|
||||
templateUrl: routeCopy.templateUrl,
|
||||
template: routeCopy.template
|
||||
};
|
||||
|
||||
var directiveFactory = function () {
|
||||
return directiveDefinition;
|
||||
};
|
||||
|
||||
// if we have route resolve options, prepare a wrapper controller
|
||||
if (directiveController && routeCopy.resolve) {
|
||||
var originalController = directiveController;
|
||||
var resolvedLocals = {};
|
||||
|
||||
directiveDefinition.controller = ['$injector', '$scope', function ($injector, $scope) {
|
||||
var locals = angular.extend({
|
||||
$scope: $scope
|
||||
}, resolvedLocals);
|
||||
|
||||
var ctrl = $injector.instantiate(originalController, locals);
|
||||
|
||||
if (routeCopy.controllerAs) {
|
||||
locals.$scope[routeCopy.controllerAs] = ctrl;
|
||||
}
|
||||
|
||||
return ctrl;
|
||||
}];
|
||||
|
||||
// we take care of controllerAs in the directive controller wrapper
|
||||
delete directiveDefinition.controllerAs;
|
||||
|
||||
// we resolve the locals in a canActivate block
|
||||
directiveFactory.$canActivate = function() {
|
||||
var locals = angular.extend({}, routeCopy.resolve);
|
||||
|
||||
angular.forEach(locals, function(value, key) {
|
||||
locals[key] = angular.isString(value) ?
|
||||
$injector.get(value) : $injector.invoke(value, null, null, key);
|
||||
});
|
||||
|
||||
return $q.all(locals).then(function (locals) {
|
||||
resolvedLocals = locals;
|
||||
}).then(function () {
|
||||
return true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// register the dynamically created directive
|
||||
$compileProvider.directive(directiveName, directiveFactory);
|
||||
}
|
||||
if (subscriptionFn) {
|
||||
subscriptionFn(routeDefinition);
|
||||
} else {
|
||||
routes.push(routeDefinition);
|
||||
}
|
||||
successCount++;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
this.otherwise = function(params) {
|
||||
if (typeof params === 'string') {
|
||||
params = {redirectTo: params};
|
||||
}
|
||||
this.when('/*rest', params);
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$q', '$injector', function (q, injector) {
|
||||
$q = q;
|
||||
$injector = injector;
|
||||
|
||||
var $route = {
|
||||
routes: routeMap,
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $route#reload
|
||||
*
|
||||
* @description
|
||||
* Causes `$route` service to reload the current route even if
|
||||
* {@link ng.$location $location} hasn't changed.
|
||||
*/
|
||||
reload: function() {
|
||||
throw new Error('Not implemented: $route.reload');
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $route#updateParams
|
||||
*/
|
||||
updateParams: function(newParams) {
|
||||
throw new Error('Not implemented: $route.updateParams');
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs the given `fn` whenever new configs are added.
|
||||
* Only one subscription is allowed.
|
||||
* Passed `fn` is called synchronously.
|
||||
*/
|
||||
$$subscribe: function(fn) {
|
||||
if (subscriptionFn) {
|
||||
throw new Error('only one subscription allowed');
|
||||
}
|
||||
subscriptionFn = fn;
|
||||
subscriptionFn(routes);
|
||||
routes = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs a string with stats about many route configs were adapted, and how many were
|
||||
* dropped because they are incompatible.
|
||||
*/
|
||||
$$getStats: consoleMetrics
|
||||
};
|
||||
|
||||
return $route;
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
function $routeParamsFactory($router, $rootScope) {
|
||||
// the identity of this object cannot change
|
||||
var paramsObj = {};
|
||||
|
||||
$rootScope.$on('$routeChangeSuccess', function () {
|
||||
var newParams = $router._currentInstruction && $router._currentInstruction.component.params;
|
||||
|
||||
angular.forEach(paramsObj, function (val, name) {
|
||||
delete paramsObj[name];
|
||||
});
|
||||
angular.forEach(newParams, function (val, name) {
|
||||
paramsObj[name] = val;
|
||||
});
|
||||
});
|
||||
|
||||
return paramsObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows normal anchor links to kick off routing.
|
||||
*/
|
||||
function anchorLinkDirective($router) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function (scope, element) {
|
||||
// If the linked element is not an anchor tag anymore, do nothing
|
||||
if (element[0].nodeName.toLowerCase() !== 'a') {
|
||||
return;
|
||||
}
|
||||
|
||||
// SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
|
||||
var hrefAttrName = Object.prototype.toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
|
||||
'xlink:href' : 'href';
|
||||
|
||||
element.on('click', function (event) {
|
||||
if (event.which !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var href = element.attr(hrefAttrName);
|
||||
if (href && $router.recognize(href)) {
|
||||
$router.navigateByUrl(href);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a route object, attempts to find a unique directive name.
|
||||
*
|
||||
* @param route – route config object passed to $routeProvider.when
|
||||
* @param path – route configuration path
|
||||
* @returns {string|name} – a normalized (camelCase) directive name
|
||||
*/
|
||||
function routeObjToRouteName(route, path) {
|
||||
var name = route.controllerAs;
|
||||
|
||||
var controller = route.controller;
|
||||
if (!name && controller) {
|
||||
if (angular.isArray(controller)) {
|
||||
controller = controller[controller.length - 1];
|
||||
}
|
||||
name = controller.name;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
var segments = path.split('/');
|
||||
name = segments[segments.length - 1];
|
||||
}
|
||||
|
||||
if (name) {
|
||||
name = name + 'AutoCmp';
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function upperCase(str) {
|
||||
return str.charAt(0).toUpperCase() + str.substr(1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Changes "/:foo*" to "/*foo"
|
||||
*/
|
||||
var WILDCARD_PARAM_RE = new RegExp('\\/:([a-z0-9]+)\\*', 'gi');
|
||||
function reformatWildcardParams(path) {
|
||||
return path.replace(WILDCARD_PARAM_RE, function (m, m1) {
|
||||
return '/*' + m1;
|
||||
});
|
||||
}
|
||||
|
||||
}());
|
|
@ -0,0 +1,182 @@
|
|||
'use strict';
|
||||
|
||||
describe('ngRoute shim', function () {
|
||||
|
||||
var elt,
|
||||
$compile,
|
||||
$rootScope,
|
||||
$router,
|
||||
$compileProvider,
|
||||
$routeProvider;
|
||||
|
||||
beforeEach(function () {
|
||||
module('ng');
|
||||
module('ngComponentRouter');
|
||||
module('ngRouteShim');
|
||||
module(function (_$compileProvider_, _$routeProvider_) {
|
||||
$compileProvider = _$compileProvider_;
|
||||
$routeProvider = _$routeProvider_;
|
||||
});
|
||||
|
||||
inject(function (_$compile_, _$rootScope_, _$router_) {
|
||||
$compile = _$compile_;
|
||||
$rootScope = _$rootScope_;
|
||||
$router = _$router_;
|
||||
});
|
||||
});
|
||||
|
||||
it('should work in a simple case', function () {
|
||||
$routeProvider.when('/', {
|
||||
controller: function OneController() {
|
||||
this.number = 'one';
|
||||
},
|
||||
controllerAs: 'oneCmp',
|
||||
template: '{{oneCmp.number}}'
|
||||
});
|
||||
|
||||
compile('<ng-outlet></ng-outlet>');
|
||||
|
||||
$router.navigateByUrl('/');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(elt.text()).toBe('one');
|
||||
});
|
||||
|
||||
it('should adapt routes with templateUrl', inject(function ($templateCache) {
|
||||
$routeProvider.when('/', {
|
||||
controller: function OneController() {
|
||||
this.number = 'one';
|
||||
},
|
||||
controllerAs: 'oneCmp',
|
||||
templateUrl: '/foo'
|
||||
});
|
||||
|
||||
$templateCache.put('/foo', [200, '{{oneCmp.number}}', {}]);
|
||||
|
||||
compile('root {<ng-outlet></ng-outlet>}');
|
||||
|
||||
$router.navigateByUrl('/');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('root {one}');
|
||||
}));
|
||||
|
||||
it('should adapt routes using the "resolve" option', inject(function ($q) {
|
||||
$routeProvider.when('/', {
|
||||
controller: function TestController(resolvedService) {
|
||||
this.resolvedValue = resolvedService;
|
||||
},
|
||||
controllerAs: 'testCmp',
|
||||
resolve: {
|
||||
resolvedService: function () {
|
||||
return $q.when(42);
|
||||
}
|
||||
},
|
||||
template: 'value: {{testCmp.resolvedValue}}'
|
||||
});
|
||||
|
||||
compile('<ng-outlet></ng-outlet>');
|
||||
|
||||
$router.navigateByUrl('/');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(elt.text()).toBe('value: 42');
|
||||
}));
|
||||
|
||||
it('should adapt routes with params', function () {
|
||||
$routeProvider.when('/user/:name', {
|
||||
controller: function UserController($routeParams) {
|
||||
this.$routeParams = $routeParams;
|
||||
},
|
||||
controllerAs: 'userCmp',
|
||||
template: 'hello {{userCmp.$routeParams.name}}'
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
compile('<ng-outlet></ng-outlet>');
|
||||
|
||||
$router.navigateByUrl('/user/brian');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('hello brian');
|
||||
|
||||
$router.navigateByUrl('/user/igor');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('hello igor');
|
||||
});
|
||||
|
||||
it('should adapt routes with wildcard params', function () {
|
||||
$routeProvider.when('/home/:params*', {
|
||||
controller: function UserController($routeParams) {
|
||||
this.$routeParams = $routeParams;
|
||||
},
|
||||
controllerAs: 'homeCmp',
|
||||
template: 'rest: {{homeCmp.$routeParams.params}}'
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
compile('<ng-outlet></ng-outlet>');
|
||||
|
||||
$router.navigateByUrl('/home/foo/bar/123');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('rest: foo/bar/123');
|
||||
});
|
||||
|
||||
it('should warn about and ignore routes with optional params', function () {
|
||||
spyOn(console, 'warn');
|
||||
$routeProvider.when('/home/:params?', {
|
||||
template: 'home'
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
compile('root {<ng-outlet></ng-outlet>}');
|
||||
|
||||
$router.navigateByUrl('/home/test');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('root {}');
|
||||
expect(console.warn)
|
||||
.toHaveBeenCalledWith('Route for "/home/:params?" ignored because it has optional parameters. Skipping.',
|
||||
'(1 skipped / 0 success / 1 total)');
|
||||
});
|
||||
|
||||
it('should adapt routes with redirects', inject(function ($location) {
|
||||
$routeProvider
|
||||
.when('/', {
|
||||
redirectTo: '/home'
|
||||
})
|
||||
.when('/home', {
|
||||
template: 'welcome home!'
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
compile('root {<ng-outlet></ng-outlet>}');
|
||||
|
||||
$router.navigateByUrl('/');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('root {welcome home!}');
|
||||
expect($location.path()).toBe('/home');
|
||||
}));
|
||||
|
||||
//TODO: this is broken in recognition. un-xit this when https://github.com/angular/angular/issues/4133 is fixed
|
||||
xit('should adapt "otherwise" routes', inject(function ($location) {
|
||||
$routeProvider
|
||||
.when('/home', {
|
||||
template: 'welcome home!'
|
||||
})
|
||||
.otherwise({
|
||||
redirectTo: '/home'
|
||||
});
|
||||
$rootScope.$digest();
|
||||
|
||||
compile('root {<ng-outlet></ng-outlet>}');
|
||||
|
||||
$router.navigateByUrl('/somewhere');
|
||||
$rootScope.$digest();
|
||||
expect(elt.text()).toBe('root {welcome home!}');
|
||||
expect($location.path()).toBe('/home');
|
||||
}));
|
||||
|
||||
function compile(template) {
|
||||
elt = $compile('<div>' + template + '</div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
return elt;
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue