fix(ivy): queries should match container node itself before matching its views (#28473)

PR Close #28473
This commit is contained in:
Pawel Kozlowski 2019-01-31 16:02:19 +01:00 committed by Matias Niemelä
parent 6656328538
commit 1b6d8a78b0
5 changed files with 45 additions and 38 deletions

View File

@ -31,11 +31,11 @@ import {CssSelectorList, NG_PROJECT_AS_ATTR_NAME} from './interfaces/projection'
import {LQueries} from './interfaces/query';
import {GlobalTargetResolver, ProceduralRenderer3, RComment, RElement, RText, Renderer3, RendererFactory3, isProceduralRenderer} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {appendChild, appendProjectedNode, createTextNode, getLViewChild, insertView, removeView} from './node_manipulation';
import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getCurrentQueryIndex, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setCurrentQueryIndex, setIsParent, setPreviousOrParentTNode} from './state';
import {getInitialClassNameValue, initializeStaticContext as initializeStaticStylingContext, patchContextWithStaticAttrs, renderInitialStylesAndClasses, renderStyling, updateClassProp as updateElementClassProp, updateContextWithBindings, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory';
import {createEmptyStylingContext, getStylingContext, hasClassInput, hasStyling, isAnimationProp} from './styling/util';
@ -324,7 +324,7 @@ export function renderTemplate<T>(
* Such lViewNode will then be renderer with renderEmbeddedTemplate() (see below).
*/
export function createEmbeddedViewAndNode<T>(
tView: TView, context: T, declarationView: LView, renderer: Renderer3, queries: LQueries | null,
tView: TView, context: T, declarationView: LView, queries: LQueries | null,
injectorIndex: number): LView {
const _isParent = getIsParent();
const _previousOrParentTNode = getPreviousOrParentTNode();
@ -2100,12 +2100,7 @@ export function template(
}
createDirectivesAndLocals(tView, lView, localRefs, localRefExtractor);
const currentQueries = lView[QUERIES];
if (currentQueries) {
lView[QUERIES] = currentQueries.addNode(tContainerNode);
}
addTContainerToQueries(lView, tContainerNode);
attachPatchData(getNativeByTNode(tContainerNode, lView), lView);
registerPostOrderHooks(tView, tContainerNode);
setIsParent(false);
@ -2126,6 +2121,7 @@ export function container(index: number): void {
if (lView[TVIEW].firstTemplatePass) {
tNode.tViews = [];
}
addTContainerToQueries(lView, tNode);
setIsParent(false);
}
@ -2148,16 +2144,28 @@ function containerInternal(
// because views can be removed and re-inserted.
addToViewTree(lView, index + HEADER_OFFSET, lContainer);
const currentQueries = lView[QUERIES];
if (currentQueries) {
// prepare place for matching nodes from views inserted into a given container
lContainer[QUERIES] = currentQueries.container();
}
ngDevMode && assertNodeType(getPreviousOrParentTNode(), TNodeType.Container);
return tNode;
}
/**
* Reporting a TContainer node queries is a 2-step process as we need to:
* - check if the container node itself is matching (query might match a <ng-template> node);
* - prepare room for nodes from views that might be created based on the TemplateRef linked to this
* container.
*
* Those 2 operations need to happen in the specific order (match the container node itself, then
* prepare space for nodes from views).
*/
function addTContainerToQueries(lView: LView, tContainerNode: TContainerNode): void {
const queries = lView[QUERIES];
if (queries) {
lView[QUERIES] = queries.addNode(tContainerNode);
const lContainer = lView[tContainerNode.index];
lContainer[QUERIES] = queries.container();
}
}
/**
* Sets a container up to receive views.
*

View File

@ -35,7 +35,7 @@ export const enum TNodeFlags {
/** This bit is set if the node has been projected */
isProjected = 0b0010,
/** This bit is set if the node has any content queries */
/** This bit is set if any directive on this node has content queries */
hasContentQuery = 0b0100,
/** This bit is set if the node has any directives that contain [class properties */

View File

@ -7,7 +7,7 @@
*/
import {ChangeDetectorRef as ViewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {Injector, NullInjector} from '../di/injector';
import {Injector} from '../di/injector';
import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref';
import {NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory';
@ -19,9 +19,7 @@ import {assertDefined, assertGreaterThan, assertLessThan} from '../util/assert';
import {NodeInjector, getParentInjectorLocation} from './di';
import {addToViewTree, createEmbeddedViewAndNode, createLContainer, renderEmbeddedTemplate} from './instructions';
import {ACTIVE_INDEX, LContainer, NATIVE, VIEWS} from './interfaces/container';
import {RenderFlags} from './interfaces/definition';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {LQueries} from './interfaces/query';
import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer';
import {CONTAINER_INDEX, CONTEXT, HOST_NODE, LView, QUERIES, RENDERER, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert';
@ -64,7 +62,7 @@ export function createElementRef(
let R3TemplateRef: {
new (
_declarationParentView: LView, elementRef: ViewEngine_ElementRef, _tView: TView,
_renderer: Renderer3, _queries: LQueries | null, _injectorIndex: number):
_renderer: Renderer3, _hostLContainer: LContainer, _injectorIndex: number):
ViewEngine_TemplateRef<any>
};
@ -97,7 +95,7 @@ export function createTemplateRef<T>(
R3TemplateRef = class TemplateRef_<T> extends TemplateRefToken<T> {
constructor(
private _declarationParentView: LView, readonly elementRef: ViewEngine_ElementRef,
private _tView: TView, private _renderer: Renderer3, private _queries: LQueries|null,
private _tView: TView, private _renderer: Renderer3, private _hostLContainer: LContainer,
private _injectorIndex: number) {
super();
}
@ -107,7 +105,7 @@ export function createTemplateRef<T>(
hostTNode?: TElementNode|TContainerNode|TElementContainerNode, hostView?: LView,
index?: number): viewEngine_EmbeddedViewRef<T> {
const lView = createEmbeddedViewAndNode(
this._tView, context, this._declarationParentView, this._renderer, this._queries,
this._tView, context, this._declarationParentView, this._hostLContainer[QUERIES],
this._injectorIndex);
if (container) {
insertView(lView, container, hostView !, index !, hostTNode !.index);
@ -125,7 +123,7 @@ export function createTemplateRef<T>(
ngDevMode && assertDefined(hostTNode.tViews, 'TView must be allocated');
return new R3TemplateRef(
hostView, createElementRef(ElementRefToken, hostTNode, hostView), hostTNode.tViews as TView,
getLView()[RENDERER], hostContainer[QUERIES], hostTNode.injectorIndex);
getLView()[RENDERER], hostContainer, hostTNode.injectorIndex);
} else {
return null;
}

View File

@ -362,6 +362,9 @@
{
"name": "addRemoveViewFromContainer"
},
{
"name": "addTContainerToQueries"
},
{
"name": "addToViewTree"
},

View File

@ -94,21 +94,19 @@ describe('Query API', () => {
]);
});
fixmeIvy(
'FW-982: For queries, ng-template own directives should be registered before the ones from inside the template')
.it('should contain the first content child when target is on <ng-template> with embedded view (issue #16568)',
() => {
const template =
'<div directive-needs-content-child><ng-template text="foo" [ngIf]="true"><div text="bar"></div></ng-template></div>' +
'<needs-content-child #q><ng-template text="foo" [ngIf]="true"><div text="bar"></div></ng-template></needs-content-child>';
const view = createTestCmp(MyComp0, template);
view.detectChanges();
const q: NeedsContentChild = view.debugElement.children[1].references !['q'];
expect(q.child.text).toEqual('foo');
const directive: DirectiveNeedsContentChild =
view.debugElement.children[0].injector.get(DirectiveNeedsContentChild);
expect(directive.child.text).toEqual('foo');
});
it('should contain the first content child when target is on <ng-template> with embedded view (issue #16568)',
() => {
const template =
'<div directive-needs-content-child><ng-template text="foo" [ngIf]="true"><div text="bar"></div></ng-template></div>' +
'<needs-content-child #q><ng-template text="foo" [ngIf]="true"><div text="bar"></div></ng-template></needs-content-child>';
const view = createTestCmp(MyComp0, template);
view.detectChanges();
const q: NeedsContentChild = view.debugElement.children[1].references !['q'];
expect(q.child.text).toEqual('foo');
const directive: DirectiveNeedsContentChild =
view.debugElement.children[0].injector.get(DirectiveNeedsContentChild);
expect(directive.child.text).toEqual('foo');
});
it('should contain the first view child', () => {
const template = '<needs-view-child #q></needs-view-child>';