From fde026a9e485d9e68b408793add7eaffcf383a86 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Wed, 29 Apr 2015 15:46:42 -0700 Subject: [PATCH] feat(router): add angular 1.x router --- .travis.yml | 1 + gulpfile.js | 35 + karma-dart.conf.js | 1 + karma-js.conf.js | 1 + modules/angular1_router/build.js | 123 +++ modules/angular1_router/index.html | 22 + modules/angular1_router/karma-router.conf.js | 28 + modules/angular1_router/lib/facades.es5 | 305 +++++++ modules/angular1_router/src/ng_outlet.js | 432 ++++++++++ .../test/component_mapper_spec.js | 77 ++ .../test/controller_introspector_spec.js | 38 + .../angular1_router/test/ng_outlet_spec.js | 800 ++++++++++++++++++ modules/angular1_router/test/util.es5.js | 85 ++ package.json | 2 + scripts/ci/build_router.sh | 11 + scripts/ci/test_router.sh | 10 + tools/broccoli/trees/browser_tree.ts | 1 + tools/broccoli/trees/dart_tree.ts | 6 +- tools/broccoli/trees/node_tree.ts | 3 +- 19 files changed, 1977 insertions(+), 4 deletions(-) create mode 100644 modules/angular1_router/build.js create mode 100644 modules/angular1_router/index.html create mode 100644 modules/angular1_router/karma-router.conf.js create mode 100644 modules/angular1_router/lib/facades.es5 create mode 100644 modules/angular1_router/src/ng_outlet.js create mode 100644 modules/angular1_router/test/component_mapper_spec.js create mode 100644 modules/angular1_router/test/controller_introspector_spec.js create mode 100644 modules/angular1_router/test/ng_outlet_spec.js create mode 100644 modules/angular1_router/test/util.es5.js create mode 100755 scripts/ci/build_router.sh create mode 100755 scripts/ci/test_router.sh 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 + *
+ * {{app.user.name}} + *
+ * ``` + */ +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', '
inner {
}
', [ + { path: '/b', component: OneController } + ]); + + $router.config([ + { path: '/a/...', component: childComponent } + ]); + compile('
outer {
}
'); + + $router.navigate('/a/b'); + $rootScope.$digest(); + + expect(elt.text()).toBe('outer { inner { one } }'); + }); + + + it('should work with recursive nested outlets', function () { + put('two', '
recur {
}
'); + $router.config([ + { path: '/recur', component: TwoController }, + { path: '/', component: OneController } + ]); + + compile('
root {
}
'); + $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', '
{{number}}
'); + + $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', '
outer {
}
'); + put('one', '
{{number}}
'); + + $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', '
outer {
}
'); + put('one', '
{{one.number}}
'); + + $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('
outer {
}
'); + + $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', '
inner {
}
', [ + { 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/**' ] });