diff --git a/.travis.yml b/.travis.yml
index 428b8ed1be..db7f319fd3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -36,6 +36,7 @@ env:
- MODE=saucelabs DART_CHANNEL=dev
- MODE=dart_experimental DART_CHANNEL=dev
- MODE=js DART_CHANNEL=dev
+ - MODE=router DART_CHANNEL=dev
- MODE=lint DART_CHANNEL=dev
matrix:
diff --git a/gulpfile.js b/gulpfile.js
index 6dc1b77cc0..92ba289cab 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -37,6 +37,7 @@ var util = require('./tools/build/util');
var bundler = require('./tools/build/bundle');
var replace = require('gulp-replace');
var insert = require('gulp-insert');
+var buildRouter = require('./modules/angular1_router/build');
var uglify = require('gulp-uglify');
var shouldLog = require('./tools/build/logging');
var tslint = require('gulp-tslint');
@@ -604,6 +605,34 @@ gulp.task('!test.unit.js/karma-run', function(done) {
runKarma('karma-js.conf.js', done);
});
+gulp.task('test.unit.router', function (done) {
+ runSequence(
+ '!test.unit.router/karma-server',
+ function() {
+ watch('modules/**', [
+ 'buildRouter.dev',
+ '!test.unit.router/karma-run'
+ ]);
+ }
+ );
+});
+
+gulp.task('!test.unit.router/karma-server', function() {
+ karma.server.start({configFile: __dirname + '/modules/angular1_router/karma-router.conf.js'});
+});
+
+
+gulp.task('!test.unit.router/karma-run', function(done) {
+ karma.runner.run({configFile: __dirname + '/modules/angular1_router/karma-router.conf.js'}, function(exitCode) {
+ // ignore exitCode, we don't want to fail the build in the interactive (non-ci) mode
+ // karma will print all test failures
+ done();
+ });
+});
+
+gulp.task('buildRouter.dev', function () {
+ buildRouter();
+});
gulp.task('test.unit.dart', function (done) {
runSequence(
@@ -641,6 +670,12 @@ gulp.task('!test.unit.dart/karma-server', function() {
});
+gulp.task('test.unit.router/ci', function (done) {
+ var browserConf = getBrowsersFromCLI();
+ karma.server.start({configFile: __dirname + '/modules/angular1_router/karma-router.conf.js',
+ singleRun: true, reporters: ['dots'], browsers: browserConf.browsersToRun}, done);
+});
+
gulp.task('test.unit.js/ci', function (done) {
var browserConf = getBrowsersFromCLI();
karma.server.start({configFile: __dirname + '/karma-js.conf.js',
diff --git a/karma-dart.conf.js b/karma-dart.conf.js
index e07c721291..c3e42ea5c7 100644
--- a/karma-dart.conf.js
+++ b/karma-dart.conf.js
@@ -27,6 +27,7 @@ module.exports = function(config) {
exclude: [
'dist/dart/**/packages/**',
+ 'modules/angular1_router/**'
],
karmaDartImports: {
diff --git a/karma-js.conf.js b/karma-js.conf.js
index ba4e6e5310..f607206ba6 100644
--- a/karma-js.conf.js
+++ b/karma-js.conf.js
@@ -31,6 +31,7 @@ module.exports = function(config) {
exclude: [
'dist/js/dev/es5/**/e2e_test/**',
+ 'dist/angular1_router.js'
],
customLaunchers: sauceConf.customLaunchers,
diff --git a/modules/angular1_router/build.js b/modules/angular1_router/build.js
new file mode 100644
index 0000000000..62c790ec7c
--- /dev/null
+++ b/modules/angular1_router/build.js
@@ -0,0 +1,123 @@
+var fs = require('fs');
+var ts = require('typescript');
+
+var files = [
+ 'lifecycle_annotations_impl.ts',
+ 'url_parser.ts',
+ 'path_recognizer.ts',
+ 'route_config_impl.ts',
+ 'async_route_handler.ts',
+ 'sync_route_handler.ts',
+ 'route_recognizer.ts',
+ 'instruction.ts',
+ 'route_config_nomalizer.ts',
+ 'route_lifecycle_reflector.ts',
+ 'route_registry.ts',
+ 'router.ts'
+];
+
+var PRELUDE = '(function(){\n';
+var POSTLUDE = '\n}());\n';
+var FACADES = fs.readFileSync(__dirname + '/lib/facades.es5', 'utf8');
+var TRACEUR_RUNTIME = fs.readFileSync(__dirname + '/../../node_modules/traceur/bin/traceur-runtime.js', 'utf8');
+var DIRECTIVES = fs.readFileSync(__dirname + '/src/ng_outlet.js', 'utf8');
+function main() {
+ var dir = __dirname + '/../angular2/src/router/';
+
+ var out = '';
+
+ var sharedCode = '';
+ files.forEach(function (file) {
+ var moduleName = 'router/' + file.replace(/\.ts$/, '');
+
+ sharedCode += transform(moduleName, fs.readFileSync(dir + file, 'utf8'));
+ });
+
+ out += "angular.module('ngComponentRouter')";
+ out += angularFactory('$router', ['$q', '$location', '$$controllerIntrospector',
+ '$browser', '$rootScope', '$injector'], [
+ FACADES,
+ "var exports = {Injectable: function () {}};",
+ "var require = function () {return exports;};",
+ sharedCode,
+ "var RouteConfig = exports.RouteConfig;",
+ "angular.annotations = {RouteConfig: RouteConfig, CanActivate: exports.CanActivate};",
+ "angular.stringifyInstruction = exports.stringifyInstruction;",
+ "var RouteRegistry = exports.RouteRegistry;",
+ "var RootRouter = exports.RootRouter;",
+ //TODO: move this code into a templated JS file
+ "var registry = new RouteRegistry();",
+ "var location = new Location();",
+
+ "$$controllerIntrospector(function (name, constructor) {",
+ "if (constructor.$canActivate) {",
+ "constructor.annotations = constructor.annotations || [];",
+ "constructor.annotations.push(new angular.annotations.CanActivate(function (instruction) {",
+ "return $injector.invoke(constructor.$canActivate, constructor, {",
+ "$routeParams: instruction.component ? instruction.component.params : instruction.params",
+ "});",
+ "}));",
+ "}",
+ "if (constructor.annotations) {",
+ "constructor.annotations.forEach(function(annotation) {",
+ "if (annotation instanceof RouteConfig) {",
+ "annotation.configs.forEach(function (config) {",
+ "registry.config(constructor, config);",
+ "});",
+ "}",
+ "});",
+ "}",
+ "});",
+
+ "var router = new RootRouter(registry, undefined, location, new Object());",
+ "$rootScope.$watch(function () { return $location.path(); }, function (path) { router.navigate(path); });",
+
+ "return router;"
+ ].join('\n'));
+
+ return PRELUDE + TRACEUR_RUNTIME + DIRECTIVES + out + POSTLUDE;
+}
+
+
+/*
+ * Given a directory name and a file's TypeScript content, return an object with the ES5 code,
+ * sourcemap, anf exported variable identifier name for the content.
+ */
+var IMPORT_RE = new RegExp("import \\{?([\\w\\n_, ]+)\\}? from '(.+)';?", 'g');
+function transform(dir, contents) {
+ contents = contents.replace(IMPORT_RE, function (match, imports, includePath) {
+ //TODO: remove special-case
+ if (isFacadeModule(includePath) || includePath === './router_outlet') {
+ return '';
+ }
+ return match;
+ });
+ return ts.transpile(contents, {
+ target: ts.ScriptTarget.ES5,
+ module: ts.ModuleKind.CommonJS,
+ sourceRoot: dir
+ });
+}
+
+
+function angularFactory(name, deps, body) {
+ return ".factory('" + name + "', [" +
+ deps.map(function (service) {
+ return "'" + service + "', ";
+ }).join('') +
+ "function (" + deps.join(', ') + ") {\n" + body + "\n}])";
+}
+
+
+function isFacadeModule(modulePath) {
+ return modulePath.indexOf('facade') > -1 ||
+ modulePath === 'angular2/src/reflection/reflection';
+}
+
+module.exports = function () {
+ var dist = __dirname + '/../../dist';
+ if (!fs.existsSync(dist)) {
+ fs.mkdirSync(dist);
+ }
+ fs.writeFileSync(dist + '/angular_1_router.js', main(files));
+};
diff --git a/modules/angular1_router/index.html b/modules/angular1_router/index.html
new file mode 100644
index 0000000000..63dfbe65c3
--- /dev/null
+++ b/modules/angular1_router/index.html
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/angular1_router/karma-router.conf.js b/modules/angular1_router/karma-router.conf.js
new file mode 100644
index 0000000000..d7d014f265
--- /dev/null
+++ b/modules/angular1_router/karma-router.conf.js
@@ -0,0 +1,28 @@
+'use strict';
+
+var sauceConf = require('../../sauce.conf');
+
+// This runs the tests for the router in Angular 1.x
+
+module.exports = function (config) {
+ var options = {
+ frameworks: ['jasmine'],
+
+ files: [
+ '../../node_modules/angular/angular.js',
+ '../../node_modules/angular-animate/angular-animate.js',
+ '../../node_modules/angular-mocks/angular-mocks.js',
+
+ '../../dist/angular_1_router.js',
+
+ 'test/*.es5.js',
+ 'test/*_spec.js'
+ ],
+
+ customLaunchers: sauceConf.customLaunchers,
+
+ browsers: ['ChromeCanary']
+ };
+
+ config.set(options);
+};
diff --git a/modules/angular1_router/lib/facades.es5 b/modules/angular1_router/lib/facades.es5
new file mode 100644
index 0000000000..e0d6024e6f
--- /dev/null
+++ b/modules/angular1_router/lib/facades.es5
@@ -0,0 +1,305 @@
+function CONST() {
+ return (function(target) {
+ return target;
+ });
+}
+
+function IMPLEMENTS(_) {
+ return (function(t) {
+ return t;
+ });
+}
+
+function CONST_EXPR(expr) {
+ return expr;
+}
+
+function isPresent (x) {
+ return !!x;
+}
+
+function isBlank (x) {
+ return !x;
+}
+
+function isString(obj) {
+ return typeof obj === 'string';
+}
+
+function isType (x) {
+ return typeof x === 'function';
+}
+
+function isStringMap(obj) {
+ return typeof obj === 'object' && obj !== null;
+}
+
+function isArray(obj) {
+ return Array.isArray(obj);
+}
+
+var PromiseWrapper = {
+ resolve: function (reason) {
+ return $q.when(reason);
+ },
+
+ reject: function (reason) {
+ return $q.reject(reason);
+ },
+
+ catchError: function (promise, fn) {
+ return promise.then(null, fn);
+ },
+ all: function (promises) {
+ return $q.all(promises);
+ }
+};
+
+var RegExpWrapper = {
+ create: function(regExpStr, flags) {
+ flags = flags ? flags.replace(/g/g, '') : '';
+ return new RegExp(regExpStr, flags + 'g');
+ },
+ firstMatch: function(regExp, input) {
+ regExp.lastIndex = 0;
+ return regExp.exec(input);
+ },
+ matcher: function (regExp, input) {
+ regExp.lastIndex = 0;
+ return { re: regExp, input: input };
+ }
+};
+
+var reflector = {
+ annotations: function (fn) {
+ //TODO: implement me
+ return fn.annotations || [];
+ }
+};
+
+var MapWrapper = {
+ create: function() {
+ return new Map();
+ },
+
+ get: function(m, k) {
+ return m.get(k);
+ },
+
+ set: function(m, k, v) {
+ return m.set(k, v);
+ },
+
+ contains: function (m, k) {
+ return m.has(k);
+ },
+
+ forEach: function (m, fn) {
+ return m.forEach(fn);
+ }
+};
+
+var StringMapWrapper = {
+ create: function () {
+ return {};
+ },
+
+ set: function (m, k, v) {
+ return m[k] = v;
+ },
+
+ get: function (m, k) {
+ return m.hasOwnProperty(k) ? m[k] : undefined;
+ },
+
+ contains: function (m, k) {
+ return m.hasOwnProperty(k);
+ },
+
+ keys: function(map) {
+ return Object.keys(map);
+ },
+
+ isEmpty: function(map) {
+ for (var prop in map) {
+ if (map.hasOwnProperty(prop)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ delete: function(map, key) {
+ delete map[key];
+ },
+
+ forEach: function (m, fn) {
+ for (prop in m) {
+ if (m.hasOwnProperty(prop)) {
+ fn(m[prop], prop);
+ }
+ }
+ },
+
+ equals: function (m1, m2) {
+ var k1 = Object.keys(m1);
+ var k2 = Object.keys(m2);
+ if (k1.length != k2.length) {
+ return false;
+ }
+ var key;
+ for (var i = 0; i < k1.length; i++) {
+ key = k1[i];
+ if (m1[key] !== m2[key]) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ merge: function(m1, m2) {
+ var m = {};
+ for (var attr in m1) {
+ if (m1.hasOwnProperty(attr)) {
+ m[attr] = m1[attr];
+ }
+ }
+ for (var attr in m2) {
+ if (m2.hasOwnProperty(attr)) {
+ m[attr] = m2[attr];
+ }
+ }
+ return m;
+ }
+};
+
+var List = Array;
+var ListWrapper = {
+ create: function () {
+ return [];
+ },
+
+ push: function (l, v) {
+ return l.push(v);
+ },
+
+ forEach: function (l, fn) {
+ return l.forEach(fn);
+ },
+
+ first: function(array) {
+ if (!array)
+ return null;
+ return array[0];
+ },
+
+ map: function (l, fn) {
+ return l.map(fn);
+ },
+
+ join: function (l, str) {
+ return l.join(str);
+ },
+
+ reduce: function(list, fn, init) {
+ return list.reduce(fn, init);
+ },
+
+ filter: function(array, pred) {
+ return array.filter(pred);
+ },
+
+ concat: function(a, b) {
+ return a.concat(b);
+ },
+
+ slice: function(l) {
+ var from = arguments[1] !== (void 0) ? arguments[1] : 0;
+ var to = arguments[2] !== (void 0) ? arguments[2] : null;
+ return l.slice(from, to === null ? undefined : to);
+ },
+
+ maximum: function(list, predicate) {
+ if (list.length == 0) {
+ return null;
+ }
+ var solution = null;
+ var maxValue = -Infinity;
+ for (var index = 0; index < list.length; index++) {
+ var candidate = list[index];
+ if (isBlank(candidate)) {
+ continue;
+ }
+ var candidateValue = predicate(candidate);
+ if (candidateValue > maxValue) {
+ solution = candidate;
+ maxValue = candidateValue;
+ }
+ }
+ return solution;
+ }
+};
+
+var StringWrapper = {
+ equals: function (s1, s2) {
+ return s1 === s2;
+ },
+
+ split: function(s, re) {
+ return s.split(re);
+ },
+
+ substring: function(s, start, end) {
+ return s.substr(start, end);
+ },
+
+ replaceAll: function(s, from, replace) {
+ return s.replace(from, replace);
+ },
+
+ startsWith: function(s, start) {
+ return s.startsWith(start);
+ },
+
+ replaceAllMapped: function(s, from, cb) {
+ return s.replace(from, function(matches) {
+ // Remove offset & string from the result array
+ matches.splice(-2, 2);
+ // The callback receives match, p1, ..., pn
+ return cb.apply(null, matches);
+ });
+ },
+
+ contains: function(s, substr) {
+ return s.indexOf(substr) != -1;
+ }
+
+};
+
+//TODO: implement?
+// I think it's too heavy to ask 1.x users to bring in Rx for the router...
+function EventEmitter() {
+
+}
+
+var BaseException = Error;
+
+var ObservableWrapper = {
+ callNext: function(){}
+};
+
+// TODO: https://github.com/angular/angular.js/blob/master/src/ng/browser.js#L227-L265
+var $__router_47_location__ = {
+ Location: Location
+};
+
+function Location(){}
+Location.prototype.subscribe = function () {
+ //TODO: implement
+};
+Location.prototype.path = function () {
+ return $location.path();
+};
+Location.prototype.go = function (url) {
+ return $location.path(url);
+};
diff --git a/modules/angular1_router/src/ng_outlet.js b/modules/angular1_router/src/ng_outlet.js
new file mode 100644
index 0000000000..7598c8a9ee
--- /dev/null
+++ b/modules/angular1_router/src/ng_outlet.js
@@ -0,0 +1,432 @@
+'use strict';
+
+/*
+ * A module for adding new a routing system Angular 1.
+ */
+angular.module('ngComponentRouter', [])
+ .factory('$componentMapper', $componentMapperFactory)
+ .directive('ngOutlet', ngOutletDirective)
+ .directive('ngOutlet', ngOutletFillContentDirective)
+ .directive('ngLink', ngLinkDirective)
+ .directive('a', anchorLinkDirective); // TODO: make the anchor link feature configurable
+
+/*
+ * A module for inspecting controller constructors
+ */
+angular.module('ng')
+ .provider('$$controllerIntrospector', $$controllerIntrospectorProvider)
+ .config(controllerProviderDecorator);
+
+/*
+ * decorates with routing info
+ */
+function controllerProviderDecorator($controllerProvider, $$controllerIntrospectorProvider) {
+ var register = $controllerProvider.register;
+ $controllerProvider.register = function (name, ctrl) {
+ $$controllerIntrospectorProvider.register(name, ctrl);
+ return register.apply(this, arguments);
+ };
+}
+
+// TODO: decorate $controller ?
+/*
+ * private service that holds route mappings for each controller
+ */
+function $$controllerIntrospectorProvider() {
+ var controllers = [];
+ var controllersByName = {};
+ var onControllerRegistered = null;
+ return {
+ register: function (name, constructor) {
+ if (angular.isArray(constructor)) {
+ constructor = constructor[constructor.length - 1];
+ }
+ controllersByName[name] = constructor;
+ constructor.$$controllerName = name;
+ if (onControllerRegistered) {
+ onControllerRegistered(name, constructor);
+ } else {
+ controllers.push({name: name, constructor: constructor});
+ }
+ },
+ $get: ['$componentMapper', function ($componentMapper) {
+ var fn = function (newOnControllerRegistered) {
+ onControllerRegistered = function (name, constructor) {
+ name = $componentMapper.component(name);
+ return newOnControllerRegistered(name, constructor);
+ };
+ while (controllers.length > 0) {
+ var rule = controllers.pop();
+ onControllerRegistered(rule.name, rule.constructor);
+ }
+ };
+
+ fn.getTypeByName = function (name) {
+ return controllersByName[name];
+ };
+
+ return fn;
+ }]
+ };
+}
+
+
+/**
+ * @name ngOutlet
+ *
+ * @description
+ * An ngOutlet is where resolved content goes.
+ *
+ * ## Use
+ *
+ * ```html
+ *
+ * ```
+ *
+ * The value for the `ngOutlet` attribute is optional.
+ */
+function ngOutletDirective($animate, $injector, $q, $router, $componentMapper, $controller,
+ $$controllerIntrospector, $templateRequest) {
+ var rootRouter = $router;
+
+ return {
+ restrict: 'AE',
+ transclude: 'element',
+ terminal: true,
+ priority: 400,
+ require: ['?^^ngOutlet', 'ngOutlet'],
+ link: outletLink,
+ controller: function () {},
+ controllerAs: '$$ngOutlet'
+ };
+
+ function outletLink(scope, $element, attrs, ctrls, $transclude) {
+ var outletName = attrs.ngOutlet || 'default',
+ parentCtrl = ctrls[0],
+ myCtrl = ctrls[1],
+ router = (parentCtrl && parentCtrl.$$router) || rootRouter;
+
+ var childRouter,
+ currentInstruction,
+ currentScope,
+ currentController,
+ currentElement,
+ previousLeaveAnimation;
+
+ function cleanupLastView() {
+ if (previousLeaveAnimation) {
+ $animate.cancel(previousLeaveAnimation);
+ previousLeaveAnimation = null;
+ }
+
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
+ }
+ if (currentElement) {
+ previousLeaveAnimation = $animate.leave(currentElement);
+ previousLeaveAnimation.then(function () {
+ previousLeaveAnimation = null;
+ });
+ currentElement = null;
+ }
+ }
+
+ router.registerOutlet({
+ commit: function (instruction) {
+ var next;
+ var componentInstruction = instruction.component;
+ if (componentInstruction.reuse) {
+ // todo(shahata): lifecycle - onReuse
+ next = $q.when(true);
+ } else {
+ var self = this;
+ next = this.deactivate(instruction).then(function () {
+ return self.activate(componentInstruction);
+ });
+ }
+ return next.then(function () {
+ if (childRouter) {
+ return childRouter.commit(instruction.child);
+ } else {
+ return $q.when(true);
+ }
+ });
+ },
+ canReuse: function (nextInstruction) {
+ var result;
+ var componentInstruction = nextInstruction.component;
+ if (!currentInstruction ||
+ currentInstruction.componentType !== componentInstruction.componentType) {
+ result = false;
+ } else {
+ // todo(shahata): lifecycle - canReuse
+ result = componentInstruction === currentInstruction ||
+ angular.equals(componentInstruction.params, currentInstruction.params);
+ }
+ return $q.when(result).then(function (result) {
+ // TODO: this is a hack
+ componentInstruction.reuse = result;
+ return result;
+ });
+ },
+ canDeactivate: function (instruction) {
+ if (currentInstruction && currentController && currentController.canDeactivate) {
+ return $q.when(currentController.canDeactivate(instruction && instruction.component, currentInstruction));
+ }
+ return $q.when(true);
+ },
+ deactivate: function (instruction) {
+ // todo(shahata): childRouter.dectivate, dispose component?
+ var result = $q.when();
+ return result.then(function () {
+ if (currentController && currentController.onDeactivate) {
+ return currentController.onDeactivate(instruction && instruction.component, currentInstruction);
+ }
+ });
+ },
+ activate: function (instruction) {
+ var previousInstruction = currentInstruction;
+ currentInstruction = instruction;
+ childRouter = router.childRouter(instruction.componentType);
+
+ var controllerConstructor, componentName;
+ controllerConstructor = instruction.componentType;
+ componentName = $componentMapper.component(controllerConstructor.$$controllerName);
+
+ var componentTemplateUrl = $componentMapper.template(componentName);
+ return $templateRequest(componentTemplateUrl).then(function (templateHtml) {
+ myCtrl.$$router = childRouter;
+ myCtrl.$$template = templateHtml;
+ }).then(function () {
+ var newScope = scope.$new();
+ var locals = {
+ $scope: newScope,
+ $router: childRouter,
+ $routeParams: (instruction.params || {})
+ };
+
+ // todo(shahata): controllerConstructor is not minify friendly
+ currentController = $controller(controllerConstructor, locals);
+
+ var clone = $transclude(newScope, function (clone) {
+ $animate.enter(clone, null, currentElement || $element);
+ cleanupLastView();
+ });
+
+ var controllerAs = $componentMapper.controllerAs(componentName) || componentName;
+ newScope[controllerAs] = currentController;
+ currentElement = clone;
+ currentScope = newScope;
+
+ if (currentController.onActivate) {
+ return currentController.onActivate(instruction, previousInstruction);
+ }
+ });
+ }
+ }, outletName);
+ }
+}
+
+function ngOutletFillContentDirective($compile) {
+ return {
+ restrict: 'EA',
+ priority: -400,
+ require: 'ngOutlet',
+ link: function (scope, $element, attrs, ctrl) {
+ var template = ctrl.$$template;
+ $element.html(template);
+ var link = $compile($element.contents());
+ link(scope);
+ }
+ };
+}
+
+
+/**
+ * @name ngLink
+ * @description
+ * Lets you link to different parts of the app, and automatically generates hrefs.
+ *
+ * ## Use
+ * The directive uses a simple syntax: `ng-link="componentName({ param: paramValue })"`
+ *
+ * ## Example
+ *
+ * ```js
+ * angular.module('myApp', ['ngFuturisticRouter'])
+ * .controller('AppController', ['$router', function($router) {
+ * $router.config({ path: '/user/:id' component: 'user' });
+ * this.user = { name: 'Brian', id: 123 };
+ * });
+ * ```
+ *
+ * ```html
+ *
+ * ```
+ */
+function ngLinkDirective($router, $location, $parse) {
+ var rootRouter = $router;
+
+ return {
+ require: '?^^ngOutlet',
+ restrict: 'A',
+ link: ngLinkDirectiveLinkFn
+ };
+
+ function ngLinkDirectiveLinkFn(scope, elt, attrs, ctrl) {
+ var router = (ctrl && ctrl.$$router) || rootRouter;
+ if (!router) {
+ return;
+ }
+
+ var link = attrs.ngLink || '';
+
+ function getLink(params) {
+ return './' + angular.stringifyInstruction(router.generate(params));
+ }
+
+ var routeParamsGetter = $parse(link);
+ // we can avoid adding a watcher if it's a literal
+ if (routeParamsGetter.constant) {
+ var params = routeParamsGetter();
+ elt.attr('href', getLink(params));
+ } else {
+ scope.$watch(function () {
+ return routeParamsGetter(scope);
+ }, function (params) {
+ elt.attr('href', getLink(params));
+ }, true);
+ }
+ }
+}
+
+
+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.navigate(href);
+ event.preventDefault();
+ }
+ });
+ }
+ };
+}
+
+/**
+ * @name $componentMapperFactory
+ * @description
+ *
+ * This lets you configure conventions for what controllers are named and where to load templates from.
+ *
+ * The default behavior is to dasherize and serve from `./components`. A component called `myWidget`
+ * uses a controller named `MyWidgetController` and a template loaded from `./components/my-widget/my-widget.html`.
+ *
+ * A component is:
+ * - a controller
+ * - a template
+ * - an optional router
+ *
+ * This service makes it easy to group all of them into a single concept.
+ */
+function $componentMapperFactory() {
+
+ var DEFAULT_SUFFIX = 'Controller';
+
+ var componentToCtrl = function componentToCtrlDefault(name) {
+ return name[0].toUpperCase() + name.substr(1) + DEFAULT_SUFFIX;
+ };
+
+ var componentToTemplate = function componentToTemplateDefault(name) {
+ var dashName = dashCase(name);
+ return './components/' + dashName + '/' + dashName + '.html';
+ };
+
+ var ctrlToComponent = function ctrlToComponentDefault(name) {
+ return name[0].toLowerCase() + name.substr(1, name.length - DEFAULT_SUFFIX.length - 1);
+ };
+
+ var componentToControllerAs = function componentToControllerAsDefault(name) {
+ return name;
+ };
+
+ return {
+ controllerName: function (name) {
+ return componentToCtrl(name);
+ },
+
+ controllerAs: function (name) {
+ return componentToControllerAs(name);
+ },
+
+ template: function (name) {
+ return componentToTemplate(name);
+ },
+
+ component: function (name) {
+ return ctrlToComponent(name);
+ },
+
+ /**
+ * @name $componentMapper#setCtrlNameMapping
+ * @description takes a function for mapping component names to component controller names
+ */
+ setCtrlNameMapping: function (newFn) {
+ componentToCtrl = newFn;
+ return this;
+ },
+
+ /**
+ * @name $componentMapper#setCtrlAsMapping
+ * @description takes a function for mapping component names to controllerAs name in the template
+ */
+ setCtrlAsMapping: function (newFn) {
+ componentToControllerAs = newFn;
+ return this;
+ },
+
+ /**
+ * @name $componentMapper#setComponentFromCtrlMapping
+ * @description takes a function for mapping component controller names to component names
+ */
+ setComponentFromCtrlMapping: function (newFn) {
+ ctrlToComponent = newFn;
+ return this;
+ },
+
+ /**
+ * @name $componentMapper#setTemplateMapping
+ * @description takes a function for mapping component names to component template URLs
+ */
+ setTemplateMapping: function (newFn) {
+ componentToTemplate = newFn;
+ return this;
+ }
+ };
+}
+
+
+function dashCase(str) {
+ return str.replace(/([A-Z])/g, function ($1) {
+ return '-' + $1.toLowerCase();
+ });
+}
diff --git a/modules/angular1_router/test/component_mapper_spec.js b/modules/angular1_router/test/component_mapper_spec.js
new file mode 100644
index 0000000000..bfe9343b0f
--- /dev/null
+++ b/modules/angular1_router/test/component_mapper_spec.js
@@ -0,0 +1,77 @@
+'use strict';
+
+describe('$componentMapper', function () {
+ var elt,
+ $compile,
+ $rootScope,
+ $router,
+ $templateCache;
+
+ function Ctrl() {
+ this.message = 'howdy';
+ }
+
+ beforeEach(function() {
+ module('ng');
+ module('ngComponentRouter');
+ module(function ($controllerProvider) {
+ $controllerProvider.register('myComponentController', Ctrl);
+ });
+ });
+
+ it('should convert a component name to a controller name', inject(function ($componentMapper) {
+ expect($componentMapper.controllerName('foo')).toBe('FooController');
+ }));
+
+ it('should convert a controller name to a component name', inject(function ($componentMapper) {
+ expect($componentMapper.component('FooController')).toBe('foo');
+ }));
+
+ it('should convert a component name to a template URL', inject(function ($componentMapper) {
+ expect($componentMapper.template('foo')).toBe('./components/foo/foo.html');
+ }));
+
+ it('should work with a controller constructor fn and a template url', inject(function ($componentMapper) {
+ var routes = {};
+ $componentMapper.setCtrlNameMapping(function (name) {
+ return routes[name].controller;
+ });
+ $componentMapper.setTemplateMapping(function (name) {
+ return routes[name].templateUrl;
+ });
+ $componentMapper.setCtrlAsMapping(function (name) {
+ return 'ctrl';
+ });
+
+ routes.myComponent = {
+ controller: Ctrl,
+ templateUrl: '/foo'
+ };
+
+ inject(function(_$compile_, _$rootScope_, _$router_, _$templateCache_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $router = _$router_;
+ $templateCache = _$templateCache_;
+ });
+
+ $templateCache.put('/foo', [200, '{{ctrl.message}}', {}]);
+
+ compile('');
+
+ $router.config([
+ { path: '/', component: Ctrl }
+ ]);
+
+ $router.navigate('/');
+ $rootScope.$digest();
+
+ expect(elt.text()).toBe('howdy');
+ }));
+
+ function compile(template) {
+ elt = $compile('' + template + '
')($rootScope);
+ $rootScope.$digest();
+ return elt;
+ }
+});
diff --git a/modules/angular1_router/test/controller_introspector_spec.js b/modules/angular1_router/test/controller_introspector_spec.js
new file mode 100644
index 0000000000..fa3d8e34da
--- /dev/null
+++ b/modules/angular1_router/test/controller_introspector_spec.js
@@ -0,0 +1,38 @@
+'use strict';
+
+describe('$$controllerIntrospector', function () {
+
+ var $controllerProvider;
+
+ beforeEach(function() {
+ module('ng');
+ module('ngComponentRouter');
+ module(function(_$controllerProvider_) {
+ $controllerProvider = _$controllerProvider_;
+ });
+ });
+
+ it('should call the introspector function whenever a controller is registered', inject(function ($$controllerIntrospector) {
+ var spy = jasmine.createSpy();
+ $$controllerIntrospector(spy);
+ function Ctrl(){}
+ $controllerProvider.register('SomeController', Ctrl);
+
+ expect(spy).toHaveBeenCalledWith('some', Ctrl);
+ }));
+
+ it('should call the introspector function whenever a controller is registered with array annotations', inject(function ($$controllerIntrospector) {
+ var spy = jasmine.createSpy();
+ $$controllerIntrospector(spy);
+ function Ctrl(foo){}
+ $controllerProvider.register('SomeController', ['foo', Ctrl]);
+
+ expect(spy).toHaveBeenCalledWith('some', Ctrl);
+ }));
+
+ it('should retrieve a constructor', inject(function ($$controllerIntrospector) {
+ function Ctrl(foo){}
+ $controllerProvider.register('SomeController', ['foo', Ctrl]);
+ expect($$controllerIntrospector.getTypeByName('SomeController')).toBe(Ctrl);
+ }));
+});
diff --git a/modules/angular1_router/test/ng_outlet_spec.js b/modules/angular1_router/test/ng_outlet_spec.js
new file mode 100644
index 0000000000..cd89ffc005
--- /dev/null
+++ b/modules/angular1_router/test/ng_outlet_spec.js
@@ -0,0 +1,800 @@
+'use strict';
+
+describe('ngOutlet', function () {
+
+ var elt,
+ $compile,
+ $rootScope,
+ $router,
+ $templateCache,
+ $controllerProvider,
+ $componentMapperProvider;
+
+ var OneController, TwoController, UserController;
+
+ function instructionFor(componentType) {
+ return jasmine.objectContaining({componentType: componentType});
+ }
+
+
+ beforeEach(function () {
+ module('ng');
+ module('ngComponentRouter');
+ module(function (_$controllerProvider_, _$componentMapperProvider_) {
+ $controllerProvider = _$controllerProvider_;
+ $componentMapperProvider = _$componentMapperProvider_;
+ });
+
+ inject(function (_$compile_, _$rootScope_, _$router_, _$templateCache_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $router = _$router_;
+ $templateCache = _$templateCache_;
+ });
+
+ UserController = registerComponent('user', 'hello {{user.name}}
', function ($routeParams) {
+ this.name = $routeParams.name;
+ });
+ OneController = registerComponent('one', '{{one.number}}
', boringController('number', 'one'));
+ TwoController = registerComponent('two', '{{two.number}}
', boringController('number', 'two'));
+ });
+
+
+ it('should work in a simple case', function () {
+ compile('');
+
+ $router.config([
+ { path: '/', component: OneController }
+ ]);
+
+ $router.navigate('/');
+ $rootScope.$digest();
+
+ expect(elt.text()).toBe('one');
+ });
+
+
+ // See https://github.com/angular/router/issues/105
+ xit('should warn when instantiating a component with no controller', function () {
+ put('noController', '{{ 2 + 2 }}
');
+ $router.config([
+ { path: '/', component: 'noController' }
+ ]);
+
+ spyOn(console, 'warn');
+ compile('');
+ $router.navigate('/');
+
+ expect(console.warn).toHaveBeenCalledWith('Could not find controller for', 'NoControllerController');
+ expect(elt.text()).toBe('4');
+ });
+
+
+ it('should navigate between components with different parameters', function () {
+ $router.config([
+ { path: '/user/:name', component: UserController }
+ ]);
+ compile('');
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('hello brian');
+
+ $router.navigate('/user/igor');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('hello igor');
+ });
+
+
+ it('should not reactivate a parent when navigating between child components with different parameters', function () {
+ var spy = jasmine.createSpy('onActivate');
+ function ParentController() {}
+ ParentController.$routeConfig = [
+ { path: '/user/:name', component: UserController }
+ ];
+ ParentController.prototype.onActivate = spy;
+
+ registerComponent('parent', 'parent { }', ParentController);
+
+ $router.config([
+ { path: '/parent/...', component: ParentController }
+ ]);
+ compile('');
+
+ $router.navigate('/parent/user/brian');
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalled();
+ expect(elt.text()).toBe('parent { hello brian }');
+
+ spy.calls.reset();
+
+ $router.navigate('/parent/user/igor');
+ $rootScope.$digest();
+ expect(spy).not.toHaveBeenCalled();
+ expect(elt.text()).toBe('parent { hello igor }');
+ });
+
+
+ it('should work with nested outlets', function () {
+ var childComponent = registerComponent('childComponent', '', [
+ { path: '/b', component: OneController }
+ ]);
+
+ $router.config([
+ { path: '/a/...', component: childComponent }
+ ]);
+ compile('');
+
+ $router.navigate('/a/b');
+ $rootScope.$digest();
+
+ expect(elt.text()).toBe('outer { inner { one } }');
+ });
+
+
+ it('should work with recursive nested outlets', function () {
+ put('two', '');
+ $router.config([
+ { path: '/recur', component: TwoController },
+ { path: '/', component: OneController }
+ ]);
+
+ compile('');
+ $router.navigate('/');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('root { one }');
+ });
+
+
+ it('should allow linking from the parent to the child', function () {
+ put('one', '{{number}}
');
+
+ $router.config([
+ { path: '/a', component: OneController },
+ { path: '/b', component: TwoController, as: 'two' }
+ ]);
+ compile('link | outer { }');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(elt.find('a').attr('href')).toBe('./b');
+ });
+
+ it('should allow linking from the child and the parent', function () {
+ put('one', '');
+
+ $router.config([
+ { path: '/a', component: OneController },
+ { path: '/b', component: TwoController, as: 'two' }
+ ]);
+ compile('outer { }');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(elt.find('a').attr('href')).toBe('./b');
+ });
+
+
+ it('should allow params in routerLink directive', function () {
+ put('router', '');
+ put('one', '');
+
+ $router.config([
+ { path: '/a', component: OneController },
+ { path: '/b/:param', component: TwoController, as: 'two' }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(elt.find('a').attr('href')).toBe('./b/lol');
+ });
+
+ // TODO: test dynamic links
+ it('should update the href of links with bound params', function () {
+ put('router', '');
+ put('one', '');
+
+ $router.config([
+ { path: '/a', component: OneController },
+ { path: '/b/:param', component: TwoController, as: 'two' }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(elt.find('a').attr('href')).toBe('./b/one');
+ });
+
+
+ it('should run the activate hook of controllers', function () {
+ var spy = jasmine.createSpy('activate');
+ var activate = registerComponent('activate', '', {
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/a', component: activate }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(spy).toHaveBeenCalled();
+ });
+
+
+ it('should pass instruction into the activate hook of a controller', function () {
+ var spy = jasmine.createSpy('activate');
+ var UserController = registerComponent('user', '', {
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/user/:name', component: UserController }
+ ]);
+ compile('');
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+
+ expect(spy).toHaveBeenCalledWith(instructionFor(UserController), undefined);
+ });
+
+
+ it('should pass previous instruction into the activate hook of a controller', function () {
+ var spy = jasmine.createSpy('activate');
+ var activate = registerComponent('activate', '', {
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/user/:name', component: OneController },
+ { path: '/post/:id', component: activate }
+ ]);
+ compile('');
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+ $router.navigate('/post/123');
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalledWith(instructionFor(activate),
+ instructionFor(OneController));
+ });
+
+
+ it('should inject $scope into the controller constructor', function () {
+
+ var injectedScope;
+ var UserController = registerComponent('user', '', function ($scope) {
+ injectedScope = $scope;
+ });
+
+ $router.config([
+ { path: '/user', component: UserController }
+ ]);
+ compile('');
+
+ $router.navigate('/user');
+ $rootScope.$digest();
+
+ expect(injectedScope).toBeDefined();
+ });
+
+
+ it('should run the deactivate hook of controllers', function () {
+ var spy = jasmine.createSpy('deactivate');
+ var deactivate = registerComponent('deactivate', '', {
+ onDeactivate: spy
+ });
+
+ $router.config([
+ { path: '/a', component: deactivate },
+ { path: '/b', component: OneController }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+ $router.navigate('/b');
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalled();
+ });
+
+
+ it('should pass instructions into the deactivate hook of controllers', function () {
+ var spy = jasmine.createSpy('deactivate');
+ var deactivate = registerComponent('deactivate', '', {
+ onDeactivate: spy
+ });
+
+ $router.config([
+ { path: '/user/:name', component: deactivate },
+ { path: '/post/:id', component: OneController }
+ ]);
+ compile('');
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+ $router.navigate('/post/123');
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalledWith(instructionFor(OneController),
+ instructionFor(deactivate));
+ });
+
+
+ it('should run the deactivate hook before the activate hook', function () {
+ var log = [];
+
+ var activate = registerComponent('activate', '', {
+ onActivate: function () {
+ log.push('activate');
+ }
+ });
+
+ var deactivate = registerComponent('deactivate', '', {
+ onDeactivate: function () {
+ log.push('deactivate');
+ }
+ });
+
+ $router.config([
+ { path: '/a', component: deactivate },
+ { path: '/b', component: activate }
+ ]);
+ compile('outer { }');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+ $router.navigate('/b');
+ $rootScope.$digest();
+
+ expect(log).toEqual(['deactivate', 'activate']);
+ });
+
+
+ it('should not activate a component when canActivate returns false', function () {
+ var spy = jasmine.createSpy('activate');
+ var activate = registerComponent('activate', '', {
+ canActivate: function () {
+ return false;
+ },
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/a', component: activate }
+ ]);
+ compile('outer { }');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(spy).not.toHaveBeenCalled();
+ expect(elt.text()).toBe('outer { }');
+ });
+
+
+ it('should activate a component when canActivate returns true', function () {
+ var spy = jasmine.createSpy('activate');
+ var activate = registerComponent('activate', 'hi', {
+ canActivate: function () {
+ return true;
+ },
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/a', component: activate }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(spy).toHaveBeenCalled();
+ expect(elt.text()).toBe('hi');
+ });
+
+
+ it('should activate a component when canActivate returns a resolved promise', inject(function ($q) {
+ var spy = jasmine.createSpy('activate');
+ var activate = registerComponent('activate', 'hi', {
+ canActivate: function () {
+ return $q.when(true);
+ },
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/a', component: activate }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(spy).toHaveBeenCalled();
+ expect(elt.text()).toBe('hi');
+ }));
+
+
+ it('should inject into the canActivate hook of controllers', inject(function ($http) {
+ var spy = jasmine.createSpy('canActivate').and.returnValue(true);
+ var activate = registerComponent('activate', '', {
+ canActivate: spy
+ });
+
+ spy.$inject = ['$routeParams', '$http'];
+
+ $router.config([
+ { path: '/user/:name', component: activate }
+ ]);
+ compile('');
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalledWith({name: 'brian'}, $http);
+ }));
+
+
+ it('should not navigate when canDeactivate returns false', function () {
+ var activate = registerComponent('activate', 'hi', {
+ canDeactivate: function () {
+ return false;
+ }
+ });
+
+ $router.config([
+ { path: '/a', component: activate },
+ { path: '/b', component: OneController }
+ ]);
+ compile('outer { }');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('outer { hi }');
+
+ $router.navigate('/b');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('outer { hi }');
+ });
+
+
+ it('should navigate when canDeactivate returns true', function () {
+ var activate = registerComponent('activate', 'hi', {
+ canDeactivate: function () {
+ return true;
+ }
+ });
+
+ $router.config([
+ { path: '/a', component: activate },
+ { path: '/b', component: OneController }
+ ]);
+ compile('outer { }');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('outer { hi }');
+
+ $router.navigate('/b');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('outer { one }');
+ });
+
+
+ it('should activate a component when canActivate returns true', function () {
+ var spy = jasmine.createSpy('activate');
+ var activate = registerComponent('activate', 'hi', {
+ canActivate: function () {
+ return true;
+ },
+ onActivate: spy
+ });
+
+ $router.config([
+ { path: '/a', component: activate }
+ ]);
+ compile('');
+
+ $router.navigate('/a');
+ $rootScope.$digest();
+
+ expect(spy).toHaveBeenCalled();
+ expect(elt.text()).toBe('hi');
+ });
+
+
+ it('should pass instructions into the canDeactivate hook of controllers', function () {
+ var spy = jasmine.createSpy('canDeactivate').and.returnValue(true);
+ var deactivate = registerComponent('deactivate', '', {
+ canDeactivate: spy
+ });
+
+ $router.config([
+ { path: '/user/:name', component: deactivate },
+ { path: '/post/:id', component: OneController }
+ ]);
+ compile('');
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+ $router.navigate('/post/123');
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalledWith(instructionFor(OneController),
+ instructionFor(deactivate));
+ });
+
+
+ it('should change location path', inject(function ($location) {
+ $router.config([
+ { path: '/user', component: UserController }
+ ]);
+
+ compile('');
+
+ $router.navigate('/user');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/user');
+ }));
+
+ // TODO: test injecting $scope
+
+ it('should navigate on left-mouse click when a link url matches a route', function () {
+ $router.config([
+ { path: '/', component: OneController },
+ { path: '/two', component: TwoController }
+ ]);
+
+ compile('link | ');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('link | one');
+ elt.find('a')[0].click();
+
+ $rootScope.$digest();
+ expect(elt.text()).toBe('link | two');
+ });
+
+
+ it('should not navigate on non-left mouse click when a link url matches a route', inject(function ($router) {
+ $router.config([
+ { path: '/', component: OneController },
+ { path: '/two', component: TwoController }
+ ]);
+
+ compile('link | ');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('link | one');
+ elt.find('a').triggerHandler({ type: 'click', which: 3 });
+
+ $rootScope.$digest();
+ expect(elt.text()).toBe('link | one');
+ }));
+
+
+ // See https://github.com/angular/router/issues/206
+ it('should not navigate a link without an href', function () {
+ $router.config([
+ { path: '/', component: OneController },
+ { path: '/two', component: TwoController }
+ ]);
+ expect(function () {
+ compile('link');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('link');
+ elt.find('a')[0].click();
+ $rootScope.$digest();
+ }).not.toThrow();
+ });
+
+
+ it('should change location to the canonical route', inject(function ($location) {
+ compile('');
+
+ $router.config([
+ { path: '/', redirectTo: '/user' },
+ { path: '/user', component: UserController }
+ ]);
+
+ $router.navigate('/');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/user');
+ }));
+
+
+ it('should change location to the canonical route with nested components', inject(function ($location) {
+ var childRouter = registerComponent('childRouter', '', [
+ { path: '/old-child', redirectTo: '/new-child' },
+ { path: '/new-child', component: OneController},
+ { path: '/old-child-two', redirectTo: '/new-child-two' },
+ { path: '/new-child-two', component: TwoController}
+ ]);
+
+ $router.config([
+ { path: '/old-parent', redirectTo: '/new-parent' },
+ { path: '/new-parent/...', component: childRouter }
+ ]);
+
+ compile('');
+
+ $router.navigate('/old-parent/old-child');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/new-parent/new-child');
+ expect(elt.text()).toBe('inner { one }');
+
+ $router.navigate('/old-parent/old-child-two');
+ $rootScope.$digest();
+
+ expect($location.path()).toBe('/new-parent/new-child-two');
+ expect(elt.text()).toBe('inner { two }');
+ }));
+
+
+ it('should navigate when the location path changes', inject(function ($location) {
+ $router.config([
+ { path: '/one', component: OneController }
+ ]);
+ compile('');
+
+ $location.path('/one');
+ $rootScope.$digest();
+
+ expect(elt.text()).toBe('one');
+ }));
+
+
+ it('should expose a "navigating" property on $router', inject(function ($q) {
+ var defer;
+ var pendingActivate = registerComponent('pendingActivate', '', {
+ onActivate: function () {
+ defer = $q.defer();
+ return defer.promise;
+ }
+ });
+ $router.config([
+ { path: '/pendingActivate', component: pendingActivate }
+ ]);
+ compile('');
+
+ $router.navigate('/pendingActivate');
+ $rootScope.$digest();
+ expect($router.navigating).toBe(true);
+ defer.resolve();
+ $rootScope.$digest();
+ expect($router.navigating).toBe(false);
+ }));
+
+
+ function registerComponent(name, template, config) {
+ var Ctrl;
+ if (!template) {
+ template = '';
+ }
+ if (!config) {
+ Ctrl = function () {};
+ } else if (angular.isArray(config)) {
+ Ctrl = function () {};
+ Ctrl.annotations = [new angular.annotations.RouteConfig(config)];
+ } else if (typeof config === 'function') {
+ Ctrl = config;
+ } else {
+ Ctrl = function () {};
+ if (config.canActivate) {
+ Ctrl.$canActivate = config.canActivate;
+ delete config.canActivate;
+ }
+ Ctrl.prototype = config;
+ }
+ $controllerProvider.register(componentControllerName(name), Ctrl);
+ put(name, template);
+ return Ctrl;
+ }
+
+ function boringController(model, value) {
+ return function () {
+ this[model] = value;
+ };
+ }
+
+ function put(name, template) {
+ $templateCache.put(componentTemplatePath(name), [200, template, {}]);
+ }
+
+ function compile(template) {
+ elt = $compile('' + template + '
')($rootScope);
+ $rootScope.$digest();
+ return elt;
+ }
+});
+
+
+describe('ngOutlet animations', function () {
+
+ var elt,
+ $animate,
+ $compile,
+ $rootScope,
+ $router,
+ $templateCache,
+ $controllerProvider;
+
+ function UserController($routeParams) {
+ this.name = $routeParams.name;
+ }
+
+ beforeEach(function () {
+ module('ngAnimate');
+ module('ngAnimateMock');
+ module('ngComponentRouter');
+ module(function (_$controllerProvider_) {
+ $controllerProvider = _$controllerProvider_;
+ });
+
+ inject(function (_$animate_, _$compile_, _$rootScope_, _$router_, _$templateCache_) {
+ $animate = _$animate_;
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $router = _$router_;
+ $templateCache = _$templateCache_;
+ });
+
+ put('user', 'hello {{user.name}}
');
+ $controllerProvider.register('UserController', UserController);
+ });
+
+ afterEach(function () {
+ expect($animate.queue).toEqual([]);
+ });
+
+ it('should work in a simple case', function () {
+ var item;
+
+ compile('');
+
+ $router.config([
+ { path: '/user/:name', component: UserController }
+ ]);
+
+ $router.navigate('/user/brian');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('hello brian');
+
+ // "user" component enters
+ item = $animate.queue.shift();
+ expect(item.event).toBe('enter');
+
+ // navigate to pete
+ $router.navigate('/user/pete');
+ $rootScope.$digest();
+ expect(elt.text()).toBe('hello pete');
+
+ // "user pete" component enters
+ item = $animate.queue.shift();
+ expect(item.event).toBe('enter');
+ expect(item.element.text()).toBe('hello pete');
+
+ // "user brian" component leaves
+ item = $animate.queue.shift();
+ expect(item.event).toBe('leave');
+ expect(item.element.text()).toBe('hello brian');
+ });
+
+ function put(name, template) {
+ $templateCache.put(componentTemplatePath(name), [200, template, {}]);
+ }
+
+ function compile(template) {
+ elt = $compile('' + template + '
')($rootScope);
+ $rootScope.$digest();
+ return elt;
+ }
+});
diff --git a/modules/angular1_router/test/util.es5.js b/modules/angular1_router/test/util.es5.js
new file mode 100644
index 0000000000..c12ed8d632
--- /dev/null
+++ b/modules/angular1_router/test/util.es5.js
@@ -0,0 +1,85 @@
+/*
+ * Helpers to keep tests DRY
+ */
+
+function componentTemplatePath(name) {
+ return './components/' + dashCase(name) + '/' + dashCase(name) + '.html';
+}
+
+function componentControllerName(name) {
+ return name[0].toUpperCase() + name.substr(1) + 'Controller';
+}
+
+function dashCase(str) {
+ return str.replace(/([A-Z])/g, function ($1) {
+ return '-' + $1.toLowerCase();
+ });
+}
+
+function boringController (model, value) {
+ return function () {
+ this[model] = value;
+ };
+}
+
+function provideHelpers(fn, preInject) {
+ return function () {
+ var elt,
+ $compile,
+ $rootScope,
+ $router,
+ $templateCache,
+ $controllerProvider;
+
+ module('ng');
+ module('ngNewRouter');
+ module(function(_$controllerProvider_) {
+ $controllerProvider = _$controllerProvider_;
+ });
+
+ inject(function(_$compile_, _$rootScope_, _$router_, _$templateCache_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $router = _$router_;
+ $templateCache = _$templateCache_;
+ });
+
+ function registerComponent(name, template, config) {
+ if (!template) {
+ template = '';
+ }
+ var ctrl;
+ if (!config) {
+ ctrl = function () {};
+ } else if (angular.isArray(config)) {
+ ctrl = function () {};
+ ctrl.$routeConfig = config;
+ } else if (typeof config === 'function') {
+ ctrl = config;
+ } else {
+ ctrl = function () {};
+ ctrl.prototype = config;
+ }
+ $controllerProvider.register(componentControllerName(name), ctrl);
+ put(name, template);
+ }
+
+
+ function put (name, template) {
+ $templateCache.put(componentTemplatePath(name), [200, template, {}]);
+ }
+
+ function compile(template) {
+ var elt = $compile('' + template + '
')($rootScope);
+ $rootScope.$digest();
+ return elt;
+ }
+
+ fn({
+ registerComponent: registerComponent,
+ $router: $router,
+ put: put,
+ compile: compile
+ })
+ }
+}
diff --git a/package.json b/package.json
index 66ca07182f..eea424363f 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,8 @@
},
"devDependencies": {
"angular": "1.3.5",
+ "angular-animate": "1.3.5",
+ "angular-mocks": "1.3.5",
"base64-js": "^0.0.8",
"bower": "^1.3.12",
"broccoli": "^0.15.3",
diff --git a/scripts/ci/build_router.sh b/scripts/ci/build_router.sh
new file mode 100755
index 0000000000..32247675ca
--- /dev/null
+++ b/scripts/ci/build_router.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -e
+
+echo =============================================================================
+# go to project dir
+SCRIPT_DIR=$(dirname $0)
+# this is needed because we're running JS tests in Dartium too
+source $SCRIPT_DIR/env_dart.sh
+cd $SCRIPT_DIR/../..
+
+./node_modules/.bin/gulp buildRouter.dev
diff --git a/scripts/ci/test_router.sh b/scripts/ci/test_router.sh
new file mode 100755
index 0000000000..ced7f9f2ca
--- /dev/null
+++ b/scripts/ci/test_router.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+set -e
+
+echo =============================================================================
+# go to project dir
+SCRIPT_DIR=$(dirname $0)
+source $SCRIPT_DIR/env_dart.sh
+cd $SCRIPT_DIR/../..
+
+./node_modules/.bin/gulp test.unit.router/ci --browsers=${KARMA_BROWSERS:-ChromeCanary}
diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts
index 669ee354ba..3d26ebc5c8 100644
--- a/tools/broccoli/trees/browser_tree.ts
+++ b/tools/broccoli/trees/browser_tree.ts
@@ -76,6 +76,7 @@ module.exports = function makeBrowserTree(options, destinationPath) {
exclude: [
'**/*.cjs',
'benchmarks/e2e_test/**',
+ 'angular1_router/**',
// Exclude ES6 polyfill typings when tsc target=ES6
'angular2/traceur-runtime.d.ts',
'angular2/typings/es6-promise/**'
diff --git a/tools/broccoli/trees/dart_tree.ts b/tools/broccoli/trees/dart_tree.ts
index 3464e50229..b482b82559 100644
--- a/tools/broccoli/trees/dart_tree.ts
+++ b/tools/broccoli/trees/dart_tree.ts
@@ -56,7 +56,7 @@ function stripModulePrefix(relativePath: string): string {
function getSourceTree() {
// Transpile everything in 'modules' except for rtts_assertions.
- var tsInputTree = modulesFunnel(['**/*.js', '**/*.ts', '**/*.dart']);
+ var tsInputTree = modulesFunnel(['**/*.js', '**/*.ts', '**/*.dart'], ['angular1_router/**/*']);
var transpiled = ts2dart(tsInputTree, {
generateLibraryName: true,
generateSourceMap: false,
@@ -147,7 +147,7 @@ function getDocsTree() {
var licenses = new MultiCopy('', {
srcPath: 'LICENSE',
targetPatterns: ['modules/*'],
- exclude: ['*/rtts_assert', '*/http', '*/upgrade'], // Not in dart.
+ exclude: ['*/rtts_assert', '*/http', '*/upgrade', '*/angular1_router'] // Not in dart.
});
licenses = stew.rename(licenses, stripModulePrefix);
@@ -157,7 +157,7 @@ function getDocsTree() {
relativePath => relativePath.replace(/\.dart\.md$/, '.md'));
// Copy all assets, ignore .js. and .dart. (handled above).
var docs = modulesFunnel(['**/*.md', '**/*.png', '**/*.html', '**/*.css', '**/*.scss'],
- ['**/*.js.md', '**/*.dart.md']);
+ ['**/*.js.md', '**/*.dart.md', 'angular1_router/**/*']);
var assets = modulesFunnel(['examples/**/*.json']);
diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts
index 99b27bb99e..4d7fcad5a2 100644
--- a/tools/broccoli/trees/node_tree.ts
+++ b/tools/broccoli/trees/node_tree.ts
@@ -23,7 +23,8 @@ module.exports = function makeNodeTree(destinationPath) {
'angular2/test/core/zone/**',
'angular2/test/test_lib/fake_async_spec.ts',
'angular2/test/render/xhr_impl_spec.ts',
- 'angular2/test/forms/**'
+ 'angular2/test/forms/**',
+ 'angular1_router/**'
]
});