diff --git a/lib/angular.dart b/lib/angular.dart index aa264f558..e43817964 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -1,5 +1,6 @@ library angular; +import "dart:collection"; import "dart:mirrors"; import "dart:async" as async; import "dart:json" as json; @@ -107,6 +108,15 @@ class AngularModule extends Module { AngularModule() { value(DirectiveRegistry, _directives); + type(Compiler, Compiler); + type(BlockFactory, BlockFactory); + type(BlockTypeFactory, BlockTypeFactory); + type(BlockListFactory, BlockListFactory); + type(ExceptionHandler, ExceptionHandler); + type(Scope, Scope); + type(Parser, Parser); + type(Interpolate, Interpolate); + type(Http, Http); } directive(Type directive) { diff --git a/lib/block.dart b/lib/block.dart index 983bcc640..8c434909f 100644 --- a/lib/block.dart +++ b/lib/block.dart @@ -28,9 +28,9 @@ class BlockCache { flush([Function callback]) { groupCache.forEach((blocks) { - while(!blocks.isEmpty) { + while(blocks.isNotEmpty) { Block block = blocks.removeLast(); - if (?callback) callback(block); + if (callback != null) callback(block); } }); } @@ -84,10 +84,10 @@ class Block implements ElementWrapper { ASSERT(elements != null); ASSERT(directivePositions != null); ASSERT(blockCaches != null); - _link(elements, directivePositions, blockCaches); + _link(elements, directivePositions, blockCaches, $injector); } - _link(List nodeList, List directivePositions, List blockCaches) { + _link(List nodeList, List directivePositions, List blockCaches, Injector parentInjector) { var stack; try {throw '';} catch(e,s) {stack = s;} var preRenderedIndexOffset = 0; @@ -112,7 +112,7 @@ class Block implements ElementWrapper { Map anchorsByName = {}; List directiveNames = []; - + var injector = parentInjector; if (directiveRefs != null) { for (var j = 0, jj = directiveRefs.length; j < jj; j++) { var blockCache; @@ -135,10 +135,10 @@ class Block implements ElementWrapper { anchorsByName[name] = $blockListFactory([node], directiveRef.blockTypes, blockCache); } } - _instantiateDirectives(directiveDefsByName, directiveNames, node, anchorsByName); + injector = _instantiateDirectives(directiveDefsByName, directiveNames, node, anchorsByName, parentInjector); } if (childDirectivePositions != null) { - _link(node.nodes, childDirectivePositions, blockCaches); + _link(node.nodes, childDirectivePositions, blockCaches, injector); } if (fakeParent) { @@ -148,10 +148,11 @@ class Block implements ElementWrapper { } } - _instantiateDirectives(Map directiveDefsByName, + Injector _instantiateDirectives(Map directiveDefsByName, List directiveNames, dom.Node node, - Map anchorsByName) { + Map anchorsByName, + Injector parentInjector) { var elementModule = new Module(); elementModule.value(Block, this); elementModule.value(dom.Element, node); @@ -160,45 +161,88 @@ class Block implements ElementWrapper { def.directive.type, def.directive.type)); for (var i = 0, ii = directiveNames.length; i < ii; i++) { - var directiveName = directiveNames[i]; - DirectiveRef directiveRef = directiveDefsByName[directiveName]; - - var directiveModule = new Module(); - - directiveModule.value(DirectiveValue, - new DirectiveValue.fromString(directiveRef.value)); - - directiveModule.value(BlockList, anchorsByName[directiveName]); - + DirectiveRef directiveRef = directiveDefsByName[directiveNames[i]]; Type directiveType = directiveRef.directive.type; + var visibility = local; + if (directiveRef.directive.$visibility == DirectiveVisibility.CHILDREN) { + visibility = null; + } else if (directiveRef.directive.$visibility == DirectiveVisibility.DIRECT_CHILDREN) { + visibility = directChildren; + } + elementModule.type(directiveType, directiveType, creation: directOnly, visibility: visibility); + } - var injector = $injector.createChild( - [elementModule, directiveModule], - [directiveType]); - - try { - var directiveInstance = injector.get(directiveType); - if (directiveRef.directive.isComponent) { - directiveInstance = new ComponentWrapper(directiveRef, directiveInstance, node, - $injector.get(Parser), $injector.get(Compiler), $injector.get(Http)); + var injector = parentInjector.createChild([elementModule]); + + int prevInstantiatedCount; + List alreadyInstantiated = []; + // TODO(pavelgj): this is a workaround for the lack of directive + // instantiation ordering. A better way is to sort directives in the + // order they must be instantiated in. + do { + prevInstantiatedCount = alreadyInstantiated.length; + for (var i = 0, ii = directiveNames.length; i < ii; i++) { + var directiveName = directiveNames[i]; + if (alreadyInstantiated.contains(directiveName)) continue; + DirectiveRef directiveRef = directiveDefsByName[directiveName]; + + Map locals = new HashMap(); + locals[DirectiveValue] = + new DirectiveValue.fromString(directiveRef.value); + locals[BlockList] = anchorsByName[directiveName]; + + Type directiveType = directiveRef.directive.type; + + try { + var directiveInstance = injector.instantiate(directiveType, locals); + alreadyInstantiated.add(directiveName); + if (directiveRef.directive.isComponent) { + directiveInstance = new ComponentWrapper(directiveRef, directiveInstance, node, + $injector.get(Parser), $injector.get(Compiler), $injector.get(Http)); - } - directives.add(directiveInstance); - } catch (e,s) { - var msg; - if (e is MirroredUncaughtExceptionError) { - //TODO(misko): why is this here? Injector should never throw this exception - msg = e.exception_string + "\n ORIGINAL Stack trace:\n" + e.stacktrace.toString(); - } else { - msg = "Creating $directiveName: " + e.toString() + + } + directives.add(directiveInstance); + } catch (e, s) { + if (e is MirroredUncaughtExceptionError) { + //TODO(misko): why is this here? Injector should never throw this exception + throw e.exception_string + "\n ORIGINAL Stack trace:\n" + e.stacktrace.toString(); + } else if (e is IndirectInstantiationError) { + // ignore. + } else { + throw "Creating $directiveName: " + e.toString() + "\n ORIGINAL Stack trace:\n" + s.toString(); + } } - - throw msg; } + } while(alreadyInstantiated.length != prevInstantiatedCount); + + if (alreadyInstantiated.length != directiveNames.length) { + throw 'Cyclic dependency in directives on $node.'; } + return injector; } + + /// DI creation strategy that only allows 'explicit' injection. + dynamic directOnly(Symbol type, + Injector requesting, + Injector defining, + bool directInstantation, + Factory factory) { + if (!directInstantation) { + throw new IndirectInstantiationError(type); + } + return factory(); + } + + /// DI visibility callback allowin node-local visibility. + bool local(Injector requesting, Injector defining) => + identical(requesting, defining); + + /// DI visibility callback allowin visibility from direct child into parent. + bool directChildren(Injector requesting, Injector defining) => + local(requesting, defining) || identical(requesting.parent, defining); + attach(Scope scope) { // Attach directives for(var i = 0, ii = directives.length; i < ii; i++) { @@ -336,6 +380,19 @@ class Block implements ElementWrapper { } } +class IndirectInstantiationError { + IndirectInstantiationError(type) + : exception_string = '$type must be directly instantated before being ' + 'injected from child injection'; + + /** The result of toString() for the exception object. */ + final String exception_string; + + String toString() { + return exception_string; + } +} + class ComponentWrapper { DirectiveRef directiveRef; dynamic controller; diff --git a/lib/directive.dart b/lib/directive.dart index 4a2d8a7d7..6439a2d22 100644 --- a/lib/directive.dart +++ b/lib/directive.dart @@ -14,6 +14,7 @@ class Directive { String $template; String $templateUrl; Map $map; + String $visibility; bool isComponent = false; bool isStructural = false; @@ -44,6 +45,10 @@ class Directive { $template = reflectStaticField(type, '\$template'); $templateUrl = reflectStaticField(type, '\$templateUrl'); $priority = reflectStaticField(type, '\$priority'); + $visibility = reflectStaticField(type, '\$visibility'); + if ($visibility == null) { + $visibility = DirectiveVisibility.LOCAL; + } $map = reflectStaticField(type, '\$map'); if ($priority == null) { $priority = 0; @@ -106,6 +111,12 @@ class DirectiveValue { DirectiveValue.fromString(this.value); } +abstract class DirectiveVisibility { + static const String LOCAL = 'local'; + static const String CHILDREN = 'children'; + static const String DIRECT_CHILDREN = 'direct_children'; +} + class Controller { } diff --git a/test/_specs.dart b/test/_specs.dart index c33e3344b..e15ee06e7 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -9,6 +9,7 @@ import 'package:angular/angular.dart'; import 'jasmine_syntax.dart'; import 'package:di/di.dart'; import 'package:unittest/mock.dart'; +import "_log.dart"; export 'package:unittest/unittest.dart'; export 'package:angular/debug.dart'; @@ -149,14 +150,16 @@ class SpecInjector { List modules = [new Module()..value(Expando, new Expando('specExpando'))]; module(Function fn) { - Module module = new AngularModule(); + Module module = new AngularModule() + ..type(Log, Log) + ..type(Logger, Logger); modules.add(module); fn(module); } inject(Function fn, declarationStack) { if (injector == null) { - injector = new Injector(modules); + injector = new Injector(modules, false); // Implicit injection is disabled. } try { injector.invoke(fn); diff --git a/test/compiler_spec.dart b/test/compiler_spec.dart index ddc4a0f3f..84c0609f4 100644 --- a/test/compiler_spec.dart +++ b/test/compiler_spec.dart @@ -1,6 +1,43 @@ import "_specs.dart"; +import "_log.dart"; import "dart:mirrors"; + +class TabComponent { + static String $visibility = DirectiveVisibility.DIRECT_CHILDREN; + int id = 0; + Log log; + LocalAttrDirective local; + TabComponent(Log this.log, LocalAttrDirective this.local); + attach(Scope scope) { + log('TabComponent-${id++}'); + local.ping(); + } +} + +class PaneComponent { + TabComponent tabComponent; + LocalAttrDirective localDirective; + Log log; + PaneComponent(TabComponent this.tabComponent, LocalAttrDirective this.localDirective, Log this.log); + attach(Scope scope) { + log('PaneComponent-${tabComponent.id++}'); + localDirective.ping(); + } +} + +class LocalAttrDirective { + static String $visibility = DirectiveVisibility.LOCAL; + int id = 0; + Log log; + LocalAttrDirective(Log this.log); + attach(Scope scope) {} + ping() { + log('LocalAttrDirective-${id++}'); + } +} + + main() { describe('dte.compiler', () { @@ -9,10 +46,12 @@ main() { DirectiveRegistry directives; beforeEach(inject((Injector injector) { - directives = injector.get(DirectiveRegistry); - - directives.register(NgBindAttrDirective); - directives.register(NgRepeatAttrDirective); + directives = injector.get(DirectiveRegistry) + ..register(NgBindAttrDirective) + ..register(NgRepeatAttrDirective) + ..register(TabComponent) + ..register(PaneComponent) + ..register(LocalAttrDirective); $rootScope = injector.get(Scope); })); @@ -454,6 +493,24 @@ main() { component.scope.ondone(); expect($rootScope.done).toEqual(true); })); + + }); + + describe('controller scoping', () { + + it('shoud make controllers available to sibling and child controllers', inject((Compiler $compile, Scope $rootScope, Log log) { + var element = $(''); + $compile(element)(element)..attach($rootScope); + expect(log.result()).toEqual('TabComponent-0; LocalAttrDirective-0; PaneComponent-1; LocalAttrDirective-0; PaneComponent-2; LocalAttrDirective-0'); + })); + + it('should throw an exception if required directive is missing', inject((Compiler $compile, Scope $rootScope) { + expect(() { + var element = $(''); + $compile(element)(element)..attach($rootScope); + }, throwsA(startsWith('Creating pane: Illegal argument(s): No provider found for LocalAttrDirective! (resolving LocalAttrDirective)'))); + })); + }); }); } diff --git a/test/selector_spec.dart b/test/selector_spec.dart index 37f0ce73e..a5cf4c5cc 100644 --- a/test/selector_spec.dart +++ b/test/selector_spec.dart @@ -111,7 +111,7 @@ class DirectiveInfosMatcher extends BaseMatcher { return description; } - bool matches(directiveRefs, MatchState matchState) { + bool matches(directiveRefs, Map matchState) { var pass = expected.length == directiveRefs.length; if (pass) { for(var i = 0, ii = expected.length; i < ii; i++) {