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
+70 -33
View File
@@ -48,11 +48,10 @@ export class ElementBinder {
// that replaced the values that should be extracted from the element
// with a local name
eventBindings: List<EventBinding>;
textBindings: List<ASTWithSource>;
readAttributes: Map<string, string>;
constructor({index, parentIndex, distanceToParent, directives, nestedProtoView, propertyBindings,
variableBindings, eventBindings, textBindings, readAttributes}: {
variableBindings, eventBindings, readAttributes}: {
index?: number,
parentIndex?: number,
distanceToParent?: number,
@@ -61,7 +60,6 @@ export class ElementBinder {
propertyBindings?: List<ElementPropertyBinding>,
variableBindings?: Map<string, string>,
eventBindings?: List<EventBinding>,
textBindings?: List<ASTWithSource>,
readAttributes?: Map<string, string>
} = {}) {
this.index = index;
@@ -72,7 +70,6 @@ export class ElementBinder {
this.propertyBindings = propertyBindings;
this.variableBindings = variableBindings;
this.eventBindings = eventBindings;
this.textBindings = textBindings;
this.readAttributes = readAttributes;
}
}
@@ -116,17 +113,20 @@ export class ProtoViewDto {
elementBinders: List<ElementBinder>;
variableBindings: Map<string, string>;
type: ViewType;
textBindings: List<ASTWithSource>;
constructor({render, elementBinders, variableBindings, type}: {
constructor({render, elementBinders, variableBindings, type, textBindings}: {
render?: RenderProtoViewRef,
elementBinders?: List<ElementBinder>,
variableBindings?: Map<string, string>,
type?: ViewType
type?: ViewType,
textBindings?: List<ASTWithSource>
}) {
this.render = render;
this.elementBinders = elementBinders;
this.variableBindings = variableBindings;
this.type = type;
this.textBindings = textBindings;
}
}
@@ -260,10 +260,13 @@ export class DirectiveMetadata {
}
}
// An opaque reference to a DomProtoView
// An opaque reference to a render proto ivew
export class RenderProtoViewRef {}
// An opaque reference to a DomView
// An opaque reference to a part of a view
export class RenderFragmentRef {}
// An opaque reference to a view
export class RenderViewRef {}
export class ViewDefinition {
@@ -291,6 +294,23 @@ export class ViewDefinition {
}
}
export class RenderProtoViewMergeMapping {
constructor(public mergedProtoViewRef: RenderProtoViewRef,
// Number of fragments in the merged ProtoView.
// Fragments are stored in depth first order of nested ProtoViews.
public fragmentCount: number,
// Mapping from app element index to render element index.
// Mappings of nested ProtoViews are in depth first order, with all
// indices for one ProtoView in a consecuitve block.
public mappedElementIndices: number[],
// Mapping from app text index to render text index.
// Mappings of nested ProtoViews are in depth first order, with all
// indices for one ProtoView in a consecuitve block.
public mappedTextIndices: number[],
// Mapping from view index to app element index
public hostElementIndicesByViewIndex: number[]) {}
}
export class RenderCompiler {
/**
* Creats a ProtoViewDto that contains a single nested component with the given componentId.
@@ -303,6 +323,24 @@ export class RenderCompiler {
* but only the needed ones based on previous calls.
*/
compile(view: ViewDefinition): Promise<ProtoViewDto> { return null; }
/**
* Merges ProtoViews.
* The first entry of the array is the protoview into which all the other entries of the array
* should be merged.
* If the array contains other arrays, they will be merged before processing the parent array.
* The array must contain an entry for every component and embedded ProtoView of the first entry.
* @param protoViewRefs List of ProtoViewRefs or nested
* @return the merge result for every input array in depth first order.
*/
mergeProtoViewsRecursively(
protoViewRefs: List<RenderProtoViewRef | List<any>>): Promise<RenderProtoViewMergeMapping[]> {
return null;
}
}
export class RenderViewWithFragments {
constructor(public viewRef: RenderViewRef, public fragmentRefs: RenderFragmentRef[]) {}
}
/**
@@ -315,33 +353,39 @@ export interface RenderElementRef {
* Reference to the {@link RenderViewRef} where the `RenderElementRef` is inside of.
*/
renderView: RenderViewRef;
/**
* Index of the element inside the {@link ViewRef}.
* Index of the element inside the {@link RenderViewRef}.
*
* This is used internally by the Angular framework to locate elements.
*/
boundElementIndex: number;
renderBoundElementIndex: number;
}
export class Renderer {
/**
* Creates a root host view that includes the given element.
* Note that the fragmentCount needs to be passed in so that we can create a result
* synchronously even when dealing with webworkers!
*
* @param {RenderProtoViewRef} hostProtoViewRef a RenderProtoViewRef of type
* ProtoViewDto.HOST_VIEW_TYPE
* @param {any} hostElementSelector css selector for the host element (will be queried against the
* main document)
* @return {RenderViewRef} the created view
* @return {RenderViewWithFragments} the created view including fragments
*/
createRootHostView(hostProtoViewRef: RenderProtoViewRef,
hostElementSelector: string): RenderViewRef {
createRootHostView(hostProtoViewRef: RenderProtoViewRef, fragmentCount: number,
hostElementSelector: string): RenderViewWithFragments {
return null;
}
/**
* Creates a regular view out of the given ProtoView
* Creates a regular view out of the given ProtoView.
* Note that the fragmentCount needs to be passed in so that we can create a result
* synchronously even when dealing with webworkers!
*/
createView(protoViewRef: RenderProtoViewRef): RenderViewRef { return null; }
createView(protoViewRef: RenderProtoViewRef, fragmentCount: number): RenderViewWithFragments {
return null;
}
/**
* Destroys the given view after it has been dehydrated and detached
@@ -349,27 +393,20 @@ export class Renderer {
destroyView(viewRef: RenderViewRef) {}
/**
* Attaches a componentView into the given hostView at the given element
* Attaches a fragment after another fragment.
*/
attachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) {}
attachFragmentAfterFragment(previousFragmentRef: RenderFragmentRef,
fragmentRef: RenderFragmentRef) {}
/**
* Detaches a componentView into the given hostView at the given element
* Attaches a fragment after an element.
*/
detachComponentView(location: RenderElementRef, componentViewRef: RenderViewRef) {}
attachFragmentAfterElement(elementRef: RenderElementRef, fragmentRef: RenderFragmentRef) {}
/**
* Attaches a view into a ViewContainer (in the given parentView at the given element) at the
* given index.
* Detaches a fragment.
*/
attachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) {}
/**
* Detaches a view into a ViewContainer (in the given parentView at the given element) at the
* given index.
*/
// TODO(tbosch): this should return a promise as it can be animated!
detachViewInContainer(location: RenderElementRef, atIndex: number, viewRef: RenderViewRef) {}
detachFragment(fragmentRef: RenderFragmentRef) {}
/**
* Hydrates a view after it has been attached. Hydration/dehydration is used for reusing views
@@ -422,18 +459,18 @@ export class Renderer {
/**
* Sets the dispatcher for all events of the given view
*/
setEventDispatcher(viewRef: RenderViewRef, dispatcher: EventDispatcher) {}
setEventDispatcher(viewRef: RenderViewRef, dispatcher: RenderEventDispatcher) {}
}
/**
* A dispatcher for all events happening in a view.
*/
export interface EventDispatcher {
export interface RenderEventDispatcher {
/**
* Called when an event was triggered for a on-* attribute on an element.
* @param {Map<string, any>} locals Locals to be used to evaluate the
* event expressions
*/
dispatchEvent(elementIndex: number, eventName: string, locals: Map<string, any>);
dispatchRenderEvent(elementIndex: number, eventName: string, locals: Map<string, any>);
}
@@ -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;
@@ -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());
}
}
@@ -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) {
@@ -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);
}
}
}
}
@@ -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);
+113 -217
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;
}
}
@@ -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); }
}
@@ -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);
@@ -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]);
}
}
}
@@ -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; }
}
@@ -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);
}
}
@@ -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 {}
+107 -1
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));
}
}
}
@@ -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) {}
}
@@ -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;
}
}
@@ -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(); }
}
@@ -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[]) {}
}
@@ -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; }
}
@@ -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;
}
+10 -23
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();
}
@@ -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;
}
}