/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef} from '../change_detection/change_detection'; import {Injector} from '../di/injector'; import {InjectFlags} from '../di/interface/injector'; import {Type} from '../interface/type'; import {ComponentFactory, ComponentRef} from '../linker/component_factory'; import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ElementRef} from '../linker/element_ref'; import {InternalNgModuleRef, NgModuleRef} from '../linker/ng_module_factory'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef, InternalViewRef, ViewRef} from '../linker/view_ref'; import {Renderer as RendererV1, Renderer2} from '../render/api'; import {stringify} from '../util/stringify'; import {VERSION} from '../version'; import {callNgModuleLifecycle, initNgModule, resolveNgModuleDep} from './ng_module'; import {DepFlags, ElementData, NgModuleData, NgModuleDefinition, NodeDef, NodeFlags, Services, TemplateData, ViewContainerData, ViewData, ViewDefinitionFactory, ViewState, asElementData, asProviderData, asTextData} from './types'; import {markParentViewsForCheck, resolveDefinition, rootRenderNodes, splitNamespace, tokenKey, viewParentEl} from './util'; import {attachEmbeddedView, detachEmbeddedView, moveEmbeddedView, renderDetachView} from './view_attach'; const EMPTY_CONTEXT = new Object(); // Attention: this function is called as top level function. // Putting any logic in here will destroy closure tree shaking! export function createComponentFactory( selector: string, componentType: Type, viewDefFactory: ViewDefinitionFactory, inputs: {[propName: string]: string} | null, outputs: {[propName: string]: string}, ngContentSelectors: string[]): ComponentFactory { return new ComponentFactory_( selector, componentType, viewDefFactory, inputs, outputs, ngContentSelectors); } export function getComponentViewDefinitionFactory(componentFactory: ComponentFactory): ViewDefinitionFactory { return (componentFactory as ComponentFactory_).viewDefFactory; } class ComponentFactory_ extends ComponentFactory { /** * @internal */ viewDefFactory: ViewDefinitionFactory; constructor( public selector: string, public componentType: Type, viewDefFactory: ViewDefinitionFactory, private _inputs: {[propName: string]: string}|null, private _outputs: {[propName: string]: string}, public ngContentSelectors: string[]) { // Attention: this ctor is called as top level function. // Putting any logic in here will destroy closure tree shaking! super(); this.viewDefFactory = viewDefFactory; } get inputs() { const inputsArr: {propName: string, templateName: string}[] = []; const inputs = this._inputs !; for (let propName in inputs) { const templateName = inputs[propName]; inputsArr.push({propName, templateName}); } return inputsArr; } get outputs() { const outputsArr: {propName: string, templateName: string}[] = []; for (let propName in this._outputs) { const templateName = this._outputs[propName]; outputsArr.push({propName, templateName}); } return outputsArr; } /** * Creates a new component. */ create( injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, ngModule?: NgModuleRef): ComponentRef { if (!ngModule) { throw new Error('ngModule should be provided'); } const viewDef = resolveDefinition(this.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element !.componentProvider !.nodeIndex; const view = Services.createRootView( injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); const component = asProviderData(view, componentNodeIndex).instance; if (rootSelectorOrNode) { view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); } return new ComponentRef_(view, new ViewRef_(view), component); } } class ComponentRef_ extends ComponentRef { public readonly hostView: ViewRef; public readonly instance: any; public readonly changeDetectorRef: ChangeDetectorRef; private _elDef: NodeDef; constructor(private _view: ViewData, private _viewRef: ViewRef, private _component: any) { super(); this._elDef = this._view.def.nodes[0]; this.hostView = _viewRef; this.changeDetectorRef = _viewRef; this.instance = _component; } get location(): ElementRef { return new ElementRef(asElementData(this._view, this._elDef.nodeIndex).renderElement); } get injector(): Injector { return new Injector_(this._view, this._elDef); } get componentType(): Type { return this._component.constructor; } destroy(): void { this._viewRef.destroy(); } onDestroy(callback: Function): void { this._viewRef.onDestroy(callback); } } export function createViewContainerData( view: ViewData, elDef: NodeDef, elData: ElementData): ViewContainerData { return new ViewContainerRef_(view, elDef, elData); } class ViewContainerRef_ implements ViewContainerData { /** * @internal */ _embeddedViews: ViewData[] = []; constructor(private _view: ViewData, private _elDef: NodeDef, private _data: ElementData) {} get element(): ElementRef { return new ElementRef(this._data.renderElement); } get injector(): Injector { return new Injector_(this._view, this._elDef); } /** @deprecated No replacement */ get parentInjector(): Injector { let view = this._view; let elDef = this._elDef.parent; while (!elDef && view) { elDef = viewParentEl(view); view = view.parent !; } return view ? new Injector_(view, elDef) : new Injector_(this._view, null); } clear(): void { const len = this._embeddedViews.length; for (let i = len - 1; i >= 0; i--) { const view = detachEmbeddedView(this._data, i) !; Services.destroyView(view); } } get(index: number): ViewRef|null { const view = this._embeddedViews[index]; if (view) { const ref = new ViewRef_(view); ref.attachToViewContainerRef(this); return ref; } return null; } get length(): number { return this._embeddedViews.length; } createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef { const viewRef = templateRef.createEmbeddedView(context || {}); this.insert(viewRef, index); return viewRef; } createComponent( componentFactory: ComponentFactory, index?: number, injector?: Injector, projectableNodes?: any[][], ngModuleRef?: NgModuleRef): ComponentRef { const contextInjector = injector || this.parentInjector; if (!ngModuleRef && !(componentFactory instanceof ComponentFactoryBoundToModule)) { ngModuleRef = contextInjector.get(NgModuleRef); } const componentRef = componentFactory.create(contextInjector, projectableNodes, undefined, ngModuleRef); this.insert(componentRef.hostView, index); return componentRef; } insert(viewRef: ViewRef, index?: number): ViewRef { if (viewRef.destroyed) { throw new Error('Cannot insert a destroyed View in a ViewContainer!'); } const viewRef_ = viewRef; const viewData = viewRef_._view; attachEmbeddedView(this._view, this._data, index, viewData); viewRef_.attachToViewContainerRef(this); return viewRef; } move(viewRef: ViewRef_, currentIndex: number): ViewRef { if (viewRef.destroyed) { throw new Error('Cannot move a destroyed View in a ViewContainer!'); } const previousIndex = this._embeddedViews.indexOf(viewRef._view); moveEmbeddedView(this._data, previousIndex, currentIndex); return viewRef; } indexOf(viewRef: ViewRef): number { return this._embeddedViews.indexOf((viewRef)._view); } remove(index?: number): void { const viewData = detachEmbeddedView(this._data, index); if (viewData) { Services.destroyView(viewData); } } detach(index?: number): ViewRef|null { const view = detachEmbeddedView(this._data, index); return view ? new ViewRef_(view) : null; } } export function createChangeDetectorRef(view: ViewData): ChangeDetectorRef { return new ViewRef_(view); } export class ViewRef_ implements EmbeddedViewRef, InternalViewRef { /** @internal */ _view: ViewData; private _viewContainerRef: ViewContainerRef|null; private _appRef: ApplicationRef|null; constructor(_view: ViewData) { this._view = _view; this._viewContainerRef = null; this._appRef = null; } get rootNodes(): any[] { return rootRenderNodes(this._view); } get context() { return this._view.context; } get destroyed(): boolean { return (this._view.state & ViewState.Destroyed) !== 0; } markForCheck(): void { markParentViewsForCheck(this._view); } detach(): void { this._view.state &= ~ViewState.Attached; } detectChanges(): void { const fs = this._view.root.rendererFactory; if (fs.begin) { fs.begin(); } try { Services.checkAndUpdateView(this._view); } finally { if (fs.end) { fs.end(); } } } checkNoChanges(): void { Services.checkNoChangesView(this._view); } reattach(): void { this._view.state |= ViewState.Attached; } onDestroy(callback: Function) { if (!this._view.disposables) { this._view.disposables = []; } this._view.disposables.push(callback); } destroy() { if (this._appRef) { this._appRef.detachView(this); } else if (this._viewContainerRef) { this._viewContainerRef.detach(this._viewContainerRef.indexOf(this)); } Services.destroyView(this._view); } detachFromAppRef() { this._appRef = null; renderDetachView(this._view); Services.dirtyParentQueries(this._view); } attachToAppRef(appRef: ApplicationRef) { if (this._viewContainerRef) { throw new Error('This view is already attached to a ViewContainer!'); } this._appRef = appRef; } attachToViewContainerRef(vcRef: ViewContainerRef) { if (this._appRef) { throw new Error('This view is already attached directly to the ApplicationRef!'); } this._viewContainerRef = vcRef; } } export function createTemplateData(view: ViewData, def: NodeDef): TemplateData { return new TemplateRef_(view, def); } class TemplateRef_ extends TemplateRef implements TemplateData { /** * @internal */ // TODO(issue/24571): remove '!'. _projectedViews !: ViewData[]; constructor(private _parentView: ViewData, private _def: NodeDef) { super(); } createEmbeddedView(context: any): EmbeddedViewRef { return new ViewRef_(Services.createEmbeddedView( this._parentView, this._def, this._def.element !.template !, context)); } get elementRef(): ElementRef { return new ElementRef(asElementData(this._parentView, this._def.nodeIndex).renderElement); } } export function createInjector(view: ViewData, elDef: NodeDef): Injector { return new Injector_(view, elDef); } class Injector_ implements Injector { constructor(private view: ViewData, private elDef: NodeDef|null) {} get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { const allowPrivateServices = this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false; return Services.resolveDep( this.view, this.elDef, allowPrivateServices, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); } } export function nodeValue(view: ViewData, index: number): any { const def = view.def.nodes[index]; if (def.flags & NodeFlags.TypeElement) { const elData = asElementData(view, def.nodeIndex); return def.element !.template ? elData.template : elData.renderElement; } else if (def.flags & NodeFlags.TypeText) { return asTextData(view, def.nodeIndex).renderText; } else if (def.flags & (NodeFlags.CatProvider | NodeFlags.TypePipe)) { return asProviderData(view, def.nodeIndex).instance; } throw new Error(`Illegal state: read nodeValue for node index ${index}`); } export function createRendererV1(view: ViewData): RendererV1 { return new RendererAdapter(view.renderer); } class RendererAdapter implements RendererV1 { constructor(private delegate: Renderer2) {} selectRootElement(selectorOrNode: string|Element): Element { return this.delegate.selectRootElement(selectorOrNode); } createElement(parent: Element|DocumentFragment, namespaceAndName: string): Element { const [ns, name] = splitNamespace(namespaceAndName); const el = this.delegate.createElement(name, ns); if (parent) { this.delegate.appendChild(parent, el); } return el; } createViewRoot(hostElement: Element): Element|DocumentFragment { return hostElement; } createTemplateAnchor(parentElement: Element|DocumentFragment): Comment { const comment = this.delegate.createComment(''); if (parentElement) { this.delegate.appendChild(parentElement, comment); } return comment; } createText(parentElement: Element|DocumentFragment, value: string): any { const node = this.delegate.createText(value); if (parentElement) { this.delegate.appendChild(parentElement, node); } return node; } projectNodes(parentElement: Element|DocumentFragment, nodes: Node[]) { for (let i = 0; i < nodes.length; i++) { this.delegate.appendChild(parentElement, nodes[i]); } } attachViewAfter(node: Node, viewRootNodes: Node[]) { const parentElement = this.delegate.parentNode(node); const nextSibling = this.delegate.nextSibling(node); for (let i = 0; i < viewRootNodes.length; i++) { this.delegate.insertBefore(parentElement, viewRootNodes[i], nextSibling); } } detachView(viewRootNodes: (Element|Text|Comment)[]) { for (let i = 0; i < viewRootNodes.length; i++) { const node = viewRootNodes[i]; const parentElement = this.delegate.parentNode(node); this.delegate.removeChild(parentElement, node); } } destroyView(hostElement: Element|DocumentFragment, viewAllNodes: Node[]) { for (let i = 0; i < viewAllNodes.length; i++) { this.delegate.destroyNode !(viewAllNodes[i]); } } listen(renderElement: any, name: string, callback: Function): Function { return this.delegate.listen(renderElement, name, callback); } listenGlobal(target: string, name: string, callback: Function): Function { return this.delegate.listen(target, name, callback); } setElementProperty( renderElement: Element|DocumentFragment, propertyName: string, propertyValue: any): void { this.delegate.setProperty(renderElement, propertyName, propertyValue); } setElementAttribute(renderElement: Element, namespaceAndName: string, attributeValue?: string): void { const [ns, name] = splitNamespace(namespaceAndName); if (attributeValue != null) { this.delegate.setAttribute(renderElement, name, attributeValue, ns); } else { this.delegate.removeAttribute(renderElement, name, ns); } } setBindingDebugInfo(renderElement: Element, propertyName: string, propertyValue: string): void {} setElementClass(renderElement: Element, className: string, isAdd: boolean): void { if (isAdd) { this.delegate.addClass(renderElement, className); } else { this.delegate.removeClass(renderElement, className); } } setElementStyle(renderElement: HTMLElement, styleName: string, styleValue?: string): void { if (styleValue != null) { this.delegate.setStyle(renderElement, styleName, styleValue); } else { this.delegate.removeStyle(renderElement, styleName); } } invokeElementMethod(renderElement: Element, methodName: string, args: any[]): void { (renderElement as any)[methodName].apply(renderElement, args); } setText(renderNode: Text, text: string): void { this.delegate.setValue(renderNode, text); } animate(): any { throw new Error('Renderer.animate is no longer supported!'); } } export function createNgModuleRef( moduleType: Type, parent: Injector, bootstrapComponents: Type[], def: NgModuleDefinition): NgModuleRef { return new NgModuleRef_(moduleType, parent, bootstrapComponents, def); } class NgModuleRef_ implements NgModuleData, InternalNgModuleRef { private _destroyListeners: (() => void)[] = []; private _destroyed: boolean = false; /** @internal */ // TODO(issue/24571): remove '!'. _providers !: any[]; /** @internal */ // TODO(issue/24571): remove '!'. _modules !: any[]; readonly injector: Injector = this; constructor( private _moduleType: Type, public _parent: Injector, public _bootstrapComponents: Type[], public _def: NgModuleDefinition) { initNgModule(this); } get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND, injectFlags: InjectFlags = InjectFlags.Default): any { let flags = DepFlags.None; if (injectFlags & InjectFlags.SkipSelf) { flags |= DepFlags.SkipSelf; } else if (injectFlags & InjectFlags.Self) { flags |= DepFlags.Self; } return resolveNgModuleDep( this, {token: token, tokenKey: tokenKey(token), flags: flags}, notFoundValue); } get instance() { return this.get(this._moduleType); } get componentFactoryResolver() { return this.get(ComponentFactoryResolver); } destroy(): void { if (this._destroyed) { throw new Error( `The ng module ${stringify(this.instance.constructor)} has already been destroyed.`); } this._destroyed = true; callNgModuleLifecycle(this, NodeFlags.OnDestroy); this._destroyListeners.forEach((listener) => listener()); } onDestroy(callback: () => void): void { this._destroyListeners.push(callback); } }