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