mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(core): built-in control flow - repeaters (#51422)
Draft of the runtime implementation for the built-in repeaters. PR Close #51422
This commit is contained in:
parent
88fcd27c6d
commit
cdcfa09ab3
4 changed files with 428 additions and 3 deletions
|
|
@ -174,6 +174,11 @@ export {
|
|||
ɵɵresolveWindow,
|
||||
ɵɵrestoreView,
|
||||
|
||||
ɵɵrepeater,
|
||||
ɵɵrepeaterCreate,
|
||||
ɵɵrepeaterTrackByIdentity,
|
||||
ɵɵrepeaterTrackByIndex,
|
||||
|
||||
ɵɵsetComponentScope,
|
||||
ɵɵsetNgModuleScope,
|
||||
ɵɵgetComponentDepsFactory,
|
||||
|
|
|
|||
|
|
@ -92,6 +92,11 @@ export {
|
|||
|
||||
ɵɵreference,
|
||||
|
||||
ɵɵrepeater,
|
||||
ɵɵrepeaterCreate,
|
||||
ɵɵrepeaterTrackByIdentity,
|
||||
ɵɵrepeaterTrackByIndex,
|
||||
|
||||
ɵɵstyleMap,
|
||||
ɵɵstyleMapInterpolate1,
|
||||
ɵɵstyleMapInterpolate2,
|
||||
|
|
|
|||
|
|
@ -6,15 +6,21 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {assertLContainer, assertTNode} from '../assert';
|
||||
import {DefaultIterableDiffer, IterableChangeRecord, TrackByFunction} from '../../change_detection';
|
||||
import {assertDefined} from '../../util/assert';
|
||||
import {assertLContainer, assertLView, assertTNode} from '../assert';
|
||||
import {bindingUpdated} from '../bindings';
|
||||
import {LContainer} from '../interfaces/container';
|
||||
import {CONTAINER_HEADER_OFFSET, LContainer} from '../interfaces/container';
|
||||
import {ComponentTemplate} from '../interfaces/definition';
|
||||
import {TNode} from '../interfaces/node';
|
||||
import {CONTEXT, HEADER_OFFSET, LView, TVIEW, TView} from '../interfaces/view';
|
||||
import {detachView} from '../node_manipulation';
|
||||
import {getLView, nextBindingIndex} from '../state';
|
||||
import {getTNode} from '../util/view_utils';
|
||||
import {addLViewToLContainer, createAndRenderEmbeddedLView, getLViewFromLContainer, removeLViewFromLContainer} from '../view_manipulation';
|
||||
|
||||
import {ɵɵtemplate} from './template';
|
||||
|
||||
/**
|
||||
* The conditional instruction represents the basic building block on the runtime side to support
|
||||
* built-in "if" and "switch". On the high level this instruction is responsible for adding and
|
||||
|
|
@ -55,6 +61,159 @@ export function ɵɵconditional<T>(containerIndex: number, matchingTemplateIndex
|
|||
}
|
||||
}
|
||||
|
||||
export class RepeaterContext<T> {
|
||||
constructor(private lContainer: LContainer, public $implicit: T, public $index: number) {}
|
||||
|
||||
get $count(): number {
|
||||
return this.lContainer.length - CONTAINER_HEADER_OFFSET;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A built-in trackBy function used for situations where users specified collection index as a
|
||||
* tracking expression. Having this function body in the runtime avoids unnecessary code generation.
|
||||
*
|
||||
* @param index
|
||||
* @returns
|
||||
*/
|
||||
export function ɵɵrepeaterTrackByIndex(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* A built-in trackBy function used for situations where users specified collection item reference
|
||||
* as a tracking expression. Having this function body in the runtime avoids unnecessary code
|
||||
* generation.
|
||||
*
|
||||
* @param index
|
||||
* @returns
|
||||
*/
|
||||
export function ɵɵrepeaterTrackByIdentity<T>(_: number, value: T) {
|
||||
return value;
|
||||
}
|
||||
|
||||
class RepeaterMetadata {
|
||||
constructor(public hasEmptyBlock: boolean, public differ: DefaultIterableDiffer<unknown>) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* The repeaterCreate instruction runs in the creation part of the template pass and initializes
|
||||
* internal data structures required by the update pass of the built-in repeater logic. Repeater
|
||||
* metadata are allocated in the data part of LView with the following layout:
|
||||
* - LView[HEADER_OFFSET + index] - metadata
|
||||
* - LView[HEADER_OFFSET + index + 1] - reference to a template function rendering an item
|
||||
* - LView[HEADER_OFFSET + index + 2] - optional reference to a template function rendering an empty
|
||||
* block
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵrepeaterCreate(
|
||||
index: number, templateFn: ComponentTemplate<unknown>, decls: number, vars: number,
|
||||
trackByFn: TrackByFunction<unknown>, emptyTemplateFn?: ComponentTemplate<unknown>,
|
||||
emptyDecls?: number, emptyVars?: number): void {
|
||||
const hasEmptyBlock = emptyTemplateFn !== undefined;
|
||||
const hostLView = getLView();
|
||||
const metadata = new RepeaterMetadata(hasEmptyBlock, new DefaultIterableDiffer(trackByFn));
|
||||
hostLView[HEADER_OFFSET + index] = metadata;
|
||||
|
||||
ɵɵtemplate(index + 1, templateFn, decls, vars);
|
||||
|
||||
if (hasEmptyBlock) {
|
||||
ngDevMode &&
|
||||
assertDefined(emptyDecls, 'Missing number of declarations for the empty repeater block.');
|
||||
ngDevMode &&
|
||||
assertDefined(emptyVars, 'Missing number of bindings for the empty repeater block.');
|
||||
|
||||
ɵɵtemplate(index + 2, emptyTemplateFn, emptyDecls!, emptyVars!);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The repeater instruction does update-time diffing of a provided collection (against the
|
||||
* collection seen previously) and maps changes in the collection to views structure (by adding,
|
||||
* removing or moving views as needed).
|
||||
* @param metadataSlotIdx - index in data where we can find an instance of RepeaterMetadata with
|
||||
* additional information (ex. differ) needed to process collection diffing and view
|
||||
* manipulation
|
||||
* @param collection - the collection instance to be checked for changes
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵrepeater(
|
||||
metadataSlotIdx: number, collection: Iterable<unknown>|undefined|null): void {
|
||||
const hostLView = getLView();
|
||||
const hostTView = hostLView[TVIEW];
|
||||
const metadata = hostLView[HEADER_OFFSET + metadataSlotIdx] as RepeaterMetadata;
|
||||
|
||||
const differ = metadata.differ;
|
||||
const changes = differ.diff(collection);
|
||||
|
||||
// handle repeater changes
|
||||
if (changes !== null) {
|
||||
const containerIndex = metadataSlotIdx + 1;
|
||||
const itemTemplateTNode = getExistingTNode(hostTView, containerIndex);
|
||||
const lContainer = getLContainer(hostLView, HEADER_OFFSET + containerIndex);
|
||||
let needsIndexUpdate = false;
|
||||
changes.forEachOperation(
|
||||
(item: IterableChangeRecord<unknown>, adjustedPreviousIndex: number|null,
|
||||
currentIndex: number|null) => {
|
||||
if (item.previousIndex === null) {
|
||||
// add
|
||||
const newViewIdx = adjustToLastLContainerIndex(lContainer, currentIndex);
|
||||
const embeddedLView = createAndRenderEmbeddedLView(
|
||||
hostLView, itemTemplateTNode,
|
||||
new RepeaterContext(lContainer, item.item, newViewIdx));
|
||||
addLViewToLContainer(lContainer, embeddedLView, newViewIdx);
|
||||
needsIndexUpdate = true;
|
||||
} else if (currentIndex === null) {
|
||||
// remove
|
||||
adjustedPreviousIndex = adjustToLastLContainerIndex(lContainer, adjustedPreviousIndex);
|
||||
removeLViewFromLContainer(lContainer, adjustedPreviousIndex);
|
||||
needsIndexUpdate = true;
|
||||
} else if (adjustedPreviousIndex !== null) {
|
||||
// move
|
||||
const existingLView =
|
||||
detachExistingView<RepeaterContext<unknown>>(lContainer, adjustedPreviousIndex);
|
||||
addLViewToLContainer(lContainer, existingLView, currentIndex);
|
||||
needsIndexUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
// A trackBy function might return the same value even if the underlying item changed - re-bind
|
||||
// it in the context.
|
||||
changes.forEachIdentityChange((record: IterableChangeRecord<unknown>) => {
|
||||
const viewIdx = adjustToLastLContainerIndex(lContainer, record.currentIndex);
|
||||
const lView = getExistingLViewFromLContainer<RepeaterContext<unknown>>(lContainer, viewIdx);
|
||||
lView[CONTEXT].$implicit = record.item;
|
||||
});
|
||||
|
||||
// moves in the container might caused context's index to get out of order, re-adjust
|
||||
if (needsIndexUpdate) {
|
||||
for (let i = 0; i < lContainer.length - CONTAINER_HEADER_OFFSET; i++) {
|
||||
const lView = getExistingLViewFromLContainer<RepeaterContext<unknown>>(lContainer, i);
|
||||
lView[CONTEXT].$index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle empty blocks
|
||||
const bindingIndex = nextBindingIndex();
|
||||
if (metadata.hasEmptyBlock) {
|
||||
const hasItemsInCollection = differ.length > 0;
|
||||
if (bindingUpdated(hostLView, bindingIndex, hasItemsInCollection)) {
|
||||
const emptyTemplateIndex = metadataSlotIdx + 2;
|
||||
const lContainer = getLContainer(hostLView, HEADER_OFFSET + emptyTemplateIndex);
|
||||
if (hasItemsInCollection) {
|
||||
removeLViewFromLContainer(lContainer, 0);
|
||||
} else {
|
||||
const emptyTemplateTNode = getExistingTNode(hostTView, emptyTemplateIndex);
|
||||
const embeddedLView =
|
||||
createAndRenderEmbeddedLView(hostLView, emptyTemplateTNode, undefined);
|
||||
addLViewToLContainer(lContainer, embeddedLView, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLContainer(lView: LView, index: number): LContainer {
|
||||
const lContainer = lView[index];
|
||||
ngDevMode && assertLContainer(lContainer);
|
||||
|
|
@ -62,6 +221,24 @@ function getLContainer(lView: LView, index: number): LContainer {
|
|||
return lContainer;
|
||||
}
|
||||
|
||||
function adjustToLastLContainerIndex(lContainer: LContainer, index: number|null): number {
|
||||
return index !== null ? index : lContainer.length - CONTAINER_HEADER_OFFSET;
|
||||
}
|
||||
|
||||
function detachExistingView<T>(lContainer: LContainer, index: number): LView<T> {
|
||||
const existingLView = detachView(lContainer, index);
|
||||
ngDevMode && assertLView(existingLView);
|
||||
|
||||
return existingLView as LView<T>;
|
||||
}
|
||||
|
||||
function getExistingLViewFromLContainer<T>(lContainer: LContainer, index: number): LView<T> {
|
||||
const existingLView = getLViewFromLContainer<T>(lContainer, index);
|
||||
ngDevMode && assertLView(existingLView);
|
||||
|
||||
return existingLView!;
|
||||
}
|
||||
|
||||
function getExistingTNode(tView: TView, index: number): TNode {
|
||||
const tNode = getTNode(tView, index + HEADER_OFFSET);
|
||||
ngDevMode && assertTNode(tNode);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
|
||||
import {ɵsetEnabledBlockTypes as setEnabledBlockTypes} from '@angular/compiler/src/jit_compiler_facade';
|
||||
import {Component, Pipe, PipeTransform} from '@angular/core';
|
||||
import {Component, Pipe, PipeTransform, ɵɵadvance, ɵɵdefineComponent, ɵɵelementEnd, ɵɵelementStart, ɵɵrepeater, ɵɵrepeaterCreate, ɵɵrepeaterTrackByIdentity, ɵɵrepeaterTrackByIndex, ɵɵtext, ɵɵtextInterpolate, ɵɵtextInterpolate2} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
describe('control flow', () => {
|
||||
|
|
@ -243,4 +243,242 @@ describe('control flow', () => {
|
|||
expect(fixture.nativeElement.textContent).toBe('default ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('for', () => {
|
||||
it('should create, remove and move views corresponding to items in a collection', () => {
|
||||
function App_ng_template_0_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
const item = ctx.$implicit;
|
||||
const idx = ctx.$index;
|
||||
ɵɵtextInterpolate2('', item, '(', idx, ')|');
|
||||
}
|
||||
}
|
||||
|
||||
class TestComponent {
|
||||
items = [1, 2, 3];
|
||||
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 2,
|
||||
vars: 0,
|
||||
|
||||
// {#for (item of items); track item; let idx = index}{{item}}({{idx}}){/for}
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: TestComponent) {
|
||||
if (rf & 1) {
|
||||
ɵɵrepeaterCreate(0, App_ng_template_0_Template, 1, 2, ɵɵrepeaterTrackByIdentity);
|
||||
}
|
||||
|
||||
if (rf & 2) {
|
||||
ɵɵrepeater(0, ctx.items);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');
|
||||
|
||||
fixture.componentInstance.items.pop();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|');
|
||||
|
||||
fixture.componentInstance.items.push(3);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');
|
||||
|
||||
fixture.componentInstance.items[0] = 3;
|
||||
fixture.componentInstance.items[2] = 1;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('3(0)|2(1)|1(2)|');
|
||||
});
|
||||
|
||||
it('should work correctly with trackBy index', () => {
|
||||
function App_ng_template_0_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
const item = ctx.$implicit;
|
||||
const idx = ctx.$index;
|
||||
ɵɵtextInterpolate2('', item, '(', idx, ')|');
|
||||
}
|
||||
}
|
||||
|
||||
class TestComponent {
|
||||
items = [1, 2, 3];
|
||||
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 2,
|
||||
vars: 0,
|
||||
|
||||
// {#for (item of items); track index; let idx = index}{{item}}|{/for}
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵrepeaterCreate(0, App_ng_template_0_Template, 1, 2, ɵɵrepeaterTrackByIndex);
|
||||
}
|
||||
|
||||
if (rf & 2) {
|
||||
ɵɵrepeater(0, ctx.items);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');
|
||||
|
||||
fixture.componentInstance.items.pop();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|');
|
||||
|
||||
fixture.componentInstance.items.push(3);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('1(0)|2(1)|3(2)|');
|
||||
|
||||
fixture.componentInstance.items[0] = 3;
|
||||
fixture.componentInstance.items[2] = 1;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('3(0)|2(1)|1(2)|');
|
||||
});
|
||||
|
||||
it('should support empty blocks', () => {
|
||||
function App_ng_template_0_Template(rf: number) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0, '|');
|
||||
}
|
||||
}
|
||||
|
||||
function App_ng_template_0_EMPTY(rf: number) {
|
||||
if (rf & 1) {
|
||||
ɵɵelementStart(0, 'div');
|
||||
ɵɵtext(1);
|
||||
ɵɵelementEnd();
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵadvance(1);
|
||||
ɵɵtextInterpolate('Empty');
|
||||
}
|
||||
}
|
||||
|
||||
class TestComponent {
|
||||
items: number[]|null|undefined = [1, 2, 3];
|
||||
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 3,
|
||||
vars: 1,
|
||||
|
||||
// {#for (item of items); track index; let idx = index}{{item}}|{/for}
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵrepeaterCreate(
|
||||
0, App_ng_template_0_Template, 1, 0, ɵɵrepeaterTrackByIndex,
|
||||
App_ng_template_0_EMPTY, 2, 1);
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵrepeater(0, ctx.items);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('|||');
|
||||
|
||||
fixture.componentInstance.items = [];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('Empty');
|
||||
|
||||
fixture.componentInstance.items = [0, 1];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('||');
|
||||
|
||||
fixture.componentInstance.items = null;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('Empty');
|
||||
|
||||
fixture.componentInstance.items = [0];
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('|');
|
||||
|
||||
fixture.componentInstance.items = undefined;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('Empty');
|
||||
});
|
||||
|
||||
it('should have access to the host context in the track function', () => {
|
||||
function App_ng_template_0_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵtext(0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵtextInterpolate(ctx.$implicit);
|
||||
}
|
||||
}
|
||||
|
||||
class TestComponent {
|
||||
offset = 0;
|
||||
items = ['a', 'b', 'c'];
|
||||
|
||||
static ɵcmp = ɵɵdefineComponent({
|
||||
type: TestComponent,
|
||||
selectors: [['some-cmp']],
|
||||
decls: 2,
|
||||
vars: 1,
|
||||
|
||||
// {#for (item of items); track $index + offset}{{item}}{{item}}{/for}
|
||||
template:
|
||||
function TestComponent_Template(rf: number, ctx: any) {
|
||||
if (rf & 1) {
|
||||
ɵɵrepeaterCreate(
|
||||
0, App_ng_template_0_Template, 1, 1, ($index) => $index + ctx.offset);
|
||||
}
|
||||
if (rf & 2) {
|
||||
ɵɵrepeater(0, ctx.items);
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
static ɵfac = function TestComponent_Factory(t: any) {
|
||||
return new (t || TestComponent)();
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = TestBed.createComponent(TestComponent);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('abc');
|
||||
|
||||
// explicitly modify the DOM text node to make sure that the list reconciliation algorithm
|
||||
// based on tracking indices overrides it.
|
||||
fixture.debugElement.childNodes[1].nativeNode.data = 'x';
|
||||
fixture.componentInstance.items.shift();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.textContent).toBe('bc');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue