feat(compiler): attach components and project light dom during compilation.

Closes #2529

BREAKING CHANGES:
- shadow dom emulation no longer
  supports the `<content>` tag. Use the new `<ng-content>` instead
  (works with all shadow dom strategies).
- removed `DomRenderer.setViewRootNodes` and `AppViewManager.getComponentView`
  -> use `DomRenderer.getNativeElementSync(elementRef)` and change shadow dom directly
- the `Renderer` interface has changed:
  * `createView` now also has to support sub views
  * the notion of a container has been removed. Instead, the renderer has
    to implement methods to attach views next to elements or other views.
  * a RenderView now contains multiple RenderFragments. Fragments
    are used to move DOM nodes around.

Internal changes / design changes:
- Introduce notion of view fragments on render side
- DomProtoViews and DomViews on render side are merged,
  AppProtoViews are not merged, AppViews are partially merged
  (they share arrays with the other merged AppViews but we keep
  individual AppView instances for now).
- DomProtoViews always have a `<template>` element as root
  * needed for storing subviews
  * we have less chunks of DOM to clone now
- remove fake ElementBinder / Bound element for root text bindings
  and model them explicitly. This removes a lot of special cases we had!
- AppView shares data with nested component views
- some methods in AppViewManager (create, hydrate, dehydrate) are iterative now
  * now possible as we have all child AppViews / ElementRefs already in an array!
This commit is contained in:
Tobias Bosch
2015-06-24 13:46:39 -07:00
parent d449ea5ca4
commit b1df54501a
82 changed files with 3418 additions and 2806 deletions

View File

@ -13,7 +13,9 @@ import {ProtoViewDto, ViewType} from '../../api';
*/
export class CompilePipeline {
_control: CompileControl;
constructor(steps: List<CompileStep>) { this._control = new CompileControl(steps); }
constructor(steps: List<CompileStep>, private _useNativeShadowDom: boolean = false) {
this._control = new CompileControl(steps);
}
process(rootElement, protoViewType: ViewType = null,
compilationCtxtDescription: string = ''): List<CompileElement> {
@ -22,7 +24,8 @@ export class CompilePipeline {
}
var results = [];
var rootCompileElement = new CompileElement(rootElement, compilationCtxtDescription);
rootCompileElement.inheritedProtoView = new ProtoViewBuilder(rootElement, protoViewType);
rootCompileElement.inheritedProtoView =
new ProtoViewBuilder(rootElement, protoViewType, this._useNativeShadowDom);
rootCompileElement.isViewRoot = true;
this._process(results, null, rootCompileElement, compilationCtxtDescription);
return results;

View File

@ -10,13 +10,15 @@ import {
ViewType,
DirectiveMetadata,
RenderCompiler,
RenderProtoViewRef
RenderProtoViewRef,
RenderProtoViewMergeMapping
} from '../../api';
import {CompilePipeline} from './compile_pipeline';
import {ViewLoader} from 'angular2/src/render/dom/compiler/view_loader';
import {CompileStepFactory, DefaultStepFactory} from './compile_step_factory';
import {Parser} from 'angular2/change_detection';
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
import * as pvm from '../view/proto_view_merger';
/**
* The compiler loads and translates the html templates of components into
@ -24,7 +26,10 @@ import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
* the CompilePipeline and the CompileSteps.
*/
export class DomCompiler extends RenderCompiler {
constructor(public _stepFactory: CompileStepFactory, public _viewLoader: ViewLoader) { super(); }
constructor(public _stepFactory: CompileStepFactory, public _viewLoader: ViewLoader,
public _useNativeShadowDom: boolean) {
super();
}
compile(view: ViewDefinition): Promise<ProtoViewDto> {
var tplPromise = this._viewLoader.load(view);
@ -42,13 +47,21 @@ export class DomCompiler extends RenderCompiler {
styleAbsUrls: null,
directives: [directiveMetadata]
});
var element = DOM.createElement(directiveMetadata.selector);
return this._compileTemplate(hostViewDef, element, ViewType.HOST);
var template = DOM.createTemplate('');
DOM.appendChild(DOM.content(template), DOM.createElement(directiveMetadata.selector));
return this._compileTemplate(hostViewDef, template, ViewType.HOST);
}
mergeProtoViewsRecursively(
protoViewRefs:
List<RenderProtoViewRef | List<any>>): Promise<List<RenderProtoViewMergeMapping>> {
return PromiseWrapper.resolve(pvm.mergeProtoViewsRecursively(protoViewRefs));
}
_compileTemplate(viewDef: ViewDefinition, tplElement,
protoViewType: ViewType): Promise<ProtoViewDto> {
var pipeline = new CompilePipeline(this._stepFactory.createSteps(viewDef));
var pipeline =
new CompilePipeline(this._stepFactory.createSteps(viewDef), this._useNativeShadowDom);
var compileElements = pipeline.process(tplElement, protoViewType, viewDef.componentId);
return PromiseWrapper.resolve(compileElements[0].inheritedProtoView.build());
@ -58,6 +71,7 @@ export class DomCompiler extends RenderCompiler {
@Injectable()
export class DefaultDomCompiler extends DomCompiler {
constructor(parser: Parser, shadowDomStrategy: ShadowDomStrategy, viewLoader: ViewLoader) {
super(new DefaultStepFactory(parser, shadowDomStrategy), viewLoader);
super(new DefaultStepFactory(parser, shadowDomStrategy), viewLoader,
shadowDomStrategy.hasNativeContentElement());
}
}

View File

@ -27,11 +27,11 @@ var _SELECTOR_REGEXP = RegExpWrapper.create(
*/
export class CssSelector {
element: string = null;
classNames: List<string> = [];
attrs: List<string> = [];
notSelectors: List<CssSelector> = [];
classNames: string[] = [];
attrs: string[] = [];
notSelectors: CssSelector[] = [];
static parse(selector: string): List<CssSelector> {
static parse(selector: string): CssSelector[] {
var results: CssSelector[] = [];
var _addResult = (res: CssSelector[], cssSel) => {
if (cssSel.notSelectors.length > 0 && isBlank(cssSel.element) &&
@ -135,21 +135,21 @@ export class CssSelector {
* are contained in a given CssSelector.
*/
export class SelectorMatcher {
static createNotMatcher(notSelectors: List<CssSelector>): SelectorMatcher {
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
var notMatcher = new SelectorMatcher();
notMatcher.addSelectables(notSelectors, null);
return notMatcher;
}
private _elementMap: Map<string, List<SelectorContext>> = new Map();
private _elementMap: Map<string, SelectorContext[]> = new Map();
private _elementPartialMap: Map<string, SelectorMatcher> = new Map();
private _classMap: Map<string, List<SelectorContext>> = new Map();
private _classMap: Map<string, SelectorContext[]> = new Map();
private _classPartialMap: Map<string, SelectorMatcher> = new Map();
private _attrValueMap: Map<string, Map<string, List<SelectorContext>>> = new Map();
private _attrValueMap: Map<string, Map<string, SelectorContext[]>> = new Map();
private _attrValuePartialMap: Map<string, Map<string, SelectorMatcher>> = new Map();
private _listContexts: List<SelectorListContext> = [];
private _listContexts: SelectorListContext[] = [];
addSelectables(cssSelectors: List<CssSelector>, callbackCtxt?: any) {
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: any) {
var listContext = null;
if (cssSelectors.length > 1) {
listContext = new SelectorListContext(cssSelectors);
@ -220,7 +220,7 @@ export class SelectorMatcher {
}
}
private _addTerminal(map: Map<string, List<SelectorContext>>, name: string,
private _addTerminal(map: Map<string, SelectorContext[]>, name: string,
selectable: SelectorContext) {
var terminalList = map.get(name);
if (isBlank(terminalList)) {
@ -298,7 +298,7 @@ export class SelectorMatcher {
return result;
}
_matchTerminal(map: Map<string, List<SelectorContext>>, name, cssSelector: CssSelector,
_matchTerminal(map: Map<string, SelectorContext[]>, name, cssSelector: CssSelector,
matchedCallback: (CssSelector, any) => void): boolean {
if (isBlank(map) || isBlank(name)) {
return false;
@ -341,12 +341,12 @@ export class SelectorMatcher {
class SelectorListContext {
alreadyMatched: boolean = false;
constructor(public selectors: List<CssSelector>) {}
constructor(public selectors: CssSelector[]) {}
}
// Store context to pass back selector and context when a selector is matched
class SelectorContext {
notSelectors: List<CssSelector>;
notSelectors: CssSelector[];
constructor(public selector: CssSelector, public cbContext: any,
public listContext: SelectorListContext) {

View File

@ -26,7 +26,11 @@ export class TextInterpolationParser implements CompileStep {
var expr = this._parser.parseInterpolation(text, current.elementDescription);
if (isPresent(expr)) {
DOM.setText(node, ' ');
current.bindElement().bindText(node, expr);
if (current.element === current.inheritedProtoView.rootElement) {
current.inheritedProtoView.bindRootText(node, expr);
} else {
current.bindElement().bindText(node, expr);
}
}
}
}

View File

@ -65,23 +65,31 @@ export class ViewSplitter implements CompileStep {
}
}
if (hasTemplateBinding) {
var newParent = new CompileElement(DOM.createTemplate(''));
newParent.inheritedProtoView = current.inheritedProtoView;
newParent.inheritedElementBinder = current.inheritedElementBinder;
newParent.distanceToInheritedBinder = current.distanceToInheritedBinder;
var anchor = new CompileElement(DOM.createTemplate(''));
anchor.inheritedProtoView = current.inheritedProtoView;
anchor.inheritedElementBinder = current.inheritedElementBinder;
anchor.distanceToInheritedBinder = current.distanceToInheritedBinder;
// newParent doesn't appear in the original template, so we associate
// the current element description to get a more meaningful message in case of error
newParent.elementDescription = current.elementDescription;
anchor.elementDescription = current.elementDescription;
current.inheritedProtoView = newParent.bindElement().bindNestedProtoView(current.element);
var viewRoot = new CompileElement(DOM.createTemplate(''));
viewRoot.inheritedProtoView = anchor.bindElement().bindNestedProtoView(viewRoot.element);
// viewRoot doesn't appear in the original template, so we associate
// the current element description to get a more meaningful message in case of error
viewRoot.elementDescription = current.elementDescription;
viewRoot.isViewRoot = true;
current.inheritedProtoView = viewRoot.inheritedProtoView;
current.inheritedElementBinder = null;
current.distanceToInheritedBinder = 0;
current.isViewRoot = true;
this._parseTemplateBindings(templateBindings, newParent);
this._addParentElement(current.element, newParent.element);
control.addParent(newParent);
DOM.remove(current.element);
this._parseTemplateBindings(templateBindings, anchor);
DOM.insertBefore(current.element, anchor.element);
control.addParent(anchor);
DOM.appendChild(DOM.content(viewRoot.element), current.element);
control.addParent(viewRoot);
}
}
}
@ -94,11 +102,6 @@ export class ViewSplitter implements CompileStep {
}
}
_addParentElement(currentElement, newParentElement) {
DOM.insertBefore(currentElement, newParentElement);
DOM.appendChild(newParentElement, currentElement);
}
_parseTemplateBindings(templateBindings: string, compileElement: CompileElement) {
var bindings =
this._parser.parseTemplateBindings(templateBindings, compileElement.elementDescription);

View File

@ -10,17 +10,26 @@ import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src
import {DOM} from 'angular2/src/dom/dom_adapter';
import {Content} from './shadow_dom/content_tag';
import {ShadowDomStrategy} from './shadow_dom/shadow_dom_strategy';
import {EventManager} from './events/event_manager';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './view/proto_view';
import {DomView, DomViewRef, resolveInternalDomView} from './view/view';
import {DomElement} from './view/element';
import {DomViewContainer} from './view/view_container';
import {NG_BINDING_CLASS_SELECTOR, NG_BINDING_CLASS, camelCaseToDashCase} from './util';
import {DomFragmentRef, resolveInternalDomFragment} from './view/fragment';
import {
NG_BINDING_CLASS_SELECTOR,
NG_BINDING_CLASS,
cloneAndQueryProtoView,
camelCaseToDashCase
} from './util';
import {Renderer, RenderProtoViewRef, RenderViewRef, RenderElementRef} from '../api';
import {
Renderer,
RenderProtoViewRef,
RenderViewRef,
RenderElementRef,
RenderFragmentRef,
RenderViewWithFragments
} from '../api';
export const DOCUMENT_TOKEN = CONST_EXPR(new OpaqueToken('DocumentToken'));
export const DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES =
@ -32,8 +41,7 @@ export class DomRenderer extends Renderer {
_document;
_reflectPropertiesAsAttributes: boolean;
constructor(public _eventManager: EventManager, public _shadowDomStrategy: ShadowDomStrategy,
@Inject(DOCUMENT_TOKEN) document,
constructor(public _eventManager: EventManager, @Inject(DOCUMENT_TOKEN) document,
@Inject(DOM_REFLECT_PROPERTIES_AS_ATTRIBUTES) reflectPropertiesAsAttributes:
boolean) {
super();
@ -41,109 +49,57 @@ export class DomRenderer extends Renderer {
this._document = document;
}
createRootHostView(hostProtoViewRef: RenderProtoViewRef,
hostElementSelector: string): RenderViewRef {
createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number,
hostElementSelector: string): RenderViewWithFragments {
var hostProtoView = resolveInternalDomProtoView(hostProtoViewRef);
var element = DOM.querySelector(this._document, hostElementSelector);
if (isBlank(element)) {
throw new BaseException(`The selector "${hostElementSelector}" did not match any elements`);
}
return new DomViewRef(this._createView(hostProtoView, element));
return this._createView(hostProtoView, element);
}
createView(protoViewRef: RenderProtoViewRef): RenderViewRef {
createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments {
var protoView = resolveInternalDomProtoView(protoViewRef);
return new DomViewRef(this._createView(protoView, null));
return this._createView(protoView, null);
}
destroyView(view: RenderViewRef) {
destroyView(viewRef: RenderViewRef) {
// noop for now
}
getNativeElementSync(location: RenderElementRef): any {
if (isBlank(location.renderBoundElementIndex)) {
return null;
}
return resolveInternalDomView(location.renderView)
.boundElements[location.boundElementIndex]
.element;
.boundElements[location.renderBoundElementIndex];
}
attachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) {
var hostView = resolveInternalDomView(location.renderView);
var componentView = resolveInternalDomView(componentViewRef);
var element = hostView.boundElements[location.boundElementIndex].element;
var lightDom = hostView.boundElements[location.boundElementIndex].lightDom;
if (isPresent(lightDom)) {
lightDom.attachShadowDomView(componentView);
getRootNodes(fragment: RenderFragmentRef): List<Node> {
return resolveInternalDomFragment(fragment);
}
attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef,
fragmentRef: RenderFragmentRef) {
var previousFragmentNodes = resolveInternalDomFragment(previousFragmentRef);
var sibling = previousFragmentNodes[previousFragmentNodes.length - 1];
moveNodesAfterSibling(sibling, resolveInternalDomFragment(fragmentRef));
}
attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {
if (isBlank(elementRef.renderBoundElementIndex)) {
return;
}
var shadowRoot = this._shadowDomStrategy.prepareShadowRoot(element);
this._moveViewNodesIntoParent(shadowRoot, componentView);
componentView.hostLightDom = lightDom;
componentView.shadowRoot = shadowRoot;
var parentView = resolveInternalDomView(elementRef.renderView);
var element = parentView.boundElements[elementRef.renderBoundElementIndex];
moveNodesAfterSibling(element, resolveInternalDomFragment(fragmentRef));
}
setComponentViewRootNodes(componentViewRef: RenderViewRef, rootNodes: List</*node*/ any>) {
var componentView = resolveInternalDomView(componentViewRef);
this._removeViewNodes(componentView);
componentView.rootNodes = rootNodes;
this._moveViewNodesIntoParent(componentView.shadowRoot, componentView);
}
getRootNodes(viewRef: RenderViewRef): List</*node*/ any> {
return resolveInternalDomView(viewRef).rootNodes;
}
detachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) {
var hostView = resolveInternalDomView(location.renderView);
var componentView = resolveInternalDomView(componentViewRef);
this._removeViewNodes(componentView);
var lightDom = hostView.boundElements[location.boundElementIndex].lightDom;
if (isPresent(lightDom)) {
lightDom.detachShadowDomView();
}
componentView.hostLightDom = null;
componentView.shadowRoot = null;
}
attachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) {
var parentView = resolveInternalDomView(location.renderView);
var view = resolveInternalDomView(viewRef);
var viewContainer = this._getOrCreateViewContainer(parentView, location.boundElementIndex);
ListWrapper.insert(viewContainer.views, atIndex, view);
view.hostLightDom = parentView.hostLightDom;
var directParentLightDom = this._directParentLightDom(parentView, location.boundElementIndex);
if (isBlank(directParentLightDom)) {
var siblingToInsertAfter;
if (atIndex == 0) {
siblingToInsertAfter = parentView.boundElements[location.boundElementIndex].element;
} else {
siblingToInsertAfter = ListWrapper.last(viewContainer.views[atIndex - 1].rootNodes);
}
this._moveViewNodesAfterSibling(siblingToInsertAfter, view);
} else {
directParentLightDom.redistribute();
}
// new content tags might have appeared, we need to redistribute.
if (isPresent(parentView.hostLightDom)) {
parentView.hostLightDom.redistribute();
}
}
detachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) {
var parentView = resolveInternalDomView(location.renderView);
var view = resolveInternalDomView(viewRef);
var viewContainer = parentView.boundElements[location.boundElementIndex].viewContainer;
var detachedView = viewContainer.views[atIndex];
ListWrapper.removeAt(viewContainer.views, atIndex);
var directParentLightDom = this._directParentLightDom(parentView, location.boundElementIndex);
if (isBlank(directParentLightDom)) {
this._removeViewNodes(detachedView);
} else {
directParentLightDom.redistribute();
}
view.hostLightDom = null;
// content tags might have disappeared we need to do redistribution.
if (isPresent(parentView.hostLightDom)) {
parentView.hostLightDom.redistribute();
detachFragment(fragmentRef: RenderFragmentRef) {
var fragmentNodes = resolveInternalDomFragment(fragmentRef);
for (var i = 0; i < fragmentNodes.length; i++) {
DOM.remove(fragmentNodes[i]);
}
}
@ -152,13 +108,6 @@ export class DomRenderer extends Renderer {
if (view.hydrated) throw new BaseException('The view is already hydrated.');
view.hydrated = true;
for (var i = 0; i < view.boundElements.length; ++i) {
var lightDom = view.boundElements[i].lightDom;
if (isPresent(lightDom)) {
lightDom.redistribute();
}
}
// add global events
view.eventHandlerRemovers = [];
var binders = view.proto.elementBinders;
@ -173,9 +122,6 @@ export class DomRenderer extends Renderer {
}
}
}
if (isPresent(view.hostLightDom)) {
view.hostLightDom.redistribute();
}
}
dehydrateView(viewRef: RenderViewRef) {
@ -191,8 +137,11 @@ export class DomRenderer extends Renderer {
}
setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementProperty(location.boundElementIndex, propertyName, propertyValue);
view.setElementProperty(location.renderBoundElementIndex, propertyName, propertyValue);
// Reflect the property value as an attribute value with ng-reflect- prefix.
if (this._reflectPropertiesAsAttributes) {
this.setElementAttribute(location, `${REFLECT_PREFIX}${camelCaseToDashCase(propertyName)}`,
@ -202,26 +151,41 @@ export class DomRenderer extends Renderer {
setElementAttribute(location: RenderElementRef, attributeName: string, attributeValue: string):
void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementAttribute(location.boundElementIndex, attributeName, attributeValue);
view.setElementAttribute(location.renderBoundElementIndex, attributeName, attributeValue);
}
setElementClass(location: RenderElementRef, className: string, isAdd: boolean): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementClass(location.boundElementIndex, className, isAdd);
view.setElementClass(location.renderBoundElementIndex, className, isAdd);
}
setElementStyle(location: RenderElementRef, styleName: string, styleValue: string): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.setElementStyle(location.boundElementIndex, styleName, styleValue);
view.setElementStyle(location.renderBoundElementIndex, styleName, styleValue);
}
invokeElementMethod(location: RenderElementRef, methodName: string, args: List<any>): void {
if (isBlank(location.renderBoundElementIndex)) {
return;
}
var view = resolveInternalDomView(location.renderView);
view.invokeElementMethod(location.boundElementIndex, methodName, args);
view.invokeElementMethod(location.renderBoundElementIndex, methodName, args);
}
setText(viewRef: RenderViewRef, textNodeIndex: number, text: string): void {
if (isBlank(textNodeIndex)) {
return;
}
var view = resolveInternalDomView(viewRef);
DOM.setText(view.boundTextNodes[textNodeIndex], text);
}
@ -231,99 +195,50 @@ export class DomRenderer extends Renderer {
view.eventDispatcher = dispatcher;
}
_createView(protoView: DomProtoView, inplaceElement): DomView {
var rootElementClone;
var elementsWithBindingsDynamic;
var viewRootNodes;
_createView(protoView: DomProtoView, inplaceElement: HTMLElement): RenderViewWithFragments {
var clonedProtoView = cloneAndQueryProtoView(protoView, true);
var boundElements = clonedProtoView.boundElements;
// adopt inplaceElement
if (isPresent(inplaceElement)) {
rootElementClone = inplaceElement;
elementsWithBindingsDynamic = [];
viewRootNodes = [inplaceElement];
} else if (protoView.isTemplateElement) {
rootElementClone = DOM.importIntoDoc(DOM.content(protoView.element));
elementsWithBindingsDynamic =
DOM.querySelectorAll(rootElementClone, NG_BINDING_CLASS_SELECTOR);
viewRootNodes = ListWrapper.createFixedSize(protoView.rootNodeCount);
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
var childNode = DOM.firstChild(rootElementClone);
for (var i = 0; i < protoView.rootNodeCount; i++, childNode = DOM.nextSibling(childNode)) {
viewRootNodes[i] = childNode;
if (protoView.fragmentsRootNodeCount[0] !== 1) {
throw new BaseException('Root proto views can only contain one element!');
}
} else {
rootElementClone = DOM.importIntoDoc(protoView.element);
elementsWithBindingsDynamic = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS);
viewRootNodes = [rootElementClone];
DOM.clearNodes(inplaceElement);
var tempRoot = clonedProtoView.fragments[0][0];
moveChildNodes(tempRoot, inplaceElement);
if (boundElements.length > 0 && boundElements[0] === tempRoot) {
boundElements[0] = inplaceElement;
}
clonedProtoView.fragments[0][0] = inplaceElement;
}
var view = new DomView(protoView, clonedProtoView.boundTextNodes, boundElements);
var binders = protoView.elementBinders;
var boundTextNodes = ListWrapper.createFixedSize(protoView.boundTextNodeCount);
var boundElements = ListWrapper.createFixedSize(binders.length);
var boundTextNodeIdx = 0;
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element;
var childNodes;
if (binderIdx === 0 && protoView.rootBindingOffset === 1) {
// Note: if the root element was a template,
// the rootElementClone is a document fragment,
// which will be empty as soon as the view gets appended
// to a parent. So we store null in the boundElements array.
element = protoView.isTemplateElement ? null : rootElementClone;
childNodes = DOM.childNodes(rootElementClone);
} else {
element = elementsWithBindingsDynamic[binderIdx - protoView.rootBindingOffset];
childNodes = DOM.childNodes(element);
}
// boundTextNodes
var textNodeIndices = binder.textNodeIndices;
for (var i = 0; i < textNodeIndices.length; i++) {
boundTextNodes[boundTextNodeIdx++] = childNodes[textNodeIndices[i]];
}
// contentTags
var contentTag = null;
if (isPresent(binder.contentTagSelector)) {
contentTag = new Content(element, binder.contentTagSelector);
}
boundElements[binderIdx] = new DomElement(binder, element, contentTag);
}
var view = new DomView(protoView, viewRootNodes, boundTextNodes, boundElements);
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
var binder = binders[binderIdx];
var element = boundElements[binderIdx];
var domEl = element.element;
// lightDoms
var lightDom = null;
// Note: for the root element we can't use the binder.elementIsEmpty
// information as we don't use the element from the ProtoView
// but an element from the document.
if (isPresent(binder.componentId) && (!binder.elementIsEmpty || isPresent(inplaceElement))) {
lightDom = this._shadowDomStrategy.constructLightDom(view, domEl);
}
element.lightDom = lightDom;
// init contentTags
var contentTag = element.contentTag;
if (isPresent(contentTag)) {
var directParentLightDom = this._directParentLightDom(view, binderIdx);
contentTag.init(directParentLightDom);
// native shadow DOM
if (binder.hasNativeShadowRoot) {
var shadowRootWrapper = DOM.firstChild(element);
moveChildNodes(shadowRootWrapper, DOM.createShadowRoot(element));
DOM.remove(shadowRootWrapper);
}
// events
if (isPresent(binder.eventLocals) && isPresent(binder.localEvents)) {
for (var i = 0; i < binder.localEvents.length; i++) {
this._createEventListener(view, domEl, binderIdx, binder.localEvents[i].name,
this._createEventListener(view, element, binderIdx, binder.localEvents[i].name,
binder.eventLocals);
}
}
}
return view;
return new RenderViewWithFragments(
new DomViewRef(view), clonedProtoView.fragments.map(nodes => new DomFragmentRef(nodes)));
}
_createEventListener(view, element, elementIndex, eventName, eventLocals) {
@ -331,45 +246,26 @@ export class DomRenderer extends Renderer {
element, eventName, (event) => { view.dispatchEvent(elementIndex, eventName, event); });
}
_moveViewNodesAfterSibling(sibling, view) {
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
DOM.insertAfter(sibling, view.rootNodes[i]);
}
}
_moveViewNodesIntoParent(parent, view) {
for (var i = 0; i < view.rootNodes.length; ++i) {
DOM.appendChild(parent, view.rootNodes[i]);
}
}
_removeViewNodes(view) {
var len = view.rootNodes.length;
if (len == 0) return;
var parent = view.rootNodes[0].parentNode;
for (var i = len - 1; i >= 0; --i) {
DOM.removeChild(parent, view.rootNodes[i]);
}
}
_getOrCreateViewContainer(parentView: DomView, boundElementIndex) {
var el = parentView.boundElements[boundElementIndex];
var vc = el.viewContainer;
if (isBlank(vc)) {
vc = new DomViewContainer();
el.viewContainer = vc;
}
return vc;
}
_directParentLightDom(view: DomView, boundElementIndex: number) {
var directParentEl = view.getDirectParentElement(boundElementIndex);
return isPresent(directParentEl) ? directParentEl.lightDom : null;
}
_createGlobalEventListener(view, elementIndex, eventName, eventTarget, fullName): Function {
return this._eventManager.addGlobalEventListener(
eventTarget, eventName, (event) => { view.dispatchEvent(elementIndex, fullName, event); });
}
}
function moveNodesAfterSibling(sibling, nodes) {
if (isPresent(DOM.parentElement(sibling))) {
for (var i = 0; i < nodes.length; i++) {
DOM.insertBefore(sibling, nodes[i]);
}
DOM.insertBefore(nodes[nodes.length - 1], sibling);
}
}
function moveChildNodes(source: Node, target: Node) {
var currChild = DOM.firstChild(source);
while (isPresent(currChild)) {
var nextChild = DOM.nextSibling(currChild);
DOM.appendChild(target, currChild);
currChild = nextChild;
}
}

View File

@ -1,75 +0,0 @@
import * as ldModule from './light_dom';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
class ContentStrategy {
nodes: List</*node*/ any>;
insert(nodes: List</*node*/ any>) {}
}
/**
* An implementation of the content tag that is used by transcluding components.
* It is used when the content tag is not a direct child of another component,
* and thus does not affect redistribution.
*/
class RenderedContent extends ContentStrategy {
beginScript: any;
endScript;
constructor(contentEl) {
super();
this.beginScript = contentEl;
this.endScript = DOM.nextSibling(this.beginScript);
this.nodes = [];
}
// Inserts the nodes in between the start and end scripts.
// Previous content is removed.
insert(nodes: List</*node*/ any>) {
this.nodes = nodes;
DOM.insertAllBefore(this.endScript, nodes);
this._removeNodesUntil(ListWrapper.isEmpty(nodes) ? this.endScript : nodes[0]);
}
_removeNodesUntil(node) {
var p = DOM.parentElement(this.beginScript);
for (var next = DOM.nextSibling(this.beginScript); next !== node;
next = DOM.nextSibling(this.beginScript)) {
DOM.removeChild(p, next);
}
}
}
/**
* An implementation of the content tag that is used by transcluding components.
* It is used when the content tag is a direct child of another component,
* and thus does not get rendered but only affect the distribution of its parent component.
*/
class IntermediateContent extends ContentStrategy {
constructor(public destinationLightDom: ldModule.LightDom) {
super();
this.nodes = [];
}
insert(nodes: List</*node*/ any>) {
this.nodes = nodes;
this.destinationLightDom.redistribute();
}
}
export class Content {
private _strategy: ContentStrategy = null;
constructor(public contentStartElement, public select: string) {}
init(destinationLightDom: ldModule.LightDom) {
this._strategy = isPresent(destinationLightDom) ? new IntermediateContent(destinationLightDom) :
new RenderedContent(this.contentStartElement);
}
nodes(): List</*node*/ any> { return this._strategy.nodes; }
insert(nodes: List</*node*/ any>) { this._strategy.insert(nodes); }
}

View File

@ -1,8 +1,5 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import * as viewModule from '../view/view';
import {LightDom} from './light_dom';
import {ShadowDomStrategy} from './shadow_dom_strategy';
import {insertSharedStyleText} from './util';
@ -20,12 +17,6 @@ export class EmulatedUnscopedShadowDomStrategy extends ShadowDomStrategy {
hasNativeContentElement(): boolean { return false; }
prepareShadowRoot(el): Node { return el; }
constructLightDom(lightDomView: viewModule.DomView, el): LightDom {
return new LightDom(lightDomView, el);
}
processStyleElement(hostComponentId: string, templateUrl: string, styleEl): void {
var cssText = DOM.getText(styleEl);
insertSharedStyleText(cssText, this.styleHost, styleEl);

View File

@ -1,141 +0,0 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import * as viewModule from '../view/view';
import * as elModule from '../view/element';
import {Content} from './content_tag';
export class DestinationLightDom {}
class _Root {
constructor(public node, public boundElement: elModule.DomElement) {}
}
// TODO: LightDom should implement DestinationLightDom
// once interfaces are supported
export class LightDom {
// The light DOM of the element is enclosed inside the lightDomView
lightDomView: viewModule.DomView;
// The shadow DOM
shadowDomView: viewModule.DomView = null;
// The nodes of the light DOM
nodes: List</*node*/ any>;
private _roots: List<_Root> = null;
constructor(lightDomView: viewModule.DomView, element) {
this.lightDomView = lightDomView;
this.nodes = DOM.childNodesAsList(element);
}
attachShadowDomView(shadowDomView: viewModule.DomView) { this.shadowDomView = shadowDomView; }
detachShadowDomView() { this.shadowDomView = null; }
redistribute() { redistributeNodes(this.contentTags(), this.expandedDomNodes()); }
contentTags(): List<Content> {
if (isPresent(this.shadowDomView)) {
return this._collectAllContentTags(this.shadowDomView, []);
} else {
return [];
}
}
// Collects the Content directives from the view and all its child views
private _collectAllContentTags(view: viewModule.DomView, acc: List<Content>): List<Content> {
// Note: exiting early here is important as we call this function for every view
// that is added, so we have O(n^2) runtime.
// TODO(tbosch): fix the root problem, see
// https://github.com/angular/angular/issues/2298
if (view.proto.transitiveContentTagCount === 0) {
return acc;
}
var els = view.boundElements;
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (isPresent(el.contentTag)) {
acc.push(el.contentTag);
}
if (isPresent(el.viewContainer)) {
ListWrapper.forEach(el.viewContainer.contentTagContainers(),
(view) => { this._collectAllContentTags(view, acc); });
}
}
return acc;
}
// Collects the nodes of the light DOM by merging:
// - nodes from enclosed ViewContainers,
// - nodes from enclosed content tags,
// - plain DOM nodes
expandedDomNodes(): List</*node*/ any> {
var res = [];
var roots = this._findRoots();
for (var i = 0; i < roots.length; ++i) {
var root = roots[i];
if (isPresent(root.boundElement)) {
var vc = root.boundElement.viewContainer;
var content = root.boundElement.contentTag;
if (isPresent(vc)) {
res = ListWrapper.concat(res, vc.nodes());
} else if (isPresent(content)) {
res = ListWrapper.concat(res, content.nodes());
} else {
res.push(root.node);
}
} else {
res.push(root.node);
}
}
return res;
}
// Returns a list of Roots for all the nodes of the light DOM.
// The Root object contains the DOM node and its corresponding boundElement
private _findRoots() {
if (isPresent(this._roots)) return this._roots;
var boundElements = this.lightDomView.boundElements;
this._roots = ListWrapper.map(this.nodes, (n) => {
var boundElement = null;
for (var i = 0; i < boundElements.length; i++) {
var boundEl = boundElements[i];
if (isPresent(boundEl) && boundEl.element === n) {
boundElement = boundEl;
break;
}
}
return new _Root(n, boundElement);
});
return this._roots;
}
}
// Projects the light DOM into the shadow DOM
function redistributeNodes(contents: List<Content>, nodes: List</*node*/ any>) {
for (var i = 0; i < contents.length; ++i) {
var content = contents[i];
var select = content.select;
// Empty selector is identical to <content/>
if (select.length === 0) {
content.insert(ListWrapper.clone(nodes));
ListWrapper.clear(nodes);
} else {
var matchSelector = (n) => DOM.elementMatches(n, select);
var matchingNodes = ListWrapper.filter(nodes, matchSelector);
content.insert(matchingNodes);
ListWrapper.removeAll(nodes, matchingNodes);
}
}
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (isPresent(node.parentNode)) {
DOM.remove(nodes[i]);
}
}
}

View File

@ -1,5 +1,4 @@
import {Injectable} from 'angular2/di';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ShadowDomStrategy} from './shadow_dom_strategy';
/**
@ -10,5 +9,5 @@ import {ShadowDomStrategy} from './shadow_dom_strategy';
*/
@Injectable()
export class NativeShadowDomStrategy extends ShadowDomStrategy {
prepareShadowRoot(el): Node { return DOM.createShadowRoot(el); }
hasNativeContentElement(): boolean { return true; }
}

View File

@ -1,5 +1,3 @@
import {isBlank, isPresent, assertionsEnabled, isPromise} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {CompileStep} from '../compiler/compile_step';
@ -15,8 +13,6 @@ export class ShadowDomCompileStep implements CompileStep {
var tagName = DOM.tagName(current.element).toUpperCase();
if (tagName == 'STYLE') {
this._processStyleElement(current, control);
} else if (tagName == 'CONTENT') {
this._processContentElement(current);
} else {
var componentId = current.isBound() ? current.inheritedElementBinder.componentId : null;
this._shadowDomStrategy.processElement(this._view.componentId, componentId, current.element);
@ -31,25 +27,4 @@ export class ShadowDomCompileStep implements CompileStep {
// bindings. Skipping further compiler steps allow speeding up the compilation process.
control.ignoreCurrentElement();
}
_processContentElement(current: CompileElement) {
if (this._shadowDomStrategy.hasNativeContentElement()) {
return;
}
var attrs = current.attrs();
var selector = attrs.get('select');
selector = isPresent(selector) ? selector : '';
var contentStart = DOM.createScriptTag('type', 'ng/contentStart');
if (assertionsEnabled()) {
DOM.setAttribute(contentStart, 'select', selector);
}
var contentEnd = DOM.createScriptTag('type', 'ng/contentEnd');
DOM.insertBefore(current.element, contentStart);
DOM.insertBefore(current.element, contentEnd);
DOM.remove(current.element);
current.element = contentStart;
current.bindElement().setContentTagSelector(selector);
}
}

View File

@ -1,17 +1,9 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import * as viewModule from '../view/view';
import {LightDom} from './light_dom';
export class ShadowDomStrategy {
// Whether the strategy understands the native <content> tag
hasNativeContentElement(): boolean { return true; }
// Prepares and returns the (emulated) shadow root for the given element.
prepareShadowRoot(el): any { return null; }
constructLightDom(lightDomView: viewModule.DomView, el): LightDom { return null; }
// An optional step that can modify the template style elements.
processStyleElement(hostComponentId: string, templateUrl: string, styleElement): void {}

View File

@ -1,13 +1,21 @@
import {StringWrapper, isPresent} from 'angular2/src/facade/lang';
import {StringWrapper, isPresent, isBlank} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {ListWrapper} from 'angular2/src/facade/collection';
import {DomProtoView} from './view/proto_view';
import {DomElementBinder} from './view/element_binder';
export const NG_BINDING_CLASS_SELECTOR = '.ng-binding';
export const NG_BINDING_CLASS = 'ng-binding';
export const EVENT_TARGET_SEPARATOR = ':';
export const NG_CONTENT_ELEMENT_NAME = 'ng-content';
export const NG_SHADOW_ROOT_ELEMENT_NAME = 'shadow-root';
var CAMEL_CASE_REGEXP = /([A-Z])/g;
var DASH_CASE_REGEXP = /-([a-z])/g;
export function camelCaseToDashCase(input: string): string {
return StringWrapper.replaceAllMapped(input, CAMEL_CASE_REGEXP,
(m) => { return '-' + m[1].toLowerCase(); });
@ -17,3 +25,101 @@ export function dashCaseToCamelCase(input: string): string {
return StringWrapper.replaceAllMapped(input, DASH_CASE_REGEXP,
(m) => { return m[1].toUpperCase(); });
}
// Attention: This is on the hot path, so don't use closures or default values!
export function queryBoundElements(templateContent: Node, isSingleElementChild: boolean):
Element[] {
var result;
var dynamicElementList;
var elementIdx = 0;
if (isSingleElementChild) {
var rootElement = DOM.firstChild(templateContent);
var rootHasBinding = DOM.hasClass(rootElement, NG_BINDING_CLASS);
dynamicElementList = DOM.getElementsByClassName(rootElement, NG_BINDING_CLASS);
result = ListWrapper.createFixedSize(dynamicElementList.length + (rootHasBinding ? 1 : 0));
if (rootHasBinding) {
result[elementIdx++] = rootElement;
}
} else {
dynamicElementList = DOM.querySelectorAll(templateContent, NG_BINDING_CLASS_SELECTOR);
result = ListWrapper.createFixedSize(dynamicElementList.length);
}
for (var i = 0; i < dynamicElementList.length; i++) {
result[elementIdx++] = dynamicElementList[i];
}
return result;
}
export class ClonedProtoView {
constructor(public original: DomProtoView, public fragments: Node[][],
public boundElements: Element[], public boundTextNodes: Node[]) {}
}
export function cloneAndQueryProtoView(pv: DomProtoView, importIntoDocument: boolean):
ClonedProtoView {
var templateContent = importIntoDocument ? DOM.importIntoDoc(DOM.content(pv.rootElement)) :
DOM.clone(DOM.content(pv.rootElement));
var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment);
var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements,
pv.elementBinders, pv.boundTextNodeCount);
var fragments = queryFragments(templateContent, pv.fragmentsRootNodeCount);
return new ClonedProtoView(pv, fragments, boundElements, boundTextNodes);
}
function queryFragments(templateContent: Node, fragmentsRootNodeCount: number[]): Node[][] {
var fragments = ListWrapper.createGrowableSize(fragmentsRootNodeCount.length);
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
var childNode = DOM.firstChild(templateContent);
for (var fragmentIndex = 0; fragmentIndex < fragments.length; fragmentIndex++) {
var fragment = ListWrapper.createFixedSize(fragmentsRootNodeCount[fragmentIndex]);
fragments[fragmentIndex] = fragment;
for (var i = 0; i < fragment.length; i++) {
fragment[i] = childNode;
childNode = DOM.nextSibling(childNode);
}
}
return fragments;
}
function queryBoundTextNodes(templateContent: Node, rootTextNodeIndices: number[],
boundElements: Element[], elementBinders: DomElementBinder[],
boundTextNodeCount: number): Node[] {
var boundTextNodes = ListWrapper.createFixedSize(boundTextNodeCount);
var textNodeIndex = 0;
if (rootTextNodeIndices.length > 0) {
var rootChildNodes = DOM.childNodes(templateContent);
for (var i = 0; i < rootTextNodeIndices.length; i++) {
boundTextNodes[textNodeIndex++] = rootChildNodes[rootTextNodeIndices[i]];
}
}
for (var i = 0; i < elementBinders.length; i++) {
var binder = elementBinders[i];
var element: Node = boundElements[i];
if (binder.textNodeIndices.length > 0) {
var childNodes = DOM.childNodes(element);
for (var j = 0; j < binder.textNodeIndices.length; j++) {
boundTextNodes[textNodeIndex++] = childNodes[binder.textNodeIndices[j]];
}
}
}
return boundTextNodes;
}
export function isElementWithTag(node: Node, elementName: string): boolean {
return DOM.isElementNode(node) && DOM.tagName(node).toLowerCase() == elementName.toLowerCase();
}
export function queryBoundTextNodeIndices(parentNode: Node, boundTextNodes: Map<Node, any>,
resultCallback: Function) {
var childNodes = DOM.childNodes(parentNode);
for (var j = 0; j < childNodes.length; j++) {
var node = childNodes[j];
if (boundTextNodes.has(node)) {
resultCallback(node, j, boundTextNodes.get(node));
}
}
}

View File

@ -1,11 +0,0 @@
import {ElementBinder} from './element_binder';
import {DomViewContainer} from './view_container';
import {LightDom} from '../shadow_dom/light_dom';
import {Content} from '../shadow_dom/content_tag';
export class DomElement {
viewContainer: DomViewContainer;
lightDom: LightDom;
constructor(public proto: ElementBinder, public element: any /* element */,
public contentTag: Content) {}
}

View File

@ -1,42 +1,29 @@
import {AST} from 'angular2/change_detection';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import * as protoViewModule from './proto_view';
export class ElementBinder {
contentTagSelector: string;
export class DomElementBinder {
textNodeIndices: List<number>;
nestedProtoView: protoViewModule.DomProtoView;
hasNestedProtoView: boolean;
eventLocals: AST;
localEvents: List<Event>;
globalEvents: List<Event>;
componentId: string;
parentIndex: number;
distanceToParent: number;
elementIsEmpty: boolean;
hasNativeShadowRoot: boolean;
constructor({textNodeIndices, contentTagSelector, nestedProtoView, componentId, eventLocals,
localEvents, globalEvents, parentIndex, distanceToParent, elementIsEmpty}: {
contentTagSelector?: string,
constructor({textNodeIndices, hasNestedProtoView, eventLocals, localEvents, globalEvents,
hasNativeShadowRoot}: {
textNodeIndices?: List<number>,
nestedProtoView?: protoViewModule.DomProtoView,
hasNestedProtoView?: boolean,
eventLocals?: AST,
localEvents?: List<Event>,
globalEvents?: List<Event>,
componentId?: string,
parentIndex?: number,
distanceToParent?: number,
elementIsEmpty?: boolean
hasNativeShadowRoot?: boolean
} = {}) {
this.textNodeIndices = textNodeIndices;
this.contentTagSelector = contentTagSelector;
this.nestedProtoView = nestedProtoView;
this.componentId = componentId;
this.hasNestedProtoView = hasNestedProtoView;
this.eventLocals = eventLocals;
this.localEvents = localEvents;
this.globalEvents = globalEvents;
this.parentIndex = parentIndex;
this.distanceToParent = distanceToParent;
this.elementIsEmpty = elementIsEmpty;
this.hasNativeShadowRoot = hasNativeShadowRoot;
}
}

View File

@ -0,0 +1,9 @@
import {RenderFragmentRef} from '../../api';
export function resolveInternalDomFragment(fragmentRef: RenderFragmentRef): Node[] {
return (<DomFragmentRef>fragmentRef)._nodes;
}
export class DomFragmentRef extends RenderFragmentRef {
constructor(public _nodes: Node[]) { super(); }
}

View File

@ -1,12 +1,10 @@
import {isPresent} from 'angular2/src/facade/lang';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isBlank} from 'angular2/src/facade/lang';
import {List, ListWrapper} from 'angular2/src/facade/collection';
import {ElementBinder} from './element_binder';
import {NG_BINDING_CLASS} from '../util';
import {DomElementBinder} from './element_binder';
import {RenderProtoViewRef, ViewType} from '../../api';
import {RenderProtoViewRef} from '../../api';
import {DOM} from 'angular2/src/dom/dom_adapter';
export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView {
return (<DomProtoViewRef>protoViewRef)._protoView;
@ -17,24 +15,40 @@ export class DomProtoViewRef extends RenderProtoViewRef {
}
export class DomProtoView {
element;
elementBinders: List<ElementBinder>;
isTemplateElement: boolean;
rootBindingOffset: number;
// the number of content tags seen in this or any child proto view.
transitiveContentTagCount: number;
boundTextNodeCount: number;
rootNodeCount: number;
constructor({elementBinders, element, transitiveContentTagCount, boundTextNodeCount}) {
this.element = element;
this.elementBinders = elementBinders;
this.transitiveContentTagCount = transitiveContentTagCount;
this.isTemplateElement = DOM.isTemplateElement(this.element);
this.rootBindingOffset =
(isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0;
this.boundTextNodeCount = boundTextNodeCount;
this.rootNodeCount =
this.isTemplateElement ? DOM.childNodes(DOM.content(this.element)).length : 1;
static create(type: ViewType, rootElement: Element, fragmentsRootNodeCount: number[],
rootTextNodeIndices: number[], elementBinders: List<DomElementBinder>,
mappedElementIndices: number[], mappedTextIndices: number[],
hostElementIndicesByViewIndex: number[]): DomProtoView {
var boundTextNodeCount = rootTextNodeIndices.length;
for (var i = 0; i < elementBinders.length; i++) {
boundTextNodeCount += elementBinders[i].textNodeIndices.length;
}
if (isBlank(mappedElementIndices)) {
mappedElementIndices = ListWrapper.createFixedSize(elementBinders.length);
for (var i = 0; i < mappedElementIndices.length; i++) {
mappedElementIndices[i] = i;
}
}
if (isBlank(mappedTextIndices)) {
mappedTextIndices = ListWrapper.createFixedSize(boundTextNodeCount);
for (var i = 0; i < mappedTextIndices.length; i++) {
mappedTextIndices[i] = i;
}
}
if (isBlank(hostElementIndicesByViewIndex)) {
hostElementIndicesByViewIndex = [null];
}
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,
mappedElementIndices, mappedTextIndices, hostElementIndicesByViewIndex);
}
constructor(public type: ViewType, public rootElement: Element,
public elementBinders: List<DomElementBinder>, public rootTextNodeIndices: number[],
public boundTextNodeCount: number, public fragmentsRootNodeCount: number[],
public isSingleElementFragment: boolean, public mappedElementIndices: number[],
public mappedTextIndices: number[], public hostElementIndicesByViewIndex: number[]) {}
}

View File

@ -19,17 +19,19 @@ import {
} from 'angular2/change_detection';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {ElementBinder, Event, HostAction} from './element_binder';
import {DomElementBinder, Event, HostAction} from './element_binder';
import * as api from '../../api';
import {NG_BINDING_CLASS, EVENT_TARGET_SEPARATOR} from '../util';
import {NG_BINDING_CLASS, EVENT_TARGET_SEPARATOR, queryBoundTextNodeIndices} from '../util';
export class ProtoViewBuilder {
variableBindings: Map<string, string> = new Map();
elements: List<ElementBinderBuilder> = [];
rootTextBindings: Map<Node, ASTWithSource> = new Map();
constructor(public rootElement, public type: api.ViewType) {}
constructor(public rootElement, public type: api.ViewType,
public useNativeShadowDom: boolean = false) {}
bindElement(element, description = null): ElementBinderBuilder {
var builder = new ElementBinderBuilder(this.elements.length, element, description);
@ -49,12 +51,22 @@ export class ProtoViewBuilder {
this.variableBindings.set(value, name);
}
// Note: We don't store the node index until the compilation is complete,
// as the compiler might change the order of elements.
bindRootText(textNode, expression) { this.rootTextBindings.set(textNode, expression); }
build(): api.ProtoViewDto {
var renderElementBinders = [];
var domElementBinders = [];
var apiElementBinders = [];
var transitiveContentTagCount = 0;
var boundTextNodeCount = 0;
var textNodeExpressions = [];
var rootTextNodeIndices = [];
queryBoundTextNodeIndices(DOM.content(this.rootElement), this.rootTextBindings,
(node, nodeIndex, expression) => {
textNodeExpressions.push(expression);
rootTextNodeIndices.push(nodeIndex);
});
ListWrapper.forEach(this.elements, (ebb: ElementBinderBuilder) => {
var directiveTemplatePropertyNames = new Set();
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (dbb: DirectiveBuilder) => {
@ -71,15 +83,12 @@ export class ProtoViewBuilder {
});
});
var nestedProtoView = isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
var nestedRenderProtoView =
isPresent(nestedProtoView) ? resolveInternalDomProtoView(nestedProtoView.render) : null;
if (isPresent(nestedRenderProtoView)) {
transitiveContentTagCount += nestedRenderProtoView.transitiveContentTagCount;
}
if (isPresent(ebb.contentTagSelector)) {
transitiveContentTagCount++;
}
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
var textNodeIndices = [];
queryBoundTextNodeIndices(ebb.element, ebb.textBindings, (node, nodeIndex, expression) => {
textNodeExpressions.push(expression);
textNodeIndices.push(nodeIndex);
});
apiElementBinders.push(new api.ElementBinder({
index: ebb.index,
parentIndex: parentIndex,
@ -91,64 +100,28 @@ export class ProtoViewBuilder {
ebb.propertyBindings, directiveTemplatePropertyNames),
variableBindings: ebb.variableBindings,
eventBindings: ebb.eventBindings,
textBindings: ebb.textBindings,
readAttributes: ebb.readAttributes
}));
var childNodeInfo = this._analyzeChildNodes(ebb.element, ebb.textBindingNodes);
boundTextNodeCount += ebb.textBindingNodes.length;
renderElementBinders.push(new ElementBinder({
textNodeIndices: childNodeInfo.boundTextNodeIndices,
contentTagSelector: ebb.contentTagSelector,
parentIndex: parentIndex,
distanceToParent: ebb.distanceToParent,
nestedProtoView:
isPresent(nestedProtoView) ? resolveInternalDomProtoView(nestedProtoView.render) : null,
componentId: ebb.componentId,
domElementBinders.push(new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: isPresent(nestedProtoView) || isPresent(ebb.componentId),
hasNativeShadowRoot: isPresent(ebb.componentId) && this.useNativeShadowDom,
eventLocals: new LiteralArray(ebb.eventBuilder.buildEventLocals()),
localEvents: ebb.eventBuilder.buildLocalEvents(),
globalEvents: ebb.eventBuilder.buildGlobalEvents(),
elementIsEmpty: childNodeInfo.elementIsEmpty
globalEvents: ebb.eventBuilder.buildGlobalEvents()
}));
});
var rootNodeCount = DOM.childNodes(DOM.content(this.rootElement)).length;
return new api.ProtoViewDto({
render: new DomProtoViewRef(new DomProtoView({
element: this.rootElement,
elementBinders: renderElementBinders,
transitiveContentTagCount: transitiveContentTagCount,
boundTextNodeCount: boundTextNodeCount
})),
render: new DomProtoViewRef(DomProtoView.create(this.type, this.rootElement, [rootNodeCount],
rootTextNodeIndices, domElementBinders, null,
null, null)),
type: this.type,
elementBinders: apiElementBinders,
variableBindings: this.variableBindings
variableBindings: this.variableBindings,
textBindings: textNodeExpressions
});
}
// Note: We need to calculate the next node indices not until the compilation is complete,
// as the compiler might change the order of elements.
private _analyzeChildNodes(parentElement: /*element*/ any,
boundTextNodes: List</*node*/ any>): _ChildNodesInfo {
var childNodes = DOM.childNodes(DOM.templateAwareRoot(parentElement));
var boundTextNodeIndices = [];
var indexInBoundTextNodes = 0;
var elementIsEmpty = true;
for (var i = 0; i < childNodes.length; i++) {
var node = childNodes[i];
if (indexInBoundTextNodes < boundTextNodes.length &&
node === boundTextNodes[indexInBoundTextNodes]) {
boundTextNodeIndices.push(i);
indexInBoundTextNodes++;
elementIsEmpty = false;
} else if ((DOM.isTextNode(node) && DOM.getText(node).trim().length > 0) ||
(DOM.isElementNode(node))) {
elementIsEmpty = false;
}
}
return new _ChildNodesInfo(boundTextNodeIndices, elementIsEmpty);
}
}
class _ChildNodesInfo {
constructor(public boundTextNodeIndices: List<number>, public elementIsEmpty: boolean) {}
}
export class ElementBinderBuilder {
@ -161,9 +134,7 @@ export class ElementBinderBuilder {
propertyBindingsToDirectives: Set<string> = new Set();
eventBindings: List<api.EventBinding> = [];
eventBuilder: EventBuilder = new EventBuilder();
textBindingNodes: List</*node*/ any> = [];
textBindings: List<ASTWithSource> = [];
contentTagSelector: string = null;
textBindings: Map<Node, ASTWithSource> = new Map();
readAttributes: Map<string, string> = new Map();
componentId: string = null;
@ -229,12 +200,9 @@ export class ElementBinderBuilder {
this.eventBindings.push(this.eventBuilder.add(name, expression, target));
}
bindText(textNode, expression) {
this.textBindingNodes.push(textNode);
this.textBindings.push(expression);
}
setContentTagSelector(value: string) { this.contentTagSelector = value; }
// Note: We don't store the node index until the compilation is complete,
// as the compiler might change the order of elements.
bindText(textNode, expression) { this.textBindings.set(textNode, expression); }
setComponentId(componentId: string) { this.componentId = componentId; }
}

View File

@ -0,0 +1,451 @@
import {DOM} from 'angular2/src/dom/dom_adapter';
import {isPresent, isBlank, BaseException, isArray} from 'angular2/src/facade/lang';
import {DomProtoView, DomProtoViewRef, resolveInternalDomProtoView} from './proto_view';
import {DomElementBinder} from './element_binder';
import {RenderProtoViewMergeMapping, RenderProtoViewRef, ViewType} from '../../api';
import {
NG_BINDING_CLASS,
NG_CONTENT_ELEMENT_NAME,
ClonedProtoView,
cloneAndQueryProtoView,
queryBoundElements,
queryBoundTextNodeIndices,
NG_SHADOW_ROOT_ELEMENT_NAME,
isElementWithTag
} from '../util';
import {CssSelector} from '../compiler/selector';
const NOT_MATCHABLE_SELECTOR = '_not-matchable_';
export function mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRef | List<any>>):
RenderProtoViewMergeMapping[] {
var target = [];
_mergeProtoViewsRecursively(protoViewRefs, target);
return target;
}
function _mergeProtoViewsRecursively(protoViewRefs: List<RenderProtoViewRef | List<any>>,
target: RenderProtoViewMergeMapping[]): RenderProtoViewRef {
var targetIndex = target.length;
target.push(null);
var resolvedProtoViewRefs = protoViewRefs.map((entry) => {
if (isArray(entry)) {
return _mergeProtoViewsRecursively(<List<any>>entry, target);
} else {
return entry;
}
});
var mapping = mergeProtoViews(resolvedProtoViewRefs);
target[targetIndex] = mapping;
return mapping.mergedProtoViewRef;
}
export function mergeProtoViews(protoViewRefs: RenderProtoViewRef[]): RenderProtoViewMergeMapping {
var hostProtoView = resolveInternalDomProtoView(protoViewRefs[0]);
var mergeableProtoViews: DomProtoView[] = [];
var hostElementIndices: number[] = [];
mergeableProtoViews.push(hostProtoView);
var protoViewIdx = 1;
for (var i = 0; i < hostProtoView.elementBinders.length; i++) {
var binder = hostProtoView.elementBinders[i];
if (binder.hasNestedProtoView) {
var nestedProtoViewRef = protoViewRefs[protoViewIdx++];
if (isPresent(nestedProtoViewRef)) {
mergeableProtoViews.push(resolveInternalDomProtoView(nestedProtoViewRef));
hostElementIndices.push(i);
}
}
}
return _mergeProtoViews(mergeableProtoViews, hostElementIndices);
}
function _mergeProtoViews(mergeableProtoViews: DomProtoView[], hostElementIndices: number[]):
RenderProtoViewMergeMapping {
var clonedProtoViews: ClonedProtoView[] =
mergeableProtoViews.map(domProtoView => cloneAndQueryProtoView(domProtoView, false));
var hostProtoView: ClonedProtoView = clonedProtoViews[0];
// modify the DOM
mergeDom(clonedProtoViews, hostElementIndices);
// create a new root element with the changed fragments and elements
var rootElement = createRootElementFromFragments(hostProtoView.fragments);
var fragmentsRootNodeCount = hostProtoView.fragments.map(fragment => fragment.length);
var rootNode = DOM.content(rootElement);
// read out the new element / text node / ElementBinder order
var mergedBoundElements = queryBoundElements(rootNode, false);
var mergedBoundTextIndices: Map<Node, number> = new Map();
var boundTextNodeMap: Map<Node, any> = indexBoundTextNodes(clonedProtoViews);
var rootTextNodeIndices =
calcRootTextNodeIndices(rootNode, boundTextNodeMap, mergedBoundTextIndices);
var mergedElementBinders = calcElementBinders(clonedProtoViews, mergedBoundElements,
boundTextNodeMap, mergedBoundTextIndices);
// create element / text index mappings
var mappedElementIndices = calcMappedElementIndices(clonedProtoViews, mergedBoundElements);
var mappedTextIndices = calcMappedTextIndices(clonedProtoViews, mergedBoundTextIndices);
var hostElementIndicesByViewIndex =
calcHostElementIndicesByViewIndex(clonedProtoViews, hostElementIndices);
// create result
var mergedProtoView = DomProtoView.create(
hostProtoView.original.type, rootElement, fragmentsRootNodeCount, rootTextNodeIndices,
mergedElementBinders, mappedElementIndices, mappedTextIndices, hostElementIndicesByViewIndex);
return new RenderProtoViewMergeMapping(new DomProtoViewRef(mergedProtoView),
fragmentsRootNodeCount.length, mappedElementIndices,
mappedTextIndices, hostElementIndicesByViewIndex);
}
function indexBoundTextNodes(mergableProtoViews: ClonedProtoView[]): Map<Node, any> {
var boundTextNodeMap = new Map();
for (var pvIndex = 0; pvIndex < mergableProtoViews.length; pvIndex++) {
var mergableProtoView = mergableProtoViews[pvIndex];
mergableProtoView.boundTextNodes.forEach(
(textNode) => { boundTextNodeMap.set(textNode, null); });
}
return boundTextNodeMap;
}
function mergeDom(clonedProtoViews: ClonedProtoView[], hostElementIndices: number[]) {
var nestedProtoViewByHostElement: Map<Element, ClonedProtoView> =
indexProtoViewsByHostElement(clonedProtoViews, hostElementIndices);
var hostProtoView = clonedProtoViews[0];
var mergableProtoViewIdx = 1;
hostElementIndices.forEach((boundElementIndex) => {
var binder = hostProtoView.original.elementBinders[boundElementIndex];
if (binder.hasNestedProtoView) {
var mergableNestedProtoView: ClonedProtoView = clonedProtoViews[mergableProtoViewIdx++];
if (mergableNestedProtoView.original.type === ViewType.COMPONENT) {
mergeComponentDom(hostProtoView, boundElementIndex, mergableNestedProtoView,
nestedProtoViewByHostElement);
} else {
mergeEmbeddedDom(hostProtoView, mergableNestedProtoView);
}
}
});
}
function indexProtoViewsByHostElement(mergableProtoViews: ClonedProtoView[],
hostElementIndices: number[]): Map<Element, ClonedProtoView> {
var hostProtoView = mergableProtoViews[0];
var mergableProtoViewIdx = 1;
var nestedProtoViewByHostElement: Map<Element, ClonedProtoView> = new Map();
hostElementIndices.forEach((hostElementIndex) => {
nestedProtoViewByHostElement.set(hostProtoView.boundElements[hostElementIndex],
mergableProtoViews[mergableProtoViewIdx++]);
});
return nestedProtoViewByHostElement;
}
function mergeComponentDom(hostProtoView: ClonedProtoView, boundElementIndex: number,
nestedProtoView: ClonedProtoView,
nestedProtoViewByHostElement: Map<Element, ClonedProtoView>) {
var hostElement = hostProtoView.boundElements[boundElementIndex];
// We wrap the fragments into elements so that we can expand <ng-content>
// even for root nodes in the fragment without special casing them.
var fragmentElements = mapFragmentsIntoElements(nestedProtoView.fragments);
var contentElements = findContentElements(fragmentElements);
var projectableNodes = DOM.childNodesAsList(hostElement);
for (var i = 0; i < contentElements.length; i++) {
var contentElement = contentElements[i];
var select = DOM.getAttribute(contentElement, 'select');
projectableNodes = projectMatchingNodes(select, contentElement, projectableNodes);
}
// unwrap the fragment elements into arrays of nodes after projecting
var fragments = extractFragmentNodesFromElements(fragmentElements);
appendComponentNodesToHost(hostProtoView, boundElementIndex, fragments[0]);
for (var i = 1; i < fragments.length; i++) {
hostProtoView.fragments.push(fragments[i]);
}
}
function mapFragmentsIntoElements(fragments: Node[][]): Element[] {
return fragments.map((fragment) => {
var fragmentElement = DOM.createTemplate('');
fragment.forEach(node => DOM.appendChild(DOM.content(fragmentElement), node));
return fragmentElement;
});
}
function extractFragmentNodesFromElements(fragmentElements: Element[]): Node[][] {
return fragmentElements.map(
(fragmentElement) => { return DOM.childNodesAsList(DOM.content(fragmentElement)); });
}
function findContentElements(fragmentElements: Element[]): Element[] {
var contentElements = [];
fragmentElements.forEach((fragmentElement: Element) => {
var fragmentContentElements =
DOM.querySelectorAll(DOM.content(fragmentElement), NG_CONTENT_ELEMENT_NAME);
for (var i = 0; i < fragmentContentElements.length; i++) {
contentElements.push(fragmentContentElements[i]);
}
});
return sortContentElements(contentElements);
}
function appendComponentNodesToHost(hostProtoView: ClonedProtoView, boundElementIndex: number,
componentRootNodes: Node[]) {
var hostElement = hostProtoView.boundElements[boundElementIndex];
var binder = hostProtoView.original.elementBinders[boundElementIndex];
if (binder.hasNativeShadowRoot) {
var shadowRootWrapper = DOM.createElement(NG_SHADOW_ROOT_ELEMENT_NAME);
for (var i = 0; i < componentRootNodes.length; i++) {
DOM.appendChild(shadowRootWrapper, componentRootNodes[i]);
}
var firstChild = DOM.firstChild(hostElement);
if (isPresent(firstChild)) {
DOM.insertBefore(firstChild, shadowRootWrapper);
} else {
DOM.appendChild(hostElement, shadowRootWrapper);
}
} else {
DOM.clearNodes(hostElement);
for (var i = 0; i < componentRootNodes.length; i++) {
DOM.appendChild(hostElement, componentRootNodes[i]);
}
}
}
function mergeEmbeddedDom(parentProtoView: ClonedProtoView, nestedProtoView: ClonedProtoView) {
nestedProtoView.fragments.forEach((fragment) => parentProtoView.fragments.push(fragment));
}
function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] {
var remaining = [];
var removeContentElement = true;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (isWildcard(selector)) {
DOM.insertBefore(contentElement, node);
} else if (DOM.isElementNode(node)) {
if (isElementWithTag(node, NG_CONTENT_ELEMENT_NAME)) {
// keep the projected content as other <ng-content> elements
// might want to use it as well.
remaining.push(node);
DOM.setAttribute(contentElement, 'select',
mergeSelectors(selector, DOM.getAttribute(node, 'select')));
removeContentElement = false;
} else {
if (DOM.elementMatches(node, selector)) {
DOM.insertBefore(contentElement, node);
} else {
remaining.push(node);
}
}
} else {
// non projected text nodes
remaining.push(node);
}
}
if (removeContentElement) {
DOM.remove(contentElement);
}
return remaining;
}
function isWildcard(selector): boolean {
return isBlank(selector) || selector.length === 0 || selector == '*';
}
export function mergeSelectors(selector1: string, selector2: string): string {
if (isWildcard(selector1)) {
return isBlank(selector2) ? '' : selector2;
} else if (isWildcard(selector2)) {
return isBlank(selector1) ? '' : selector1;
} else {
var sels1 = CssSelector.parse(selector1);
var sels2 = CssSelector.parse(selector2);
if (sels1.length > 1 || sels2.length > 1) {
throw new BaseException('multiple selectors are not supported in ng-content');
}
var sel1 = sels1[0];
var sel2 = sels2[0];
if (sel1.notSelectors.length > 0 || sel2.notSelectors.length > 0) {
throw new BaseException(':not selector is not supported in ng-content');
}
var merged = new CssSelector();
if (isBlank(sel1.element)) {
merged.setElement(sel2.element);
} else if (isBlank(sel2.element)) {
merged.setElement(sel1.element);
} else {
return NOT_MATCHABLE_SELECTOR;
}
merged.attrs = sel1.attrs.concat(sel2.attrs);
merged.classNames = sel1.classNames.concat(sel2.classNames);
return merged.toString();
}
}
// we need to sort content elements as they can originate from
// different sub views
function sortContentElements(contentElements: Element[]): Element[] {
// for now, only move the wildcard selector to the end.
// TODO(tbosch): think about sorting by selector specifity...
var firstWildcard = null;
var sorted = [];
contentElements.forEach((contentElement) => {
var select = DOM.getAttribute(contentElement, 'select');
if (isWildcard(select)) {
if (isBlank(firstWildcard)) {
firstWildcard = contentElement;
}
} else {
sorted.push(contentElement);
}
});
if (isPresent(firstWildcard)) {
sorted.push(firstWildcard);
}
return sorted;
}
function createRootElementFromFragments(fragments: Node[][]): Element {
var rootElement = DOM.createTemplate('');
var rootNode = DOM.content(rootElement);
fragments.forEach(
(fragment) => { fragment.forEach((node) => { DOM.appendChild(rootNode, node); }); });
return rootElement;
}
function calcRootTextNodeIndices(rootNode: Node, boundTextNodes: Map<Node, any>,
targetBoundTextIndices: Map<Node, number>): number[] {
var rootTextNodeIndices = [];
queryBoundTextNodeIndices(rootNode, boundTextNodes, (textNode, nodeIndex, _) => {
rootTextNodeIndices.push(nodeIndex);
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
});
return rootTextNodeIndices;
}
function calcElementBinders(clonedProtoViews: ClonedProtoView[], mergedBoundElements: Element[],
boundTextNodes: Map<Node, any>,
targetBoundTextIndices: Map<Node, number>): DomElementBinder[] {
var elementBinderByElement: Map<Element, DomElementBinder> =
indexElementBindersByElement(clonedProtoViews);
var mergedElementBinders = [];
for (var i = 0; i < mergedBoundElements.length; i++) {
var element = mergedBoundElements[i];
var textNodeIndices = [];
queryBoundTextNodeIndices(element, boundTextNodes, (textNode, nodeIndex, _) => {
textNodeIndices.push(nodeIndex);
targetBoundTextIndices.set(textNode, targetBoundTextIndices.size);
});
mergedElementBinders.push(
updateElementBinderTextNodeIndices(elementBinderByElement.get(element), textNodeIndices));
}
return mergedElementBinders;
}
function indexElementBindersByElement(mergableProtoViews: ClonedProtoView[]):
Map<Element, DomElementBinder> {
var elementBinderByElement = new Map();
mergableProtoViews.forEach((mergableProtoView) => {
for (var i = 0; i < mergableProtoView.boundElements.length; i++) {
var el = mergableProtoView.boundElements[i];
if (isPresent(el)) {
elementBinderByElement.set(el, mergableProtoView.original.elementBinders[i]);
}
}
});
return elementBinderByElement;
}
function updateElementBinderTextNodeIndices(elementBinder: DomElementBinder,
textNodeIndices: number[]): DomElementBinder {
var result;
if (isBlank(elementBinder)) {
result = new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: false,
eventLocals: null,
localEvents: [],
globalEvents: [],
hasNativeShadowRoot: null
});
} else {
result = new DomElementBinder({
textNodeIndices: textNodeIndices,
hasNestedProtoView: false,
eventLocals: elementBinder.eventLocals,
localEvents: elementBinder.localEvents,
globalEvents: elementBinder.globalEvents,
hasNativeShadowRoot: elementBinder.hasNativeShadowRoot
});
}
return result;
}
function calcMappedElementIndices(clonedProtoViews: ClonedProtoView[],
mergedBoundElements: Element[]): number[] {
var mergedBoundElementIndices: Map<Element, number> = indexArray(mergedBoundElements);
var mappedElementIndices = [];
clonedProtoViews.forEach((clonedProtoView) => {
clonedProtoView.original.mappedElementIndices.forEach((boundElementIndex) => {
var mappedElementIndex = null;
if (isPresent(boundElementIndex)) {
var boundElement = clonedProtoView.boundElements[boundElementIndex];
mappedElementIndex = mergedBoundElementIndices.get(boundElement);
}
mappedElementIndices.push(mappedElementIndex);
});
});
return mappedElementIndices;
}
function calcMappedTextIndices(clonedProtoViews: ClonedProtoView[],
mergedBoundTextIndices: Map<Node, number>): number[] {
var mappedTextIndices = [];
clonedProtoViews.forEach((clonedProtoView) => {
clonedProtoView.original.mappedTextIndices.forEach((textNodeIndex) => {
var mappedTextIndex = null;
if (isPresent(textNodeIndex)) {
var textNode = clonedProtoView.boundTextNodes[textNodeIndex];
mappedTextIndex = mergedBoundTextIndices.get(textNode);
}
mappedTextIndices.push(mappedTextIndex);
});
});
return mappedTextIndices;
}
function calcHostElementIndicesByViewIndex(clonedProtoViews: ClonedProtoView[],
hostElementIndices: number[]): number[] {
var mergedElementCount = 0;
var hostElementIndicesByViewIndex = [];
for (var i = 0; i < clonedProtoViews.length; i++) {
var clonedProtoView = clonedProtoViews[i];
clonedProtoView.original.hostElementIndicesByViewIndex.forEach((hostElementIndex) => {
var mappedHostElementIndex;
if (isBlank(hostElementIndex)) {
mappedHostElementIndex = i > 0 ? hostElementIndices[i - 1] : null;
} else {
mappedHostElementIndex = hostElementIndex + mergedElementCount;
}
hostElementIndicesByViewIndex.push(mappedHostElementIndex);
});
mergedElementCount += clonedProtoView.original.mappedElementIndices.length;
}
return hostElementIndicesByViewIndex;
}
function indexArray(arr: any[]): Map<any, number> {
var map = new Map();
for (var i = 0; i < arr.length; i++) {
map.set(arr[i], i);
}
return map;
}

View File

@ -3,10 +3,8 @@ import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src
import {isPresent, isBlank, BaseException, stringify} from 'angular2/src/facade/lang';
import {DomProtoView} from './proto_view';
import {LightDom} from '../shadow_dom/light_dom';
import {DomElement} from './element';
import {RenderViewRef, EventDispatcher} from '../../api';
import {RenderViewRef, RenderEventDispatcher} from '../../api';
import {camelCaseToDashCase} from '../util';
export function resolveInternalDomView(viewRef: RenderViewRef): DomView {
@ -21,30 +19,19 @@ export class DomViewRef extends RenderViewRef {
* Const of making objects: http://jsperf.com/instantiate-size-of-object
*/
export class DomView {
hostLightDom: LightDom = null;
shadowRoot = null;
hydrated: boolean = false;
eventDispatcher: EventDispatcher = null;
eventDispatcher: RenderEventDispatcher = null;
eventHandlerRemovers: List<Function> = [];
constructor(public proto: DomProtoView, public rootNodes: List</*node*/ any>,
public boundTextNodes: List</*node*/ any>, public boundElements: List<DomElement>) {}
getDirectParentElement(boundElementIndex: number): DomElement {
var binder = this.proto.elementBinders[boundElementIndex];
var parent = null;
if (binder.parentIndex !== -1 && binder.distanceToParent === 1) {
parent = this.boundElements[binder.parentIndex];
}
return parent;
}
constructor(public proto: DomProtoView, public boundTextNodes: List<Node>,
public boundElements: Element[]) {}
setElementProperty(elementIndex: number, propertyName: string, value: any) {
DOM.setProperty(this.boundElements[elementIndex].element, propertyName, value);
DOM.setProperty(this.boundElements[elementIndex], propertyName, value);
}
setElementAttribute(elementIndex: number, attributeName: string, value: string) {
var element = this.boundElements[elementIndex].element;
var element = this.boundElements[elementIndex];
var dashCasedAttributeName = camelCaseToDashCase(attributeName);
if (isPresent(value)) {
DOM.setAttribute(element, dashCasedAttributeName, stringify(value));
@ -54,7 +41,7 @@ export class DomView {
}
setElementClass(elementIndex: number, className: string, isAdd: boolean) {
var element = this.boundElements[elementIndex].element;
var element = this.boundElements[elementIndex];
var dashCasedClassName = camelCaseToDashCase(className);
if (isAdd) {
DOM.addClass(element, dashCasedClassName);
@ -64,7 +51,7 @@ export class DomView {
}
setElementStyle(elementIndex: number, styleName: string, value: string) {
var element = this.boundElements[elementIndex].element;
var element = this.boundElements[elementIndex];
var dashCasedStyleName = camelCaseToDashCase(styleName);
if (isPresent(value)) {
DOM.setStyle(element, dashCasedStyleName, stringify(value));
@ -74,7 +61,7 @@ export class DomView {
}
invokeElementMethod(elementIndex: number, methodName: string, args: List<any>) {
var element = this.boundElements[elementIndex].element;
var element = this.boundElements[elementIndex];
DOM.invoke(element, methodName, args);
}
@ -91,7 +78,7 @@ export class DomView {
// Locals(null, evalLocals));
// this.eventDispatcher.dispatchEvent(elementIndex, eventName, localValues);
allowDefaultBehavior =
this.eventDispatcher.dispatchEvent(elementIndex, eventName, evalLocals);
this.eventDispatcher.dispatchRenderEvent(elementIndex, eventName, evalLocals);
if (!allowDefaultBehavior) {
event.preventDefault();
}

View File

@ -5,14 +5,4 @@ import * as viewModule from './view';
export class DomViewContainer {
// The order in this list matches the DOM order.
views: List<viewModule.DomView> = [];
contentTagContainers(): List<viewModule.DomView> { return this.views; }
nodes(): List</*node*/ any> {
var r = [];
for (var i = 0; i < this.views.length; ++i) {
r = ListWrapper.concat(r, this.views[i].rootNodes);
}
return r;
}
}