/** * @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 {WrappedValue, devModeEqual} from '../change_detection/change_detection'; import {ViewEncapsulation} from '../metadata/view'; import {RendererType2} from '../render/api'; import {looseIdentical, stringify} from '../util'; import {expressionChangedAfterItHasBeenCheckedError} from './errors'; import {BindingDef, BindingFlags, ElementData, NodeDef, NodeFlags, QueryValueType, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asTextData} from './types'; export const NOOP: any = () => {}; const _tokenKeyCache = new Map(); export function tokenKey(token: any): string { let key = _tokenKeyCache.get(token); if (!key) { key = stringify(token) + '_' + _tokenKeyCache.size; _tokenKeyCache.set(token, key); } return key; } export function unwrapValue(view: ViewData, nodeIdx: number, bindingIdx: number, value: any): any { if (value instanceof WrappedValue) { value = value.wrapped; let globalBindingIdx = view.def.nodes[nodeIdx].bindingIndex + bindingIdx; let oldValue = view.oldValues[globalBindingIdx]; if (oldValue instanceof WrappedValue) { oldValue = oldValue.wrapped; } view.oldValues[globalBindingIdx] = new WrappedValue(oldValue); } return value; } const UNDEFINED_RENDERER_TYPE_ID = '$$undefined'; const EMPTY_RENDERER_TYPE_ID = '$$empty'; // Attention: this function is called as top level function. // Putting any logic in here will destroy closure tree shaking! export function createRendererType2(values: { styles: (string | any[])[], encapsulation: ViewEncapsulation, data: {[kind: string]: any[]} }): RendererType2 { return { id: UNDEFINED_RENDERER_TYPE_ID, styles: values.styles, encapsulation: values.encapsulation, data: values.data }; } let _renderCompCount = 0; export function resolveRendererType2(type: RendererType2): RendererType2 { if (type && type.id === UNDEFINED_RENDERER_TYPE_ID) { // first time we see this RendererType2. Initialize it... const isFilled = ((type.encapsulation != null && type.encapsulation !== ViewEncapsulation.None) || type.styles.length || Object.keys(type.data).length); if (isFilled) { type.id = `c${_renderCompCount++}`; } else { type.id = EMPTY_RENDERER_TYPE_ID; } } if (type && type.id === EMPTY_RENDERER_TYPE_ID) { type = null; } return type; } export function checkBinding( view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean { const oldValues = view.oldValues; if ((view.state & ViewState.FirstCheck) || !looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) { return true; } return false; } export function checkAndUpdateBinding( view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean { if (checkBinding(view, def, bindingIdx, value)) { view.oldValues[def.bindingIndex + bindingIdx] = value; return true; } return false; } export function checkBindingNoChanges( view: ViewData, def: NodeDef, bindingIdx: number, value: any) { const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; if ((view.state & ViewState.FirstCheck) || !devModeEqual(oldValue, value)) { throw expressionChangedAfterItHasBeenCheckedError( Services.createDebugContext(view, def.index), oldValue, value, (view.state & ViewState.FirstCheck) !== 0); } } export function markParentViewsForCheck(view: ViewData) { let currView = view; while (currView) { if (currView.def.flags & ViewFlags.OnPush) { currView.state |= ViewState.ChecksEnabled; } currView = currView.viewContainerParent || currView.parent; } } export function dispatchEvent( view: ViewData, nodeIndex: number, eventName: string, event: any): boolean { const nodeDef = view.def.nodes[nodeIndex]; const startView = nodeDef.flags & NodeFlags.ComponentView ? asElementData(view, nodeIndex).componentView : view; markParentViewsForCheck(startView); return Services.handleEvent(view, nodeIndex, eventName, event); } export function declaredViewContainer(view: ViewData): ElementData { if (view.parent) { const parentView = view.parent; return asElementData(parentView, view.parentNodeDef.index); } return undefined; } /** * for component views, this is the host element. * for embedded views, this is the index of the parent node * that contains the view container. */ export function viewParentEl(view: ViewData): NodeDef { const parentView = view.parent; if (parentView) { return view.parentNodeDef.parent; } else { return null; } } export function renderNode(view: ViewData, def: NodeDef): any { switch (def.flags & NodeFlags.Types) { case NodeFlags.TypeElement: return asElementData(view, def.index).renderElement; case NodeFlags.TypeText: return asTextData(view, def.index).renderText; } } export function elementEventFullName(target: string, name: string): string { return target ? `${target}:${name}` : name; } export function isComponentView(view: ViewData): boolean { return !!view.parent && !!(view.parentNodeDef.flags & NodeFlags.Component); } export function isEmbeddedView(view: ViewData): boolean { return !!view.parent && !(view.parentNodeDef.flags & NodeFlags.Component); } export function filterQueryId(queryId: number): number { return 1 << (queryId % 32); } export function splitMatchedQueriesDsl(matchedQueriesDsl: [string | number, QueryValueType][]): { matchedQueries: {[queryId: string]: QueryValueType}, references: {[refId: string]: QueryValueType}, matchedQueryIds: number } { const matchedQueries: {[queryId: string]: QueryValueType} = {}; let matchedQueryIds = 0; const references: {[refId: string]: QueryValueType} = {}; if (matchedQueriesDsl) { matchedQueriesDsl.forEach(([queryId, valueType]) => { if (typeof queryId === 'number') { matchedQueries[queryId] = valueType; matchedQueryIds |= filterQueryId(queryId); } else { references[queryId] = valueType; } }); } return {matchedQueries, references, matchedQueryIds}; } export function getParentRenderElement(view: ViewData, renderHost: any, def: NodeDef): any { let renderParent = def.renderParent; if (renderParent) { if ((renderParent.flags & NodeFlags.TypeElement) === 0 || (renderParent.flags & NodeFlags.ComponentView) === 0 || (renderParent.element.componentRendererType && renderParent.element.componentRendererType.encapsulation === ViewEncapsulation.Native)) { // only children of non components, or children of components with native encapsulation should // be attached. return asElementData(view, def.renderParent.index).renderElement; } } else { return renderHost; } } const VIEW_DEFINITION_CACHE = new WeakMap(); export function resolveViewDefinition(factory: ViewDefinitionFactory): ViewDefinition { let value: ViewDefinition = VIEW_DEFINITION_CACHE.get(factory); if (!value) { value = factory(() => NOOP); value.factory = factory; VIEW_DEFINITION_CACHE.set(factory, value); } return value; } export function rootRenderNodes(view: ViewData): any[] { const renderNodes: any[] = []; visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes); return renderNodes; } export const enum RenderNodeAction {Collect, AppendChild, InsertBefore, RemoveChild} export function visitRootRenderNodes( view: ViewData, action: RenderNodeAction, parentNode: any, nextSibling: any, target: any[]) { // We need to re-compute the parent node in case the nodes have been moved around manually if (action === RenderNodeAction.RemoveChild) { parentNode = view.renderer.parentNode(renderNode(view, view.def.lastRenderRootNode)); } visitSiblingRenderNodes( view, action, 0, view.def.nodes.length - 1, parentNode, nextSibling, target); } export function visitSiblingRenderNodes( view: ViewData, action: RenderNodeAction, startIndex: number, endIndex: number, parentNode: any, nextSibling: any, target: any[]) { for (let i = startIndex; i <= endIndex; i++) { const nodeDef = view.def.nodes[i]; if (nodeDef.flags & (NodeFlags.TypeElement | NodeFlags.TypeText | NodeFlags.TypeNgContent)) { visitRenderNode(view, nodeDef, action, parentNode, nextSibling, target); } // jump to next sibling i += nodeDef.childCount; } } export function visitProjectedRenderNodes( view: ViewData, ngContentIndex: number, action: RenderNodeAction, parentNode: any, nextSibling: any, target: any[]) { let compView = view; while (compView && !isComponentView(compView)) { compView = compView.parent; } const hostView = compView.parent; const hostElDef = viewParentEl(compView); const startIndex = hostElDef.index + 1; const endIndex = hostElDef.index + hostElDef.childCount; for (let i = startIndex; i <= endIndex; i++) { const nodeDef = hostView.def.nodes[i]; if (nodeDef.ngContentIndex === ngContentIndex) { visitRenderNode(hostView, nodeDef, action, parentNode, nextSibling, target); } // jump to next sibling i += nodeDef.childCount; } if (!hostView.parent) { // a root view const projectedNodes = view.root.projectableNodes[ngContentIndex]; if (projectedNodes) { for (let i = 0; i < projectedNodes.length; i++) { execRenderNodeAction(view, projectedNodes[i], action, parentNode, nextSibling, target); } } } } function visitRenderNode( view: ViewData, nodeDef: NodeDef, action: RenderNodeAction, parentNode: any, nextSibling: any, target: any[]) { if (nodeDef.flags & NodeFlags.TypeNgContent) { visitProjectedRenderNodes( view, nodeDef.ngContent.index, action, parentNode, nextSibling, target); } else { const rn = renderNode(view, nodeDef); if (action === RenderNodeAction.RemoveChild && (nodeDef.flags & NodeFlags.ComponentView) && (nodeDef.bindingFlags & BindingFlags.CatSyntheticProperty)) { // Note: we might need to do both actions. if (nodeDef.bindingFlags & (BindingFlags.SyntheticProperty)) { execRenderNodeAction(view, rn, action, parentNode, nextSibling, target); } if (nodeDef.bindingFlags & (BindingFlags.SyntheticHostProperty)) { const compView = asElementData(view, nodeDef.index).componentView; execRenderNodeAction(compView, rn, action, parentNode, nextSibling, target); } } else { execRenderNodeAction(view, rn, action, parentNode, nextSibling, target); } if (nodeDef.flags & NodeFlags.EmbeddedViews) { const embeddedViews = asElementData(view, nodeDef.index).viewContainer._embeddedViews; for (let k = 0; k < embeddedViews.length; k++) { visitRootRenderNodes(embeddedViews[k], action, parentNode, nextSibling, target); } } if (nodeDef.flags & NodeFlags.TypeElement && !nodeDef.element.name) { visitSiblingRenderNodes( view, action, nodeDef.index + 1, nodeDef.index + nodeDef.childCount, parentNode, nextSibling, target); } } } function execRenderNodeAction( view: ViewData, renderNode: any, action: RenderNodeAction, parentNode: any, nextSibling: any, target: any[]) { const renderer = view.renderer; switch (action) { case RenderNodeAction.AppendChild: renderer.appendChild(parentNode, renderNode); break; case RenderNodeAction.InsertBefore: renderer.insertBefore(parentNode, renderNode, nextSibling); break; case RenderNodeAction.RemoveChild: renderer.removeChild(parentNode, renderNode); break; case RenderNodeAction.Collect: target.push(renderNode); break; } } const NS_PREFIX_RE = /^:([^:]+):(.+)$/; export function splitNamespace(name: string): string[] { if (name[0] === ':') { const match = name.match(NS_PREFIX_RE); return [match[1], match[2]]; } return ['', name]; } export function calcBindingFlags(bindings: BindingDef[]): BindingFlags { let flags = 0; for (let i = 0; i < bindings.length; i++) { flags |= bindings[i].flags; } return flags; } export function interpolate(valueCount: number, constAndInterp: string[]): string { let result = ''; for (let i = 0; i < valueCount * 2; i = i + 2) { result = result + constAndInterp[i] + _toStringWithNull(constAndInterp[i + 1]); } return result + constAndInterp[valueCount * 2]; } export function inlineInterpolate( valueCount: number, c0: string, a1: any, c1: string, a2?: any, c2?: string, a3?: any, c3?: string, a4?: any, c4?: string, a5?: any, c5?: string, a6?: any, c6?: string, a7?: any, c7?: string, a8?: any, c8?: string, a9?: any, c9?: string): string { switch (valueCount) { case 1: return c0 + _toStringWithNull(a1) + c1; case 2: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2; case 3: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3; case 4: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3 + _toStringWithNull(a4) + c4; case 5: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5; case 6: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) + c6; case 7: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) + c6 + _toStringWithNull(a7) + c7; case 8: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) + c6 + _toStringWithNull(a7) + c7 + _toStringWithNull(a8) + c8; case 9: return c0 + _toStringWithNull(a1) + c1 + _toStringWithNull(a2) + c2 + _toStringWithNull(a3) + c3 + _toStringWithNull(a4) + c4 + _toStringWithNull(a5) + c5 + _toStringWithNull(a6) + c6 + _toStringWithNull(a7) + c7 + _toStringWithNull(a8) + c8 + _toStringWithNull(a9) + c9; default: throw new Error(`Does not support more than 9 expressions`); } } function _toStringWithNull(v: any): string { return v != null ? v.toString() : ''; } export const EMPTY_ARRAY: any[] = []; export const EMPTY_MAP: {[key: string]: any} = {};