Skip to content
This repository has been archived by the owner on Feb 22, 2018. It is now read-only.

Commit

Permalink
feat(routing): new DSL and deferred module loading
Browse files Browse the repository at this point in the history
Introduced new routing DSL that allows adding new modules for views.
Modules can be loaded synchronously or asynchronously, for example in case of deferred library loading.
  • Loading branch information
pavelgj committed Feb 7, 2014
1 parent 95f6503 commit 3db9ddd
Show file tree
Hide file tree
Showing 6 changed files with 538 additions and 32 deletions.
11 changes: 11 additions & 0 deletions lib/core_dom/common.dart
Expand Up @@ -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<Module> modules) {
modules.add(new Module()
..factory(Scope,
(i) => i.parent.get(Scope).$new(filters: i.get(FilterMap))));
return injector.createChild(modules,
forceNewInstances: [DirectiveMap, FilterMap]);
}
1 change: 1 addition & 0 deletions lib/routing/module.dart
Expand Up @@ -175,6 +175,7 @@ class NgRoutingModule extends Module {
type(NgRoutingHelper);
value(RouteProvider, null);
value(RouteInitializer, null);
value(RouteInitializerFn, null);

// directives
value(NgViewDirective, null);
Expand Down
20 changes: 12 additions & 8 deletions lib/routing/ng_view.dart
Expand Up @@ -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) {
Expand All @@ -98,7 +95,7 @@ class NgViewDirective implements NgDetachAware, RouteProvider {
locationService._unregisterPortal(this);
}

_show(String templateUrl, Route route) {
_show(String templateUrl, Route route, List<Module> modules) {
assert(route.isActive);

if (_viewRoute != null) return;
Expand All @@ -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));
});
Expand Down
117 changes: 106 additions & 11 deletions lib/routing/routing.dart
Expand Up @@ -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<Module> modules]) =>
locationService._route(event.route, templateUrl, fromEvent: true, modules: modules);

configure(Map<String, NgRouteCfg> config) =>
_configure(locationService.router.root, config);

_configure(Route route, Map<String, NgRouteCfg> config) {
config.forEach((name, cfg) {
var moduledCalled = false;
List<Module> 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<Module> 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<String, NgRouteCfg> 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<String, NgRouteCfg> 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});
}

/**
Expand All @@ -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.
Expand All @@ -33,15 +113,22 @@ class NgRoutingHelper {
final Router router;
final NgApp _ngApp;
List<NgViewDirective> portals = <NgViewDirective>[];
Map<String, String> _templates = new Map<String, String>();
Map<String, _View> _templates = new Map<String, _View>();

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) {
Expand All @@ -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<Module> modules}) {
_templates[_routePath(route)] = new _View(template, modules);
}

_registerPortal(NgViewDirective ngView) {
Expand All @@ -88,6 +176,13 @@ class NgRoutingHelper {
}
}

class _View {
final String template;
final List<Module> modules;

_View(this.template, this.modules);
}

String _routePath(Route route) {
var path = [];
var p = route;
Expand Down
87 changes: 75 additions & 12 deletions test/core_dom/block_spec.dart
Expand Up @@ -2,6 +2,12 @@ library block_spec;

import '../_specs.dart';

class Log {
List<String> log = <String>[];

add(String msg) => log.add(msg);
}

@NgDirective(children: NgAnnotation.TRANSCLUDE_CHILDREN, selector: 'foo')
class LoggerBlockDirective {
LoggerBlockDirective(BlockHole hole, BlockFactory blockFactory,
Expand All @@ -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;
Expand Down Expand Up @@ -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('<dir-a>{{\'a\' | filterA}}</dir-a><dir-b></dir-b>'), 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('<dir-a probe="dirA"></dir-a>{{\'a\' | filterA}}'
'<dir-b probe="dirB"></dir-b>{{\'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
Expand Down

0 comments on commit 3db9ddd

Please sign in to comment.