diff --git a/lib/routing/ng_view.dart b/lib/routing/ng_view.dart index be507a9ca..94b0139ca 100644 --- a/lib/routing/ng_view.dart +++ b/lib/routing/ng_view.dart @@ -100,7 +100,7 @@ class NgViewDirective implements NgDetachAware, RouteProvider { locationService._unregisterPortal(this); } - _show(String templateUrl, Route route, List modules) { + _show(_View viewDef, Route route, List modules) { assert(route.isActive); if (_viewRoute != null) return; @@ -120,7 +120,10 @@ class NgViewDirective implements NgDetachAware, RouteProvider { } var newDirectives = viewInjector.get(DirectiveMap); - viewCache.fromUrl(templateUrl, newDirectives).then((viewFactory) { + var viewFuture = viewDef.templateHtml != null ? + new Future.value(viewCache.fromHtml(viewDef.templateHtml, newDirectives)) : + viewCache.fromUrl(viewDef.template, newDirectives); + viewFuture.then((viewFactory) { _cleanUp(); _scope = scope.createChild(new PrototypeMap(scope.context)); _view = viewFactory( diff --git a/lib/routing/routing.dart b/lib/routing/routing.dart index 464076703..4a978ae72 100644 --- a/lib/routing/routing.dart +++ b/lib/routing/routing.dart @@ -12,9 +12,9 @@ class RouteViewFactory { (RouteEnterEvent event) => _enterHandler(event, templateUrl); _enterHandler(RouteEnterEvent event, String templateUrl, - [List modules]) => + {List modules, String templateHtml}) => locationService._route(event.route, templateUrl, fromEvent: true, - modules: modules); + modules: modules, templateHtml: templateHtml); configure(Map config) => _configure(locationService.router.root, config); @@ -28,8 +28,9 @@ class RouteViewFactory { path: cfg.path, defaultRoute: cfg.defaultRoute, enter: (RouteEnterEvent e) { - if (cfg.view != null) { - _enterHandler(e, cfg.view, newModules); + if (cfg.view != null || cfg.viewHtml != null) { + _enterHandler(e, cfg.view, + modules: newModules, templateHtml: cfg.viewHtml); } if (cfg.enter != null) { cfg.enter(e); @@ -62,16 +63,18 @@ class RouteViewFactory { } } -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); +NgRouteCfg ngRoute({String path, String view, String viewHtml, + Map mount, modules(), bool defaultRoute: false, + RoutePreEnterEventHandler preEnter, RouteEnterEventHandler enter, + RouteLeaveEventHandler leave}) => + new NgRouteCfg(path: path, view: view, viewHtml: viewHtml, mount: mount, + modules: modules, defaultRoute: defaultRoute, preEnter: preEnter, + enter: enter, leave: leave); class NgRouteCfg { final String path; final String view; + final String viewHtml; final Map mount; final Function modules; final bool defaultRoute; @@ -79,8 +82,8 @@ class NgRouteCfg { final RoutePreEnterEventHandler preEnter; final RouteLeaveEventHandler leave; - NgRouteCfg({this.view, this.path, this.mount, this.modules, this.defaultRoute, - this.enter, this.preEnter, this.leave}); + NgRouteCfg({this.view, this.viewHtml, this.path, this.mount, this.modules, + this.defaultRoute, this.enter, this.preEnter, this.leave}); } /** @@ -159,15 +162,16 @@ class NgRoutingHelper { _routePath(route).startsWith(_routePath(v._route)); }, orElse: () => null); if (view != null && !alreadyActiveViews.contains(view)) { - view._show(templateUrl, route, viewDef.modules); + view._show(viewDef, route, viewDef.modules); alreadyActiveViews.add(view); break; } } } - _route(Route route, String template, {bool fromEvent, List modules}) { - _templates[_routePath(route)] = new _View(template, modules); + _route(Route route, String template, {bool fromEvent, List modules, + String templateHtml}) { + _templates[_routePath(route)] = new _View(template, templateHtml, modules); } _registerPortal(NgViewDirective ngView) { @@ -181,9 +185,10 @@ class NgRoutingHelper { class _View { final String template; + final String templateHtml; final List modules; - _View(this.template, this.modules); + _View(this.template, this.templateHtml, this.modules); } String _routePath(Route route) { diff --git a/lib/tools/html_extractor.dart b/lib/tools/html_extractor.dart index a9b0b02bf..3919b3913 100644 --- a/lib/tools/html_extractor.dart +++ b/lib/tools/html_extractor.dart @@ -13,7 +13,7 @@ RegExp _MUSTACHE_REGEXP = new RegExp(r'{{([^}]*)}}'); RegExp _NG_REPEAT_SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$'); class HtmlExpressionExtractor { - List directiveInfos; + Iterable directiveInfos; HtmlExpressionExtractor(this.directiveInfos) { for (DirectiveInfo directiveInfo in directiveInfos) { @@ -55,7 +55,8 @@ class HtmlExpressionExtractor { } for (DirectiveInfo directiveInfo in directiveInfos) { - if (matchesNode(node, directiveInfo.selector)) { + if (directiveInfo.selector != null && + matchesNode(node, directiveInfo.selector)) { directiveInfo.expressionAttrs.forEach((attr) { if (node.attributes[attr] != null && attr != 'ng-repeat') { expressions.add(node.attributes[attr]); diff --git a/lib/tools/source_metadata_extractor.dart b/lib/tools/source_metadata_extractor.dart index 2319657e9..586860e49 100644 --- a/lib/tools/source_metadata_extractor.dart +++ b/lib/tools/source_metadata_extractor.dart @@ -107,92 +107,120 @@ class SourceMetadataExtractor { }); dirInfo.expressionAttrs = reprocessedAttrs; directives.add(dirInfo); - }); + directives.addAll(metadataVisitor.templates.map( + (tmpl) => new DirectiveInfo()..template = tmpl)); + return directives; } } -class DirectiveMetadataCollectingVisitor { - List metadata = []; +class DirectiveMetadataCollectingAstVisitor extends RecursiveAstVisitor { + final List metadata; + final List templates; + + DirectiveMetadataCollectingAstVisitor(this.metadata, this.templates); + + visitMethodInvocation(MethodInvocation node) { + if (node.methodName.name == 'ngRoute') { + NamedExpression viewHtmlExpression = + node.argumentList.arguments + .firstWhere((e) => e is NamedExpression && + e.name.label.name == 'viewHtml', orElse: () => null); + if (viewHtmlExpression != null) { + if (viewHtmlExpression.expression is! StringLiteral) { + throw 'viewHtml must be a string literal'; + } + templates.add( + (viewHtmlExpression.expression as StringLiteral).stringValue); + } + } + super.visitMethodInvocation(node); + } - call(CompilationUnit cu) { - cu.declarations.forEach((CompilationUnitMember declaration) { - // We only care about classes. - if (declaration is! ClassDeclaration) return; - ClassDeclaration clazz = declaration; - // Check class annotations for presense of NgComponent/NgDirective. - DirectiveMetadata meta; - clazz.metadata.forEach((Annotation ann) { - if (ann.arguments == null) return; // Ignore non-class annotations. - // TODO(pavelj): this is not a safe check for the type of the - // annotations, but good enough for now. - if (ann.name.name != 'NgComponent' - && ann.name.name != 'NgDirective') return; - - bool isComponent = ann.name.name == 'NgComponent'; - - meta = new DirectiveMetadata() - ..className = clazz.name.name - ..type = isComponent ? COMPONENT : DIRECTIVE; - metadata.add(meta); - - ann.arguments.arguments.forEach((Expression arg) { - if (arg is NamedExpression) { - NamedExpression namedArg = arg; - var paramName = namedArg.name.label.name; - if (paramName == 'selector') { - meta.selector = assertString(namedArg.expression).stringValue; - } - if (paramName == 'template') { - meta.template = assertString(namedArg.expression).stringValue; - } - if (paramName == 'map') { - MapLiteral map = namedArg.expression; - map.entries.forEach((MapLiteralEntry entry) { - meta.attributeMappings[assertString(entry.key).stringValue] = - assertString(entry.value).stringValue; - }); - } - if (paramName == 'exportExpressions') { - meta.exportExpressions = getStringValues(namedArg.expression); - } - if (paramName == 'exportExpressionAttrs') { - meta.exportExpressionAttrs = getStringValues(namedArg.expression); - } + visitClassDeclaration(ClassDeclaration clazz) { + // Check class annotations for presense of NgComponent/NgDirective. + DirectiveMetadata meta; + clazz.metadata.forEach((Annotation ann) { + if (ann.arguments == null) return; // Ignore non-class annotations. + // TODO(pavelj): this is not a safe check for the type of the + // annotations, but good enough for now. + if (ann.name.name != 'NgComponent' + && ann.name.name != 'NgDirective') return; + + bool isComponent = ann.name.name == 'NgComponent'; + + meta = new DirectiveMetadata() + ..className = clazz.name.name + ..type = isComponent ? COMPONENT : DIRECTIVE; + metadata.add(meta); + + ann.arguments.arguments.forEach((Expression arg) { + if (arg is NamedExpression) { + NamedExpression namedArg = arg; + var paramName = namedArg.name.label.name; + if (paramName == 'selector') { + meta.selector = assertString(namedArg.expression).stringValue; } - }); - }); - - // Check fields/getters/setter for presense of attr mapping annotations. - if (meta != null) { - clazz.members.forEach((ClassMember member) { - if (member is FieldDeclaration || - (member is MethodDeclaration && - (member.isSetter || member.isGetter))) { - member.metadata.forEach((Annotation ann) { - if (_attrAnnotationsToSpec.containsKey(ann.name.name)) { - String fieldName; - if (member is FieldDeclaration) { - fieldName = member.fields.variables.first.name.name; - } else { // MethodDeclaration - fieldName = (member as MethodDeclaration).name.name; - } - StringLiteral attNameLiteral = ann.arguments.arguments.first; - if (meta.attributeMappings - .containsKey(attNameLiteral.stringValue)) { - throw 'Attribute mapping already defined for ' - '${clazz.name}.$fieldName'; - } - meta.attributeMappings[attNameLiteral.stringValue] = - _attrAnnotationsToSpec[ann.name.name] + fieldName; - } + if (paramName == 'template') { + meta.template = assertString(namedArg.expression).stringValue; + } + if (paramName == 'map') { + MapLiteral map = namedArg.expression; + map.entries.forEach((MapLiteralEntry entry) { + meta.attributeMappings[assertString(entry.key).stringValue] = + assertString(entry.value).stringValue; }); } - }); - } + if (paramName == 'exportExpressions') { + meta.exportExpressions = getStringValues(namedArg.expression); + } + if (paramName == 'exportExpressionAttrs') { + meta.exportExpressionAttrs = getStringValues(namedArg.expression); + } + } + }); }); + + // Check fields/getters/setter for presense of attr mapping annotations. + if (meta != null) { + clazz.members.forEach((ClassMember member) { + if (member is FieldDeclaration || + (member is MethodDeclaration && + (member.isSetter || member.isGetter))) { + member.metadata.forEach((Annotation ann) { + if (_attrAnnotationsToSpec.containsKey(ann.name.name)) { + String fieldName; + if (member is FieldDeclaration) { + fieldName = member.fields.variables.first.name.name; + } else { // MethodDeclaration + fieldName = (member as MethodDeclaration).name.name; + } + StringLiteral attNameLiteral = ann.arguments.arguments.first; + if (meta.attributeMappings + .containsKey(attNameLiteral.stringValue)) { + throw 'Attribute mapping already defined for ' + '${clazz.name}.$fieldName'; + } + meta.attributeMappings[attNameLiteral.stringValue] = + _attrAnnotationsToSpec[ann.name.name] + fieldName; + } + }); + } + }); + } + + return super.visitClassDeclaration(clazz); + } +} + +class DirectiveMetadataCollectingVisitor { + List metadata = []; + List templates = []; + + call(CompilationUnit cu) { + cu.accept(new DirectiveMetadataCollectingAstVisitor(metadata, templates)); } } diff --git a/test/io/expression_extractor_spec.dart b/test/io/expression_extractor_spec.dart index 16b13f5e3..cf04340a3 100644 --- a/test/io/expression_extractor_spec.dart +++ b/test/io/expression_extractor_spec.dart @@ -13,9 +13,9 @@ import 'package:unittest/unittest.dart'; void main() { describe('expression_extractor', () { - it('should extract all expressions from source and templates', () { - Module module = new Module(); + Iterable _extractExpressions(file) { + Module module = new Module(); Injector injector = new DynamicInjector(modules: [module], allowImplicitInjection: true); @@ -24,11 +24,16 @@ void main() { var sourceMetadataExtractor = new SourceMetadataExtractor(); List directives = sourceMetadataExtractor - .gatherDirectiveInfo('test/io/test_files/main.dart', sourceCrawler); + .gatherDirectiveInfo(file, sourceCrawler); var htmlExtractor = new HtmlExpressionExtractor(directives); htmlExtractor.crawl('test/io/test_files/', ioService); - var expressions = htmlExtractor.expressions; + return htmlExtractor.expressions; + } + + it('should extract all expressions from source and templates', () { + var expressions = _extractExpressions('test/io/test_files/main.dart'); + expect(expressions, unorderedEquals([ 'ctrl.expr', 'ctrl.anotherExpression', @@ -45,5 +50,11 @@ void main() { 'ctrl.if' ])); }); + + it('should extract expressions from ngRoute viewHtml', () { + var expressions = _extractExpressions('test/io/test_files/routing.dart'); + expect(expressions, contains('foo')); + expect(expressions, contains('bar')); + }); }); } diff --git a/test/io/source_metadata_extractor_spec.dart b/test/io/source_metadata_extractor_spec.dart index cbf19d355..808da469e 100644 --- a/test/io/source_metadata_extractor_spec.dart +++ b/test/io/source_metadata_extractor_spec.dart @@ -11,18 +11,31 @@ void main() { it('should extract all attribute mappings including annotations', () { var sourceCrawler = new SourceCrawlerImpl(['packages/']); var sourceMetadataExtractor = new SourceMetadataExtractor(); - List directives = - sourceMetadataExtractor - .gatherDirectiveInfo('test/io/test_files/main.dart', sourceCrawler); + List directives = sourceMetadataExtractor + .gatherDirectiveInfo('test/io/test_files/main.dart', sourceCrawler); - expect(directives.map((d) => d.selector), unorderedEquals(['[ng-if]', 'my-component'])); + expect(directives.map((d) => d.selector), + unorderedEquals(['[ng-if]', 'my-component'])); DirectiveInfo info = directives.elementAt(1); expect(info.expressionAttrs, unorderedEquals(['expr', 'another-expression', - 'callback', 'two-way-stuff', 'exported-attr'])); + 'callback', 'two-way-stuff', 'exported-attr'])); expect(info.expressions, unorderedEquals(['attr', 'expr', - 'anotherExpression', 'callback', 'twoWayStuff', - 'exported + expression'])); + 'anotherExpression', 'callback', 'twoWayStuff', 'exported + expression'])); + }); + + it('should extract ngRoute templates from ngRoute viewHtml', () { + var sourceCrawler = new SourceCrawlerImpl(['packages/']); + var sourceMetadataExtractor = new SourceMetadataExtractor(); + List directives = sourceMetadataExtractor + .gatherDirectiveInfo('test/io/test_files/routing.dart', sourceCrawler); + + var templates = directives + .where((i) => i.selector == null) + .map((i) => i.template); + expect(templates, hasLength(2)); + expect(templates, + unorderedEquals(['
', '
'])); }); }); } diff --git a/test/io/test_files/routing.dart b/test/io/test_files/routing.dart new file mode 100644 index 000000000..34c610b3b --- /dev/null +++ b/test/io/test_files/routing.dart @@ -0,0 +1,27 @@ +library test_files.main; + +import 'package:angular/core/annotation_src.dart'; +import 'package:angular/routing/module.dart'; +import 'package:di/di.dart'; + +@NgDirective( + selector:'[ng-if]', + map: const {'.': '=>ngIfCondition'}) +class NgIfDirective { + bool ngIfCondition; +} + +main() { + var barRoute = ngRoute( + path: '/bar', + viewHtml: '
'); + var module = new Module() + ..value(RouteInitializerFn, (router, views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + viewHtml: '
'), + 'bar': barRoute, + }); + }); +} \ No newline at end of file diff --git a/test/routing/ng_view_spec.dart b/test/routing/ng_view_spec.dart index 896ce2cbf..ae9208769 100644 --- a/test/routing/ng_view_spec.dart +++ b/test/routing/ng_view_spec.dart @@ -124,6 +124,38 @@ main() { expect(root.text).toEqual('LibraryRead Book 1234'); })); }); + + + describe('Inline template ngView', () { + TestBed _; + Router router; + + beforeEachModule((Module m) { + m + ..install(new AngularMockModule()) + ..value(RouteInitializerFn, (router, views) { + views.configure({ + 'foo': ngRoute( + path: '/foo', + viewHtml: '

Hello

') + }); + }); + }); + + beforeEach((TestBed tb, Router _router, TemplateCache templates) { + _ = tb; + router = _router; + }); + + it('should switch inline templates', async(() { + Element root = _.compile(''); + expect(root.text).toEqual(''); + + router.route('/foo'); + microLeap(); + expect(root.text).toEqual('Hello'); + })); + }); } class FlatRouteInitializer implements Function { diff --git a/test/routing/routing_spec.dart b/test/routing/routing_spec.dart index 1d0585822..9e81b9885 100644 --- a/test/routing/routing_spec.dart +++ b/test/routing/routing_spec.dart @@ -83,10 +83,10 @@ main() { }); }); - 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'); + expect(router.root.findRoute('foo').name).toEqual('foo'); + expect(router.root.findRoute('foo.bar').name).toEqual('bar'); + expect(router.root.findRoute('foo.baz').name).toEqual('baz'); + expect(router.root.findRoute('aux').name).toEqual('aux'); router.route('/foo'); microLeap(); diff --git a/test/tools/source_metadata_extractor_spec.dart b/test/tools/source_metadata_extractor_spec.dart index 49f1beb80..f15972499 100644 --- a/test/tools/source_metadata_extractor_spec.dart +++ b/test/tools/source_metadata_extractor_spec.dart @@ -128,6 +128,7 @@ List extractDirectiveInfo(List metadata) { class MockDirectiveMetadataCollectingVisitor implements DirectiveMetadataCollectingVisitor { List metadata; + List templates = []; MockDirectiveMetadataCollectingVisitor(List this.metadata);