refactor(shadow_dom): remove ShadowDomStrategy in favor of @View(encapsulation)

BREAKING CHANGES:
- `ShadowDomStrategy` was removed. To specify the encapsulation of a component use `@View(encapsulation: ViewEncapsulation.NONE | ViewEncapsulation.EMULATED | ViewEncapsulation.NATIVE)`
- The default encapsulation strategy is now `ViewEncapsulation.EMULATED` if a component contains styles and `ViewEncapsulation.NONE` if it does not. Before this was always `NONE`.
- `ViewLoader` now returns the template as a string and the styles as a separate array
This commit is contained in:
Tobias Bosch
2015-07-24 15:28:44 -07:00
parent e40ff36832
commit 16e3d7e96e
77 changed files with 1228 additions and 890 deletions

View File

@ -273,6 +273,25 @@ export class RenderFragmentRef {}
// An opaque reference to a view
export class RenderViewRef {}
/**
* How the template and styles of a view should be encapsulated.
*/
export enum ViewEncapsulation {
/**
* Emulate scoping of styles by preprocessing the style rules
* and adding additional attributes to elements. This is the default.
*/
EMULATED,
/**
* Uses the native mechanism of the renderer. For the DOM this means creating a ShadowRoot.
*/
NATIVE,
/**
* Don't scope the template nor the styles.
*/
NONE
}
export class ViewDefinition {
componentId: string;
templateAbsUrl: string;
@ -280,21 +299,25 @@ export class ViewDefinition {
directives: List<DirectiveMetadata>;
styleAbsUrls: List<string>;
styles: List<string>;
encapsulation: ViewEncapsulation;
constructor({componentId, templateAbsUrl, template, styleAbsUrls, styles, directives}: {
constructor({componentId, templateAbsUrl, template, styleAbsUrls, styles, directives,
encapsulation}: {
componentId?: string,
templateAbsUrl?: string,
template?: string,
styleAbsUrls?: List<string>,
styles?: List<string>,
directives?: List<DirectiveMetadata>
}) {
directives?: List<DirectiveMetadata>,
encapsulation?: ViewEncapsulation
} = {}) {
this.componentId = componentId;
this.templateAbsUrl = templateAbsUrl;
this.template = template;
this.styleAbsUrls = styleAbsUrls;
this.styles = styles;
this.directives = directives;
this.encapsulation = isPresent(encapsulation) ? encapsulation : ViewEncapsulation.EMULATED;
}
}

View File

@ -29,7 +29,7 @@ export class CompileControl {
var step = this._steps[i];
this._parent = parent;
this._currentStepIndex = i;
step.process(parent, current, this);
step.processElement(parent, current, this);
parent = this._parent;
}

View File

@ -5,7 +5,7 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control';
import {CompileStep} from './compile_step';
import {ProtoViewBuilder} from '../view/proto_view_builder';
import {ProtoViewDto, ViewType} from '../../api';
import {ProtoViewDto, ViewType, ViewDefinition} from '../../api';
/**
* CompilePipeline for executing CompileSteps recursively for
@ -13,26 +13,29 @@ import {ProtoViewDto, ViewType} from '../../api';
*/
export class CompilePipeline {
_control: CompileControl;
constructor(steps: List<CompileStep>, private _useNativeShadowDom: boolean = false) {
this._control = new CompileControl(steps);
constructor(public steps: List<CompileStep>) { this._control = new CompileControl(steps); }
processStyles(styles: string[]): string[] {
return styles.map(style => {
this.steps.forEach(step => { style = step.processStyle(style); });
return style;
});
}
process(rootElement: HTMLElement, protoViewType: ViewType = null,
compilationCtxtDescription: string = ''): List<CompileElement> {
if (isBlank(protoViewType)) {
protoViewType = ViewType.COMPONENT;
}
var results = [];
processElements(rootElement: Element, protoViewType: ViewType,
viewDef: ViewDefinition): CompileElement[] {
var results: CompileElement[] = [];
var compilationCtxtDescription = viewDef.componentId;
var rootCompileElement = new CompileElement(rootElement, compilationCtxtDescription);
rootCompileElement.inheritedProtoView =
new ProtoViewBuilder(rootElement, protoViewType, this._useNativeShadowDom);
new ProtoViewBuilder(rootElement, protoViewType, viewDef.encapsulation);
rootCompileElement.isViewRoot = true;
this._process(results, null, rootCompileElement, compilationCtxtDescription);
this._processElement(results, null, rootCompileElement, compilationCtxtDescription);
return results;
}
_process(results, parent: CompileElement, current: CompileElement,
compilationCtxtDescription: string = '') {
_processElement(results: CompileElement[], parent: CompileElement, current: CompileElement,
compilationCtxtDescription: string = '') {
var additionalChildren = this._control.internalProcess(results, 0, parent, current);
if (current.compileChildren) {
@ -46,7 +49,7 @@ export class CompilePipeline {
childCompileElement.inheritedProtoView = current.inheritedProtoView;
childCompileElement.inheritedElementBinder = current.inheritedElementBinder;
childCompileElement.distanceToInheritedBinder = current.distanceToInheritedBinder + 1;
this._process(results, current, childCompileElement);
this._processElement(results, current, childCompileElement);
}
node = nextNode;
}
@ -54,7 +57,7 @@ export class CompilePipeline {
if (isPresent(additionalChildren)) {
for (var i = 0; i < additionalChildren.length; i++) {
this._process(results, current, additionalChildren[i]);
this._processElement(results, current, additionalChildren[i]);
}
}
}

View File

@ -6,6 +6,8 @@ import * as compileControlModule from './compile_control';
* Is guaranteed to be called in depth first order
*/
export interface CompileStep {
process(parent: CompileElement, current: CompileElement,
control: compileControlModule.CompileControl): void;
processElement(parent: CompileElement, current: CompileElement,
control: compileControlModule.CompileControl): void;
processStyle(style: string): string;
}

View File

@ -6,15 +6,15 @@ import {PropertyBindingParser} from './property_binding_parser';
import {TextInterpolationParser} from './text_interpolation_parser';
import {DirectiveParser} from './directive_parser';
import {ViewSplitter} from './view_splitter';
import {ShadowDomCompileStep} from '../shadow_dom/shadow_dom_compile_step';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import {StyleEncapsulator} from './style_encapsulator';
export class CompileStepFactory {
createSteps(view: ViewDefinition): List<CompileStep> { return null; }
}
export class DefaultStepFactory extends CompileStepFactory {
constructor(public _parser: Parser, public _shadowDomStrategy: ShadowDomStrategy) { super(); }
private _componentUIDsCache: Map<string, string> = new Map();
constructor(private _parser: Parser, private _appId: string) { super(); }
createSteps(view: ViewDefinition): List<CompileStep> {
return [
@ -22,7 +22,7 @@ export class DefaultStepFactory extends CompileStepFactory {
new PropertyBindingParser(this._parser),
new DirectiveParser(this._parser, view.directives),
new TextInterpolationParser(this._parser),
new ShadowDomCompileStep(this._shadowDomStrategy, view)
new StyleEncapsulator(this._appId, view, this._componentUIDsCache)
];
}
}

View File

@ -1,7 +1,7 @@
import {Injectable} from 'angular2/di';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {BaseException, isPresent} from 'angular2/src/facade/lang';
import {BaseException, isPresent, isBlank} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {
@ -11,14 +11,18 @@ import {
DirectiveMetadata,
RenderCompiler,
RenderProtoViewRef,
RenderProtoViewMergeMapping
RenderProtoViewMergeMapping,
ViewEncapsulation
} from '../../api';
import {CompilePipeline} from './compile_pipeline';
import {ViewLoader} from 'angular2/src/render/dom/compiler/view_loader';
import {ViewLoader, TemplateAndStyles} from 'angular2/src/render/dom/compiler/view_loader';
import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory';
import {Parser} from 'angular2/src/change_detection/change_detection';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import * as pvm from '../view/proto_view_merger';
import {DOCUMENT_TOKEN, APP_ID_TOKEN} from '../dom_tokens';
import {Inject} from 'angular2/di';
import {SharedStylesHost} from '../view/shared_styles_host';
import {prependAll} from '../util';
/**
* The compiler loads and translates the html templates of components into
@ -26,15 +30,17 @@ import * as pvm from '../view/proto_view_merger';
* the CompilePipeline and the CompileSteps.
*/
export class DomCompiler extends RenderCompiler {
constructor(public _stepFactory: CompileStepFactory, public _viewLoader: ViewLoader,
public _useNativeShadowDom: boolean) {
constructor(private _stepFactory: CompileStepFactory, private _viewLoader: ViewLoader,
private _sharedStylesHost: SharedStylesHost) {
super();
}
compile(view: ViewDefinition): Promise<ProtoViewDto> {
var tplPromise = this._viewLoader.load(view);
return PromiseWrapper.then(
tplPromise, (el) => this._compileTemplate(view, el, ViewType.COMPONENT), (e) => {
tplPromise, (tplAndStyles: TemplateAndStyles) =>
this._compileView(view, tplAndStyles, ViewType.COMPONENT),
(e) => {
throw new BaseException(`Failed to load the template for "${view.componentId}" : ${e}`);
return null;
});
@ -46,11 +52,13 @@ export class DomCompiler extends RenderCompiler {
templateAbsUrl: null, template: null,
styles: null,
styleAbsUrls: null,
directives: [directiveMetadata]
directives: [directiveMetadata],
encapsulation: ViewEncapsulation.NONE
});
var template = DOM.createTemplate('');
DOM.appendChild(DOM.content(template), DOM.createElement(directiveMetadata.selector));
return this._compileTemplate(hostViewDef, template, ViewType.HOST);
return this._compileView(
hostViewDef, new TemplateAndStyles(
`<${directiveMetadata.selector}></${directiveMetadata.selector}>`, []),
ViewType.HOST);
}
mergeProtoViewsRecursively(
@ -58,20 +66,47 @@ export class DomCompiler extends RenderCompiler {
return PromiseWrapper.resolve(pvm.mergeProtoViewsRecursively(protoViewRefs));
}
_compileTemplate(viewDef: ViewDefinition, tplElement,
protoViewType: ViewType): Promise<ProtoViewDto> {
var pipeline =
new CompilePipeline(this._stepFactory.createSteps(viewDef), this._useNativeShadowDom);
var compileElements = pipeline.process(tplElement, protoViewType, viewDef.componentId);
_compileView(viewDef: ViewDefinition, templateAndStyles: TemplateAndStyles,
protoViewType: ViewType): Promise<ProtoViewDto> {
if (viewDef.encapsulation === ViewEncapsulation.EMULATED &&
templateAndStyles.styles.length === 0) {
viewDef = this._normalizeViewEncapsulationIfThereAreNoStyles(viewDef);
}
var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef));
var compiledStyles = pipeline.processStyles(templateAndStyles.styles);
var compileElements = pipeline.processElements(DOM.createTemplate(templateAndStyles.template),
protoViewType, viewDef);
if (viewDef.encapsulation === ViewEncapsulation.NATIVE) {
prependAll(DOM.content(compileElements[0].element),
compiledStyles.map(style => DOM.createStyleElement(style)));
} else {
this._sharedStylesHost.addStyles(compiledStyles);
}
return PromiseWrapper.resolve(compileElements[0].inheritedProtoView.build());
}
_normalizeViewEncapsulationIfThereAreNoStyles(viewDef: ViewDefinition): ViewDefinition {
if (viewDef.encapsulation === ViewEncapsulation.EMULATED) {
return new ViewDefinition({
componentId: viewDef.componentId,
templateAbsUrl: viewDef.templateAbsUrl, template: viewDef.template,
styleAbsUrls: viewDef.styleAbsUrls,
styles: viewDef.styles,
directives: viewDef.directives,
encapsulation: ViewEncapsulation.NONE
});
} else {
return viewDef;
}
}
}
@Injectable()
export class DefaultDomCompiler extends DomCompiler {
constructor(parser: Parser, shadowDomStrategy: ShadowDomStrategy, viewLoader: ViewLoader) {
super(new DefaultStepFactory(parser, shadowDomStrategy), viewLoader,
shadowDomStrategy.hasNativeContentElement());
constructor(parser: Parser, viewLoader: ViewLoader, sharedStylesHost: SharedStylesHost,
@Inject(APP_ID_TOKEN) appId: any) {
super(new DefaultStepFactory(parser, appId), viewLoader, sharedStylesHost);
}
}

View File

@ -29,6 +29,8 @@ export class DirectiveParser implements CompileStep {
}
}
processStyle(style: string): string { return style; }
_ensureComponentOnlyHasElementSelector(selector, directive) {
var isElementSelector = selector.length === 1 && selector[0].isElementSelector();
if (!isElementSelector && directive.type === DirectiveMetadata.COMPONENT_TYPE) {
@ -37,7 +39,7 @@ export class DirectiveParser implements CompileStep {
}
}
process(parent: CompileElement, current: CompileElement, control: CompileControl) {
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
var attrs = current.attrs();
var classList = current.classList();
var cssSelector = new CssSelector();

View File

@ -25,7 +25,9 @@ var BIND_NAME_REGEXP =
export class PropertyBindingParser implements CompileStep {
constructor(private _parser: Parser) {}
process(parent: CompileElement, current: CompileElement, control: CompileControl) {
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
var attrs = current.attrs();
var newAttrs = new Map();

View File

@ -0,0 +1,73 @@
import {CompileStep} from '../compiler/compile_step';
import {CompileElement} from '../compiler/compile_element';
import {CompileControl} from '../compiler/compile_control';
import {ViewDefinition, ViewEncapsulation, ViewType} from '../../api';
import {NG_CONTENT_ELEMENT_NAME, isElementWithTag} from '../util';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {ShadowCss} from './shadow_css';
export class StyleEncapsulator implements CompileStep {
constructor(private _appId: string, private _view: ViewDefinition,
private _componentUIDsCache: Map<string, string>) {}
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
if (isElementWithTag(current.element, NG_CONTENT_ELEMENT_NAME)) {
current.inheritedProtoView.bindNgContent();
} else {
if (this._view.encapsulation === ViewEncapsulation.EMULATED) {
this._processEmulatedScopedElement(current, parent);
}
}
}
processStyle(style: string): string {
var encapsulation = this._view.encapsulation;
if (encapsulation === ViewEncapsulation.EMULATED) {
return this._shimCssForComponent(style, this._view.componentId);
} else {
return style;
}
}
_processEmulatedScopedElement(current: CompileElement, parent: CompileElement): void {
var element = current.element;
var hostComponentId = this._view.componentId;
var viewType = current.inheritedProtoView.type;
// Shim the element as a child of the compiled component
if (viewType !== ViewType.HOST && isPresent(hostComponentId)) {
var contentAttribute = getContentAttribute(this._getComponentId(hostComponentId));
DOM.setAttribute(element, contentAttribute, '');
// also shim the host
if (isBlank(parent) && viewType == ViewType.COMPONENT) {
var hostAttribute = getHostAttribute(this._getComponentId(hostComponentId));
current.inheritedProtoView.setHostAttribute(hostAttribute, '');
}
}
}
_shimCssForComponent(cssText: string, componentId: string): string {
var id = this._getComponentId(componentId);
var shadowCss = new ShadowCss();
return shadowCss.shimCssText(cssText, getContentAttribute(id), getHostAttribute(id));
}
_getComponentId(componentStringId: string): string {
var id = this._componentUIDsCache.get(componentStringId);
if (isBlank(id)) {
id = `${this._appId}-${this._componentUIDsCache.size}`;
this._componentUIDsCache.set(componentStringId, id);
}
return id;
}
}
// Return the attribute to be added to the component
function getHostAttribute(compId: string): string {
return `_nghost-${compId}`;
}
// Returns the attribute to be added on every single element nodes in the component
function getContentAttribute(compId: string): string {
return `_ngcontent-${compId}`;
}

View File

@ -13,7 +13,9 @@ import {CompileControl} from './compile_control';
export class TextInterpolationParser implements CompileStep {
constructor(public _parser: Parser) {}
process(parent: CompileElement, current: CompileElement, control: CompileControl) {
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
if (!current.compileChildren) {
return;
}

View File

@ -10,14 +10,17 @@ import {
import {Map, MapWrapper, ListWrapper, List} from 'angular2/src/facade/collection';
import {PromiseWrapper, Promise} from 'angular2/src/facade/async';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ViewDefinition} from '../../api';
import {XHR} from 'angular2/src/render/xhr';
import {ViewDefinition} from '../../api';
import {StyleInliner} from './style_inliner';
import {StyleUrlResolver} from './style_url_resolver';
export class TemplateAndStyles {
constructor(public template: string, public styles: string[]) {}
}
/**
* Strategy to load component views.
* TODO: Make public API once we are more confident in this approach.
@ -29,33 +32,32 @@ export class ViewLoader {
constructor(private _xhr: XHR, private _styleInliner: StyleInliner,
private _styleUrlResolver: StyleUrlResolver) {}
load(view: ViewDefinition): Promise</*element*/ any> {
let tplElAndStyles: List<string | Promise<string>> = [this._loadHtml(view)];
if (isPresent(view.styles)) {
view.styles.forEach((cssText: string) => {
let textOrPromise = this._resolveAndInlineCssText(cssText, view.templateAbsUrl);
tplElAndStyles.push(textOrPromise);
load(viewDef: ViewDefinition): Promise<TemplateAndStyles> {
let tplAndStyles: List<Promise<TemplateAndStyles>| Promise<string>| string> =
[this._loadHtml(viewDef.template, viewDef.templateAbsUrl)];
if (isPresent(viewDef.styles)) {
viewDef.styles.forEach((cssText: string) => {
let textOrPromise = this._resolveAndInlineCssText(cssText, viewDef.templateAbsUrl);
tplAndStyles.push(textOrPromise);
});
}
if (isPresent(view.styleAbsUrls)) {
view.styleAbsUrls.forEach(url => {
if (isPresent(viewDef.styleAbsUrls)) {
viewDef.styleAbsUrls.forEach(url => {
let promise = this._loadText(url).then(
cssText => this._resolveAndInlineCssText(cssText, view.templateAbsUrl));
tplElAndStyles.push(promise);
cssText => this._resolveAndInlineCssText(cssText, viewDef.templateAbsUrl));
tplAndStyles.push(promise);
});
}
// Inline the styles from the @View annotation and return a template element
return PromiseWrapper.all(tplElAndStyles)
.then((res: List<string>) => {
let tplEl = res[0];
let cssTexts = ListWrapper.slice(res, 1);
// Inline the styles from the @View annotation
return PromiseWrapper.all(tplAndStyles)
.then((res: List<TemplateAndStyles | string>) => {
let loadedTplAndStyles = <TemplateAndStyles>res[0];
let styles = <string[]>ListWrapper.slice(res, 1);
_insertCssTexts(DOM.content(tplEl), cssTexts);
return tplEl;
return new TemplateAndStyles(loadedTplAndStyles.template,
loadedTplAndStyles.styles.concat(styles));
});
}
@ -77,40 +79,54 @@ export class ViewLoader {
}
// Load the html and inline any style tags
private _loadHtml(view: ViewDefinition): Promise<any /* element */> {
private _loadHtml(template: string, templateAbsUrl: string): Promise<TemplateAndStyles> {
let html;
// Load the HTML
if (isPresent(view.template)) {
html = PromiseWrapper.resolve(view.template);
} else if (isPresent(view.templateAbsUrl)) {
html = this._loadText(view.templateAbsUrl);
if (isPresent(template)) {
html = PromiseWrapper.resolve(template);
} else if (isPresent(templateAbsUrl)) {
html = this._loadText(templateAbsUrl);
} else {
throw new BaseException('View should have either the templateUrl or template property set');
}
return html.then(html => {
var tplEl = DOM.createTemplate(html);
// Replace $baseUrl with the base url for the template
let templateAbsUrl = view.templateAbsUrl;
if (isPresent(templateAbsUrl) && templateAbsUrl.indexOf("/") >= 0) {
let baseUrl = templateAbsUrl.substring(0, templateAbsUrl.lastIndexOf("/"));
this._substituteBaseUrl(DOM.content(tplEl), baseUrl);
}
let styleEls = DOM.querySelectorAll(DOM.content(tplEl), 'STYLE');
let unresolvedStyles: string[] = [];
for (let i = 0; i < styleEls.length; i++) {
var styleEl = styleEls[i];
unresolvedStyles.push(DOM.getText(styleEl));
DOM.remove(styleEl);
}
let syncStyles: string[] = [];
let asyncStyles: Promise<string>[] = [];
// Inline the style tags from the html
let styleEls = DOM.querySelectorAll(DOM.content(tplEl), 'STYLE');
let promises: List<Promise<string>> = [];
for (let i = 0; i < styleEls.length; i++) {
let promise = this._resolveAndInlineElement(styleEls[i], view.templateAbsUrl);
if (isPromise(promise)) {
promises.push(promise);
let styleEl = styleEls[i];
let resolvedStyled = this._resolveAndInlineCssText(DOM.getText(styleEl), templateAbsUrl);
if (isPromise(resolvedStyled)) {
asyncStyles.push(<Promise<string>>resolvedStyled);
} else {
syncStyles.push(<string>resolvedStyled);
}
}
return promises.length > 0 ? PromiseWrapper.all(promises).then(_ => tplEl) : tplEl;
if (asyncStyles.length === 0) {
return PromiseWrapper.resolve(new TemplateAndStyles(DOM.getInnerHTML(tplEl), syncStyles));
} else {
return PromiseWrapper.all(asyncStyles)
.then(loadedStyles => new TemplateAndStyles(DOM.getInnerHTML(tplEl),
syncStyles.concat(<string[]>loadedStyles)));
}
});
}
@ -139,43 +155,8 @@ export class ViewLoader {
}
}
/**
* Inlines a style element.
*
* @param styleEl The style element
* @param baseUrl The base url
* @returns {Promise<any>} null when no @import rule exist in the css or a Promise
* @private
*/
private _resolveAndInlineElement(styleEl, baseUrl: string): Promise<any> {
let textOrPromise = this._resolveAndInlineCssText(DOM.getText(styleEl), baseUrl);
if (isPromise(textOrPromise)) {
return (<Promise<string>>textOrPromise).then(css => { DOM.setText(styleEl, css); });
} else {
DOM.setText(styleEl, <string>textOrPromise);
return null;
}
}
private _resolveAndInlineCssText(cssText: string, baseUrl: string): string | Promise<string> {
cssText = this._styleUrlResolver.resolveUrls(cssText, baseUrl);
return this._styleInliner.inlineImports(cssText, baseUrl);
}
}
function _insertCssTexts(element, cssTexts: List<string>): void {
if (cssTexts.length == 0) return;
let insertBefore = DOM.firstChild(element);
for (let i = cssTexts.length - 1; i >= 0; i--) {
let styleEl = DOM.createStyleElement(cssTexts[i]);
if (isPresent(insertBefore)) {
DOM.insertBefore(insertBefore, styleEl);
} else {
DOM.appendChild(element, styleEl);
}
insertBefore = styleEl;
}
}

View File

@ -28,7 +28,9 @@ import {dashCaseToCamelCase} from '../util';
export class ViewSplitter implements CompileStep {
constructor(public _parser: Parser) {}
process(parent: CompileElement, current: CompileElement, control: CompileControl) {
processStyle(style: string): string { return style; }
processElement(parent: CompileElement, current: CompileElement, control: CompileControl) {
var attrs = current.attrs();
var templateBindings = attrs.get('template');
var hasTemplateBinding = isPresent(templateBindings);

View File

@ -15,6 +15,7 @@ import {EventManager} from './events/event_manager';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './view/proto_view';
import {DomView, DomViewRef, resolveInternalDomView} from './view/view';
import {DomFragmentRef, resolveInternalDomFragment} from './view/fragment';
import {DomSharedStylesHost} from './view/shared_styles_host';
import {
NG_BINDING_CLASS_SELECTOR,
NG_BINDING_CLASS,
@ -31,9 +32,8 @@ import {
RenderViewWithFragments
} from '../api';
export const DOCUMENT_TOKEN: OpaqueToken = CONST_EXPR(new OpaqueToken('DocumentToken'));
export const DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES: OpaqueToken =
CONST_EXPR(new OpaqueToken('DomReflectPropertiesAsAttributes'));
import {DOCUMENT_TOKEN, DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES} from './dom_tokens';
const REFLECT_PREFIX: string = 'ng-reflect-';
@Injectable()
@ -41,7 +41,8 @@ export class DomRenderer extends Renderer {
_document;
_reflectPropertiesAsAttributes: boolean;
constructor(public _eventManager: EventManager, @Inject(DOCUMENT_TOKEN) document,
constructor(public _eventManager: EventManager, private _domSharedStylesHost: DomSharedStylesHost,
@Inject(DOCUMENT_TOKEN) document,
@Inject(DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES) reflectPropertiesAsAttributes:
boolean) {
super();
@ -65,7 +66,14 @@ export class DomRenderer extends Renderer {
}
destroyView(viewRef: RenderViewRef) {
// noop for now
var view = resolveInternalDomView(viewRef);
var elementBinders = view.proto.elementBinders;
for (var i = 0; i < elementBinders.length; i++) {
var binder = elementBinders[i];
if (binder.hasNativeShadowRoot) {
this._domSharedStylesHost.removeHost(DOM.getShadowRoot(view.boundElements[i]));
}
}
}
getNativeElementSync(location: RenderElementRef): any {
@ -226,7 +234,9 @@ export class DomRenderer extends Renderer {
// native shadow DOM
if (binder.hasNativeShadowRoot) {
var shadowRootWrapper = DOM.firstChild(element);
moveChildNodes(shadowRootWrapper, DOM.createShadowRoot(element));
var shadowRoot = DOM.createShadowRoot(element);
this._domSharedStylesHost.addHost(shadowRoot);
moveChildNodes(shadowRootWrapper, shadowRoot);
DOM.remove(shadowRootWrapper);
}

View File

@ -0,0 +1,24 @@
import {OpaqueToken, bind, Binding} from 'angular2/di';
import {CONST_EXPR, StringWrapper, Math} from 'angular2/src/facade/lang';
export const DOCUMENT_TOKEN: OpaqueToken = CONST_EXPR(new OpaqueToken('DocumentToken'));
export const DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES: OpaqueToken =
CONST_EXPR(new OpaqueToken('DomReflectPropertiesAsAttributes'));
/**
* A unique id (string) for an angular application.
*/
export const APP_ID_TOKEN: OpaqueToken = CONST_EXPR(new OpaqueToken('AppId'));
/**
* Bindings that will generate a random APP_ID_TOKEN.
*/
export var APP_ID_RANDOM_BINDING: Binding =
bind(APP_ID_TOKEN).toFactory(() => `${randomChar()}${randomChar()}${randomChar()}`, []);
function randomChar(): string {
return StringWrapper.fromCharCode(97 + Math.floor(Math.random() * 25));
}

View File

@ -1,53 +0,0 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {EmulatedUnscopedShadowDomStrategy} from './emulated_unscoped_shadow_dom_strategy';
import {
getContentAttribute,
getHostAttribute,
getComponentId,
shimCssForComponent,
insertStyleElement
} from './util';
/**
* This strategy emulates the Shadow DOM for the templates, styles **included**:
* - components templates are added as children of their component element,
* - both the template and the styles are modified so that styles are scoped to the component
* they belong to,
* - styles are moved from the templates to the styleHost (i.e. the document head).
*
* Notes:
* - styles are scoped to their component and will apply only to it,
* - a common subset of shadow DOM selectors are supported,
* - see `ShadowCss` for more information and limitations.
*/
export class EmulatedScopedShadowDomStrategy extends EmulatedUnscopedShadowDomStrategy {
constructor(styleHost) { super(styleHost); }
processStyleElement(hostComponentId: string, templateUrl: string, styleEl: Element): void {
let cssText = DOM.getText(styleEl);
cssText = shimCssForComponent(cssText, hostComponentId);
DOM.setText(styleEl, cssText);
this._moveToStyleHost(styleEl);
}
_moveToStyleHost(styleEl): void {
DOM.remove(styleEl);
insertStyleElement(this.styleHost, styleEl);
}
processElement(hostComponentId: string, elementComponentId: string, element: Element): void {
// Shim the element as a child of the compiled component
if (isPresent(hostComponentId)) {
var contentAttribute = getContentAttribute(getComponentId(hostComponentId));
DOM.setAttribute(element, contentAttribute, '');
}
// If the current element is also a component, shim it as a host
if (isPresent(elementComponentId)) {
var hostAttribute = getHostAttribute(getComponentId(elementComponentId));
DOM.setAttribute(element, hostAttribute, '');
}
}
}

View File

@ -1,24 +0,0 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {insertSharedStyleText} from './util';
/**
* This strategy emulates the Shadow DOM for the templates, styles **excluded**:
* - components templates are added as children of their component element,
* - styles are moved from the templates to the styleHost (i.e. the document head).
*
* Notes:
* - styles are **not** scoped to their component and will apply to the whole document,
* - you can **not** use shadow DOM specific selectors in the styles
*/
export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy {
constructor(public styleHost) { super(); }
hasNativeContentElement(): boolean { return false; }
processStyleElement(hostComponentId: string, templateUrl: string, styleEl: Element): void {
var cssText = DOM.getText(styleEl);
insertSharedStyleText(cssText, this.styleHost, styleEl);
}
}

View File

@ -1,13 +0,0 @@
import {Injectable} from 'angular2/di';
import {ShadowDomStrategy} from './shadow_dom_strategy';
/**
* This strategies uses the native Shadow DOM support.
*
* The templates for the component are inserted in a Shadow Root created on the component element.
* Hence they are strictly isolated.
*/
@Injectable()
export class NativeShadowDomStrategy extends ShadowDomStrategy {
hasNativeContentElement(): boolean { return true; }
}

View File

@ -1,30 +0,0 @@
import {CompileStep} from '../compiler/compile_step';
import {CompileElement} from '../compiler/compile_element';
import {CompileControl} from '../compiler/compile_control';
import {ViewDefinition} from '../../api';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {NG_CONTENT_ELEMENT_NAME, isElementWithTag} from '../util';
export class ShadowDomCompileStep implements CompileStep {
constructor(public _shadowDomStrategy: ShadowDomStrategy, public _view: ViewDefinition) {}
process(parent: CompileElement, current: CompileElement, control: CompileControl) {
if (isElementWithTag(current.element, NG_CONTENT_ELEMENT_NAME)) {
current.inheritedProtoView.bindNgContent();
} else if (isElementWithTag(current.element, 'style')) {
this._processStyleElement(current, control);
} else {
var componentId = current.isBound() ? current.inheritedElementBinder.componentId : null;
this._shadowDomStrategy.processElement(this._view.componentId, componentId, current.element);
}
}
_processStyleElement(current: CompileElement, control: CompileControl) {
this._shadowDomStrategy.processStyleElement(this._view.componentId, this._view.templateAbsUrl,
current.element);
// Style elements should not be further processed by the compiler, as they can not contain
// bindings. Skipping further compiler steps allow speeding up the compilation process.
control.ignoreCurrentElement();
}
}

View File

@ -1,13 +0,0 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
export class ShadowDomStrategy {
// Whether the strategy understands the native <content> tag
hasNativeContentElement(): boolean { return true; }
// An optional step that can modify the template style elements.
processStyleElement(hostComponentId: string, templateUrl: string, styleElement: HTMLStyleElement):
void {}
// An optional step that can modify the template elements (style elements exlcuded).
processElement(hostComponentId: string, elementComponentId: string, element: HTMLElement): void {}
}

View File

@ -1,67 +0,0 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {MapWrapper, Map} from 'angular2/src/facade/collection';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ShadowCss} from './shadow_css';
var _componentUIDs: Map<string, int> = new Map();
var _nextComponentUID: int = 0;
var _sharedStyleTexts: Map<string, boolean> = new Map();
var _lastInsertedStyleEl;
export function getComponentId(componentStringId: string): number {
var id = _componentUIDs.get(componentStringId);
if (isBlank(id)) {
id = _nextComponentUID++;
_componentUIDs.set(componentStringId, id);
}
return id;
}
export function insertSharedStyleText(cssText, styleHost, styleEl) {
if (!_sharedStyleTexts.has(cssText)) {
// Styles are unscoped and shared across components, only append them to the head
// when there are not present yet
_sharedStyleTexts.set(cssText, true);
insertStyleElement(styleHost, styleEl);
}
}
export function insertStyleElement(host, styleEl) {
if (isBlank(_lastInsertedStyleEl)) {
var firstChild = DOM.firstChild(host);
if (isPresent(firstChild)) {
DOM.insertBefore(firstChild, styleEl);
} else {
DOM.appendChild(host, styleEl);
}
} else {
DOM.insertAfter(_lastInsertedStyleEl, styleEl);
}
_lastInsertedStyleEl = styleEl;
}
// Return the attribute to be added to the component
export function getHostAttribute(id: int): string {
return `_nghost-${id}`;
}
// Returns the attribute to be added on every single element nodes in the component
export function getContentAttribute(id: int): string {
return `_ngcontent-${id}`;
}
export function shimCssForComponent(cssText: string, componentId: string): string {
var id = getComponentId(componentId);
var shadowCss = new ShadowCss();
return shadowCss.shimCssText(cssText, getContentAttribute(id), getHostAttribute(id));
}
// Reset the caches - used for tests only
export function resetShadowDomCache() {
_componentUIDs.clear();
_nextComponentUID = 0;
_sharedStyleTexts.clear();
_lastInsertedStyleEl = null;
}

View File

@ -122,4 +122,21 @@ export function queryBoundTextNodeIndices(parentNode: Node, boundTextNodes: Map<
resultCallback(node, j, boundTextNodes.get(node));
}
}
}
}
export function prependAll(parentNode: Node, nodes: Node[]) {
var lastInsertedNode = null;
nodes.forEach(node => {
if (isBlank(lastInsertedNode)) {
var firstChild = DOM.firstChild(parentNode);
if (isPresent(firstChild)) {
DOM.insertBefore(firstChild, node);
} else {
DOM.appendChild(parentNode, node);
}
} else {
DOM.insertAfter(lastInsertedNode, node);
}
lastInsertedNode = node;
});
}

View File

@ -1,7 +1,7 @@
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {DomElementBinder} from './element_binder';
import {RenderProtoViewRef, ViewType} from '../../api';
import {RenderProtoViewRef, ViewType, ViewEncapsulation} from '../../api';
import {DOM} from 'angular2/src/dom/dom_adapter';
@ -14,9 +14,10 @@ export class DomProtoViewRef extends RenderProtoViewRef {
}
export class DomProtoView {
static create(type: ViewType, rootElement: Element, fragmentsRootNodeCount: number[],
rootTextNodeIndices: number[],
elementBinders: List<DomElementBinder>): DomProtoView {
static create(type: ViewType, rootElement: Element, viewEncapsulation: ViewEncapsulation,
fragmentsRootNodeCount: number[], rootTextNodeIndices: number[],
elementBinders: List<DomElementBinder>,
hostAttributes: Map<string, string>): DomProtoView {
var boundTextNodeCount = rootTextNodeIndices.length;
for (var i = 0; i < elementBinders.length; i++) {
boundTextNodeCount += elementBinders[i].textNodeIndices.length;
@ -24,12 +25,15 @@ export class DomProtoView {
var isSingleElementFragment = fragmentsRootNodeCount.length === 1 &&
fragmentsRootNodeCount[0] === 1 &&
DOM.isElementNode(DOM.firstChild(DOM.content(rootElement)));
return new DomProtoView(type, rootElement, elementBinders, rootTextNodeIndices,
boundTextNodeCount, fragmentsRootNodeCount, isSingleElementFragment);
return new DomProtoView(type, rootElement, viewEncapsulation, elementBinders, hostAttributes,
rootTextNodeIndices, boundTextNodeCount, fragmentsRootNodeCount,
isSingleElementFragment);
}
constructor(public type: ViewType, public rootElement: Element,
public elementBinders: List<DomElementBinder>, public rootTextNodeIndices: number[],
public encapsulation: ViewEncapsulation,
public elementBinders: List<DomElementBinder>,
public hostAttributes: Map<string, string>, public rootTextNodeIndices: number[],
public boundTextNodeCount: number, public fragmentsRootNodeCount: number[],
public isSingleElementFragment: boolean) {}
}

View File

@ -35,9 +35,10 @@ export class ProtoViewBuilder {
elements: List<ElementBinderBuilder> = [];
rootTextBindings: Map<Node, ASTWithSource> = new Map();
ngContentCount: number = 0;
hostAttributes: Map<string, string> = new Map();
constructor(public rootElement, public type: api.ViewType,
public useNativeShadowDom: boolean = false) {}
public viewEncapsulation: api.ViewEncapsulation) {}
bindElement(element: HTMLElement, description: string = null): ElementBinderBuilder {
var builder = new ElementBinderBuilder(this.elements.length, element, description);
@ -65,6 +66,8 @@ export class ProtoViewBuilder {
bindNgContent() { this.ngContentCount++; }
setHostAttribute(name: string, value: string) { this.hostAttributes.set(name, value); }
build(): api.ProtoViewDto {
var domElementBinders = [];
@ -119,7 +122,7 @@ export class ProtoViewBuilder {
domElementBinders.push(new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: isPresent(nestedProtoView) || isPresent(ebb.componentId),
hasNativeShadowRoot: isPresent(ebb.componentId) && this.useNativeShadowDom,
hasNativeShadowRoot: false,
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents()
@ -127,8 +130,9 @@ export class ProtoViewBuilder {
});
var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length;
return new api.ProtoViewDto({
render: new DomProtoViewRef(DomProtoView.create(this.type, this.rootElement, [rootNodeCount],
rootTextNodeIndices, domElementBinders)),
render: new DomProtoViewRef(
DomProtoView.create(this.type, this.rootElement, this.viewEncapsulation, [rootNodeCount],
rootTextNodeIndices, domElementBinders, this.hostAttributes)),
type: this.type,
elementBinders: apiElementBinders,
variableBindings: this.variableBindings,
@ -178,7 +182,8 @@ export class ElementBinderBuilder {
if (isPresent(this.nestedProtoView)) {
throw new BaseException('Only one nested view per element is allowed');
}
this.nestedProtoView = new ProtoViewBuilder(rootElement, api.ViewType.EMBEDDED);
this.nestedProtoView =
new ProtoViewBuilder(rootElement, api.ViewType.EMBEDDED, api.ViewEncapsulation.NONE);
return this.nestedProtoView;
}

View File

@ -1,10 +1,15 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang';
import {ListWrapper} from 'angular2/src/facade/collection';
import {ListWrapper, SetWrapper, MapWrapper} from 'angular2/src/facade/collection';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {DomElementBinder} from './element_binder';
import {RenderProtoViewMergeMapping, RenderProtoViewRef, ViewType} from '../../api';
import {
RenderProtoViewMergeMapping,
RenderProtoViewRef,
ViewType,
ViewEncapsulation
} from '../../api';
import {
NG_BINDING_CLASS,
NG_CONTENT_ELEMENT_NAME,
@ -12,7 +17,9 @@ import {
cloneAndQueryProtoView,
queryBoundElements,
queryBoundTextNodeIndices,
NG_SHADOW_ROOT_ELEMENT_NAME
NG_SHADOW_ROOT_ELEMENT_NAME,
isElementWithTag,
prependAll
} from '../util';
export function mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRef | List<any>>):
@ -26,7 +33,9 @@ export function mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRe
// modify the DOM
mergeEmbeddedPvsIntoComponentOrRootPv(clonedProtoViews, hostViewAndBinderIndices);
var fragments = [];
mergeComponents(clonedProtoViews, hostViewAndBinderIndices, fragments);
var elementsWithNativeShadowRoot: Set<Element> = new Set();
mergeComponents(clonedProtoViews, hostViewAndBinderIndices, fragments,
elementsWithNativeShadowRoot);
// Note: Need to remark parent elements of bound text nodes
// so that we can find them later via queryBoundElements!
markBoundTextNodeParentsAsBoundElements(clonedProtoViews);
@ -42,8 +51,9 @@ export function mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRe
var boundTextNodeMap: Map<Node, any> = indexBoundTextNodes(clonedProtoViews);
var rootTextNodeIndices =
calcRootTextNodeIndices(rootNode, boundTextNodeMap, mergedBoundTextIndices);
var mergedElementBinders = calcElementBinders(clonedProtoViews, mergedBoundElements,
boundTextNodeMap, mergedBoundTextIndices);
var mergedElementBinders =
calcElementBinders(clonedProtoViews, mergedBoundElements, elementsWithNativeShadowRoot,
boundTextNodeMap, mergedBoundTextIndices);
// create element / text index mappings
var mappedElementIndices = calcMappedElementIndices(clonedProtoViews, mergedBoundElements);
@ -53,9 +63,9 @@ export function mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRe
var hostElementIndicesByViewIndex =
calcHostElementIndicesByViewIndex(clonedProtoViews, hostViewAndBinderIndices);
var nestedViewCounts = calcNestedViewCounts(hostViewAndBinderIndices);
var mergedProtoView =
DomProtoView.create(mainProtoView.original.type, rootElement, fragmentsRootNodeCount,
rootTextNodeIndices, mergedElementBinders);
var mergedProtoView = DomProtoView.create(
mainProtoView.original.type, rootElement, mainProtoView.original.encapsulation,
fragmentsRootNodeCount, rootTextNodeIndices, mergedElementBinders, new Map());
return new RenderProtoViewMergeMapping(new DomProtoViewRef(mergedProtoView),
fragmentsRootNodeCount.length, mappedElementIndices,
mergedBoundElements.length, mappedTextIndices,
@ -143,7 +153,8 @@ function calcNearestHostComponentOrRootPvIndices(clonedProtoViews: ClonedProtoVi
}
function mergeComponents(clonedProtoViews: ClonedProtoView[], hostViewAndBinderIndices: number[][],
targetFragments: Node[][]) {
targetFragments: Node[][],
targetElementsWithNativeShadowRoot: Set<Element>) {
var hostProtoView = clonedProtoViews[0];
hostProtoView.fragments.forEach((fragment) => targetFragments.push(fragment));
@ -153,13 +164,15 @@ function mergeComponents(clonedProtoViews: ClonedProtoView[], hostViewAndBinderI
var hostProtoView = clonedProtoViews[hostViewIdx];
var clonedProtoView = clonedProtoViews[viewIdx];
if (clonedProtoView.original.type === ViewType.COMPONENT) {
mergeComponent(hostProtoView, hostBinderIdx, clonedProtoView, targetFragments);
mergeComponent(hostProtoView, hostBinderIdx, clonedProtoView, targetFragments,
targetElementsWithNativeShadowRoot);
}
}
}
function mergeComponent(hostProtoView: ClonedProtoView, binderIdx: number,
nestedProtoView: ClonedProtoView, targetFragments: Node[][]) {
nestedProtoView: ClonedProtoView, targetFragments: Node[][],
targetElementsWithNativeShadowRoot: Set<Element>) {
var hostElement = hostProtoView.boundElements[binderIdx];
// We wrap the fragments into elements so that we can expand <ng-content>
@ -176,8 +189,14 @@ function mergeComponent(hostProtoView: ClonedProtoView, binderIdx: number,
// unwrap the fragment elements into arrays of nodes after projecting
var fragments = extractFragmentNodesFromElements(fragmentElements);
appendComponentNodesToHost(hostProtoView, binderIdx, fragments[0]);
var useNativeShadowRoot = nestedProtoView.original.encapsulation === ViewEncapsulation.NATIVE;
if (useNativeShadowRoot) {
targetElementsWithNativeShadowRoot.add(hostElement);
}
MapWrapper.forEach(nestedProtoView.original.hostAttributes, (attrValue, attrName) => {
DOM.setAttribute(hostElement, attrName, attrValue);
});
appendComponentNodesToHost(hostProtoView, binderIdx, fragments[0], useNativeShadowRoot);
for (var i = 1; i < fragments.length; i++) {
targetFragments.push(fragments[i]);
}
@ -209,10 +228,9 @@ function findContentElements(fragmentElements: Element[]): Element[] {
}
function appendComponentNodesToHost(hostProtoView: ClonedProtoView, binderIdx: number,
componentRootNodes: Node[]) {
componentRootNodes: Node[], useNativeShadowRoot: boolean) {
var hostElement = hostProtoView.boundElements[binderIdx];
var binder = hostProtoView.original.elementBinders[binderIdx];
if (binder.hasNativeShadowRoot) {
if (useNativeShadowRoot) {
var shadowRootWrapper = DOM.createElement(NG_SHADOW_ROOT_ELEMENT_NAME);
for (var i = 0; i < componentRootNodes.length; i++) {
DOM.appendChild(shadowRootWrapper, componentRootNodes[i]);
@ -298,6 +316,7 @@ function calcRootTextNodeIndices(rootNode: Node, boundTextNodes: Map<Node, any>,
}
function calcElementBinders(clonedProtoViews: ClonedProtoView[], mergedBoundElements: Element[],
elementsWithNativeShadowRoot: Set<Element>,
boundTextNodes: Map<Node, any>,
targetBoundTextIndices: Map<Node, number>): DomElementBinder[] {
var elementBinderByElement: Map<Element, DomElementBinder> =
@ -311,7 +330,8 @@ function calcElementBinders(clonedProtoViews: ClonedProtoView[], mergedBoundElem
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
});
mergedElementBinders.push(
updateElementBinderTextNodeIndices(elementBinderByElement.get(element), textNodeIndices));
updateElementBinders(elementBinderByElement.get(element), textNodeIndices,
SetWrapper.has(elementsWithNativeShadowRoot, element)));
}
return mergedElementBinders;
}
@ -330,8 +350,8 @@ function indexElementBindersByElement(mergableProtoViews: ClonedProtoView[]):
return elementBinderByElement;
}
function updateElementBinderTextNodeIndices(elementBinder: DomElementBinder,
textNodeIndices: number[]): DomElementBinder {
function updateElementBinders(elementBinder: DomElementBinder, textNodeIndices: number[],
hasNativeShadowRoot: boolean): DomElementBinder {
var result;
if (isBlank(elementBinder)) {
result = new DomElementBinder({
@ -349,7 +369,7 @@ function updateElementBinderTextNodeIndices(elementBinder: DomElementBinder,
eventLocals: elementBinder.eventLocals,
localEvents: elementBinder.localEvents,
globalEvents: elementBinder.globalEvents,
hasNativeShadowRoot: elementBinder.hasNativeShadowRoot
hasNativeShadowRoot: hasNativeShadowRoot
});
}
return result;

View File

@ -0,0 +1,52 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Inject, Injectable} from 'angular2/di';
import {SetWrapper} from 'angular2/src/facade/collection';
import {DOCUMENT_TOKEN} from '../dom_tokens';
@Injectable()
export class SharedStylesHost {
protected _styles: string[] = [];
protected _stylesSet: Set<string> = new Set();
constructor() {}
addStyles(styles: string[]) {
var additions = [];
styles.forEach(style => {
if (!SetWrapper.has(this._stylesSet, style)) {
this._stylesSet.add(style);
this._styles.push(style);
additions.push(style);
}
});
this.onStylesAdded(additions);
}
protected onStylesAdded(additions: string[]) {}
getAllStyles(): string[] { return this._styles; }
}
@Injectable()
export class DomSharedStylesHost extends SharedStylesHost {
private _hostNodes: Set<Node> = new Set();
constructor(@Inject(DOCUMENT_TOKEN) doc: any) {
super();
this._hostNodes.add(doc.head);
}
_addStylesToHost(styles: string[], host: Node) {
for (var i = 0; i < styles.length; i++) {
var style = styles[i];
DOM.appendChild(host, DOM.createStyleElement(style));
}
}
addHost(hostNode: Node) {
this._addStylesToHost(this._styles, hostNode);
this._hostNodes.add(hostNode);
}
removeHost(hostNode: Node) { SetWrapper.delete(this._hostNodes, hostNode); }
onStylesAdded(additions: string[]) {
this._hostNodes.forEach((hostNode) => { this._addStylesToHost(additions, hostNode); });
}
}

View File

@ -5,9 +5,8 @@
*/
export * from './dom/compiler/view_loader';
export * from './dom/view/shared_styles_host';
export * from './dom/compiler/compiler';
export * from './dom/dom_renderer';
export * from './dom/shadow_dom/shadow_dom_strategy';
export * from './dom/shadow_dom/native_shadow_dom_strategy';
export * from './dom/shadow_dom/emulated_scoped_shadow_dom_strategy';
export * from './dom/shadow_dom/emulated_unscoped_shadow_dom_strategy';
export * from './dom/dom_tokens';
export * from './api';