Skip to content

Commit

Permalink
refactor(core): built-in control flow - repeaters (angular#51422)
Browse files Browse the repository at this point in the history
Draft of the runtime implementation for the built-in repeaters.

PR Close angular#51422
  • Loading branch information
pkozlowski-opensource authored and thePunderWoman committed Aug 28, 2023
1 parent 88fcd27 commit cdcfa09
Show file tree
Hide file tree
Showing 4 changed files with 428 additions and 3 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ export {
ɵɵresolveWindow,
ɵɵrestoreView,

ɵɵrepeater,
ɵɵrepeaterCreate,
ɵɵrepeaterTrackByIdentity,
ɵɵrepeaterTrackByIndex,

ɵɵsetComponentScope,
ɵɵsetNgModuleScope,
ɵɵgetComponentDepsFactory,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/render3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ export {

ɵɵreference,

ɵɵrepeater,
ɵɵrepeaterCreate,
ɵɵrepeaterTrackByIdentity,
ɵɵrepeaterTrackByIndex,

ɵɵstyleMap,
ɵɵstyleMapInterpolate1,
ɵɵstyleMapInterpolate2,
Expand Down
181 changes: 179 additions & 2 deletions packages/core/src/render3/instructions/control_flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,13 +61,184 @@ 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);

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);
Expand Down
Loading

0 comments on commit cdcfa09

Please sign in to comment.