diff --git a/modules/angular2/src/core/annotations_impl/view.ts b/modules/angular2/src/core/annotations_impl/view.ts index f4b5bd5605..45d01c4716 100644 --- a/modules/angular2/src/core/annotations_impl/view.ts +++ b/modules/angular2/src/core/annotations_impl/view.ts @@ -49,6 +49,16 @@ export class View { */ template: string; + /** + * Specifies stylesheet URLs for an angular component. + */ + styleUrls: List; + + /** + * Specifies an inline stylesheet for an angular component. + */ + styles: List; + /** * Specifies a list of directives that can be used within a template. * @@ -78,13 +88,16 @@ export class View { /** * Specify a custom renderer for this View. - * If this is set, neither `template`, `templateURL` nor `directives` are used. + * If this is set, neither `template`, `templateUrl`, `styles`, `styleUrls` nor `directives` are + * used. */ renderer: string; - constructor({templateUrl, template, directives, renderer}: ViewArgs = {}) { + constructor({templateUrl, template, directives, renderer, styles, styleUrls}: ViewArgs = {}) { this.templateUrl = templateUrl; this.template = template; + this.styleUrls = styleUrls; + this.styles = styles; this.directives = directives; this.renderer = renderer; } @@ -94,4 +107,6 @@ export interface ViewArgs { template?: string; directives?: List>; renderer?: string; + styles?: List; + styleUrls?: List; } diff --git a/modules/angular2/src/core/compiler/compiler.ts b/modules/angular2/src/core/compiler/compiler.ts index 2d60a6ca0a..bdbb781e29 100644 --- a/modules/angular2/src/core/compiler/compiler.ts +++ b/modules/angular2/src/core/compiler/compiler.ts @@ -205,6 +205,7 @@ export class Compiler { var componentUrl = this._urlResolver.resolve(this._appUrl, this._componentUrlMapper.getUrl(component)); var templateAbsUrl = null; + var styleAbsUrls = null; if (isPresent(view.templateUrl)) { templateAbsUrl = this._urlResolver.resolve(componentUrl, view.templateUrl); } else if (isPresent(view.template)) { @@ -213,9 +214,15 @@ export class Compiler { // is able to resolve urls in stylesheets. templateAbsUrl = componentUrl; } + if (isPresent(view.styleUrls)) { + styleAbsUrls = + ListWrapper.map(view.styleUrls, url => this._urlResolver.resolve(componentUrl, url)); + } return new renderApi.ViewDefinition({ componentId: stringify(component), - absUrl: templateAbsUrl, template: view.template, + templateAbsUrl: templateAbsUrl, template: view.template, + styleAbsUrls: styleAbsUrls, + styles: view.styles, directives: ListWrapper.map(directives, directiveBinding => directiveBinding.metadata) }); } diff --git a/modules/angular2/src/render/api.ts b/modules/angular2/src/render/api.ts index 81b8d34d90..063c0cf43b 100644 --- a/modules/angular2/src/render/api.ts +++ b/modules/angular2/src/render/api.ts @@ -256,19 +256,25 @@ export class RenderViewRef {} export class ViewDefinition { componentId: string; - absUrl: string; + templateAbsUrl: string; template: string; directives: List; + styleAbsUrls: List; + styles: List; - constructor({componentId, absUrl, template, directives}: { + constructor({componentId, templateAbsUrl, template, styleAbsUrls, styles, directives}: { componentId?: string, - absUrl?: string, + templateAbsUrl?: string, template?: string, + styleAbsUrls?: List, + styles?: List, directives?: List }) { this.componentId = componentId; - this.absUrl = absUrl; + this.templateAbsUrl = templateAbsUrl; this.template = template; + this.styleAbsUrls = styleAbsUrls; + this.styles = styles; this.directives = directives; } } diff --git a/modules/angular2/src/render/dom/compiler/compiler.ts b/modules/angular2/src/render/dom/compiler/compiler.ts index 7c9b0b6d3b..ef346d6c9c 100644 --- a/modules/angular2/src/render/dom/compiler/compiler.ts +++ b/modules/angular2/src/render/dom/compiler/compiler.ts @@ -43,7 +43,9 @@ export class DomCompiler extends RenderCompiler { compileHost(directiveMetadata: DirectiveMetadata): Promise { var hostViewDef = new ViewDefinition({ componentId: directiveMetadata.id, - absUrl: null, template: null, + templateAbsUrl: null, template: null, + styles: null, + styleAbsUrls: null, directives: [directiveMetadata] }); var element = DOM.createElement(directiveMetadata.selector); diff --git a/modules/angular2/src/render/dom/compiler/template_loader.ts b/modules/angular2/src/render/dom/compiler/template_loader.ts index 981b24e152..b22506edbe 100644 --- a/modules/angular2/src/render/dom/compiler/template_loader.ts +++ b/modules/angular2/src/render/dom/compiler/template_loader.ts @@ -1,6 +1,6 @@ import {Injectable} from 'angular2/di'; import {isBlank, isPresent, BaseException, stringify} from 'angular2/src/facade/lang'; -import {Map, MapWrapper, StringMapWrapper, StringMap} from 'angular2/src/facade/collection'; +import {Map, MapWrapper, ListWrapper} from 'angular2/src/facade/collection'; import {PromiseWrapper, Promise} from 'angular2/src/facade/async'; import {DOM} from 'angular2/src/dom/dom_adapter'; @@ -15,35 +15,62 @@ import {UrlResolver} from 'angular2/src/services/url_resolver'; */ @Injectable() export class TemplateLoader { - _htmlCache: StringMap = StringMapWrapper.create(); + _cache: Map> = MapWrapper.create(); - constructor(public _xhr: XHR, urlResolver: UrlResolver) {} + constructor(private _xhr: XHR, urlResolver: UrlResolver) {} - load(template: ViewDefinition): Promise { - if (isPresent(template.template)) { - return PromiseWrapper.resolve(DOM.createTemplate(template.template)); - } - var url = template.absUrl; - if (isPresent(url)) { - var promise = StringMapWrapper.get(this._htmlCache, url); + load(view: ViewDefinition): Promise { + let html; + let fetchedStyles; - if (isBlank(promise)) { - // TODO(vicb): change error when TS gets fixed - // https://github.com/angular/angular/issues/2280 - // throw new BaseException(`Failed to fetch url "${url}"`); - promise = PromiseWrapper.then(this._xhr.get(url), html => { - var template = DOM.createTemplate(html); - return template; - }, _ => PromiseWrapper.reject(new BaseException(`Failed to fetch url "${url}"`), null)); - - StringMapWrapper.set(this._htmlCache, url, promise); - } - - // We need to clone the result as others might change it - // (e.g. the compiler). - return promise.then((tplElement) => DOM.clone(tplElement)); + // Load the HTML + if (isPresent(view.template)) { + html = PromiseWrapper.resolve(view.template); + } else if (isPresent(view.templateAbsUrl)) { + html = this._loadText(view.templateAbsUrl); + } else { + throw new BaseException('View should have either the templateUrl or template property set'); } - throw new BaseException('View should have either the url or template property set'); + // Load the styles + if (isPresent(view.styleAbsUrls) && view.styleAbsUrls.length > 0) { + fetchedStyles = ListWrapper.map(view.styleAbsUrls, url => this._loadText(url)); + } else { + fetchedStyles = []; + } + + // Inline the styles and return a template element + return PromiseWrapper.all(ListWrapper.concat([html], fetchedStyles)) + .then((res: List) => { + let html = res[0]; + let fetchedStyles = ListWrapper.slice(res, 1); + + html = _createStyleTags(view.styles) + _createStyleTags(fetchedStyles) + html; + + return DOM.createTemplate(html); + }); + } + + private _loadText(url: string): Promise { + var response = MapWrapper.get(this._cache, url); + + if (isBlank(response)) { + // TODO(vicb): change error when TS gets fixed + // https://github.com/angular/angular/issues/2280 + // throw new BaseException(`Failed to fetch url "${url}"`); + response = PromiseWrapper.catchError( + this._xhr.get(url), + _ => PromiseWrapper.reject(new BaseException(`Failed to fetch url "${url}"`), null)); + + MapWrapper.set(this._cache, url, response); + } + + return response; } } + +function _createStyleTags(styles?: List): string { + return isBlank(styles) ? + '' : + ListWrapper.map(styles, css => ``).join(''); +} diff --git a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts index 6d4fc5c2b1..ed6a5557ba 100644 --- a/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts +++ b/modules/angular2/src/render/dom/shadow_dom/shadow_dom_compile_step.ts @@ -29,7 +29,7 @@ export class ShadowDomCompileStep implements CompileStep { _processStyleElement(current: CompileElement, control: CompileControl) { var stylePromise = this._shadowDomStrategy.processStyleElement( - this._template.componentId, this._template.absUrl, current.element); + this._template.componentId, this._template.templateAbsUrl, current.element); if (isPresent(stylePromise) && PromiseWrapper.isPromise(stylePromise)) { ListWrapper.push(this._subTaskPromises, stylePromise); } diff --git a/modules/angular2/src/render/dom/view/proto_view.ts b/modules/angular2/src/render/dom/view/proto_view.ts index 1041d0d896..bdc0ca37a9 100644 --- a/modules/angular2/src/render/dom/view/proto_view.ts +++ b/modules/angular2/src/render/dom/view/proto_view.ts @@ -8,7 +8,7 @@ import {NG_BINDING_CLASS} from '../util'; import {RenderProtoViewRef} from '../../api'; -export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef) { +export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView { return (protoViewRef)._protoView; } diff --git a/modules/angular2/src/transform/template_compiler/generator.dart b/modules/angular2/src/transform/template_compiler/generator.dart index fe0f515f5a..0bf7b9080e 100644 --- a/modules/angular2/src/transform/template_compiler/generator.dart +++ b/modules/angular2/src/transform/template_compiler/generator.dart @@ -88,7 +88,7 @@ class _TemplateExtractor { Future<_ExtractResult> extractTemplates(ViewDefinition viewDef) async { // Check for "imperative views". - if (viewDef.template == null && viewDef.absUrl == null) return null; + if (viewDef.template == null && viewDef.templateAbsUrl == null) return null; var templateEl = await _loader.load(viewDef); diff --git a/modules/angular2/src/transform/template_compiler/view_definition_creator.dart b/modules/angular2/src/transform/template_compiler/view_definition_creator.dart index f876f593ed..701dd06d75 100644 --- a/modules/angular2/src/transform/template_compiler/view_definition_creator.dart +++ b/modules/angular2/src/transform/template_compiler/view_definition_creator.dart @@ -210,11 +210,11 @@ class _TemplateExtractVisitor extends Object with RecursiveAstVisitor { return null; } if (keyString == 'templateUrl') { - if (viewDef.absUrl != null) { + if (viewDef.templateAbsUrl != null) { logger.error( 'Found multiple values for "templateUrl". Source: ${node}'); } - viewDef.absUrl = valueString; + viewDef.templateAbsUrl = valueString; } else { // keyString == 'template' if (viewDef.template != null) { diff --git a/modules/angular2/test/core/compiler/compiler_spec.ts b/modules/angular2/test/core/compiler/compiler_spec.ts index ddfa2ad6ea..ac69bf9447 100644 --- a/modules/angular2/test/core/compiler/compiler_spec.ts +++ b/modules/angular2/test/core/compiler/compiler_spec.ts @@ -100,30 +100,45 @@ export function main() { }); })); - it('should fill absUrl given inline templates', inject([AsyncTestCompleter], (async) => { + it('should fill templateAbsUrl given inline templates', + inject([AsyncTestCompleter], (async) => { cmpUrlMapper.setComponentUrl(MainComponent, '/mainComponent'); captureTemplate(new viewAnn.View({template: '
'})) .then((renderTpl) => { - expect(renderTpl.absUrl).toEqual('http://www.app.com/mainComponent'); + expect(renderTpl.templateAbsUrl).toEqual('http://www.app.com/mainComponent'); async.done(); }); })); - it('should not fill absUrl given no inline template or template url', + it('should not fill templateAbsUrl given no inline template or template url', inject([AsyncTestCompleter], (async) => { cmpUrlMapper.setComponentUrl(MainComponent, '/mainComponent'); captureTemplate(new viewAnn.View({template: null, templateUrl: null})) .then((renderTpl) => { - expect(renderTpl.absUrl).toBe(null); + expect(renderTpl.templateAbsUrl).toBe(null); async.done(); }); })); - it('should fill absUrl given url template', inject([AsyncTestCompleter], (async) => { + it('should fill templateAbsUrl given url template', inject([AsyncTestCompleter], (async) => { cmpUrlMapper.setComponentUrl(MainComponent, '/mainComponent'); captureTemplate(new viewAnn.View({templateUrl: '/someTemplate'})) .then((renderTpl) => { - expect(renderTpl.absUrl).toEqual('http://www.app.com/mainComponent/someTemplate'); + expect(renderTpl.templateAbsUrl) + .toEqual('http://www.app.com/mainComponent/someTemplate'); + async.done(); + }); + })); + + it('should fill styleAbsUrls given styleUrls', inject([AsyncTestCompleter], (async) => { + cmpUrlMapper.setComponentUrl(MainComponent, '/mainComponent'); + captureTemplate(new viewAnn.View({styleUrls: ['/1.css', '/2.css']})) + .then((renderTpl) => { + expect(renderTpl.styleAbsUrls) + .toEqual([ + 'http://www.app.com/mainComponent/1.css', + 'http://www.app.com/mainComponent/2.css' + ]); async.done(); }); })); diff --git a/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts b/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts index 9b8db2f5a7..19c259b61d 100644 --- a/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts +++ b/modules/angular2/test/render/dom/compiler/compiler_common_tests.ts @@ -13,7 +13,7 @@ import { import {DOM} from 'angular2/src/dom/dom_adapter'; import {List, ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; -import {Type, isBlank, stringify, isPresent} from 'angular2/src/facade/lang'; +import {Type, isBlank, stringify, isPresent, BaseException} from 'angular2/src/facade/lang'; import {PromiseWrapper, Promise} from 'angular2/src/facade/async'; import {DomCompiler} from 'angular2/src/render/dom/compiler/compiler'; @@ -90,7 +90,7 @@ export function runCompilerCommonTests() { it('should load url templates', inject([AsyncTestCompleter], (async) => { var urlData = MapWrapper.createFromStringMap({'someUrl': 'url component'}); var compiler = createCompiler(EMPTY_STEP, urlData); - compiler.compile(new ViewDefinition({componentId: 'someId', absUrl: 'someUrl'})) + compiler.compile(new ViewDefinition({componentId: 'someId', templateAbsUrl: 'someUrl'})) .then((protoView) => { expect(DOM.getInnerHTML(resolveInternalDomProtoView(protoView.render).element)) .toEqual('url component'); @@ -101,7 +101,8 @@ export function runCompilerCommonTests() { it('should report loading errors', inject([AsyncTestCompleter], (async) => { var compiler = createCompiler(EMPTY_STEP, MapWrapper.create()); PromiseWrapper.catchError( - compiler.compile(new ViewDefinition({componentId: 'someId', absUrl: 'someUrl'})), + compiler.compile( + new ViewDefinition({componentId: 'someId', templateAbsUrl: 'someUrl'})), (e) => { expect(e.message).toEqual( 'Failed to load the template for "someId" : Failed to fetch url "someUrl"'); @@ -207,14 +208,14 @@ class FakeTemplateLoader extends TemplateLoader { return PromiseWrapper.resolve(DOM.createTemplate(template.template)); } - if (isPresent(template.absUrl)) { - var content = MapWrapper.get(this._urlData, template.absUrl); + if (isPresent(template.templateAbsUrl)) { + var content = MapWrapper.get(this._urlData, template.templateAbsUrl); return isPresent(content) ? PromiseWrapper.resolve(DOM.createTemplate(content)) : - PromiseWrapper.reject(`Failed to fetch url "${template.absUrl}"`, null); + PromiseWrapper.reject(`Failed to fetch url "${template.templateAbsUrl}"`, null); } - return PromiseWrapper.reject('Load failed', null); + throw new BaseException('View should have either the templateUrl or template property set'); } } diff --git a/modules/angular2/test/render/dom/compiler/template_loader_spec.ts b/modules/angular2/test/render/dom/compiler/template_loader_spec.ts index 316a74a06f..35eb1830d2 100644 --- a/modules/angular2/test/render/dom/compiler/template_loader_spec.ts +++ b/modules/angular2/test/render/dom/compiler/template_loader_spec.ts @@ -27,62 +27,107 @@ export function main() { loader = new TemplateLoader(xhr, new FakeUrlResolver()); }); - it('should load inline templates', inject([AsyncTestCompleter], (async) => { - var template = new ViewDefinition({template: 'template template'}); - loader.load(template).then((el) => { - expect(DOM.content(el)).toHaveText('template template'); - async.done(); - }); - })); + describe('html', () => { + it('should load inline templates', inject([AsyncTestCompleter], (async) => { + var template = new ViewDefinition({template: 'template template'}); + loader.load(template).then((el) => { + expect(DOM.content(el)).toHaveText('template template'); + async.done(); + }); + })); - it('should load templates through XHR', inject([AsyncTestCompleter], (async) => { - xhr.expect('base/foo', 'xhr template'); - var template = new ViewDefinition({absUrl: 'base/foo'}); - loader.load(template).then((el) => { - expect(DOM.content(el)).toHaveText('xhr template'); - async.done(); - }); - xhr.flush(); - })); + it('should load templates through XHR', inject([AsyncTestCompleter], (async) => { + xhr.expect('base/foo.html', 'xhr template'); + var template = new ViewDefinition({templateAbsUrl: 'base/foo.html'}); + loader.load(template).then((el) => { + expect(DOM.content(el)).toHaveText('xhr template'); + async.done(); + }); + xhr.flush(); + })); - it('should cache template loaded through XHR but clone it as the compiler might change it', - inject([AsyncTestCompleter], (async) => { - var firstEl; - // we have only one xhr.expect, so there can only be one xhr call! - xhr.expect('base/foo', 'xhr template'); - var template = new ViewDefinition({absUrl: 'base/foo'}); - loader.load(template) - .then((el) => { - expect(DOM.content(el)).toHaveText('xhr template'); - firstEl = el; - return loader.load(template); - }) - .then((el) => { - expect(el).not.toBe(firstEl); - expect(DOM.content(el)).toHaveText('xhr template'); - async.done(); - }); - xhr.flush(); - })); + it('should return a new template element on each call', + inject([AsyncTestCompleter], (async) => { + var firstEl; + // we have only one xhr.expect, so there can only be one xhr call! + xhr.expect('base/foo.html', 'xhr template'); + var template = new ViewDefinition({templateAbsUrl: 'base/foo.html'}); + loader.load(template) + .then((el) => { + expect(DOM.content(el)).toHaveText('xhr template'); + firstEl = el; + return loader.load(template); + }) + .then((el) => { + expect(el).not.toBe(firstEl); + expect(DOM.content(el)).toHaveText('xhr template'); + async.done(); + }); + xhr.flush(); + })); - it('should throw when no template is defined', () => { - var template = new ViewDefinition({template: null, absUrl: null}); - expect(() => loader.load(template)) - .toThrowError('View should have either the url or template property set'); + it('should throw when no template is defined', () => { + var template = new ViewDefinition({template: null, templateAbsUrl: null}); + expect(() => loader.load(template)) + .toThrowError('View should have either the templateUrl or template property set'); + }); + + it('should return a rejected Promise when XHR loading fails', + inject([AsyncTestCompleter], (async) => { + xhr.expect('base/foo.html', null); + var template = new ViewDefinition({templateAbsUrl: 'base/foo.html'}); + PromiseWrapper.then(loader.load(template), function(_) { throw 'Unexpected response'; }, + function(error) { + expect(error.message) + .toEqual('Failed to fetch url "base/foo.html"'); + async.done(); + }); + xhr.flush(); + })); }); - it('should return a rejected Promise when xhr loading fails', - inject([AsyncTestCompleter], (async) => { - xhr.expect('base/foo', null); - var template = new ViewDefinition({absUrl: 'base/foo'}); - PromiseWrapper.then(loader.load(template), function(_) { throw 'Unexpected response'; }, - function(error) { - expect(error.message).toEqual('Failed to fetch url "base/foo"'); - async.done(); - }); - xhr.flush(); - })); + describe('css', () => { + it('should load inline styles', inject([AsyncTestCompleter], (async) => { + var template = new ViewDefinition({template: 'html', styles: ['style 1', 'style 2']}); + loader.load(template).then((el) => { + expect(DOM.getInnerHTML(el)) + .toEqual( + 'html'); + async.done(); + }); + })); + it('should load templates through XHR', inject([AsyncTestCompleter], (async) => { + xhr.expect('base/foo.html', 'xhr template'); + xhr.expect('base/foo-1.css', '1'); + xhr.expect('base/foo-2.css', '2'); + var template = new ViewDefinition({ + templateAbsUrl: 'base/foo.html', + styles: ['i1'], + styleAbsUrls: ['base/foo-1.css', 'base/foo-2.css'] + }); + loader.load(template).then((el) => { + expect(DOM.getInnerHTML(el)) + .toEqual( + 'xhr template'); + async.done(); + }); + xhr.flush(); + })); + + it('should return a rejected Promise when XHR loading fails', + inject([AsyncTestCompleter], (async) => { + xhr.expect('base/foo.css', null); + var template = new ViewDefinition({template: '', styleAbsUrls: ['base/foo.css']}); + PromiseWrapper.then(loader.load(template), function(_) { throw 'Unexpected response'; }, + function(error) { + expect(error.message) + .toEqual('Failed to fetch url "base/foo.css"'); + async.done(); + }); + xhr.flush(); + })); + }); }); }