fix(core): Store ICU state in LView rather than in TView (#39233)

Before this refactoring/fix the ICU would store the current selected
index in `TView`. This is incorrect, since if ICU is in `ngFor` it will
cause issues in some circumstances. This refactoring properly moves the
state to `LView`.

closes #37021
closes #38144
closes #38073

PR Close #39233
This commit is contained in:
Misko Hevery
2020-09-25 15:01:56 -07:00
committed by Alex Rickabaugh
parent 6790848f68
commit ca11ef2376
53 changed files with 3270 additions and 1643 deletions

View File

@ -7,6 +7,7 @@
*/
import {KeyValueArray} from '../../util/array_utils';
import {TStylingRange} from '../interfaces/styling';
import {TIcu} from './i18n';
import {CssSelector} from './projection';
import {RNode} from './renderer';
import {LView, TView} from './view';
@ -16,9 +17,11 @@ import {LView, TView} from './view';
* TNodeType corresponds to the {@link TNode} `type` property.
*/
export const enum TNodeType {
// FIXME(misko): Add `Text` type so that it would be much easier to reason/debug about `TNode`s.
/**
* The TNode contains information about an {@link LContainer} for embedded views.
*/
// FIXME(misko): Verify that we still need a `Container`, at the very least update the text.
Container = 0,
/**
* The TNode contains information about an `<ng-content>` projection
@ -36,6 +39,20 @@ export const enum TNodeType {
* The TNode contains information about an ICU comment used in `i18n`.
*/
IcuContainer = 4,
/**
* Special node type representing a placeholder for future `TNode` at this location.
*
* I18n translation blocks are created before the element nodes which they contain. (I18n blocks
* can span over many elements.) Because i18n `TNode`s (representing text) are created first they
* often may need to point to element `TNode`s which are not yet created. In such a case we create
* a `Placeholder` `TNode`. This allows the i18n to structurally link the `TNode`s together
* without knowing any information about the future nodes which will be at that location.
*
* On `firstCreatePass` When element instruction executes it will try to create a `TNode` at that
* location. Seeing a `Placeholder` `TNode` already there tells the system that it should reuse
* existing `TNode` (rather than create a new one) and just update the missing information.
*/
Placeholder = 5,
}
/**
@ -47,7 +64,8 @@ export const TNodeTypeAsString = [
'Projection', // 1
'Element', // 2
'ElementContainer', // 3
'IcuContainer' // 4
'IcuContainer', // 4
'Placeholder', // 5
] as const;
@ -293,6 +311,59 @@ export interface TNode {
*/
index: number;
/**
* Insert before existing DOM node index.
*
* When DOM nodes are being inserted, normally they are being appended as they are created.
* Under i18n case, the translated text nodes are created ahead of time as part of the
* `ɵɵi18nStart` instruction which means that this `TNode` can't just be appended and instead
* needs to be inserted using `insertBeforeIndex` semantics.
*
* Additionally sometimes it is necessary to insert new text nodes as a child of this `TNode`. In
* such a case the value stores an array of text nodes to insert.
*
* Example:
* ```
* <div i18n>
* Hello <span>World</span>!
* </div>
* ```
* In the above example the `ɵɵi18nStart` instruction can create `Hello `, `World` and `!` text
* nodes. It can also insert `Hello ` and `!` text node as a child of `<div>`, but it can't
* insert `World` because the `<span>` node has not yet been created. In such a case the
* `<span>` `TNode` will have an array which will direct the `<span>` to not only insert
* itself in front of `!` but also to insert the `World` (created by `ɵɵi18nStart`) into `<span>`
* itself.
*
* Pseudo code:
* ```
* if (insertBeforeIndex === null) {
* // append as normal
* } else if (Array.isArray(insertBeforeIndex)) {
* // First insert current `TNode` at correct location
* const currentNode = lView[this.index];
* parentNode.insertBefore(currentNode, lView[this.insertBeforeIndex[0]]);
* // Now append all of the children
* for(let i=1; i<this.insertBeforeIndex; i++) {
* currentNode.appendChild(lView[this.insertBeforeIndex[i]]);
* }
* } else {
* parentNode.insertBefore(lView[this.index], lView[this.insertBeforeIndex])
* }
* ```
* - null: Append as normal using `parentNode.appendChild`
* - `number`: Append using
* `parentNode.insertBefore(lView[this.index], lView[this.insertBeforeIndex])`
*
* *Initialization*
*
* Because `ɵɵi18nStart` executes before nodes are created, on `TView.firstCreatePass` it is not
* possible for `ɵɵi18nStart` to set the `insertBeforeIndex` value as the corresponding `TNode`
* has not yet been created. For this reason the `ɵɵi18nStart` creates a `TNodeType.Placeholder`
* `TNode` at that location. See `TNodeType.Placeholder` for more information.
*/
insertBeforeIndex: InsertBeforeIndex;
/**
* The index of the closest injector in this node's LView.
*
@ -357,6 +428,8 @@ export interface TNode {
providerIndexes: TNodeProviderIndexes;
/** The tag name associated with this node. */
// FIXME(misko): rename to `value` and change the type to `any` so that
// subclasses of `TNode` can use it to link additional payload
tagName: string|null;
/**
@ -643,6 +716,11 @@ export interface TNode {
styleBindings: TStylingRange;
}
/**
* See `TNode.insertBeforeIndex`
*/
export type InsertBeforeIndex = null|number|number[];
/** Static data for an element */
export interface TElementNode extends TNode {
/** Index in the data[] array */
@ -715,10 +793,12 @@ export interface TElementContainerNode extends TNode {
export interface TIcuContainerNode extends TNode {
/** Index in the LView[] array. */
index: number;
child: TElementNode|TTextNode|null;
child: null;
parent: TElementNode|TElementContainerNode|null;
tViews: null;
projection: null;
// FIXME(misko): Refactor to enable the next line
// tagName: TIcu;
}
/** Static data for an LProjectionNode */