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',
|
'../../node_modules/angular-mocks/angular-mocks.js',
|
||||||
|
|
||||||
'../../dist/angular_1_router.js',
|
'../../dist/angular_1_router.js',
|
||||||
|
'src/ng_route_shim.js',
|
||||||
|
|
||||||
'test/*.es5.js',
|
'test/*.es5.js',
|
||||||
'test/**/*_spec.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