feat(core): view engine - add support for `OnPush` and detached views. (#14216)
Part of #14013 PR Close #14216
This commit is contained in:
parent
08ff67ea11
commit
45e1e36477
|
@ -10,7 +10,7 @@ import {isDevMode} from '../application_ref';
|
||||||
import {SecurityContext} from '../security';
|
import {SecurityContext} from '../security';
|
||||||
|
|
||||||
import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementOutputDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types';
|
import {BindingDef, BindingType, DebugContext, DisposableFn, ElementData, ElementOutputDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, QueryValueType, ViewData, ViewDefinition, ViewFlags, asElementData} from './types';
|
||||||
import {checkAndUpdateBinding, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack, unwrapValue} from './util';
|
import {checkAndUpdateBinding, dispatchEvent, entryAction, setBindingDebugInfo, setCurrentNode, sliceErrorStack, unwrapValue} from './util';
|
||||||
|
|
||||||
export function anchorDef(
|
export function anchorDef(
|
||||||
flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
|
flags: NodeFlags, matchedQueries: [string, QueryValueType][], ngContentIndex: number,
|
||||||
|
@ -192,17 +192,14 @@ export function createElement(view: ViewData, renderHost: any, def: NodeDef): El
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
function renderEventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
||||||
return entryAction(EntryAction.HandleEvent, (event: any) => {
|
return entryAction(
|
||||||
setCurrentNode(view, index);
|
EntryAction.HandleEvent, (event: any) => dispatchEvent(view, index, eventName, event));
|
||||||
return view.def.handleEvent(view, index, eventName, event);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
function directDomEventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
||||||
return entryAction(EntryAction.HandleEvent, (event: any) => {
|
return entryAction(EntryAction.HandleEvent, (event: any) => {
|
||||||
setCurrentNode(view, index);
|
const result = dispatchEvent(view, index, eventName, event);
|
||||||
const result = view.def.handleEvent(view, index, eventName, event);
|
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,10 @@
|
||||||
|
|
||||||
import {BaseError, WrappedError} from '../facade/errors';
|
import {BaseError, WrappedError} from '../facade/errors';
|
||||||
|
|
||||||
import {DebugContext} from './types';
|
import {DebugContext, EntryAction, ViewState} from './types';
|
||||||
|
|
||||||
export function expressionChangedAfterItHasBeenCheckedError(
|
export function expressionChangedAfterItHasBeenCheckedError(
|
||||||
context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewError {
|
context: DebugContext, oldValue: any, currValue: any, isFirstCheck: boolean): ViewDebugError {
|
||||||
let msg =
|
let msg =
|
||||||
`Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`;
|
`Expression has changed after it was checked. Previous value: '${oldValue}'. Current value: '${currValue}'.`;
|
||||||
if (isFirstCheck) {
|
if (isFirstCheck) {
|
||||||
|
@ -19,25 +19,30 @@ export function expressionChangedAfterItHasBeenCheckedError(
|
||||||
` It seems like the view has been created after its parent and its children have been dirty checked.` +
|
` It seems like the view has been created after its parent and its children have been dirty checked.` +
|
||||||
` Has it been created in a change detection hook ?`;
|
` Has it been created in a change detection hook ?`;
|
||||||
}
|
}
|
||||||
return viewError(msg, context);
|
return viewDebugError(msg, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function viewWrappedError(originalError: any, context: DebugContext): WrappedError&
|
export function viewWrappedDebugError(originalError: any, context: DebugContext): WrappedError&
|
||||||
ViewError {
|
ViewDebugError {
|
||||||
const err = viewError(originalError.message, context) as WrappedError & ViewError;
|
const err = viewDebugError(originalError.message, context) as WrappedError & ViewDebugError;
|
||||||
err.originalError = originalError;
|
err.originalError = originalError;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ViewError { context: DebugContext; }
|
export interface ViewDebugError { context: DebugContext; }
|
||||||
|
|
||||||
export function viewError(msg: string, context: DebugContext): ViewError {
|
export function viewDebugError(msg: string, context: DebugContext): ViewDebugError {
|
||||||
const err = new Error(msg) as any;
|
const err = new Error(msg) as any;
|
||||||
err.context = context;
|
err.context = context;
|
||||||
err.stack = context.source;
|
err.stack = context.source;
|
||||||
|
context.view.state = ViewState.Errored;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isViewError(err: any): boolean {
|
export function isViewDebugError(err: any): boolean {
|
||||||
return err.context;
|
return err.context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function viewDestroyedError(action: EntryAction): Error {
|
||||||
|
return new Error(`View has been used after destroy for ${EntryAction[action]}`);
|
||||||
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@ import {ViewContainerRef} from '../linker/view_container_ref';
|
||||||
import {Renderer} from '../render/api';
|
import {Renderer} from '../render/api';
|
||||||
|
|
||||||
import {queryDef} from './query';
|
import {queryDef} from './query';
|
||||||
import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, asElementData, asProviderData} from './types';
|
import {BindingDef, BindingType, DepDef, DepFlags, DisposableFn, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderOutputDef, QueryBindingType, QueryDef, QueryValueType, Services, ViewData, ViewDefinition, ViewFlags, ViewState, asElementData, asProviderData} from './types';
|
||||||
import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util';
|
import {checkAndUpdateBinding, dispatchEvent, entryAction, setBindingDebugInfo, setCurrentNode, unwrapValue} from './util';
|
||||||
|
|
||||||
const _tokenKeyCache = new Map<any, string>();
|
const _tokenKeyCache = new Map<any, string>();
|
||||||
|
|
||||||
|
@ -122,10 +122,8 @@ export function createProvider(
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
function eventHandlerClosure(view: ViewData, index: number, eventName: string) {
|
||||||
return entryAction(EntryAction.HandleEvent, (event: any) => {
|
return entryAction(
|
||||||
setCurrentNode(view, index);
|
EntryAction.HandleEvent, (event: any) => dispatchEvent(view, index, eventName, event));
|
||||||
view.def.handleEvent(view, index, eventName, event);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkAndUpdateProviderInline(
|
export function checkAndUpdateProviderInline(
|
||||||
|
@ -159,7 +157,7 @@ export function checkAndUpdateProviderInline(
|
||||||
if (changes) {
|
if (changes) {
|
||||||
provider.ngOnChanges(changes);
|
provider.ngOnChanges(changes);
|
||||||
}
|
}
|
||||||
if (view.firstChange && (def.flags & NodeFlags.OnInit)) {
|
if (view.state === ViewState.FirstCheck && (def.flags & NodeFlags.OnInit)) {
|
||||||
provider.ngOnInit();
|
provider.ngOnInit();
|
||||||
}
|
}
|
||||||
if (def.flags & NodeFlags.DoCheck) {
|
if (def.flags & NodeFlags.DoCheck) {
|
||||||
|
@ -176,7 +174,7 @@ export function checkAndUpdateProviderDynamic(view: ViewData, def: NodeDef, valu
|
||||||
if (changes) {
|
if (changes) {
|
||||||
provider.ngOnChanges(changes);
|
provider.ngOnChanges(changes);
|
||||||
}
|
}
|
||||||
if (view.firstChange && (def.flags & NodeFlags.OnInit)) {
|
if (view.state === ViewState.FirstCheck && (def.flags & NodeFlags.OnInit)) {
|
||||||
provider.ngOnInit();
|
provider.ngOnInit();
|
||||||
}
|
}
|
||||||
if (def.flags & NodeFlags.DoCheck) {
|
if (def.flags & NodeFlags.DoCheck) {
|
||||||
|
@ -272,8 +270,10 @@ function checkAndUpdateProp(
|
||||||
let change: SimpleChange;
|
let change: SimpleChange;
|
||||||
let changed: boolean;
|
let changed: boolean;
|
||||||
if (def.flags & NodeFlags.OnChanges) {
|
if (def.flags & NodeFlags.OnChanges) {
|
||||||
change = checkAndUpdateBindingWithChange(view, def, bindingIdx, value);
|
const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
|
||||||
changed = !!change;
|
changed = checkAndUpdateBinding(view, def, bindingIdx, value);
|
||||||
|
change =
|
||||||
|
changed ? new SimpleChange(oldValue, value, view.state === ViewState.FirstCheck) : null;
|
||||||
} else {
|
} else {
|
||||||
changed = checkAndUpdateBinding(view, def, bindingIdx, value);
|
changed = checkAndUpdateBinding(view, def, bindingIdx, value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,7 +132,7 @@ export function checkAndUpdatePureExpressionInline(
|
||||||
case 4:
|
case 4:
|
||||||
value[3] = v3;
|
value[3] = v3;
|
||||||
case 3:
|
case 3:
|
||||||
value[3] = v2;
|
value[2] = v2;
|
||||||
case 2:
|
case 2:
|
||||||
value[1] = v1;
|
value[1] = v1;
|
||||||
case 1:
|
case 1:
|
||||||
|
@ -235,7 +235,7 @@ export function checkAndUpdatePureExpressionDynamic(view: ViewData, def: NodeDef
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
params[i] = unwrapValue(values[i]);
|
params[i] = unwrapValue(values[i]);
|
||||||
}
|
}
|
||||||
value = data.pipe.transform(params[0], ...params.slice(1));
|
value = (<any>data.pipe.transform)(...params);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
data.value = value;
|
data.value = value;
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {Sanitizer, SecurityContext} from '../security';
|
||||||
|
|
||||||
import {createInjector} from './provider';
|
import {createInjector} from './provider';
|
||||||
import {getQueryValue} from './query';
|
import {getQueryValue} from './query';
|
||||||
import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, asElementData} from './types';
|
import {DebugContext, ElementData, NodeData, NodeDef, NodeType, Services, ViewData, ViewDefinition, ViewState, asElementData} from './types';
|
||||||
import {isComponentView, renderNode, rootRenderNodes} from './util';
|
import {isComponentView, renderNode, rootRenderNodes} from './util';
|
||||||
import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view';
|
import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view';
|
||||||
import {attachEmbeddedView, detachEmbeddedView} from './view_attach';
|
import {attachEmbeddedView, detachEmbeddedView} from './view_attach';
|
||||||
|
@ -112,13 +112,22 @@ class ViewRef_ implements EmbeddedViewRef<any> {
|
||||||
|
|
||||||
get context() { return this._view.context; }
|
get context() { return this._view.context; }
|
||||||
|
|
||||||
get destroyed(): boolean { return unimplemented(); }
|
get destroyed(): boolean { return this._view.state === ViewState.Destroyed; }
|
||||||
|
|
||||||
markForCheck(): void { unimplemented(); }
|
markForCheck(): void { this.reattach(); }
|
||||||
detach(): void { unimplemented(); }
|
detach(): void {
|
||||||
|
if (this._view.state === ViewState.ChecksEnabled) {
|
||||||
|
this._view.state = ViewState.ChecksDisabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
detectChanges(): void { checkAndUpdateView(this._view); }
|
detectChanges(): void { checkAndUpdateView(this._view); }
|
||||||
checkNoChanges(): void { checkNoChangesView(this._view); }
|
checkNoChanges(): void { checkNoChangesView(this._view); }
|
||||||
reattach(): void { unimplemented(); }
|
|
||||||
|
reattach(): void {
|
||||||
|
if (this._view.state === ViewState.ChecksDisabled) {
|
||||||
|
this._view.state = ViewState.ChecksEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
onDestroy(callback: Function) { unimplemented(); }
|
onDestroy(callback: Function) { unimplemented(); }
|
||||||
|
|
||||||
destroy() { unimplemented(); }
|
destroy() { unimplemented(); }
|
||||||
|
|
|
@ -56,7 +56,8 @@ export type ViewHandleEventFn =
|
||||||
*/
|
*/
|
||||||
export enum ViewFlags {
|
export enum ViewFlags {
|
||||||
None = 0,
|
None = 0,
|
||||||
DirectDom = 1 << 1
|
DirectDom = 1 << 1,
|
||||||
|
OnPush = 1 << 2
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -271,11 +272,19 @@ export interface ViewData {
|
||||||
// and call the right accessor (e.g. `elementData`) based on
|
// and call the right accessor (e.g. `elementData`) based on
|
||||||
// the NodeType.
|
// the NodeType.
|
||||||
nodes: {[key: number]: NodeData};
|
nodes: {[key: number]: NodeData};
|
||||||
firstChange: boolean;
|
state: ViewState;
|
||||||
oldValues: any[];
|
oldValues: any[];
|
||||||
disposables: DisposableFn[];
|
disposables: DisposableFn[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ViewState {
|
||||||
|
FirstCheck,
|
||||||
|
ChecksEnabled,
|
||||||
|
ChecksDisabled,
|
||||||
|
Errored,
|
||||||
|
Destroyed
|
||||||
|
}
|
||||||
|
|
||||||
export type DisposableFn = () => void;
|
export type DisposableFn = () => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -12,8 +12,8 @@ import {SimpleChange} from '../change_detection/change_detection_util';
|
||||||
import {looseIdentical} from '../facade/lang';
|
import {looseIdentical} from '../facade/lang';
|
||||||
import {Renderer} from '../render/api';
|
import {Renderer} from '../render/api';
|
||||||
|
|
||||||
import {expressionChangedAfterItHasBeenCheckedError, isViewError, viewWrappedError} from './errors';
|
import {expressionChangedAfterItHasBeenCheckedError, isViewDebugError, viewDestroyedError, viewWrappedDebugError} from './errors';
|
||||||
import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, asElementData, asTextData} from './types';
|
import {ElementData, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewState, asElementData, asProviderData, asTextData} from './types';
|
||||||
|
|
||||||
export function setBindingDebugInfo(
|
export function setBindingDebugInfo(
|
||||||
renderer: Renderer, renderNode: any, propName: string, value: any) {
|
renderer: Renderer, renderNode: any, propName: string, value: any) {
|
||||||
|
@ -36,31 +36,41 @@ function camelCaseToDashCase(input: string): string {
|
||||||
export function checkBindingNoChanges(
|
export function checkBindingNoChanges(
|
||||||
view: ViewData, def: NodeDef, bindingIdx: number, value: any) {
|
view: ViewData, def: NodeDef, bindingIdx: number, value: any) {
|
||||||
const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
|
const oldValue = view.oldValues[def.bindingIndex + bindingIdx];
|
||||||
if (view.firstChange || !devModeEqual(oldValue, value)) {
|
if (view.state === ViewState.FirstCheck || !devModeEqual(oldValue, value)) {
|
||||||
throw expressionChangedAfterItHasBeenCheckedError(
|
throw expressionChangedAfterItHasBeenCheckedError(
|
||||||
view.services.createDebugContext(view, def.index), oldValue, value, view.firstChange);
|
view.services.createDebugContext(view, def.index), oldValue, value,
|
||||||
|
view.state === ViewState.FirstCheck);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkAndUpdateBinding(
|
export function checkAndUpdateBinding(
|
||||||
view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean {
|
view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean {
|
||||||
const oldValues = view.oldValues;
|
const oldValues = view.oldValues;
|
||||||
if (view.firstChange || !looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) {
|
if (view.state === ViewState.FirstCheck ||
|
||||||
|
!looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) {
|
||||||
oldValues[def.bindingIndex + bindingIdx] = value;
|
oldValues[def.bindingIndex + bindingIdx] = value;
|
||||||
|
if (def.flags & NodeFlags.HasComponent) {
|
||||||
|
const compView = asProviderData(view, def.index).componentView;
|
||||||
|
if (compView.state === ViewState.ChecksDisabled && compView.def.flags & ViewFlags.OnPush) {
|
||||||
|
compView.state = ViewState.ChecksEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkAndUpdateBindingWithChange(
|
export function dispatchEvent(
|
||||||
view: ViewData, def: NodeDef, bindingIdx: number, value: any): SimpleChange {
|
view: ViewData, nodeIndex: number, eventName: string, event: any): boolean {
|
||||||
const oldValues = view.oldValues;
|
setCurrentNode(view, nodeIndex);
|
||||||
const oldValue = oldValues[def.bindingIndex + bindingIdx];
|
let currView = view;
|
||||||
if (view.firstChange || !looseIdentical(oldValue, value)) {
|
while (currView) {
|
||||||
oldValues[def.bindingIndex + bindingIdx] = value;
|
if (currView.state === ViewState.ChecksDisabled && currView.def.flags & ViewFlags.OnPush) {
|
||||||
return new SimpleChange(oldValue, value, view.firstChange);
|
currView.state = ViewState.ChecksEnabled;
|
||||||
|
}
|
||||||
|
currView = currView.parent;
|
||||||
}
|
}
|
||||||
return null;
|
return view.def.handleEvent(view, nodeIndex, eventName, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapValue(value: any): any {
|
export function unwrapValue(value: any): any {
|
||||||
|
@ -141,6 +151,9 @@ export function currentAction() {
|
||||||
* or code of the framework that might throw as a valid use case.
|
* or code of the framework that might throw as a valid use case.
|
||||||
*/
|
*/
|
||||||
export function setCurrentNode(view: ViewData, nodeIndex: number) {
|
export function setCurrentNode(view: ViewData, nodeIndex: number) {
|
||||||
|
if (view.state === ViewState.Destroyed) {
|
||||||
|
throw viewDestroyedError(_currentAction);
|
||||||
|
}
|
||||||
_currentView = view;
|
_currentView = view;
|
||||||
_currentNodeIndex = nodeIndex;
|
_currentNodeIndex = nodeIndex;
|
||||||
}
|
}
|
||||||
|
@ -170,15 +183,14 @@ function callWithTryCatch(fn: (a: any) => any, arg: any): any {
|
||||||
try {
|
try {
|
||||||
return fn(arg);
|
return fn(arg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isViewError(e) || !_currentView) {
|
if (isViewDebugError(e) || !_currentView) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
const debugContext = _currentView.services.createDebugContext(_currentView, _currentNodeIndex);
|
const debugContext = _currentView.services.createDebugContext(_currentView, _currentNodeIndex);
|
||||||
throw viewWrappedError(e, debugContext);
|
throw viewWrappedDebugError(e, debugContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function rootRenderNodes(view: ViewData): any[] {
|
export function rootRenderNodes(view: ViewData): any[] {
|
||||||
const renderNodes: any[] = [];
|
const renderNodes: any[] = [];
|
||||||
visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes);
|
visitRootRenderNodes(view, RenderNodeAction.Collect, undefined, undefined, renderNodes);
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAnd
|
||||||
import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression';
|
import {checkAndUpdatePureExpressionDynamic, checkAndUpdatePureExpressionInline, createPureExpression} from './pure_expression';
|
||||||
import {checkAndUpdateQuery, createQuery, queryDef} from './query';
|
import {checkAndUpdateQuery, createQuery, queryDef} from './query';
|
||||||
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
|
import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text';
|
||||||
import {ElementDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList} from './types';
|
import {ElementDef, EntryAction, NodeData, NodeDef, NodeFlags, NodeType, ProviderData, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, asElementData, asProviderData, asPureExpressionData, asQueryList} from './types';
|
||||||
import {checkBindingNoChanges, currentAction, currentNodeIndex, currentView, entryAction, isComponentView, resolveViewDefinition, setCurrentNode} from './util';
|
import {checkBindingNoChanges, currentAction, currentNodeIndex, currentView, entryAction, isComponentView, resolveViewDefinition, setCurrentNode} from './util';
|
||||||
|
|
||||||
const NOOP = (): any => undefined;
|
const NOOP = (): any => undefined;
|
||||||
|
@ -260,7 +260,7 @@ function createView(
|
||||||
parentDiIndex,
|
parentDiIndex,
|
||||||
context: undefined,
|
context: undefined,
|
||||||
component: undefined, nodes,
|
component: undefined, nodes,
|
||||||
firstChange: true, renderer, services,
|
state: ViewState.FirstCheck, renderer, services,
|
||||||
oldValues: new Array(def.bindingCount), disposables
|
oldValues: new Array(def.bindingCount), disposables
|
||||||
};
|
};
|
||||||
return view;
|
return view;
|
||||||
|
@ -348,13 +348,22 @@ function _checkAndUpdateView(view: ViewData) {
|
||||||
execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckAndUpdate);
|
execQueriesAction(view, NodeFlags.HasContentQuery, QueryAction.CheckAndUpdate);
|
||||||
|
|
||||||
callLifecycleHooksChildrenFirst(
|
callLifecycleHooksChildrenFirst(
|
||||||
view, NodeFlags.AfterContentChecked | (view.firstChange ? NodeFlags.AfterContentInit : 0));
|
view, NodeFlags.AfterContentChecked |
|
||||||
|
(view.state === ViewState.FirstCheck ? NodeFlags.AfterContentInit : 0));
|
||||||
execComponentViewsAction(view, ViewAction.CheckAndUpdate);
|
execComponentViewsAction(view, ViewAction.CheckAndUpdate);
|
||||||
execQueriesAction(view, NodeFlags.HasViewQuery, QueryAction.CheckAndUpdate);
|
execQueriesAction(view, NodeFlags.HasViewQuery, QueryAction.CheckAndUpdate);
|
||||||
|
|
||||||
callLifecycleHooksChildrenFirst(
|
callLifecycleHooksChildrenFirst(
|
||||||
view, NodeFlags.AfterViewChecked | (view.firstChange ? NodeFlags.AfterViewInit : 0));
|
view, NodeFlags.AfterViewChecked |
|
||||||
view.firstChange = false;
|
(view.state === ViewState.FirstCheck ? NodeFlags.AfterViewInit : 0));
|
||||||
|
|
||||||
|
if (view.state === ViewState.FirstCheck || view.state === ViewState.ChecksEnabled) {
|
||||||
|
if (view.def.flags & ViewFlags.OnPush) {
|
||||||
|
view.state = ViewState.ChecksDisabled;
|
||||||
|
} else {
|
||||||
|
view.state = ViewState.ChecksEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkNodeInline(
|
export function checkNodeInline(
|
||||||
|
@ -465,7 +474,8 @@ function checkNoChangesQuery(view: ViewData, nodeDef: NodeDef) {
|
||||||
if (queryList.dirty) {
|
if (queryList.dirty) {
|
||||||
throw expressionChangedAfterItHasBeenCheckedError(
|
throw expressionChangedAfterItHasBeenCheckedError(
|
||||||
view.services.createDebugContext(view, nodeDef.index),
|
view.services.createDebugContext(view, nodeDef.index),
|
||||||
`Query ${nodeDef.query.id} not dirty`, `Query ${nodeDef.query.id} dirty`, view.firstChange);
|
`Query ${nodeDef.query.id} not dirty`, `Query ${nodeDef.query.id} dirty`,
|
||||||
|
view.state === ViewState.FirstCheck);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,6 +490,7 @@ function _destroyView(view: ViewData) {
|
||||||
}
|
}
|
||||||
execComponentViewsAction(view, ViewAction.Destroy);
|
execComponentViewsAction(view, ViewAction.Destroy);
|
||||||
execEmbeddedViewsAction(view, ViewAction.Destroy);
|
execEmbeddedViewsAction(view, ViewAction.Destroy);
|
||||||
|
view.state = ViewState.Destroyed;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ViewAction {
|
enum ViewAction {
|
||||||
|
@ -536,10 +547,14 @@ function execEmbeddedViewsAction(view: ViewData, action: ViewAction) {
|
||||||
function callViewAction(view: ViewData, action: ViewAction) {
|
function callViewAction(view: ViewData, action: ViewAction) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case ViewAction.CheckNoChanges:
|
case ViewAction.CheckNoChanges:
|
||||||
_checkNoChangesView(view);
|
if (view.state === ViewState.ChecksEnabled || view.state === ViewState.FirstCheck) {
|
||||||
|
_checkNoChangesView(view);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ViewAction.CheckAndUpdate:
|
case ViewAction.CheckAndUpdate:
|
||||||
_checkAndUpdateView(view);
|
if (view.state === ViewState.ChecksEnabled || view.state === ViewState.FirstCheck) {
|
||||||
|
_checkAndUpdateView(view);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case ViewAction.Destroy:
|
case ViewAction.Destroy:
|
||||||
_destroyView(view);
|
_destroyView(view);
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
|
import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
|
||||||
import {BindingType, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
|
import {BindingType, DefaultServices, NodeDef, NodeFlags, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, anchorDef, asProviderData, checkAndUpdateView, checkNoChangesView, checkNodeDynamic, checkNodeInline, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, setCurrentNode, textDef, viewDef} from '@angular/core/src/view/index';
|
||||||
import {inject} from '@angular/core/testing';
|
import {inject} from '@angular/core/testing';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
|
|
||||||
|
@ -35,8 +35,9 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function compViewDef(
|
function compViewDef(
|
||||||
nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn): ViewDefinition {
|
nodes: NodeDef[], update?: ViewUpdateFn, handleEvent?: ViewHandleEventFn,
|
||||||
return viewDef(config.viewFlags, nodes, update, handleEvent, renderComponentType);
|
flags?: ViewFlags): ViewDefinition {
|
||||||
|
return viewDef(config.viewFlags | flags, nodes, update, handleEvent, renderComponentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
|
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
|
||||||
|
@ -69,65 +70,211 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
|
||||||
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
|
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dirty check component views', () => {
|
describe('data binding', () => {
|
||||||
let value = 'v1';
|
it('should dirty check component views', () => {
|
||||||
class AComp {
|
let value: any;
|
||||||
a: any;
|
class AComp {
|
||||||
}
|
a: any;
|
||||||
|
}
|
||||||
|
|
||||||
const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => {
|
const update = jasmine.createSpy('updater').and.callFake((view: ViewData) => {
|
||||||
setCurrentNode(view, 0);
|
setCurrentNode(view, 0);
|
||||||
checkNodeInline(value);
|
checkNodeInline(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {view, rootNodes} = createAndGetRootNodes(
|
||||||
|
compViewDef([
|
||||||
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||||
|
providerDef(NodeFlags.None, null, 0, AComp, [], null, null, () => compViewDef(
|
||||||
|
[
|
||||||
|
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
|
||||||
|
], update
|
||||||
|
)),
|
||||||
|
]));
|
||||||
|
const compView = asProviderData(view, 1).componentView;
|
||||||
|
|
||||||
|
value = 'v1';
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
|
||||||
|
expect(update).toHaveBeenCalledWith(compView);
|
||||||
|
|
||||||
|
update.calls.reset();
|
||||||
|
checkNoChangesView(view);
|
||||||
|
|
||||||
|
expect(update).toHaveBeenCalledWith(compView);
|
||||||
|
|
||||||
|
value = 'v2';
|
||||||
|
expect(() => checkNoChangesView(view))
|
||||||
|
.toThrowError(
|
||||||
|
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const {view, rootNodes} = createAndGetRootNodes(
|
it('should support detaching and attaching component views for dirty checking', () => {
|
||||||
compViewDef([
|
class AComp {
|
||||||
|
a: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = jasmine.createSpy('updater');
|
||||||
|
|
||||||
|
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||||
providerDef(NodeFlags.None, null, 0, AComp, [], null, null, () => compViewDef(
|
providerDef(
|
||||||
[
|
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||||
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
|
() => compViewDef(
|
||||||
], update
|
[
|
||||||
)),
|
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||||
], jasmine.createSpy('parentUpdater')));
|
],
|
||||||
const compView = asProviderData(view, 1).componentView;
|
update)),
|
||||||
|
]));
|
||||||
|
|
||||||
checkAndUpdateView(view);
|
const compView = asProviderData(view, 1).componentView;
|
||||||
|
|
||||||
expect(update).toHaveBeenCalledWith(compView);
|
checkAndUpdateView(view);
|
||||||
|
update.calls.reset();
|
||||||
|
|
||||||
update.calls.reset();
|
compView.state = ViewState.ChecksDisabled;
|
||||||
checkNoChangesView(view);
|
checkAndUpdateView(view);
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(update).toHaveBeenCalledWith(compView);
|
compView.state = ViewState.ChecksEnabled;
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
value = 'v2';
|
if (isBrowser()) {
|
||||||
expect(() => checkNoChangesView(view))
|
it('should support OnPush components', () => {
|
||||||
.toThrowError(
|
let compInputValue: any;
|
||||||
`Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
|
class AComp {
|
||||||
});
|
a: any;
|
||||||
|
}
|
||||||
|
|
||||||
it('should destroy component views', () => {
|
const update = jasmine.createSpy('updater');
|
||||||
const log: string[] = [];
|
|
||||||
|
|
||||||
class AComp {}
|
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
|
||||||
|
const {view, rootNodes} =
|
||||||
|
createAndGetRootNodes(
|
||||||
|
compViewDef(
|
||||||
|
[
|
||||||
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||||
|
providerDef(
|
||||||
|
NodeFlags.None, null, 0, AComp, [], {a: [0, 'a']}, null,
|
||||||
|
() =>
|
||||||
|
compViewDef(
|
||||||
|
[
|
||||||
|
elementDef(NodeFlags.None, null, null, 0, 'span', null, null, ['click']),
|
||||||
|
],
|
||||||
|
update, null, ViewFlags.OnPush)),
|
||||||
|
],
|
||||||
|
(view) => {
|
||||||
|
setCurrentNode(view, 1);
|
||||||
|
checkNodeInline(compInputValue);
|
||||||
|
}));
|
||||||
|
|
||||||
class ChildProvider {
|
const compView = asProviderData(view, 1).componentView;
|
||||||
ngOnDestroy() { log.push('ngOnDestroy'); };
|
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
|
||||||
|
// auto detach
|
||||||
|
update.calls.reset();
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// auto attach on input changes
|
||||||
|
update.calls.reset();
|
||||||
|
compInputValue = 'v1';
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// auto detach
|
||||||
|
update.calls.reset();
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
// auto attach on events
|
||||||
|
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
|
||||||
|
update.calls.reset();
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// auto detach
|
||||||
|
update.calls.reset();
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
it('should stop dirty checking views that threw errors in change detection', () => {
|
||||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
class AComp {
|
||||||
providerDef(
|
a: any;
|
||||||
NodeFlags.None, null, 0, AComp, [], null, null,
|
}
|
||||||
() => compViewDef([
|
|
||||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
|
||||||
providerDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
|
|
||||||
])),
|
|
||||||
]));
|
|
||||||
|
|
||||||
destroyView(view);
|
const update = jasmine.createSpy('updater');
|
||||||
|
|
||||||
|
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||||
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||||
|
providerDef(
|
||||||
|
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||||
|
() => compViewDef(
|
||||||
|
[
|
||||||
|
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||||
|
],
|
||||||
|
update)),
|
||||||
|
]));
|
||||||
|
|
||||||
|
const compView = asProviderData(view, 1).componentView;
|
||||||
|
|
||||||
|
update.and.callFake((view: ViewData) => {
|
||||||
|
setCurrentNode(view, 0);
|
||||||
|
throw new Error('Test');
|
||||||
|
});
|
||||||
|
expect(() => checkAndUpdateView(view)).toThrow();
|
||||||
|
expect(update).toHaveBeenCalled();
|
||||||
|
|
||||||
|
update.calls.reset();
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
expect(log).toEqual(['ngOnDestroy']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('destroy', () => {
|
||||||
|
it('should destroy component views', () => {
|
||||||
|
const log: string[] = [];
|
||||||
|
|
||||||
|
class AComp {}
|
||||||
|
|
||||||
|
class ChildProvider {
|
||||||
|
ngOnDestroy() { log.push('ngOnDestroy'); };
|
||||||
|
}
|
||||||
|
|
||||||
|
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||||
|
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||||
|
providerDef(
|
||||||
|
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||||
|
() => compViewDef([
|
||||||
|
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||||
|
providerDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
|
||||||
|
])),
|
||||||
|
]));
|
||||||
|
|
||||||
|
destroyView(view);
|
||||||
|
|
||||||
|
expect(log).toEqual(['ngOnDestroy']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on dirty checking destroyed views', () => {
|
||||||
|
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||||
|
[
|
||||||
|
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||||
|
],
|
||||||
|
(view) => { setCurrentNode(view, 0); }));
|
||||||
|
|
||||||
|
destroyView(view);
|
||||||
|
|
||||||
|
expect(() => checkAndUpdateView(view))
|
||||||
|
.toThrowError('View has been used after destroy for CheckAndUpdate');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -207,7 +207,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
|
||||||
elementDef(
|
elementDef(
|
||||||
NodeFlags.None, null, null, 0, 'input', null,
|
NodeFlags.None, null, null, 0, 'input', null,
|
||||||
[
|
[
|
||||||
[BindingType.ElementProperty, 'title', SecurityContext.NONE],
|
[BindingType.ElementProperty, 'someProp', SecurityContext.NONE],
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
(view) => {
|
(view) => {
|
||||||
|
@ -216,7 +216,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const setterSpy = jasmine.createSpy('set');
|
const setterSpy = jasmine.createSpy('set');
|
||||||
Object.defineProperty(rootNodes[0], 'title', {set: setterSpy});
|
Object.defineProperty(rootNodes[0], 'someProp', {set: setterSpy});
|
||||||
|
|
||||||
bindingValue = 'v1';
|
bindingValue = 'v1';
|
||||||
checkAndUpdateView(view);
|
checkAndUpdateView(view);
|
||||||
|
@ -234,7 +234,7 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (getDOM().supportsDOMEvents()) {
|
if (isBrowser()) {
|
||||||
describe('listen to DOM events', () => {
|
describe('listen to DOM events', () => {
|
||||||
let removeNodes: Node[];
|
let removeNodes: Node[];
|
||||||
beforeEach(() => { removeNodes = []; });
|
beforeEach(() => { removeNodes = []; });
|
||||||
|
|
|
@ -98,34 +98,42 @@ function defineTests(config: {directDom: boolean, viewFlags: number}) {
|
||||||
expect(getDOM().getText(rootNodes[0])).toBe('0a1b2');
|
expect(getDOM().getText(rootNodes[0])).toBe('0a1b2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
|
if (isBrowser()) {
|
||||||
let bindingValue: any;
|
it(`should unwrap values with ${InlineDynamic[inlineDynamic]}`, () => {
|
||||||
|
let bindingValue: any;
|
||||||
|
const setterSpy = jasmine.createSpy('set');
|
||||||
|
|
||||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
class FakeTextNode {
|
||||||
[
|
set nodeValue(value: any) { setterSpy(value); }
|
||||||
textDef(null, ['', '']),
|
}
|
||||||
],
|
|
||||||
(view: ViewData) => {
|
|
||||||
setCurrentNode(view, 0);
|
|
||||||
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
|
|
||||||
}));
|
|
||||||
|
|
||||||
const setterSpy = jasmine.createSpy('set');
|
spyOn(document, 'createTextNode').and.returnValue(new FakeTextNode());
|
||||||
Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy});
|
|
||||||
|
|
||||||
bindingValue = 'v1';
|
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||||
checkAndUpdateView(view);
|
[
|
||||||
expect(setterSpy).toHaveBeenCalledWith('v1');
|
textDef(null, ['', '']),
|
||||||
|
],
|
||||||
|
(view: ViewData) => {
|
||||||
|
setCurrentNode(view, 0);
|
||||||
|
checkNodeInlineOrDynamic(inlineDynamic, [bindingValue]);
|
||||||
|
}));
|
||||||
|
|
||||||
setterSpy.calls.reset();
|
Object.defineProperty(rootNodes[0], 'nodeValue', {set: setterSpy});
|
||||||
checkAndUpdateView(view);
|
|
||||||
expect(setterSpy).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
setterSpy.calls.reset();
|
bindingValue = 'v1';
|
||||||
bindingValue = WrappedValue.wrap('v1');
|
checkAndUpdateView(view);
|
||||||
checkAndUpdateView(view);
|
expect(setterSpy).toHaveBeenCalledWith('v1');
|
||||||
expect(setterSpy).toHaveBeenCalledWith('v1');
|
|
||||||
});
|
setterSpy.calls.reset();
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(setterSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
setterSpy.calls.reset();
|
||||||
|
bindingValue = WrappedValue.wrap('v1');
|
||||||
|
checkAndUpdateView(view);
|
||||||
|
expect(setterSpy).toHaveBeenCalledWith('v1');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue