Tobias Bosch b1df54501a 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!
2015-07-15 20:23:27 -07:00

312 lines
12 KiB
TypeScript

import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
import {
AST,
Locals,
ChangeDispatcher,
ProtoChangeDetector,
ChangeDetector,
BindingRecord,
DirectiveRecord,
DirectiveIndex,
ChangeDetectorRef
} from 'angular2/change_detection';
import {
ProtoElementInjector,
ElementInjector,
PreBuiltObjects,
DirectiveBinding
} from './element_injector';
import {ElementBinder} from './element_binder';
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
import * as renderApi from 'angular2/src/render/api';
import {RenderEventDispatcher} from 'angular2/src/render/api';
import {ViewRef, internalView} from './view_ref';
import {ElementRef} from './element_ref';
export class AppProtoViewMergeMapping {
renderProtoViewRef: renderApi.RenderProtoViewRef;
renderFragmentCount: number;
renderElementIndices: number[];
renderInverseElementIndices: number[];
renderTextIndices: number[];
nestedViewIndicesByElementIndex: number[];
hostElementIndicesByViewIndex: number[];
constructor(renderProtoViewMergeMapping: renderApi.RenderProtoViewMergeMapping) {
this.renderProtoViewRef = renderProtoViewMergeMapping.mergedProtoViewRef;
this.renderFragmentCount = renderProtoViewMergeMapping.fragmentCount;
this.renderElementIndices = renderProtoViewMergeMapping.mappedElementIndices;
this.renderInverseElementIndices =
inverseIndexMapping(this.renderElementIndices, this.renderElementIndices.length);
this.renderTextIndices = renderProtoViewMergeMapping.mappedTextIndices;
this.hostElementIndicesByViewIndex = renderProtoViewMergeMapping.hostElementIndicesByViewIndex;
this.nestedViewIndicesByElementIndex =
inverseIndexMapping(this.hostElementIndicesByViewIndex, this.renderElementIndices.length);
}
get viewCount() { return this.hostElementIndicesByViewIndex.length; }
get elementCount() { return this.renderElementIndices.length; }
}
function inverseIndexMapping(input: number[], resultLength: number): number[] {
var result = ListWrapper.createFixedSize(resultLength);
for (var i = 0; i < input.length; i++) {
var value = input[i];
if (isPresent(value)) {
result[input[i]] = i;
}
}
return result;
}
export class AppViewContainer {
// The order in this list matches the DOM order.
views: List<AppView> = [];
}
/**
* Cost of making objects: http://jsperf.com/instantiate-size-of-object
*
*/
export class AppView implements ChangeDispatcher, RenderEventDispatcher {
// AppViews that have been merged in depth first order.
// This list is shared between all merged views. Use this.elementOffset to get the local
// entries.
views: List<AppView> = null;
// root elementInjectors of this AppView
// This list is local to this AppView and not shared with other Views.
rootElementInjectors: List<ElementInjector>;
// ElementInjectors of all AppViews in views grouped by view.
// This list is shared between all merged views. Use this.elementOffset to get the local
// entries.
elementInjectors: List<ElementInjector> = null;
// ViewContainers of all AppViews in views grouped by view.
// This list is shared between all merged views. Use this.elementOffset to get the local
// entries.
viewContainers: List<AppViewContainer> = null;
// PreBuiltObjects of all AppViews in views grouped by view.
// This list is shared between all merged views. Use this.elementOffset to get the local
// entries.
preBuiltObjects: List<PreBuiltObjects> = null;
// ElementRef of all AppViews in views grouped by view.
// This list is shared between all merged views. Use this.elementOffset to get the local
// entries.
elementRefs: List<ElementRef>;
ref: ViewRef;
changeDetector: ChangeDetector = null;
/**
* The context against which data-binding expressions in this view are evaluated against.
* This is always a component instance.
*/
context: any = null;
/**
* Variables, local to this view, that can be used in binding expressions (in addition to the
* context). This is used for thing like `<video #player>` or
* `<li template="for #item of items">`, where "player" and "item" are locals, respectively.
*/
locals: Locals;
constructor(public renderer: renderApi.Renderer, public proto: AppProtoView,
public mainMergeMapping: AppProtoViewMergeMapping, public viewOffset: number,
public elementOffset: number, public textOffset: number,
protoLocals: Map<string, any>, public render: renderApi.RenderViewRef,
public renderFragment: renderApi.RenderFragmentRef) {
this.ref = new ViewRef(this);
this.locals = new Locals(null, MapWrapper.clone(protoLocals)); // TODO optimize this
}
init(changeDetector: ChangeDetector, elementInjectors: List<ElementInjector>,
rootElementInjectors: List<ElementInjector>, preBuiltObjects: List<PreBuiltObjects>,
views: List<AppView>, elementRefs: List<ElementRef>,
viewContainers: List<AppViewContainer>) {
this.changeDetector = changeDetector;
this.elementInjectors = elementInjectors;
this.rootElementInjectors = rootElementInjectors;
this.preBuiltObjects = preBuiltObjects;
this.views = views;
this.elementRefs = elementRefs;
this.viewContainers = viewContainers;
}
setLocal(contextName: string, value): void {
if (!this.hydrated()) throw new BaseException('Cannot set locals on dehydrated view.');
if (!this.proto.variableBindings.has(contextName)) {
return;
}
var templateName = this.proto.variableBindings.get(contextName);
this.locals.set(templateName, value);
}
hydrated(): boolean { return isPresent(this.context); }
/**
* Triggers the event handlers for the element and the directives.
*
* This method is intended to be called from directive EventEmitters.
*
* @param {string} eventName
* @param {*} eventObj
* @param {int} boundElementIndex
*/
triggerEventHandlers(eventName: string, eventObj, boundElementIndex: int): void {
var locals = new Map();
locals.set('$event', eventObj);
this.dispatchEvent(boundElementIndex, eventName, locals);
}
// dispatch to element injector or text nodes based on context
notifyOnBinding(b: BindingRecord, currentValue: any): void {
if (b.isTextNode()) {
this.renderer.setText(
this.render, this.mainMergeMapping.renderTextIndices[b.elementIndex + this.textOffset],
currentValue);
} else {
var elementRef = this.elementRefs[this.elementOffset + b.elementIndex];
if (b.isElementProperty()) {
this.renderer.setElementProperty(elementRef, b.propertyName, currentValue);
} else if (b.isElementAttribute()) {
this.renderer.setElementAttribute(elementRef, b.propertyName, currentValue);
} else if (b.isElementClass()) {
this.renderer.setElementClass(elementRef, b.propertyName, currentValue);
} else if (b.isElementStyle()) {
var unit = isPresent(b.propertyUnit) ? b.propertyUnit : '';
this.renderer.setElementStyle(elementRef, b.propertyName, `${currentValue}${unit}`);
} else {
throw new BaseException('Unsupported directive record');
}
}
}
notifyOnAllChangesDone(): void {
var eiCount = this.proto.elementBinders.length;
var ei = this.elementInjectors;
for (var i = eiCount - 1; i >= 0; i--) {
if (isPresent(ei[i + this.elementOffset])) ei[i + this.elementOffset].onAllChangesDone();
}
}
getDirectiveFor(directive: DirectiveIndex): any {
var elementInjector = this.elementInjectors[this.elementOffset + directive.elementIndex];
return elementInjector.getDirectiveAtIndex(directive.directiveIndex);
}
getNestedView(boundElementIndex: number): AppView {
var viewIndex = this.mainMergeMapping.nestedViewIndicesByElementIndex[boundElementIndex];
return isPresent(viewIndex) ? this.views[viewIndex] : null;
}
getDetectorFor(directive: DirectiveIndex): any {
var childView = this.getNestedView(this.elementOffset + directive.elementIndex);
return isPresent(childView) ? childView.changeDetector : null;
}
invokeElementMethod(elementIndex: number, methodName: string, args: List<any>) {
this.renderer.invokeElementMethod(this.elementRefs[elementIndex], methodName, args);
}
// implementation of RenderEventDispatcher#dispatchRenderEvent
dispatchRenderEvent(renderElementIndex: number, eventName: string,
locals: Map<string, any>): boolean {
var elementRef =
this.elementRefs[this.proto.mergeMapping.renderInverseElementIndices[renderElementIndex]];
var view = internalView(elementRef.parentView);
return view.dispatchEvent(elementRef.boundElementIndex, eventName, locals);
}
// returns false if preventDefault must be applied to the DOM event
dispatchEvent(boundElementIndex: number, eventName: string, locals: Map<string, any>): boolean {
// Most of the time the event will be fired only when the view is in the live document.
// However, in a rare circumstance the view might get dehydrated, in between the event
// queuing up and firing.
var allowDefaultBehavior = true;
if (this.hydrated()) {
var elBinder = this.proto.elementBinders[boundElementIndex - this.elementOffset];
if (isBlank(elBinder.hostListeners)) return allowDefaultBehavior;
var eventMap = elBinder.hostListeners[eventName];
if (isBlank(eventMap)) return allowDefaultBehavior;
MapWrapper.forEach(eventMap, (expr, directiveIndex) => {
var context;
if (directiveIndex === -1) {
context = this.context;
} else {
context = this.elementInjectors[boundElementIndex].getDirectiveAtIndex(directiveIndex);
}
var result = expr.eval(context, new Locals(this.locals, locals));
if (isPresent(result)) {
allowDefaultBehavior = allowDefaultBehavior && result == true;
}
});
}
return allowDefaultBehavior;
}
}
/**
*
*/
export class AppProtoView {
elementBinders: List<ElementBinder> = [];
protoLocals: Map<string, any> = new Map();
mergeMapping: AppProtoViewMergeMapping;
constructor(public type: renderApi.ViewType, public protoChangeDetector: ProtoChangeDetector,
public variableBindings: Map<string, string>,
public variableLocations: Map<string, number>, public textBindingCount: number) {
if (isPresent(variableBindings)) {
MapWrapper.forEach(variableBindings,
(templateName, _) => { this.protoLocals.set(templateName, null); });
}
}
bindElement(parent: ElementBinder, distanceToParent: int,
protoElementInjector: ProtoElementInjector,
componentDirective: DirectiveBinding = null): ElementBinder {
var elBinder = new ElementBinder(this.elementBinders.length, parent, distanceToParent,
protoElementInjector, componentDirective);
this.elementBinders.push(elBinder);
return elBinder;
}
/**
* Adds an event binding for the last created ElementBinder via bindElement.
*
* If the directive index is a positive integer, the event is evaluated in the context of
* the given directive.
*
* If the directive index is -1, the event is evaluated in the context of the enclosing view.
*
* @param {string} eventName
* @param {AST} expression
* @param {int} directiveIndex The directive index in the binder or -1 when the event is not bound
* to a directive
*/
bindEvent(eventBindings: List<renderApi.EventBinding>, boundElementIndex: number,
directiveIndex: int = -1): void {
var elBinder = this.elementBinders[boundElementIndex];
var events = elBinder.hostListeners;
if (isBlank(events)) {
events = StringMapWrapper.create();
elBinder.hostListeners = events;
}
for (var i = 0; i < eventBindings.length; i++) {
var eventBinding = eventBindings[i];
var eventName = eventBinding.fullName;
var event = StringMapWrapper.get(events, eventName);
if (isBlank(event)) {
event = new Map();
StringMapWrapper.set(events, eventName, event);
}
event.set(directiveIndex, eventBinding.source);
}
}
}