diff --git a/lib/core_dom/common.dart b/lib/core_dom/common.dart index f95789020..012023c00 100644 --- a/lib/core_dom/common.dart +++ b/lib/core_dom/common.dart @@ -23,3 +23,14 @@ class DirectiveRef { } } +/** + * Creates a child injector that allows loading new directives, filters and + * services from the provided modules. + */ +Injector forceNewDirectivesAndFilters(Injector injector, List modules) { + modules.add(new Module() + ..factory(Scope, + (i) => i.parent.get(Scope).$new(filters: i.get(FilterMap)))); + return injector.createChild(modules, + forceNewInstances: [DirectiveMap, FilterMap]); +} diff --git a/lib/routing/module.dart b/lib/routing/module.dart index e32c9682a..ef5998f2c 100644 --- a/lib/routing/module.dart +++ b/lib/routing/module.dart @@ -175,6 +175,7 @@ class NgRoutingModule extends Module { type(NgRoutingHelper); value(RouteProvider, null); value(RouteInitializer, null); + value(RouteInitializerFn, null); // directives value(NgViewDirective, null); diff --git a/lib/routing/ng_view.dart b/lib/routing/ng_view.dart index fb8da9988..74a65a81b 100644 --- a/lib/routing/ng_view.dart +++ b/lib/routing/ng_view.dart @@ -64,18 +64,15 @@ part of angular.routing; class NgViewDirective implements NgDetachAware, RouteProvider { final NgRoutingHelper locationService; final BlockCache blockCache; - final Scope scope; final Injector injector; final Element element; - final DirectiveMap directives; RouteHandle _route; Block _previousBlock; Scope _previousScope; Route _viewRoute; - NgViewDirective(this.element, this.blockCache, this.scope, Injector injector, - Router router, this.directives) + NgViewDirective(this.element, this.blockCache, Injector injector, Router router) : injector = injector, locationService = injector.get(NgRoutingHelper) { RouteProvider routeProvider = injector.parent.get(NgViewDirective); if (routeProvider != null) { @@ -98,7 +95,7 @@ class NgViewDirective implements NgDetachAware, RouteProvider { locationService._unregisterPortal(this); } - _show(String templateUrl, Route route) { + _show(String templateUrl, Route route, List modules) { assert(route.isActive); if (_viewRoute != null) return; @@ -112,11 +109,18 @@ class NgViewDirective implements NgDetachAware, RouteProvider { _cleanUp(); }); - blockCache.fromUrl(templateUrl, directives).then((blockFactory) { + var viewInjector = injector; + if (modules != null) { + viewInjector = forceNewDirectivesAndFilters(viewInjector, modules); + } + + var newDirectives = viewInjector.get(DirectiveMap); + blockCache.fromUrl(templateUrl, newDirectives).then((blockFactory) { _cleanUp(); - _previousScope = scope.$new(); + _previousScope = viewInjector.get(Scope).$new(); _previousBlock = blockFactory( - injector.createChild([new Module()..value(Scope, _previousScope)])); + viewInjector.createChild( + [new Module()..value(Scope, _previousScope)])); _previousBlock.elements.forEach((elm) => element.append(elm)); }); diff --git a/lib/routing/routing.dart b/lib/routing/routing.dart index 098cb2e13..4a26bf6f8 100644 --- a/lib/routing/routing.dart +++ b/lib/routing/routing.dart @@ -9,8 +9,76 @@ class ViewFactory { ViewFactory(this.locationService); call(String templateUrl) => - (RouteEvent event) => - locationService._route(event.route, templateUrl, fromEvent: true); + (RouteEnterEvent event) => _enterHandler(event, templateUrl); + + _enterHandler(RouteEnterEvent event, String templateUrl, [List modules]) => + locationService._route(event.route, templateUrl, fromEvent: true, modules: modules); + + configure(Map config) => + _configure(locationService.router.root, config); + + _configure(Route route, Map config) { + config.forEach((name, cfg) { + var moduledCalled = false; + List newModules; + route.addRoute( + name: name, + path: cfg.path, + defaultRoute: cfg.defaultRoute, + enter: (RouteEnterEvent e) { + if (cfg.view != null) { + _enterHandler(e, cfg.view, newModules); + } + if (cfg.enter != null) { + cfg.enter(e); + } + }, + preEnter: (RoutePreEnterEvent e) { + if (cfg.modules != null && !moduledCalled) { + moduledCalled = true; + var modules = cfg.modules(); + if (modules is Future) { + e.allowEnter(modules.then((List m) { + newModules = m; + return true; + })); + } else { + newModules = modules; + } + } + if (cfg.preEnter != null) { + cfg.preEnter(e); + } + }, + leave: cfg.leave, + mount: (Route mountRoute) { + if (cfg.mount != null) { + _configure(mountRoute, cfg.mount); + } + }); + }); + } +} + +NgRouteCfg ngRoute({String path, String view, Map mount, + modules(), bool defaultRoute: false, RoutePreEnterEventHandler preEnter, + RouteEnterEventHandler enter, RouteLeaveEventHandler leave}) => + new NgRouteCfg(path: path, view: view, mount: mount, modules: modules, + defaultRoute: defaultRoute, preEnter: preEnter, enter: enter, + leave: leave); + +class NgRouteCfg { + final String path; + final String view; + final Map mount; + final Function modules; + final bool defaultRoute; + final RouteEnterEventHandler enter; + final RoutePreEnterEventHandler preEnter; + final RouteLeaveEventHandler leave; + + NgRouteCfg({this.view, this.path, this.mount, this.modules, this.defaultRoute, + this.enter, this.preEnter, this.leave}); } /** @@ -19,11 +87,23 @@ class ViewFactory { * * The [init] method will be called by the framework once the router is * instantiated but before [NgBindRouteDirective] and [NgViewDirective]. + * + * Deprecated: use RouteInitializerFn instead. */ +@deprecated abstract class RouteInitializer { void init(Router router, ViewFactory viewFactory); } +/** + * An typedef that must be implemented by the user of routing library and + * should include the route initialization. + * + * The function will be called by the framework once the router is + * instantiated but before [NgBindRouteDirective] and [NgViewDirective]. + */ +typedef void RouteInitializerFn(Router router, ViewFactory viewFactory); + /** * A singleton helper service that handles routing initialization, global * events and view registries. @@ -33,15 +113,22 @@ class NgRoutingHelper { final Router router; final NgApp _ngApp; List portals = []; - Map _templates = new Map(); + Map _templates = new Map(); - NgRoutingHelper(RouteInitializer initializer, this.router, this._ngApp) { - if (initializer == null) { + NgRoutingHelper(RouteInitializer initializer, Injector injector, this.router, this._ngApp) { + // TODO: move this to constructor parameters when di issue is fixed: + // https://github.com/angular/di.dart/issues/40 + RouteInitializerFn initializerFn = injector.get(RouteInitializerFn); + if (initializer == null && initializerFn == null) { window.console.error('No RouteInitializer implementation provided.'); return; }; - initializer.init(router, new ViewFactory(this)); + if (initializerFn != null) { + initializerFn(router, new ViewFactory(this)); + } else { + initializer.init(router, new ViewFactory(this)); + } router.onRouteStart.listen((RouteStartEvent routeEvent) { routeEvent.completed.then((success) { if (success) { @@ -60,23 +147,24 @@ class NgRoutingHelper { activePath = activePath.skip(_routeDepth(startingFrom)); } for (Route route in activePath) { - var templateUrl = _templates[_routePath(route)]; - if (templateUrl == null) continue; + var viewDef = _templates[_routePath(route)]; + if (viewDef == null) continue; + var templateUrl = viewDef.template; NgViewDirective view = portals.lastWhere((NgViewDirective v) { return _routePath(route) != _routePath(v._route) && _routePath(route).startsWith(_routePath(v._route)); }, orElse: () => null); if (view != null && !alreadyActiveViews.contains(view)) { - view._show(templateUrl, route); + view._show(templateUrl, route, viewDef.modules); alreadyActiveViews.add(view); break; } } } - _route(Route route, String template, {bool fromEvent}) { - _templates[_routePath(route)] = template; + _route(Route route, String template, {bool fromEvent, List modules}) { + _templates[_routePath(route)] = new _View(template, modules); } _registerPortal(NgViewDirective ngView) { @@ -88,6 +176,13 @@ class NgRoutingHelper { } } +class _View { + final String template; + final List modules; + + _View(this.template, this.modules); +} + String _routePath(Route route) { var path = []; var p = route; diff --git a/test/core_dom/block_spec.dart b/test/core_dom/block_spec.dart index e5a08bcc7..c94fee0ed 100644 --- a/test/core_dom/block_spec.dart +++ b/test/core_dom/block_spec.dart @@ -2,6 +2,12 @@ library block_spec; import '../_specs.dart'; +class Log { + List log = []; + + add(String msg) => log.add(msg); +} + @NgDirective(children: NgAnnotation.TRANSCLUDE_CHILDREN, selector: 'foo') class LoggerBlockDirective { LoggerBlockDirective(BlockHole hole, BlockFactory blockFactory, @@ -16,24 +22,43 @@ class LoggerBlockDirective { } } -class ReplaceBlockDirective { - ReplaceBlockDirective(BlockHole hole, BoundBlockFactory boundBlockFactory, Node node, Scope scope) { - var block = boundBlockFactory(scope); - block.insertAfter(hole); - node.remove(); +@NgDirective(selector: 'dir-a') +class ADirective { + ADirective(Log log) { + log.add('ADirective'); + } +} + +@NgDirective(selector: 'dir-b') +class BDirective { + BDirective(Log log) { + log.add('BDirective'); + } +} + +@NgFilter(name:'filterA') +class AFilter { + Log log; + + AFilter(this.log) { + log.add('AFilter'); } + + call(value) => value; } -class ShadowBlockDirective { - ShadowBlockDirective(BlockHole hole, BoundBlockFactory boundBlockFactory, Element element, Scope scope) { - var block = boundBlockFactory(scope); - var shadowRoot = element.createShadowRoot(); - for (var i = 0, ii = block.elements.length; i < ii; i++) { - shadowRoot.append(block.elements[i]); - } +@NgFilter(name:'filterB') +class BFilter { + Log log; + + BFilter(this.log) { + log.add('BFilter'); } + + call(value) => value; } + main() { describe('Block', () { var anchor; @@ -201,6 +226,44 @@ main() { }); }); + describe('deferred', () { + + it('should load directives/filters from the child injector', () { + Module rootModule = new Module() + ..type(Probe) + ..type(Log) + ..type(AFilter) + ..type(ADirective); + + Injector rootInjector = + new DynamicInjector(modules: [new AngularModule(), rootModule]); + Log log = rootInjector.get(Log); + Scope rootScope = rootInjector.get(Scope); + + Compiler compiler = rootInjector.get(Compiler); + DirectiveMap directives = rootInjector.get(DirectiveMap); + compiler(es('{{\'a\' | filterA}}'), directives)(rootInjector); + rootScope.$digest(); + + expect(log.log, equals(['ADirective', 'AFilter'])); + + + Module childModule = new Module() + ..type(BFilter) + ..type(BDirective); + + var childInjector = forceNewDirectivesAndFilters(rootInjector, [childModule]); + + DirectiveMap newDirectives = childInjector.get(DirectiveMap); + compiler(es('{{\'a\' | filterA}}' + '{{\'b\' | filterB}}'), newDirectives)(childInjector); + rootScope.$digest(); + + expect(log.log, equals(['ADirective', 'AFilter', 'ADirective', 'BDirective', 'BFilter'])); + }); + + }); + //TODO: tests for attach/detach //TODO: animation/transitions //TODO: tests for re-usability of blocks diff --git a/test/routing/routing_spec.dart b/test/routing/routing_spec.dart index a01833407..be2b6aba9 100644 --- a/test/routing/routing_spec.dart +++ b/test/routing/routing_spec.dart @@ -1,8 +1,8 @@ library routing_spec; import '../_specs.dart'; -import 'package:angular/routing/module.dart'; import 'package:angular/mock/module.dart'; +import 'dart:async'; main() { describe('routing', () { @@ -35,6 +35,322 @@ main() { })); }); + + describe('routing DSL', () { + Router router; + TestBed _; + + afterEach(() { + router = _ = null; + }); + + initRouter(initializer) { + var module = new Module() + ..value(RouteInitializerFn, initializer); + var injector = new DynamicInjector( + modules: [new AngularModule(), new AngularMockModule(), module]); + injector.get(NgRoutingHelper); // force routing initialization + router = injector.get(Router); + _ = injector.get(TestBed); + } + + it('should configure route hierarchy from provided config', async(() { + var counters = { + 'foo': 0, + 'bar': 0, + 'baz': 0, + 'aux': 0, + }; + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + enter: (_) => counters['foo']++, + mount: { + 'bar': ngRoute( + path: '/bar', + enter: (_) => counters['bar']++ + ), + 'baz': ngRoute( + path: '/baz', + enter: (_) => counters['baz']++ + ) + } + ), + 'aux': ngRoute( + path: '/aux', + enter: (_) => counters['aux']++ + ) + }); + }); + + expect(router.root.getRoute('foo').name).toEqual('foo'); + expect(router.root.getRoute('foo.bar').name).toEqual('bar'); + expect(router.root.getRoute('foo.baz').name).toEqual('baz'); + expect(router.root.getRoute('aux').name).toEqual('aux'); + + router.route('/foo'); + microLeap(); + expect(counters, equals({ + 'foo': 1, + 'bar': 0, + 'baz': 0, + 'aux': 0, + })); + + router.route('/foo/bar'); + microLeap(); + expect(counters, equals({ + 'foo': 1, + 'bar': 1, + 'baz': 0, + 'aux': 0, + })); + + router.route('/foo/baz'); + microLeap(); + expect(counters, equals({ + 'foo': 1, + 'bar': 1, + 'baz': 1, + 'aux': 0, + })); + + router.route('/aux'); + microLeap(); + expect(counters, equals({ + 'foo': 1, + 'bar': 1, + 'baz': 1, + 'aux': 1, + })); + })); + + + it('should set the default route', async(() { + int enterCount = 0; + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute(path: '/foo'), + 'bar': ngRoute(path: '/bar', defaultRoute: true), + 'baz': ngRoute(path: '/baz'), + }); + }); + + router.route('/invalidRoute'); + microLeap(); + + expect(router.activePath.length).toBe(1); + expect(router.activePath.first.name).toBe('bar'); + })); + + + it('should call enter callback and show the view when routed', async(() { + int enterCount = 0; + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + enter: (_) => enterCount++, + view: 'foo.html' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '

Foo

')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + + expect(enterCount).toBe(1); + expect(root.text).toEqual('Foo'); + })); + + + it('should call preEnter callback and load modules', async(() { + int preEnterCount = 0; + int modulesCount = 0; + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + preEnter: (_) => preEnterCount++, + modules: () { + modulesCount++; + return new Future.value(); + } + ), + 'bar': ngRoute( + path: '/bar' + ) + }); + }); + + router.route('/foo'); + microLeap(); + + expect(preEnterCount).toBe(1); + expect(modulesCount).toBe(1); + + router.route('/foo'); + microLeap(); + + expect(preEnterCount).toBe(1); + expect(modulesCount).toBe(1); + + router.route('/bar'); + microLeap(); + + expect(preEnterCount).toBe(1); + expect(modulesCount).toBe(1); + + router.route('/foo'); + microLeap(); + + expect(preEnterCount).toBe(2); + expect(modulesCount).toBe(1); + })); + + + it('should clear view on leave an call leave callback', async(() { + int leaveCount = 0; + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + leave: (_) => leaveCount++, + view: 'foo.html' + ), + 'bar': ngRoute( + path: '/bar' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '

Foo

')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + + expect(root.text).toEqual('Foo'); + expect(leaveCount).toBe(0); + + router.route('/bar'); + microLeap(); + + expect(root.text).toEqual(''); + expect(leaveCount).toBe(1); + })); + + + it('should synchronously load new directives from modules ', async(() { + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + modules: () => [ + new Module()..type(NewDirective) + ], + view: 'foo.html' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '
Old!
')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + + expect(root.text).toEqual('New!'); + })); + + + it('should asynchronously load new directives from modules ', async(() { + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + modules: () => new Future.value([ + new Module()..type(NewDirective) + ]), + view: 'foo.html' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '
Old!
')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + + expect(root.text).toEqual('New!'); + })); + + + it('should synchronously load new filters from modules ', async(() { + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + modules: () => [ + new Module()..type(HelloFilter) + ], + view: 'foo.html' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '
{{\'World\' | hello}}
')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + _.rootScope.$digest(); + + expect(root.text).toEqual('Hello, World!'); + })); + + + it('should asynchronously load new filters from modules ', async(() { + initRouter((Router router, ViewFactory views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + modules: () => new Future.value([ + new Module()..type(HelloFilter) + ]), + view: 'foo.html' + ), + }); + }); + _.injector.get(TemplateCache) + .put('foo.html', new HttpResponse(200, '
{{\'World\' | hello}}
')); + + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + _.rootScope.$digest(); + + expect(root.text).toEqual('Hello, World!'); + })); + + }); } class TestRouteInitializer implements RouteInitializer { @@ -46,3 +362,19 @@ class TestRouteInitializer implements RouteInitializer { this.router = router; } } + + +@NgDirective(selector: '[make-it-new]') +class NewDirective { + NewDirective(Element element) { + element.innerHtml = 'New!'; + } +} + +@NgFilter(name:'hello') +class HelloFilter { + call(String str) { + return 'Hello, $str!'; + } +} +