UNPKG

@rx-angular/cdk

Version:

@rx-angular/cdk is a Component Development Kit for ergonomic and highly performant angular applications. It helps to to build Large scale applications, UI libs, state management, rendering systems and much more. Furthermore the unique way of mixing reacti

1,199 lines (1,187 loc) 46.1 kB
import { onStrategy, strategyHandling } from '@rx-angular/cdk/render-strategies'; import { of, BehaviorSubject, concat, combineLatest, ReplaySubject, EMPTY, merge } from 'rxjs'; import { switchMap, ignoreElements, catchError, distinctUntilChanged, map, tap, withLatestFrom } from 'rxjs/operators'; // copied from https://github.com/angular/angular/blob/main/packages/core/src/render3/list_reconciliation.ts /** * A type representing the live collection to be reconciled with any new (incoming) collection. This * is an adapter class that makes it possible to work with different internal data structures, * regardless of the actual values of the incoming collection. */ class LiveCollection { destroy(item) { // noop by default } updateValue(index, value) { // noop by default } // operations below could be implemented on top of the operations defined so far, but having // them explicitly allow clear expression of intent and potentially more performant // implementations swap(index1, index2) { const startIdx = Math.min(index1, index2); const endIdx = Math.max(index1, index2); const endItem = this.detach(endIdx); if (endIdx - startIdx > 1) { const startItem = this.detach(startIdx); this.attach(startIdx, endItem); this.attach(endIdx, startItem); } else { this.attach(startIdx, endItem); } } move(prevIndex, newIdx) { this.attach(newIdx, this.detach(prevIndex)); } } function valuesMatching(liveIdx, liveValue, newIdx, newValue, trackBy) { if (liveIdx === newIdx && Object.is(liveValue, newValue)) { // matching and no value identity to update return 1; } else if (Object.is(trackBy(liveIdx, liveValue), trackBy(newIdx, newValue))) { // matching but requires value identity update return -1; } return 0; } function recordDuplicateKeys(keyToIdx, key, idx) { const idxSoFar = keyToIdx.get(key); if (idxSoFar !== undefined) { idxSoFar.add(idx); } else { keyToIdx.set(key, new Set([idx])); } } /** * The live collection reconciliation algorithm that perform various in-place operations, so it * reflects the content of the new (incoming) collection. * * The reconciliation algorithm has 2 code paths: * - "fast" path that don't require any memory allocation; * - "slow" path that requires additional memory allocation for intermediate data structures used to * collect additional information about the live collection. * It might happen that the algorithm switches between the two modes in question in a single * reconciliation path - generally it tries to stay on the "fast" path as much as possible. * * The overall complexity of the algorithm is O(n + m) for speed and O(n) for memory (where n is the * length of the live collection and m is the length of the incoming collection). Given the problem * at hand the complexity / performance constraints makes it impossible to perform the absolute * minimum of operation to reconcile the 2 collections. The algorithm makes different tradeoffs to * stay within reasonable performance bounds and may apply sub-optimal number of operations in * certain situations. * * @param liveCollection the current, live collection; * @param newCollection the new, incoming collection; * @param trackByFn key generation function that determines equality between items in the life and * incoming collection; */ function reconcile(liveCollection, newCollection, trackByFn) { let detachedItems = undefined; let liveKeysInTheFuture = undefined; let liveStartIdx = 0; let liveEndIdx = liveCollection.length - 1; const duplicateKeys = ngDevMode ? new Map() : undefined; if (Array.isArray(newCollection)) { let newEndIdx = newCollection.length - 1; while (liveStartIdx <= liveEndIdx && liveStartIdx <= newEndIdx) { // compare from the beginning const liveStartValue = liveCollection.at(liveStartIdx); const newStartValue = newCollection[liveStartIdx]; if (ngDevMode) { recordDuplicateKeys(duplicateKeys, trackByFn(liveStartIdx, newStartValue), liveStartIdx); } const isStartMatching = valuesMatching(liveStartIdx, liveStartValue, liveStartIdx, newStartValue, trackByFn); if (isStartMatching !== 0) { if (isStartMatching < 0) { liveCollection.updateValue(liveStartIdx, newStartValue); } liveStartIdx++; continue; } // compare from the end // TODO(perf): do _all_ the matching from the end const liveEndValue = liveCollection.at(liveEndIdx); const newEndValue = newCollection[newEndIdx]; if (ngDevMode) { recordDuplicateKeys(duplicateKeys, trackByFn(newEndIdx, newEndValue), newEndIdx); } const isEndMatching = valuesMatching(liveEndIdx, liveEndValue, newEndIdx, newEndValue, trackByFn); if (isEndMatching !== 0) { if (isEndMatching < 0) { liveCollection.updateValue(liveEndIdx, newEndValue); } liveEndIdx--; newEndIdx--; continue; } // Detect swap and moves: const liveStartKey = trackByFn(liveStartIdx, liveStartValue); const liveEndKey = trackByFn(liveEndIdx, liveEndValue); const newStartKey = trackByFn(liveStartIdx, newStartValue); if (Object.is(newStartKey, liveEndKey)) { const newEndKey = trackByFn(newEndIdx, newEndValue); // detect swap on both ends; if (Object.is(newEndKey, liveStartKey)) { liveCollection.swap(liveStartIdx, liveEndIdx); liveCollection.updateValue(liveEndIdx, newEndValue); newEndIdx--; liveEndIdx--; } else { // the new item is the same as the live item with the end pointer - this is a move forward // to an earlier index; liveCollection.move(liveEndIdx, liveStartIdx); } liveCollection.updateValue(liveStartIdx, newStartValue); liveStartIdx++; continue; } // Fallback to the slow path: we need to learn more about the content of the live and new // collections. detachedItems ??= new UniqueValueMultiKeyMap(); liveKeysInTheFuture ??= initLiveItemsInTheFuture(liveCollection, liveStartIdx, liveEndIdx, trackByFn); // Check if I'm inserting a previously detached item: if so, attach it here if (attachPreviouslyDetached(liveCollection, detachedItems, liveStartIdx, newStartKey)) { liveCollection.updateValue(liveStartIdx, newStartValue); liveStartIdx++; liveEndIdx++; } else if (!liveKeysInTheFuture.has(newStartKey)) { // Check if we seen a new item that doesn't exist in the old collection and must be INSERTED const newItem = liveCollection.create(liveStartIdx, newCollection[liveStartIdx]); liveCollection.attach(liveStartIdx, newItem); liveStartIdx++; liveEndIdx++; } else { // We know that the new item exists later on in old collection but we don't know its index // and as the consequence can't move it (don't know where to find it). Detach the old item, // hoping that it unlocks the fast path again. detachedItems.set(liveStartKey, liveCollection.detach(liveStartIdx)); liveEndIdx--; } } // Final cleanup steps: // - more items in the new collection => insert while (liveStartIdx <= newEndIdx) { createOrAttach(liveCollection, detachedItems, trackByFn, liveStartIdx, newCollection[liveStartIdx]); liveStartIdx++; } } else if (newCollection != null) { // iterable - immediately fallback to the slow path const newCollectionIterator = newCollection[Symbol.iterator](); let newIterationResult = newCollectionIterator.next(); while (!newIterationResult.done && liveStartIdx <= liveEndIdx) { const liveValue = liveCollection.at(liveStartIdx); const newValue = newIterationResult.value; if (ngDevMode) { recordDuplicateKeys(duplicateKeys, trackByFn(liveStartIdx, newValue), liveStartIdx); } const isStartMatching = valuesMatching(liveStartIdx, liveValue, liveStartIdx, newValue, trackByFn); if (isStartMatching !== 0) { // found a match - move on, but update value if (isStartMatching < 0) { liveCollection.updateValue(liveStartIdx, newValue); } liveStartIdx++; newIterationResult = newCollectionIterator.next(); } else { detachedItems ??= new UniqueValueMultiKeyMap(); liveKeysInTheFuture ??= initLiveItemsInTheFuture(liveCollection, liveStartIdx, liveEndIdx, trackByFn); // Check if I'm inserting a previously detached item: if so, attach it here const newKey = trackByFn(liveStartIdx, newValue); if (attachPreviouslyDetached(liveCollection, detachedItems, liveStartIdx, newKey)) { liveCollection.updateValue(liveStartIdx, newValue); liveStartIdx++; liveEndIdx++; newIterationResult = newCollectionIterator.next(); } else if (!liveKeysInTheFuture.has(newKey)) { liveCollection.attach(liveStartIdx, liveCollection.create(liveStartIdx, newValue)); liveStartIdx++; liveEndIdx++; newIterationResult = newCollectionIterator.next(); } else { // it is a move forward - detach the current item without advancing in collections const liveKey = trackByFn(liveStartIdx, liveValue); detachedItems.set(liveKey, liveCollection.detach(liveStartIdx)); liveEndIdx--; } } } // this is a new item as we run out of the items in the old collection - create or attach a // previously detached one while (!newIterationResult.done) { createOrAttach(liveCollection, detachedItems, trackByFn, liveCollection.length, newIterationResult.value); newIterationResult = newCollectionIterator.next(); } } // Cleanups common to the array and iterable: // - more items in the live collection => delete starting from the end; while (liveStartIdx <= liveEndIdx) { liveCollection.destroy(liveCollection.detach(liveEndIdx--)); } // - destroy items that were detached but never attached again. detachedItems?.forEach((item) => { liveCollection.destroy(item); }); // report duplicate keys (dev mode only) if (ngDevMode) { const duplicatedKeysMsg = []; for (const [key, idxSet] of duplicateKeys) { if (idxSet.size > 1) { const idx = [...idxSet].sort((a, b) => a - b); for (let i = 1; i < idx.length; i++) { duplicatedKeysMsg.push(`key "${key}" at index "${idx[i - 1]}" and "${idx[i]}"`); } } } if (duplicatedKeysMsg.length > 0) { const message = 'The provided track expression resulted in duplicated keys for a given collection. ' + 'Adjust the tracking expression such that it uniquely identifies all the items in the collection. ' + 'Duplicated keys were: \n' + duplicatedKeysMsg.join(', \n') + '.'; // tslint:disable-next-line:no-console console.warn(message); } } } function attachPreviouslyDetached(prevCollection, detachedItems, index, key) { if (detachedItems !== undefined && detachedItems.has(key)) { prevCollection.attach(index, detachedItems.get(key)); detachedItems.delete(key); return true; } return false; } function createOrAttach(liveCollection, detachedItems, trackByFn, index, value) { if (!attachPreviouslyDetached(liveCollection, detachedItems, index, trackByFn(index, value))) { const newItem = liveCollection.create(index, value); liveCollection.attach(index, newItem); } else { liveCollection.updateValue(index, value); } } function initLiveItemsInTheFuture(liveCollection, start, end, trackByFn) { const keys = new Set(); for (let i = start; i <= end; i++) { keys.add(trackByFn(i, liveCollection.at(i))); } return keys; } /** * A specific, partial implementation of the Map interface with the following characteristics: * - allows multiple values for a given key; * - maintain FIFO order for multiple values corresponding to a given key; * - assumes that all values are unique. * * The implementation aims at having the minimal overhead for cases where keys are _not_ duplicated * (the most common case in the list reconciliation algorithm). To achieve this, the first value for * a given key is stored in a regular map. Then, when more values are set for a given key, we * maintain a form of linked list in a separate map. To maintain this linked list we assume that all * values (in the entire collection) are unique. */ class UniqueValueMultiKeyMap { constructor() { // A map from a key to the first value corresponding to this key. this.kvMap = new Map(); // A map that acts as a linked list of values - each value maps to the next value in this "linked // list" (this only works if values are unique). Allocated lazily to avoid memory consumption when // there are no duplicated values. this._vMap = undefined; } has(key) { return this.kvMap.has(key); } delete(key) { if (!this.has(key)) return false; const value = this.kvMap.get(key); if (this._vMap !== undefined && this._vMap.has(value)) { this.kvMap.set(key, this._vMap.get(value)); this._vMap.delete(value); } else { this.kvMap.delete(key); } return true; } get(key) { return this.kvMap.get(key); } set(key, value) { if (this.kvMap.has(key)) { let prevValue = this.kvMap.get(key); // Note: we don't use `assertNotSame`, because the value needs to be stringified even if // there is no error which can freeze the browser for large values (see #58509). /*if (ngDevMode && prevValue === value) { throw new Error( `Detected a duplicated value ${value} for the key ${key}`, ); }*/ if (this._vMap === undefined) { this._vMap = new Map(); } const vMap = this._vMap; while (vMap.has(prevValue)) { prevValue = vMap.get(prevValue); } vMap.set(prevValue, value); } else { this.kvMap.set(key, value); } } forEach(cb) { // eslint-disable-next-line prefer-const for (let [key, value] of this.kvMap) { cb(value, key); if (this._vMap !== undefined) { const vMap = this._vMap; while (vMap.has(value)) { value = vMap.get(value); cb(value, key); } } } } } /** * @internal * creates an embeddedViewRef * * @param viewContainerRef * @param templateRef * @param context * @param index * @return EmbeddedViewRef<C> */ function createEmbeddedView(viewContainerRef, templateRef, context, index = 0) { const view = viewContainerRef.createEmbeddedView(templateRef, context, index); view.detectChanges(); return view; } /** * @internal * * A factory function returning an object to handle `TemplateRef`'s. * You can add and get a `TemplateRef`. * */ function templateHandling(viewContainerRef) { const templateCache = new Map(); const get$ = (name) => { return templateCache.get(name) || of(undefined); }; const get = (name) => { let ref; const templatRef$ = get$(name); if (templatRef$) { const sub = templatRef$.subscribe((r) => (ref = r)); sub.unsubscribe(); } return ref; }; return { add(name, templateRef) { assertTemplate(name, templateRef); if (!templateCache.has(name)) { templateCache.set(name, new BehaviorSubject(templateRef)); } else { templateCache.get(name).next(templateRef); } }, get$, get, createEmbeddedView: (name, context) => createEmbeddedView(viewContainerRef, get(name), context), }; // function assertTemplate(property, templateRef) { const isTemplateRefOrNull = !!(!templateRef || templateRef.createEmbeddedView); if (!isTemplateRefOrNull) { throw new Error(`${property} must be a TemplateRef, but received ${typeof templateRef}`); } return isTemplateRefOrNull; } } /** * @internal * * A side effect operator similar to `tap` but with a static internal logic. * It calls detect changes on the 'VirtualParent' and the injectingViewCdRef. * * @param injectingViewCdRef * @param strategy * @param notifyNeeded * @param ngZone */ function notifyAllParentsIfNeeded(injectingViewCdRef, strategy, notifyNeeded, ngZone) { return (o$) => o$.pipe(switchMap((v) => { const notifyParent = notifyNeeded(); if (!notifyParent) { return of(v); } return concat(of(v), onStrategy(injectingViewCdRef, strategy, (_v, work, options) => { /*console.log( 'notifyAllParentsIfNeeded injectingView', (injectingViewCdRef as any).context );*/ work(injectingViewCdRef, options.scope); }, { scope: injectingViewCdRef.context || injectingViewCdRef, ngZone, }).pipe(ignoreElements())); })); } /** * @internal * * Factory that returns a `ListTemplateManager` for the passed params. * * @param templateSettings */ function getTemplateHandler(templateSettings) { const { viewContainerRef, initialTemplateRef, createViewContext, updateViewContext, } = templateSettings; return { updateUnchangedContext, insertView, moveView, removeView, getListChanges, updateView, }; // ===== function updateUnchangedContext(item, index, count) { const view = viewContainerRef.get(index); updateViewContext(item, view, { count, index, }); view.detectChanges(); } function moveView(oldIndex, item, index, count) { const oldView = viewContainerRef.get(oldIndex); const view = viewContainerRef.move(oldView, index); updateViewContext(item, view, { count, index, }); view.detectChanges(); } function updateView(item, index, count) { const view = viewContainerRef.get(index); updateViewContext(item, view, { count, index, }); view.detectChanges(); } function removeView(index) { return viewContainerRef.remove(index); } function insertView(item, index, count) { createEmbeddedView(viewContainerRef, initialTemplateRef, createViewContext(item, { count, index, }), index); } } /** * @internal * * @param changes * @param items */ function getListChanges(changes, items) { const changedIdxs = new Set(); const changesArr = []; let notifyParent = false; changes.forEachOperation((record, adjustedPreviousIndex, currentIndex) => { const item = record.item; if (record.previousIndex == null) { // insert changesArr.push(getInsertChange(item, currentIndex === null ? undefined : currentIndex)); changedIdxs.add(item); notifyParent = true; } else if (currentIndex == null) { // remove changesArr.push(getRemoveChange(item, adjustedPreviousIndex === null ? undefined : adjustedPreviousIndex)); notifyParent = true; } else if (adjustedPreviousIndex !== null) { // move changesArr.push(getMoveChange(item, currentIndex, adjustedPreviousIndex)); changedIdxs.add(item); notifyParent = true; } }); changes.forEachIdentityChange((record) => { const item = record.item; if (!changedIdxs.has(item)) { changesArr.push(getUpdateChange(item, record.currentIndex)); changedIdxs.add(item); } }); items.forEach((item, index) => { if (!changedIdxs.has(item)) { changesArr.push(getUnchangedChange(item, index)); } }); return [changesArr, notifyParent]; // ========== function getMoveChange(item, currentIndex, adjustedPreviousIndex) { return [ 2 /* RxListTemplateChangeType.move */, [item, currentIndex, adjustedPreviousIndex], ]; } function getUpdateChange(item, currentIndex) { return [3 /* RxListTemplateChangeType.update */, [item, currentIndex]]; } function getUnchangedChange(item, index) { return [4 /* RxListTemplateChangeType.context */, [item, index]]; } function getInsertChange(item, currentIndex) { return [ 0 /* RxListTemplateChangeType.insert */, [item, currentIndex === null ? undefined : currentIndex], ]; } function getRemoveChange(item, adjustedPreviousIndex) { return [ 1 /* RxListTemplateChangeType.remove */, [ item, adjustedPreviousIndex === null ? undefined : adjustedPreviousIndex, ], ]; } } /** @internal **/ function isRxRenderError(e) { return (e != null && Array.isArray(e) && e.length === 2 && e[0] instanceof Error); } /** @internal **/ function createErrorHandler(_handler) { const _handleError = _handler ? (e) => _handler.handleError(e) : console.error; return { handleError: (error) => { if (isRxRenderError(error)) { _handleError(error[0]); console.error('additionalErrorContext', error[1]); } else { _handleError(error); } }, }; } /** @internal **/ function toRenderError(e, context) { return [e, context]; } function createListTemplateManager(config) { const { templateSettings, renderSettings, trackBy, iterableDiffers } = config; const { defaultStrategyName, strategies, cdRef: injectingViewCdRef, patchZone, parent, } = renderSettings; const errorHandler = createErrorHandler(renderSettings.errorHandler); const ngZone = patchZone ? patchZone : undefined; const strategyHandling$ = strategyHandling(defaultStrategyName, strategies); let _differ; function getDiffer(values) { if (_differ) { return _differ; } return values ? (_differ = iterableDiffers.find(values).create(trackBy)) : null; } // type, context /* TODO (regarding createView): this is currently not in use. for the list-manager this would mean to provide functions for not only create. developers than should have to provide create, move, remove,... the whole thing. i don't know if this is the right decision for a first RC */ const listViewHandler = getTemplateHandler({ ...templateSettings, initialTemplateRef: templateSettings.templateRef, }); const viewContainerRef = templateSettings.viewContainerRef; let notifyParent = false; let changesArr; let partiallyFinished = false; return { nextStrategy(nextConfig) { strategyHandling$.next(nextConfig); }, render(values$) { return values$.pipe(render()); }, }; function handleError() { return (o$) => o$.pipe(catchError((err) => { partiallyFinished = false; errorHandler.handleError(err); return of(null); })); } function render() { return (o$) => combineLatest([ o$, strategyHandling$.strategy$.pipe(distinctUntilChanged()), ]).pipe(map(([iterable, strategy]) => { try { const differ = getDiffer(iterable); let changes; if (differ) { if (partiallyFinished) { const currentIterable = []; for (let i = 0, ilen = viewContainerRef.length; i < ilen; i++) { const viewRef = viewContainerRef.get(i); currentIterable[i] = viewRef.context.$implicit; } differ.diff(currentIterable); } changes = differ.diff(iterable); } return { changes, iterable, strategy, }; } catch { throw new Error(`Error trying to diff '${iterable}'. Only arrays and iterables are allowed`); } }), // Cancel old renders switchMap(({ changes, iterable, strategy }) => { if (!changes) { return of([]); } const values = iterable || []; // TODO: we might want to treat other iterables in a more performant way than Array.from() const items = Array.isArray(values) ? values : Array.from(iterable); const listChanges = listViewHandler.getListChanges(changes, items); changesArr = listChanges[0]; const insertedOrRemoved = listChanges[1]; const applyChanges$ = getObservablesFromChangesArray(changesArr, strategy, items.length); partiallyFinished = true; notifyParent = insertedOrRemoved && parent; return combineLatest(applyChanges$.length > 0 ? applyChanges$ : [of(null)]).pipe(tap(() => (partiallyFinished = false)), notifyAllParentsIfNeeded(injectingViewCdRef, strategy, () => notifyParent, ngZone), handleError(), map(() => iterable)); }), handleError()); } /** * @internal * * returns an array of streams which process all of the view updates needed to reflect the latest diff to the * viewContainer. * I * * @param changes * @param strategy * @param count */ function getObservablesFromChangesArray(changes, strategy, count) { return changes.length > 0 ? changes.map((change) => { const payload = change[1]; return onStrategy(change[0], strategy, (type) => { switch (type) { case 0 /* RxListTemplateChangeType.insert */: listViewHandler.insertView(payload[0], payload[1], count); break; case 2 /* RxListTemplateChangeType.move */: listViewHandler.moveView(payload[2], payload[0], payload[1], count); break; case 1 /* RxListTemplateChangeType.remove */: listViewHandler.removeView(payload[1]); break; case 3 /* RxListTemplateChangeType.update */: listViewHandler.updateView(payload[0], payload[1], count); break; case 4 /* RxListTemplateChangeType.context */: listViewHandler.updateUnchangedContext(payload[0], payload[1], count); break; } }, { ngZone }); }) : [of(null)]; } } const computeFirst = ({ count, index }) => index === 0; const computeLast = ({ count, index }) => index === count - 1; const computeEven = ({ count, index }) => index % 2 === 0; class RxDefaultListViewContext { set $implicit($implicit) { this._$implicit = $implicit; this._item.next($implicit); } get $implicit() { return this._$implicit; } get $complete() { return this._$complete; } get $error() { return this._$error; } get $suspense() { return this._$suspense; } get index() { return this._context$.getValue().index; } get count() { return this._context$.getValue().count; } get first() { return computeFirst(this._context$.getValue()); } get last() { return computeLast(this._context$.getValue()); } get even() { return computeEven(this._context$.getValue()); } get odd() { return !this.even; } get index$() { return this._context$.pipe(map((c) => c.index), distinctUntilChanged()); } get count$() { return this._context$.pipe(map((s) => s.count), distinctUntilChanged()); } get first$() { return this._context$.pipe(map(computeFirst), distinctUntilChanged()); } get last$() { return this._context$.pipe(map(computeLast), distinctUntilChanged()); } get even$() { return this._context$.pipe(map(computeEven), distinctUntilChanged()); } get odd$() { return this.even$.pipe(map((even) => !even)); } constructor(item, customProps) { this._item = new ReplaySubject(1); this.item$ = this._item.asObservable(); this._context$ = new BehaviorSubject({ index: -1, count: -1, }); this.select = (props) => { return this.item$.pipe(map((r) => props.reduce((acc, key) => acc?.[key], r))); }; this.$implicit = item; if (customProps) { this.updateContext(customProps); } } updateContext(newProps) { this._context$.next({ ...this._context$.getValue(), ...newProps, }); } } var RxBaseTemplateNames; (function (RxBaseTemplateNames) { RxBaseTemplateNames["error"] = "errorTpl"; RxBaseTemplateNames["complete"] = "completeTpl"; RxBaseTemplateNames["suspense"] = "suspenseTpl"; })(RxBaseTemplateNames || (RxBaseTemplateNames = {})); class WorkQueue { constructor(strategyProvider) { this.strategyProvider = strategyProvider; this.queue = new Map(); this.length = 0; } patch(view, data) { if (this.queue.has(view)) { const entries = this.queue.get(view); const lastEntry = entries[entries.length - 1]; /*console.log( 'patch I has a work in queue', data.type, this.queue.get(view).map((w) => w.type), );*/ const work = lastEntry.work; lastEntry.work = () => { const view = work(); const view2 = data.work(); return view ?? view2; }; } else { this.set(view, data); } } override(view, data) { if (this.queue.has(view)) { const entries = this.queue.get(view); const lastEntry = entries[entries.length - 1]; this.queue.set(view, [ { work: data.work, type: 'remove', order: lastEntry.order, }, ]); } else { this.set(view, data); } } set(view, data) { if (this.queue.has(view)) { /* console.log( 'I has a work in queue', data.type, this.queue.get(view).map((w) => w.type), );*/ this.queue .get(view) .push({ work: data.work, type: data.type, order: this.length++ }); } else { this.queue.set(view, [ { work: data.work, type: data.type, order: this.length++ }, ]); } } flush(strategy, ngZone) { // console.log('operations', this.length); return combineLatest(Array.from(this.queue.values()) .flatMap((entry) => entry) .sort((a, b) => a.order - b.order) .map(({ work }) => { // console.log('operation', type); return onStrategy(null, this.strategyProvider.strategies[strategy], () => { // console.log('exec order', order, type); const view = work(); view?.detectChanges(); }, { ngZone }); })); } clear() { this.queue.clear(); this.length = 0; } } class RxLiveCollection extends LiveCollection { set needHostUpdate(needHostUpdate) { this._needHostUpdate = needHostUpdate; } get needHostUpdate() { return this._needHostUpdate; } constructor(viewContainer, templateRef, strategyProvider, createViewContext, updateViewContext) { super(); this.viewContainer = viewContainer; this.templateRef = templateRef; this.strategyProvider = strategyProvider; this.createViewContext = createViewContext; this.updateViewContext = updateViewContext; /** Property indicating if indexes in the repeater context need to be updated following the live collection changes. Index updates are necessary if and only if views are inserted / removed in the middle of LContainer. Adds and removals at the end don't require index updates. */ this.needsIndexUpdate = false; this._needHostUpdate = false; this.lastCount = undefined; this.workQueue = new WorkQueue(this.strategyProvider); } flushQueue(strategy, ngZone) { return this.workQueue.flush(strategy, ngZone); } get length() { return this._virtualViews.length; } at(index) { // console.log('live-coll: at', { index }); return this.getView(index).context.$implicit; } attach(index, view) { this.needsIndexUpdate ||= index !== this.length; this.needHostUpdate = true; addToArray(this._virtualViews, index, view); // console.log('live-coll: attach', { index, existingWork }); this.workQueue.set(view.context.$implicit, { work: () => { return this.attachView(view, index); }, type: 'attach', }); } attachView(view, index) { if (view._tempView) { // fake view return (this._virtualViews[index] = (this.viewContainer.createEmbeddedView(this.templateRef, this.createViewContext(view.context.$implicit, { index, count: this.length, }), { index }))); } // TODO: this is only here because at the time of `create` we don't have information about the count yet this.updateViewContext(view.context.$implicit, view, { index, count: this.length, }); return this.viewContainer.insert(view, index); } detach(index) { this.needsIndexUpdate ||= index !== this.length - 1; const detachedView = removeFromArray(this._virtualViews, index); // console.log('live-coll: detach', { index, existingWork }); this.workQueue.set(detachedView.context.$implicit, { work: () => { // return undefined, to prevent `.detectChanges` being called return this.detachView(index); }, type: 'detach', }); return detachedView; } detachView(index) { this.viewContainer.detach(index); return undefined; } create(index, value) { // console.log('live-coll: create', { index, value }); // only create a fake EmbeddedView return { context: { $implicit: value, index }, _tempView: true, }; } destroy(view) { // console.log('live-coll: destroy', { existingWork }); this.needHostUpdate = true; this.workQueue.override(view.context.$implicit, { work: () => { this.destroyView(view); // return undefined, to prevent `.detectChanges` being called return undefined; }, type: 'remove', }); } destroyView(view) { view.destroy(); return view; } updateValue(index, value) { const view = this.getView(index); // console.log('live-coll: updateValue', { index, value, existingWork }); this.workQueue.patch(view.context.$implicit, { work: () => { return this.updateView(value, index, view); }, type: 'update', }); } updateView(value, index, view) { this.updateViewContext(value, view, { index, count: this.length }); return view; } reset() { this._virtualViews = []; this.workQueue.clear(); for (let i = 0; i < this.viewContainer.length; i++) { this._virtualViews[i] = this.viewContainer.get(i); } this.needsIndexUpdate = false; this.needHostUpdate = false; } updateIndexes() { const count = this.length; if (this.needsIndexUpdate || (this.lastCount !== undefined && this.lastCount !== count)) { // console.log('live-coll: updateIndexes'); for (let i = 0; i < count; i++) { const view = this.getView(i); this.workQueue.patch(view.context.$implicit, { work: () => { const v = this.getView(i); if (v.context.index !== i || v.context.count !== count) { return this.updateView(v.context.$implicit, i, v); } }, type: 'update', }); } } this.lastCount = count; } getView(index) { return (this._virtualViews[index] ?? this.viewContainer.get(index)); } } function addToArray(arr, index, value) { // perf: array.push is faster than array.splice! if (index >= arr.length) { arr.push(value); } else { arr.splice(index, 0, value); } } function removeFromArray(arr, index) { // perf: array.pop is faster than array.splice! if (index >= arr.length - 1) { return arr.pop(); } else { return arr.splice(index, 1)[0]; } } /** * @internal * * A factory function that returns a map of projections to turn a notification of a Observable (next, error, complete) * * @param customNextContext - projection function to provide custom properties as well as override existing */ function notificationKindToViewContext(customNextContext) { // @TODO rethink overrides return { suspense: (notification) => { const $implicit = notification.value; return { $implicit, suspense: true, error: false, complete: false, ...customNextContext($implicit), }; }, next: (notification) => { const $implicit = notification.value; return { $implicit, suspense: false, error: false, complete: false, ...customNextContext($implicit), }; }, error: (notification) => { const $implicit = notification.value; return { $implicit, complete: false, error: notification.error || true, suspense: false, ...customNextContext($implicit), }; }, complete: (notification) => { const $implicit = notification.value; return { $implicit, error: false, complete: true, suspense: false, ...customNextContext($implicit), }; }, }; } function createTemplateManager(config) { const { renderSettings, notificationToTemplateName, templateSettings } = config; const { defaultStrategyName, strategies, cdRef: injectingViewCdRef, patchZone, parent, } = renderSettings; const errorHandler = createErrorHandler(renderSettings.errorHandler); const ngZone = patchZone ? patchZone : undefined; let activeTemplate; const strategyHandling$ = strategyHandling(defaultStrategyName, strategies); const templates = templateHandling(templateSettings.viewContainerRef); const viewContainerRef = templateSettings.viewContainerRef; const triggerHandling = config.templateTrigger$ || EMPTY; const getContext = notificationKindToViewContext(templateSettings.customContext || (() => ({}))); return { addTemplateRef: (name, templateRef) => { templates.add(name, templateRef); }, nextStrategy: strategyHandling$.next, render(values$) { let trg; let notification = { value: undefined, complete: false, error: false, kind: "suspense" /* RxNotificationKind.Suspense */, hasValue: false, }; return merge(values$.pipe(tap((n) => (notification = n))), triggerHandling.pipe(tap((trigger) => (trg = trigger)))).pipe(switchMap(() => { const contextKind = trg || notification.kind; trg = undefined; const value = notification.value; const templateName = notificationToTemplateName[contextKind](value, templates); return templates.get$(templateName).pipe(map((template) => ({ template, templateName, notification, contextKind, }))); }), withLatestFrom(strategyHandling$.strategy$), // Cancel old renders switchMap(([{ template, templateName, notification, contextKind }, strategy,]) => { const isNewTemplate = activeTemplate !== template || !template; const notifyParent = isNewTemplate && parent; return onStrategy(notification.value, strategy, (v, work, options) => { const context = getContext[contextKind](notification); if (isNewTemplate) { // template has changed (undefined => next; suspense => next; ...) // handle remove & insert // remove current view if there is any if (viewContainerRef.length > 0) { // patch removal if needed viewContainerRef.clear(); } // create new view if any if (template) { // createEmbeddedView is already patched, no need for workFactory templates.createEmbeddedView(templateName, context); } } else if (template) { // template didn't change, update it // handle update const view = viewContainerRef.get(0); Object.keys(context).forEach((k) => { view.context[k] = context[k]; }); // update view context, patch if needed work(view, options.scope, notification); } activeTemplate = template; }, { ngZone } // we don't need to specify any scope here. The template manager is the only one // who will call `viewRef#detectChanges` on any of the templates it manages. // whenever a new value comes in, any pre-scheduled work of this taskManager will // be nooped before a new work will be scheduled. This happens because of the implementation // of `StrategyCredential#behavior` ).pipe(notifyAllParentsIfNeeded(injectingViewCdRef, strategy, () => notifyParent, ngZone), catchError((e) => { errorHandler.handleError(e); return of(e); })); })); }, }; } /** * Generated bundle index. Do not edit. */ export { LiveCollection, RxBaseTemplateNames, RxDefaultListViewContext, RxLiveCollection, createListTemplateManager, createTemplateManager, reconcile, templateHandling }; //# sourceMappingURL=cdk-template.mjs.map