feat(render): add initial implementation of render layer
This commit is contained in:
59
modules/angular2/src/render/dom/view/element_binder.js
vendored
Normal file
59
modules/angular2/src/render/dom/view/element_binder.js
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
import {AST} from 'angular2/change_detection';
|
||||
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||
import {List, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import * as protoViewModule from './proto_view';
|
||||
|
||||
/**
|
||||
* Note: Code that uses this class assumes that is immutable!
|
||||
*/
|
||||
export class ElementBinder {
|
||||
contentTagSelector: string;
|
||||
textNodeIndices: List<number>;
|
||||
nestedProtoView: protoViewModule.ProtoView;
|
||||
eventLocals: AST;
|
||||
eventNames: List<string>;
|
||||
componentId: string;
|
||||
parentIndex:number;
|
||||
distanceToParent:number;
|
||||
|
||||
constructor({
|
||||
textNodeIndices,
|
||||
contentTagSelector,
|
||||
nestedProtoView,
|
||||
componentId,
|
||||
eventLocals,
|
||||
eventNames,
|
||||
parentIndex,
|
||||
distanceToParent
|
||||
}) {
|
||||
this.textNodeIndices = textNodeIndices;
|
||||
this.contentTagSelector = contentTagSelector;
|
||||
this.nestedProtoView = nestedProtoView;
|
||||
this.componentId = componentId;
|
||||
this.eventLocals = eventLocals;
|
||||
this.eventNames = eventNames;
|
||||
this.parentIndex = parentIndex;
|
||||
this.distanceToParent = distanceToParent;
|
||||
}
|
||||
|
||||
mergeChildComponentProtoViews(protoViews:List<protoViewModule.ProtoView>, target:List<protoViewModule.ProtoView>):ElementBinder {
|
||||
var nestedProtoView;
|
||||
if (isPresent(this.componentId)) {
|
||||
nestedProtoView = ListWrapper.removeAt(protoViews, 0);
|
||||
} else if (isPresent(this.nestedProtoView)) {
|
||||
nestedProtoView = this.nestedProtoView.mergeChildComponentProtoViews(protoViews, target);
|
||||
}
|
||||
return new ElementBinder({
|
||||
parentIndex: this.parentIndex,
|
||||
// Don't clone as we assume immutability!
|
||||
textNodeIndices: this.textNodeIndices,
|
||||
contentTagSelector: this.contentTagSelector,
|
||||
nestedProtoView: nestedProtoView,
|
||||
componentId: this.componentId,
|
||||
// Don't clone as we assume immutability!
|
||||
eventLocals: this.eventLocals,
|
||||
eventNames: this.eventNames,
|
||||
distanceToParent: this.distanceToParent
|
||||
});
|
||||
}
|
||||
}
|
55
modules/angular2/src/render/dom/view/proto_view.js
vendored
Normal file
55
modules/angular2/src/render/dom/view/proto_view.js
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {SetterFn} from 'angular2/src/reflection/types';
|
||||
|
||||
import {List, Map, ListWrapper, MapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {ElementBinder} from './element_binder';
|
||||
import {NG_BINDING_CLASS} from '../util';
|
||||
|
||||
/**
|
||||
* Note: Code that uses this class assumes that is immutable!
|
||||
*/
|
||||
export class ProtoView {
|
||||
element;
|
||||
elementBinders:List<ElementBinder>;
|
||||
isTemplateElement:boolean;
|
||||
isRootView:boolean;
|
||||
rootBindingOffset:int;
|
||||
propertySetters: Map<string, SetterFn>;
|
||||
|
||||
constructor({
|
||||
elementBinders,
|
||||
element,
|
||||
isRootView,
|
||||
propertySetters
|
||||
}) {
|
||||
this.element = element;
|
||||
this.elementBinders = elementBinders;
|
||||
this.isTemplateElement = DOM.isTemplateElement(this.element);
|
||||
this.isRootView = isRootView;
|
||||
this.rootBindingOffset = (isPresent(this.element) && DOM.hasClass(this.element, NG_BINDING_CLASS)) ? 1 : 0;
|
||||
this.propertySetters = propertySetters;
|
||||
}
|
||||
|
||||
mergeChildComponentProtoViews(protoViews:List<ProtoView>, target:List<ProtoView>):ProtoView {
|
||||
var elementBinders = ListWrapper.createFixedSize(this.elementBinders.length);
|
||||
for (var i=0; i<this.elementBinders.length; i++) {
|
||||
var eb = this.elementBinders[i];
|
||||
if (isPresent(eb.componentId) || isPresent(eb.nestedProtoView)) {
|
||||
elementBinders[i] = eb.mergeChildComponentProtoViews(protoViews, target);
|
||||
} else {
|
||||
elementBinders[i] = eb;
|
||||
}
|
||||
}
|
||||
var result = new ProtoView({
|
||||
elementBinders: elementBinders,
|
||||
element: this.element,
|
||||
isRootView: this.isRootView,
|
||||
// Don't clone as we assume immutability!
|
||||
propertySetters: this.propertySetters
|
||||
});
|
||||
ListWrapper.insert(target, 0, result);
|
||||
return result
|
||||
}
|
||||
}
|
297
modules/angular2/src/render/dom/view/proto_view_builder.js
vendored
Normal file
297
modules/angular2/src/render/dom/view/proto_view_builder.js
vendored
Normal file
@ -0,0 +1,297 @@
|
||||
import {isPresent, BaseException} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, MapWrapper, Set, SetWrapper} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import {
|
||||
ASTWithSource, AST, AstTransformer, AccessMember, LiteralArray, ImplicitReceiver
|
||||
} from 'angular2/change_detection';
|
||||
import {SetterFn} from 'angular2/src/reflection/types';
|
||||
|
||||
import {ProtoView} from './proto_view';
|
||||
import {ElementBinder} from './element_binder';
|
||||
|
||||
import * as api from '../../api';
|
||||
import * as directDomRenderer from '../direct_dom_renderer';
|
||||
|
||||
import {NG_BINDING_CLASS} from '../util';
|
||||
|
||||
export class ProtoViewBuilder {
|
||||
rootElement;
|
||||
variableBindings: Map<string, string>;
|
||||
elements:List<ElementBinderBuilder>;
|
||||
isRootView:boolean;
|
||||
propertySetters:Set<string>;
|
||||
|
||||
constructor(rootElement) {
|
||||
this.rootElement = rootElement;
|
||||
this.elements = [];
|
||||
this.isRootView = false;
|
||||
this.variableBindings = MapWrapper.create();
|
||||
this.propertySetters = new Set();
|
||||
}
|
||||
|
||||
bindElement(element, description = null):ElementBinderBuilder {
|
||||
var builder = new ElementBinderBuilder(this.elements.length, element, description);
|
||||
ListWrapper.push(this.elements, builder);
|
||||
DOM.addClass(element, NG_BINDING_CLASS);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
bindVariable(name, value) {
|
||||
// Store the variable map from value to variable, reflecting how it will be used later by
|
||||
// View. When a local is set to the view, a lookup for the variable name will take place keyed
|
||||
// by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
|
||||
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
|
||||
// it.
|
||||
MapWrapper.set(this.variableBindings, value, name);
|
||||
}
|
||||
|
||||
setIsRootView(value) {
|
||||
this.isRootView = value;
|
||||
}
|
||||
|
||||
build():api.ProtoView {
|
||||
var renderElementBinders = [];
|
||||
|
||||
var apiElementBinders = [];
|
||||
var propertySetters = MapWrapper.create();
|
||||
ListWrapper.forEach(this.elements, (ebb) => {
|
||||
var eventLocalsAstSplitter = new EventLocalsAstSplitter();
|
||||
var apiDirectiveBinders = ListWrapper.map(ebb.directives, (db) => {
|
||||
MapWrapper.forEach(db.propertySetters, (setter, propertyName) => {
|
||||
MapWrapper.set(propertySetters, propertyName, setter);
|
||||
});
|
||||
return new api.DirectiveBinder({
|
||||
directiveIndex: db.directiveIndex,
|
||||
propertyBindings: db.propertyBindings,
|
||||
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(db.eventBindings)
|
||||
});
|
||||
});
|
||||
MapWrapper.forEach(ebb.propertySetters, (setter, propertyName) => {
|
||||
MapWrapper.set(propertySetters, propertyName, setter);
|
||||
});
|
||||
var nestedProtoView =
|
||||
isPresent(ebb.nestedProtoView) ? ebb.nestedProtoView.build() : null;
|
||||
var parentIndex = isPresent(ebb.parent) ? ebb.parent.index : -1;
|
||||
var parentWithDirectivesIndex = isPresent(ebb.parentWithDirectives) ? ebb.parentWithDirectives.index : -1;
|
||||
ListWrapper.push(apiElementBinders, new api.ElementBinder({
|
||||
index: ebb.index, parentIndex:parentIndex, distanceToParent:ebb.distanceToParent,
|
||||
parentWithDirectivesIndex: parentWithDirectivesIndex, distanceToParentWithDirectives: ebb.distanceToParentWithDirectives,
|
||||
directives: apiDirectiveBinders,
|
||||
nestedProtoView: nestedProtoView,
|
||||
propertyBindings: ebb.propertyBindings, variableBindings: ebb.variableBindings,
|
||||
eventBindings: eventLocalsAstSplitter.splitEventAstIntoLocals(ebb.eventBindings),
|
||||
textBindings: ebb.textBindings
|
||||
}));
|
||||
ListWrapper.push(renderElementBinders, new ElementBinder({
|
||||
textNodeIndices: ebb.textBindingIndices,
|
||||
contentTagSelector: ebb.contentTagSelector,
|
||||
parentIndex: parentIndex,
|
||||
distanceToParent: ebb.distanceToParent,
|
||||
nestedProtoView: isPresent(nestedProtoView) ? nestedProtoView.render.delegate : null,
|
||||
componentId: ebb.componentId,
|
||||
eventLocals: eventLocalsAstSplitter.buildEventLocals(),
|
||||
eventNames: eventLocalsAstSplitter.buildEventNames()
|
||||
}));
|
||||
});
|
||||
return new api.ProtoView({
|
||||
render: new directDomRenderer.DirectDomProtoViewRef(new ProtoView({
|
||||
element: this.rootElement,
|
||||
elementBinders: renderElementBinders,
|
||||
isRootView: this.isRootView,
|
||||
propertySetters: propertySetters
|
||||
})),
|
||||
elementBinders: apiElementBinders,
|
||||
variableBindings: this.variableBindings
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ElementBinderBuilder {
|
||||
element;
|
||||
index:number;
|
||||
parent:ElementBinderBuilder;
|
||||
distanceToParent:number;
|
||||
parentWithDirectives:ElementBinderBuilder;
|
||||
distanceToParentWithDirectives:number;
|
||||
directives:List<DirectiveBuilder>;
|
||||
nestedProtoView:ProtoViewBuilder;
|
||||
propertyBindings: Map<string, ASTWithSource>;
|
||||
variableBindings: Map<string, string>;
|
||||
eventBindings: Map<string, ASTWithSource>;
|
||||
textBindingIndices: List<number>;
|
||||
textBindings: List<ASTWithSource>;
|
||||
contentTagSelector:string;
|
||||
propertySetters: Map<string, SetterFn>;
|
||||
componentId: string;
|
||||
|
||||
constructor(index, element, description) {
|
||||
this.element = element;
|
||||
this.index = index;
|
||||
this.parent = null;
|
||||
this.distanceToParent = 0;
|
||||
this.parentWithDirectives = null;
|
||||
this.distanceToParentWithDirectives = 0;
|
||||
this.directives = [];
|
||||
this.nestedProtoView = null;
|
||||
this.propertyBindings = MapWrapper.create();
|
||||
this.variableBindings = MapWrapper.create();
|
||||
this.eventBindings = MapWrapper.create();
|
||||
this.textBindings = [];
|
||||
this.textBindingIndices = [];
|
||||
this.contentTagSelector = null;
|
||||
this.propertySetters = MapWrapper.create();
|
||||
this.componentId = null;
|
||||
}
|
||||
|
||||
setParent(parent:ElementBinderBuilder, distanceToParent):ElementBinderBuilder {
|
||||
this.parent = parent;
|
||||
if (isPresent(parent)) {
|
||||
this.distanceToParent = distanceToParent;
|
||||
if (parent.directives.length > 0) {
|
||||
this.parentWithDirectives = parent;
|
||||
this.distanceToParentWithDirectives = distanceToParent;
|
||||
} else {
|
||||
this.parentWithDirectives = parent.parentWithDirectives;
|
||||
if (isPresent(this.parentWithDirectives)) {
|
||||
this.distanceToParentWithDirectives = distanceToParent + parent.distanceToParentWithDirectives;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
bindDirective(directiveIndex:number):DirectiveBuilder {
|
||||
var directive = new DirectiveBuilder(directiveIndex);
|
||||
ListWrapper.push(this.directives, directive);
|
||||
return directive;
|
||||
}
|
||||
|
||||
bindNestedProtoView(rootElement):ProtoViewBuilder {
|
||||
if (isPresent(this.nestedProtoView)) {
|
||||
throw new BaseException('Only one nested view per element is allowed');
|
||||
}
|
||||
this.nestedProtoView = new ProtoViewBuilder(rootElement);
|
||||
return this.nestedProtoView;
|
||||
}
|
||||
|
||||
bindProperty(name, expression) {
|
||||
MapWrapper.set(this.propertyBindings, name, expression);
|
||||
}
|
||||
|
||||
bindVariable(name, value) {
|
||||
// When current is a view root, the variable bindings are set to the *nested* proto view.
|
||||
// The root view conceptually signifies a new "block scope" (the nested view), to which
|
||||
// the variables are bound.
|
||||
if (isPresent(this.nestedProtoView)) {
|
||||
this.nestedProtoView.bindVariable(name, value);
|
||||
} else {
|
||||
// Store the variable map from value to variable, reflecting how it will be used later by
|
||||
// View. When a local is set to the view, a lookup for the variable name will take place keyed
|
||||
// by the "value", or exported identifier. For example, ng-repeat sets a view local of "index".
|
||||
// When this occurs, a lookup keyed by "index" must occur to find if there is a var referencing
|
||||
// it.
|
||||
MapWrapper.set(this.variableBindings, value, name);
|
||||
}
|
||||
}
|
||||
|
||||
bindEvent(name, expression) {
|
||||
MapWrapper.set(this.eventBindings, name, expression);
|
||||
}
|
||||
|
||||
bindText(index, expression) {
|
||||
ListWrapper.push(this.textBindingIndices, index);
|
||||
ListWrapper.push(this.textBindings, expression);
|
||||
}
|
||||
|
||||
setContentTagSelector(value:string) {
|
||||
this.contentTagSelector = value;
|
||||
}
|
||||
|
||||
bindPropertySetter(propertyName, setter) {
|
||||
MapWrapper.set(this.propertySetters, propertyName, setter);
|
||||
}
|
||||
|
||||
setComponentId(componentId:string) {
|
||||
this.componentId = componentId;
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectiveBuilder {
|
||||
directiveIndex:number;
|
||||
propertyBindings: Map<string, ASTWithSource>;
|
||||
eventBindings: Map<string, ASTWithSource>;
|
||||
propertySetters: Map<string, SetterFn>;
|
||||
|
||||
constructor(directiveIndex) {
|
||||
this.directiveIndex = directiveIndex;
|
||||
this.propertyBindings = MapWrapper.create();
|
||||
this.eventBindings = MapWrapper.create();
|
||||
this.propertySetters = MapWrapper.create();
|
||||
}
|
||||
|
||||
bindProperty(name, expression) {
|
||||
MapWrapper.set(this.propertyBindings, name, expression);
|
||||
}
|
||||
|
||||
bindEvent(name, expression) {
|
||||
MapWrapper.set(this.eventBindings, name, expression);
|
||||
}
|
||||
|
||||
bindPropertySetter(propertyName, setter) {
|
||||
MapWrapper.set(this.propertySetters, propertyName, setter);
|
||||
}
|
||||
}
|
||||
|
||||
export class EventLocalsAstSplitter extends AstTransformer {
|
||||
locals:List<AST>;
|
||||
eventNames:List<string>;
|
||||
_implicitReceiver:AST;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.locals = [];
|
||||
this.eventNames = [];
|
||||
this._implicitReceiver = new ImplicitReceiver();
|
||||
}
|
||||
|
||||
splitEventAstIntoLocals(eventBindings:Map<string, ASTWithSource>):Map<string, ASTWithSource> {
|
||||
if (isPresent(eventBindings)) {
|
||||
var result = MapWrapper.create();
|
||||
MapWrapper.forEach(eventBindings, (astWithSource, eventName) => {
|
||||
MapWrapper.set(result, eventName, astWithSource.ast.visit(this));
|
||||
ListWrapper.push(this.eventNames, eventName);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
visitAccessMember(ast:AccessMember) {
|
||||
var isEventAccess = false;
|
||||
var current = ast;
|
||||
while (!isEventAccess && (current instanceof AccessMember)) {
|
||||
if (current.name == '$event') {
|
||||
isEventAccess = true;
|
||||
}
|
||||
current = current.receiver;
|
||||
}
|
||||
|
||||
if (isEventAccess) {
|
||||
ListWrapper.push(this.locals, ast);
|
||||
var index = this.locals.length - 1;
|
||||
return new AccessMember(this._implicitReceiver, `${index}`, (arr) => arr[index], null);
|
||||
} else {
|
||||
return ast;
|
||||
}
|
||||
}
|
||||
|
||||
buildEventLocals() {
|
||||
return new LiteralArray(this.locals);
|
||||
}
|
||||
|
||||
buildEventNames() {
|
||||
return this.eventNames;
|
||||
}
|
||||
}
|
168
modules/angular2/src/render/dom/view/view.js
vendored
Normal file
168
modules/angular2/src/render/dom/view/view.js
vendored
Normal file
@ -0,0 +1,168 @@
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
|
||||
import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||
import {Locals} from 'angular2/change_detection';
|
||||
|
||||
import {ViewContainer} from './view_container';
|
||||
import {ProtoView} from './proto_view';
|
||||
import {LightDom} from '../shadow_dom/light_dom';
|
||||
import {Content} from '../shadow_dom/content_tag';
|
||||
|
||||
import {ShadowDomStrategy} from '../shadow_dom/shadow_dom_strategy';
|
||||
|
||||
import {EventDispatcher} from '../../api';
|
||||
|
||||
const NG_BINDING_CLASS = 'ng-binding';
|
||||
|
||||
/**
|
||||
* Const of making objects: http://jsperf.com/instantiate-size-of-object
|
||||
*/
|
||||
export class View {
|
||||
boundElements:List;
|
||||
boundTextNodes:List;
|
||||
/// When the view is part of render tree, the DocumentFragment is empty, which is why we need
|
||||
/// to keep track of the nodes.
|
||||
rootNodes:List;
|
||||
// TODO(tbosch): move componentChildViews, viewContainers, contentTags, lightDoms into
|
||||
// a single array with records inside
|
||||
componentChildViews: List<View>;
|
||||
viewContainers: List<ViewContainer>;
|
||||
contentTags: List<Content>;
|
||||
lightDoms: List<LightDom>;
|
||||
proto: ProtoView;
|
||||
_hydrated: boolean;
|
||||
_eventDispatcher: EventDispatcher;
|
||||
|
||||
constructor(
|
||||
proto:ProtoView, rootNodes:List,
|
||||
boundTextNodes: List, boundElements:List, viewContainers:List, contentTags:List) {
|
||||
this.proto = proto;
|
||||
this.rootNodes = rootNodes;
|
||||
this.boundTextNodes = boundTextNodes;
|
||||
this.boundElements = boundElements;
|
||||
this.viewContainers = viewContainers;
|
||||
this.contentTags = contentTags;
|
||||
this.lightDoms = ListWrapper.createFixedSize(boundElements.length);
|
||||
this.componentChildViews = ListWrapper.createFixedSize(boundElements.length);
|
||||
this._hydrated = false;
|
||||
}
|
||||
|
||||
hydrated() {
|
||||
return this._hydrated;
|
||||
}
|
||||
|
||||
setElementProperty(elementIndex:number, propertyName:string, value:any) {
|
||||
var setter = MapWrapper.get(this.proto.propertySetters, propertyName);
|
||||
setter(this.boundElements[elementIndex], value);
|
||||
}
|
||||
|
||||
setText(textIndex:number, value:string) {
|
||||
DOM.setText(this.boundTextNodes[textIndex], value);
|
||||
}
|
||||
|
||||
setComponentView(strategy: ShadowDomStrategy,
|
||||
elementIndex:number, childView:View) {
|
||||
var element = this.boundElements[elementIndex];
|
||||
var lightDom = strategy.constructLightDom(this, childView, element);
|
||||
strategy.attachTemplate(element, childView);
|
||||
this.lightDoms[elementIndex] = lightDom;
|
||||
this.componentChildViews[elementIndex] = childView;
|
||||
if (this._hydrated) {
|
||||
childView.hydrate(lightDom);
|
||||
}
|
||||
}
|
||||
|
||||
getViewContainer(index:number):ViewContainer {
|
||||
return this.viewContainers[index];
|
||||
}
|
||||
|
||||
_getDestLightDom(binderIndex) {
|
||||
var binder = this.proto.elementBinders[binderIndex];
|
||||
var destLightDom = null;
|
||||
if (binder.parentIndex !== -1 && binder.distanceToParent === 1) {
|
||||
destLightDom = this.lightDoms[binder.parentIndex];
|
||||
}
|
||||
return destLightDom;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dehydrated view is a state of the view that allows it to be moved around
|
||||
* the view tree.
|
||||
*
|
||||
* A dehydrated view has the following properties:
|
||||
*
|
||||
* - all viewcontainers are empty.
|
||||
*
|
||||
* A call to hydrate/dehydrate does not attach/detach the view from the view
|
||||
* tree.
|
||||
*/
|
||||
hydrate(hostLightDom: LightDom) {
|
||||
if (this._hydrated) throw new BaseException('The view is already hydrated.');
|
||||
this._hydrated = true;
|
||||
|
||||
// viewContainers and content tags
|
||||
for (var i = 0; i < this.viewContainers.length; i++) {
|
||||
var vc = this.viewContainers[i];
|
||||
var destLightDom = this._getDestLightDom(i);
|
||||
if (isPresent(vc)) {
|
||||
vc.hydrate(destLightDom, hostLightDom);
|
||||
}
|
||||
var ct = this.contentTags[i];
|
||||
if (isPresent(ct)) {
|
||||
ct.hydrate(destLightDom);
|
||||
}
|
||||
}
|
||||
|
||||
// componentChildViews
|
||||
for (var i = 0; i < this.componentChildViews.length; i++) {
|
||||
var cv = this.componentChildViews[i];
|
||||
if (isPresent(cv)) {
|
||||
cv.hydrate(this.lightDoms[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.lightDoms.length; ++i) {
|
||||
var lightDom = this.lightDoms[i];
|
||||
if (isPresent(lightDom)) {
|
||||
lightDom.redistribute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
// Note: preserve the opposite order of the hydration process.
|
||||
|
||||
// componentChildViews
|
||||
for (var i = 0; i < this.componentChildViews.length; i++) {
|
||||
this.componentChildViews[i].dehydrate();
|
||||
}
|
||||
|
||||
// viewContainers and content tags
|
||||
if (isPresent(this.viewContainers)) {
|
||||
for (var i = 0; i < this.viewContainers.length; i++) {
|
||||
var vc = this.viewContainers[i];
|
||||
if (isPresent(vc)) {
|
||||
vc.dehydrate();
|
||||
}
|
||||
var ct = this.contentTags[i];
|
||||
if (isPresent(ct)) {
|
||||
ct.dehydrate();
|
||||
}
|
||||
}
|
||||
}
|
||||
this._hydrated = false;
|
||||
}
|
||||
|
||||
setEventDispatcher(dispatcher:EventDispatcher) {
|
||||
this._eventDispatcher = dispatcher;
|
||||
}
|
||||
|
||||
dispatchEvent(elementIndex, eventName, event) {
|
||||
if (isPresent(this._eventDispatcher)) {
|
||||
var evalLocals = MapWrapper.create();
|
||||
MapWrapper.set(evalLocals, '$event', event);
|
||||
var localValues = this.proto.elementBinders[elementIndex].eventLocals.eval(null, new Locals(null, evalLocals));
|
||||
this._eventDispatcher.dispatchEvent(elementIndex, eventName, localValues);
|
||||
}
|
||||
}
|
||||
}
|
137
modules/angular2/src/render/dom/view/view_container.js
vendored
Normal file
137
modules/angular2/src/render/dom/view/view_container.js
vendored
Normal file
@ -0,0 +1,137 @@
|
||||
import {isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, MapWrapper, List} from 'angular2/src/facade/collection';
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
import * as viewModule from './view';
|
||||
import * as ldModule from '../shadow_dom/light_dom';
|
||||
import * as vfModule from './view_factory';
|
||||
|
||||
export class ViewContainer {
|
||||
_viewFactory: vfModule.ViewFactory;
|
||||
templateElement;
|
||||
_views: List<viewModule.View>;
|
||||
_lightDom: ldModule.LightDom;
|
||||
_hostLightDom: ldModule.LightDom;
|
||||
_hydrated: boolean;
|
||||
|
||||
constructor(viewFactory: vfModule.ViewFactory,
|
||||
templateElement) {
|
||||
this._viewFactory = viewFactory;
|
||||
this.templateElement = templateElement;
|
||||
|
||||
// The order in this list matches the DOM order.
|
||||
this._views = [];
|
||||
this._hostLightDom = null;
|
||||
this._hydrated = false;
|
||||
}
|
||||
|
||||
hydrate(destLightDom: ldModule.LightDom, hostLightDom: ldModule.LightDom) {
|
||||
this._hydrated = true;
|
||||
this._hostLightDom = hostLightDom;
|
||||
this._lightDom = destLightDom;
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
if (isBlank(this._lightDom)) {
|
||||
for (var i = this._views.length - 1; i >= 0; i--) {
|
||||
var view = this._views[i];
|
||||
ViewContainer.removeViewNodesFromParent(this.templateElement.parentNode, view);
|
||||
this._viewFactory.returnView(view);
|
||||
}
|
||||
this._views = [];
|
||||
} else {
|
||||
for (var i=0; i<this._views.length; i++) {
|
||||
var view = this._views[i];
|
||||
this._viewFactory.returnView(view);
|
||||
}
|
||||
this._views = [];
|
||||
this._lightDom.redistribute();
|
||||
}
|
||||
|
||||
this._hostLightDom = null;
|
||||
this._lightDom = null;
|
||||
this._hydrated = false;
|
||||
}
|
||||
|
||||
get(index: number): viewModule.View {
|
||||
return this._views[index];
|
||||
}
|
||||
|
||||
size() {
|
||||
return this._views.length;
|
||||
}
|
||||
|
||||
_siblingToInsertAfter(index: number) {
|
||||
if (index == 0) return this.templateElement;
|
||||
return ListWrapper.last(this._views[index - 1].rootNodes);
|
||||
}
|
||||
|
||||
_checkHydrated() {
|
||||
if (!this._hydrated) throw new BaseException(
|
||||
'Cannot change dehydrated ViewContainer');
|
||||
}
|
||||
|
||||
insert(view, atIndex=-1): viewModule.View {
|
||||
this._checkHydrated();
|
||||
if (atIndex == -1) atIndex = this._views.length;
|
||||
ListWrapper.insert(this._views, atIndex, view);
|
||||
if (!view.hydrated()) {
|
||||
view.hydrate(this._hostLightDom);
|
||||
}
|
||||
|
||||
if (isBlank(this._lightDom)) {
|
||||
ViewContainer.moveViewNodesAfterSibling(this._siblingToInsertAfter(atIndex), view);
|
||||
} else {
|
||||
this._lightDom.redistribute();
|
||||
}
|
||||
// new content tags might have appeared, we need to redistribute.
|
||||
if (isPresent(this._hostLightDom)) {
|
||||
this._hostLightDom.redistribute();
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method can be used together with insert to implement a view move, i.e.
|
||||
* moving the dom nodes while the directives in the view stay intact.
|
||||
*/
|
||||
detach(atIndex:number) {
|
||||
this._checkHydrated();
|
||||
var detachedView = this.get(atIndex);
|
||||
ListWrapper.removeAt(this._views, atIndex);
|
||||
if (isBlank(this._lightDom)) {
|
||||
ViewContainer.removeViewNodesFromParent(this.templateElement.parentNode, detachedView);
|
||||
} else {
|
||||
this._lightDom.redistribute();
|
||||
}
|
||||
// content tags might have disappeared we need to do redistribution.
|
||||
if (isPresent(this._hostLightDom)) {
|
||||
this._hostLightDom.redistribute();
|
||||
}
|
||||
return detachedView;
|
||||
}
|
||||
|
||||
contentTagContainers() {
|
||||
return this._views;
|
||||
}
|
||||
|
||||
nodes():List {
|
||||
var r = [];
|
||||
for (var i = 0; i < this._views.length; ++i) {
|
||||
r = ListWrapper.concat(r, this._views[i].rootNodes);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static moveViewNodesAfterSibling(sibling, view) {
|
||||
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
|
||||
DOM.insertAfter(sibling, view.rootNodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static removeViewNodesFromParent(parent, view) {
|
||||
for (var i = view.rootNodes.length - 1; i >= 0; --i) {
|
||||
DOM.removeChild(parent, view.rootNodes[i]);
|
||||
}
|
||||
}
|
||||
}
|
158
modules/angular2/src/render/dom/view/view_factory.js
vendored
Normal file
158
modules/angular2/src/render/dom/view/view_factory.js
vendored
Normal file
@ -0,0 +1,158 @@
|
||||
import {OpaqueToken} from 'angular2/di';
|
||||
import {int, isPresent, isBlank, BaseException} from 'angular2/src/facade/lang';
|
||||
import {ListWrapper, MapWrapper, Map, StringMapWrapper, List} from 'angular2/src/facade/collection';
|
||||
|
||||
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 {ViewContainer} from './view_container';
|
||||
import {ProtoView} from './proto_view';
|
||||
import {View} from './view';
|
||||
import {NG_BINDING_CLASS_SELECTOR, NG_BINDING_CLASS} from '../util';
|
||||
|
||||
export var VIEW_POOL_CAPACITY = new OpaqueToken('ViewFactory.viewPoolCapacity');
|
||||
|
||||
|
||||
export class ViewFactory {
|
||||
_poolCapacity:number;
|
||||
_pooledViews:List<View>;
|
||||
_eventManager:EventManager;
|
||||
_shadowDomStrategy:ShadowDomStrategy;
|
||||
|
||||
constructor(capacity, eventManager, shadowDomStrategy) {
|
||||
this._poolCapacity = capacity;
|
||||
this._pooledViews = ListWrapper.create();
|
||||
this._eventManager = eventManager;
|
||||
this._shadowDomStrategy = shadowDomStrategy;
|
||||
}
|
||||
|
||||
getView(protoView:ProtoView):View {
|
||||
// TODO(tbosch): benchmark this scanning of views and maybe
|
||||
// replace it with a fancy LRU Map/List combination...
|
||||
var view;
|
||||
for (var i=0; i<this._pooledViews.length; i++) {
|
||||
var pooledView = this._pooledViews[i];
|
||||
if (pooledView.proto === protoView) {
|
||||
view = ListWrapper.removeAt(this._pooledViews, i);
|
||||
}
|
||||
}
|
||||
if (isBlank(view)) {
|
||||
view = this._createView(protoView);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
returnView(view:View) {
|
||||
if (view.hydrated()) {
|
||||
view.dehydrate();
|
||||
}
|
||||
ListWrapper.push(this._pooledViews, view);
|
||||
while (this._pooledViews.length > this._poolCapacity) {
|
||||
ListWrapper.removeAt(this._pooledViews, 0);
|
||||
}
|
||||
}
|
||||
|
||||
_createView(protoView:ProtoView): View {
|
||||
var rootElementClone = protoView.isRootView ? protoView.element : DOM.importIntoDoc(protoView.element);
|
||||
var elementsWithBindingsDynamic;
|
||||
if (protoView.isTemplateElement) {
|
||||
elementsWithBindingsDynamic = DOM.querySelectorAll(DOM.content(rootElementClone), NG_BINDING_CLASS_SELECTOR);
|
||||
} else {
|
||||
elementsWithBindingsDynamic = DOM.getElementsByClassName(rootElementClone, NG_BINDING_CLASS);
|
||||
}
|
||||
|
||||
var elementsWithBindings = ListWrapper.createFixedSize(elementsWithBindingsDynamic.length);
|
||||
for (var binderIdx = 0; binderIdx < elementsWithBindingsDynamic.length; ++binderIdx) {
|
||||
elementsWithBindings[binderIdx] = elementsWithBindingsDynamic[binderIdx];
|
||||
}
|
||||
|
||||
var viewRootNodes;
|
||||
if (protoView.isTemplateElement) {
|
||||
var childNode = DOM.firstChild(DOM.content(rootElementClone));
|
||||
viewRootNodes = []; // TODO(perf): Should be fixed size, since we could pre-compute in in ProtoView
|
||||
// Note: An explicit loop is the fastest way to convert a DOM array into a JS array!
|
||||
while(childNode != null) {
|
||||
ListWrapper.push(viewRootNodes, childNode);
|
||||
childNode = DOM.nextSibling(childNode);
|
||||
}
|
||||
} else {
|
||||
viewRootNodes = [rootElementClone];
|
||||
}
|
||||
|
||||
var binders = protoView.elementBinders;
|
||||
var boundTextNodes = [];
|
||||
var boundElements = ListWrapper.createFixedSize(binders.length);
|
||||
var viewContainers = ListWrapper.createFixedSize(binders.length);
|
||||
var contentTags = ListWrapper.createFixedSize(binders.length);
|
||||
|
||||
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
|
||||
var binder = binders[binderIdx];
|
||||
var element;
|
||||
if (binderIdx === 0 && protoView.rootBindingOffset === 1) {
|
||||
element = rootElementClone;
|
||||
} else {
|
||||
element = elementsWithBindings[binderIdx - protoView.rootBindingOffset];
|
||||
}
|
||||
boundElements[binderIdx] = element;
|
||||
|
||||
// boundTextNodes
|
||||
var childNodes = DOM.childNodes(DOM.templateAwareRoot(element));
|
||||
var textNodeIndices = binder.textNodeIndices;
|
||||
for (var i = 0; i<textNodeIndices.length; i++) {
|
||||
ListWrapper.push(boundTextNodes, childNodes[textNodeIndices[i]]);
|
||||
}
|
||||
|
||||
// viewContainers
|
||||
var viewContainer = null;
|
||||
if (isBlank(binder.componentId) && isPresent(binder.nestedProtoView)) {
|
||||
viewContainer = new ViewContainer(this, element);
|
||||
}
|
||||
viewContainers[binderIdx] = viewContainer;
|
||||
|
||||
// contentTags
|
||||
var contentTag = null;
|
||||
if (isPresent(binder.contentTagSelector)) {
|
||||
contentTag = new Content(element, binder.contentTagSelector);
|
||||
}
|
||||
contentTags[binderIdx] = contentTag;
|
||||
}
|
||||
|
||||
var view = new View(
|
||||
protoView, viewRootNodes,
|
||||
boundTextNodes, boundElements, viewContainers, contentTags
|
||||
);
|
||||
|
||||
for (var binderIdx = 0; binderIdx < binders.length; binderIdx++) {
|
||||
var binder = binders[binderIdx];
|
||||
var element = boundElements[binderIdx];
|
||||
|
||||
// static child components
|
||||
if (isPresent(binder.componentId) && isPresent(binder.nestedProtoView)) {
|
||||
var childView = this._createView(binder.nestedProtoView);
|
||||
view.setComponentView(this._shadowDomStrategy, binderIdx, childView);
|
||||
}
|
||||
|
||||
// events
|
||||
if (isPresent(binder.eventLocals)) {
|
||||
ListWrapper.forEach(binder.eventNames, (eventName) => {
|
||||
this._createEventListener(view, element, binderIdx, eventName, binder.eventLocals);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (protoView.isRootView) {
|
||||
view.hydrate(null);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
_createEventListener(view, element, elementIndex, eventName, eventLocals) {
|
||||
this._eventManager.addEventListener(element, eventName, (event) => {
|
||||
view.dispatchEvent(elementIndex, eventName, event);
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user