fix(ivy): unable to project into multiple slots with default selector (#30561)
With View engine it was possible to declare multiple projection definitions and to programmatically project nodes into the slots. e.g. ```html <ng-content></ng-content> <ng-content></ng-content> ``` Using `ViewContainerRef#createComponent` allowed projecting nodes into one of the projection defs (through index) This no longer works with Ivy as the `projectionDef` instruction only retrieves a list of selectors instead of also retrieving entries for reserved projection slots which appear when using the default selector multiple times (as seen above). In order to fix this issue, the Ivy compiler now passes all projection slots to the `projectionDef` instruction. Meaning that there can be multiple projection slots with the same wildcard selector. This allows multi-slot projection as seen in the example above, and it also allows us to match the multi-slot node projection order from View Engine (to avoid breaking changes). It basically ensures that Ivy fully matches the View Engine behavior except of a very small edge case that has already been discussed in FW-886 (with the conclusion of working as intended). Read more here: https://hackmd.io/s/Sy2kQlgTE PR Close #30561
This commit is contained in:

committed by
Miško Hevery

parent
f4cd3740b2
commit
aca339e864
@ -123,10 +123,8 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
|
||||
super();
|
||||
this.componentType = componentDef.type;
|
||||
this.selector = componentDef.selectors[0][0] as string;
|
||||
// The component definition does not include the wildcard ('*') selector in its list.
|
||||
// It is implicitly expected as the first item in the projectable nodes array.
|
||||
this.ngContentSelectors =
|
||||
componentDef.ngContentSelectors ? ['*', ...componentDef.ngContentSelectors] : [];
|
||||
componentDef.ngContentSelectors ? componentDef.ngContentSelectors : [];
|
||||
this.isBoundToModule = !!ngModule;
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ export {
|
||||
ɵɵtextInterpolateV,
|
||||
} from './instructions/all';
|
||||
export {RenderFlags} from './interfaces/definition';
|
||||
export {CssSelectorList} from './interfaces/projection';
|
||||
export {CssSelectorList, ProjectionSlots} from './interfaces/projection';
|
||||
|
||||
export {
|
||||
ɵɵrestoreView,
|
||||
|
@ -5,18 +5,47 @@
|
||||
* 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 {assertDataInRange} from '../../util/assert';
|
||||
import {TAttributes, TElementNode, TNode, TNodeType} from '../interfaces/node';
|
||||
import {CssSelectorList} from '../interfaces/projection';
|
||||
import {HEADER_OFFSET, TVIEW, T_HOST} from '../interfaces/view';
|
||||
import {ProjectionSlots} from '../interfaces/projection';
|
||||
import {TVIEW, T_HOST} from '../interfaces/view';
|
||||
import {appendProjectedNodes} from '../node_manipulation';
|
||||
import {matchingProjectionSelectorIndex} from '../node_selector_matcher';
|
||||
import {getProjectAsAttrValue, isNodeMatchingSelectorList, isSelectorInSelectorList} from '../node_selector_matcher';
|
||||
import {getLView, setIsNotParent} from '../state';
|
||||
import {findComponentView} from '../util/view_traversal_utils';
|
||||
|
||||
import {getOrCreateTNode} from './shared';
|
||||
|
||||
|
||||
/**
|
||||
* Checks a given node against matching projection slots and returns the
|
||||
* determined slot index. Returns "null" if no slot matched the given node.
|
||||
*
|
||||
* This function takes into account the parsed ngProjectAs selector from the
|
||||
* node's attributes. If present, it will check whether the ngProjectAs selector
|
||||
* matches any of the projection slot selectors.
|
||||
*/
|
||||
export function matchingProjectionSlotIndex(tNode: TNode, projectionSlots: ProjectionSlots): number|
|
||||
null {
|
||||
let wildcardNgContentIndex = null;
|
||||
const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
|
||||
for (let i = 0; i < projectionSlots.length; i++) {
|
||||
const slotValue = projectionSlots[i];
|
||||
// The last wildcard projection slot should match all nodes which aren't matching
|
||||
// any selector. This is necessary to be backwards compatible with view engine.
|
||||
if (slotValue === '*') {
|
||||
wildcardNgContentIndex = i;
|
||||
continue;
|
||||
}
|
||||
// If we ran into an `ngProjectAs` attribute, we should match its parsed selector
|
||||
// to the list of selectors, otherwise we fall back to matching against the node.
|
||||
if (ngProjectAsAttrVal === null ?
|
||||
isNodeMatchingSelectorList(tNode, slotValue, /* isProjectionMode */ true) :
|
||||
isSelectorInSelectorList(ngProjectAsAttrVal, slotValue)) {
|
||||
return i; // first matching selector "captures" a given node
|
||||
}
|
||||
}
|
||||
return wildcardNgContentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruction to distribute projectable nodes among <ng-content> occurrences in a given template.
|
||||
@ -36,32 +65,38 @@ import {getOrCreateTNode} from './shared';
|
||||
* - we can't have only a parsed as we can't re-construct textual form from it (as entered by a
|
||||
* template author).
|
||||
*
|
||||
* @param selectors A collection of parsed CSS selectors
|
||||
* @param rawSelectors A collection of CSS selectors in the raw, un-parsed form
|
||||
* @param projectionSlots? A collection of projection slots. A projection slot can be based
|
||||
* on a parsed CSS selectors or set to the wildcard selector ("*") in order to match
|
||||
* all nodes which do not match any selector. If not specified, a single wildcard
|
||||
* selector projection slot will be defined.
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵprojectionDef(selectors?: CssSelectorList[]): void {
|
||||
export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
|
||||
const componentNode = findComponentView(getLView())[T_HOST] as TElementNode;
|
||||
|
||||
if (!componentNode.projection) {
|
||||
const noOfNodeBuckets = selectors ? selectors.length + 1 : 1;
|
||||
// If no explicit projection slots are defined, fall back to a single
|
||||
// projection slot with the wildcard selector.
|
||||
const numProjectionSlots = projectionSlots ? projectionSlots.length : 1;
|
||||
const projectionHeads: (TNode | null)[] = componentNode.projection =
|
||||
new Array(noOfNodeBuckets).fill(null);
|
||||
new Array(numProjectionSlots).fill(null);
|
||||
const tails: (TNode | null)[] = projectionHeads.slice();
|
||||
|
||||
let componentChild: TNode|null = componentNode.child;
|
||||
|
||||
while (componentChild !== null) {
|
||||
const bucketIndex =
|
||||
selectors ? matchingProjectionSelectorIndex(componentChild, selectors) : 0;
|
||||
const slotIndex =
|
||||
projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0;
|
||||
|
||||
if (tails[bucketIndex]) {
|
||||
tails[bucketIndex] !.projectionNext = componentChild;
|
||||
} else {
|
||||
projectionHeads[bucketIndex] = componentChild;
|
||||
if (slotIndex !== null) {
|
||||
if (tails[slotIndex]) {
|
||||
tails[slotIndex] !.projectionNext = componentChild;
|
||||
} else {
|
||||
projectionHeads[slotIndex] = componentChild;
|
||||
}
|
||||
tails[slotIndex] = componentChild;
|
||||
}
|
||||
tails[bucketIndex] = componentChild;
|
||||
|
||||
componentChild = componentChild.next;
|
||||
}
|
||||
|
@ -50,6 +50,16 @@ export type CssSelector = (string | SelectorFlags)[];
|
||||
*/
|
||||
export type CssSelectorList = CssSelector[];
|
||||
|
||||
/**
|
||||
* List of slots for a projection. A slot can be either based on a parsed CSS selector
|
||||
* which will be used to determine nodes which are projected into that slot.
|
||||
*
|
||||
* When set to "*", the slot is reserved and can be used for multi-slot projection
|
||||
* using {@link ViewContainerRef#createComponent}. The last slot that specifies the
|
||||
* wildcard selector will retrieve all projectable nodes which do not match any selector.
|
||||
*/
|
||||
export type ProjectionSlots = (CssSelectorList | '*')[];
|
||||
|
||||
/** Flags used to build up CssSelectors */
|
||||
export const enum SelectorFlags {
|
||||
/** Indicates this is the beginning of a new negative selector */
|
||||
|
@ -257,29 +257,6 @@ export function getProjectAsAttrValue(tNode: TNode): CssSelector|null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a given node against matching projection selectors and returns
|
||||
* selector index (or 0 if none matched).
|
||||
*
|
||||
* This function takes into account the parsed ngProjectAs selector from the node's attributes.
|
||||
* If present, it will check whether the ngProjectAs selector matches any of the projection
|
||||
* selectors.
|
||||
*/
|
||||
export function matchingProjectionSelectorIndex(
|
||||
tNode: TNode, selectors: CssSelectorList[]): number {
|
||||
const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
|
||||
for (let i = 0; i < selectors.length; i++) {
|
||||
// If we ran into an `ngProjectAs` attribute, we should match its parsed selector
|
||||
// to the list of selectors, otherwise we fall back to matching against the node.
|
||||
if (ngProjectAsAttrVal === null ?
|
||||
isNodeMatchingSelectorList(tNode, selectors[i], /* isProjectionMode */ true) :
|
||||
isSelectorInSelectorList(ngProjectAsAttrVal, selectors[i])) {
|
||||
return i + 1; // first matching selector "captures" a given node
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getNameOnlyMarkerIndex(nodeAttrs: TAttributes) {
|
||||
for (let i = 0; i < nodeAttrs.length; i++) {
|
||||
const nodeAttr = nodeAttrs[i];
|
||||
@ -307,7 +284,7 @@ function matchTemplateAttribute(attrs: TAttributes, name: string): number {
|
||||
* @param selector Selector to be checked.
|
||||
* @param list List in which to look for the selector.
|
||||
*/
|
||||
function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean {
|
||||
export function isSelectorInSelectorList(selector: CssSelector, list: CssSelectorList): boolean {
|
||||
selectorListLoop: for (let i = 0; i < list.length; i++) {
|
||||
const currentSelectorInList = list[i];
|
||||
if (selector.length !== currentSelectorInList.length) {
|
||||
|
Reference in New Issue
Block a user