Skip to content

Commit 01c770a

Browse files
committed
feat(template): implement new reconciliation algorithm
This commit introduces an abstraction layer for the way rxFor diffs values. The new `provideExperimentalRxForReconciliation` provider function allows developers to enable the same reconciliation algorithm for rxFor as used in the new @for control flow.
1 parent 568d8b1 commit 01c770a

File tree

7 files changed

+245
-37
lines changed

7 files changed

+245
-37
lines changed

libs/template/for/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { RxFor } from './lib/for.directive';
22
export { RxForViewContext } from './lib/for-view-context';
3+
export { provideExperimentalRxForReconciliation } from './lib/provide-experimental-reconciler';
4+
export { provideLegacyRxForReconciliation } from './lib/provide-legacy-reconciler';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { InjectionToken } from '@angular/core';
2+
import { LEGACY_RXFOR_RECONCILIATION_FACTORY } from './provide-legacy-reconciler';
3+
import { RxReconcileFactory } from './reconcile-factory';
4+
5+
/** @internal */
6+
export const INTERNAL_RX_FOR_RECONCILER_TOKEN =
7+
new InjectionToken<RxReconcileFactory>('rx-for-reconciler', {
8+
providedIn: 'root',
9+
factory: LEGACY_RXFOR_RECONCILIATION_FACTORY,
10+
});

libs/template/for/src/lib/for.directive.ts

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
Injector,
99
Input,
1010
isSignal,
11-
IterableDiffers,
1211
NgIterable,
1312
NgZone,
1413
OnDestroy,
@@ -27,11 +26,7 @@ import {
2726
RxStrategyNames,
2827
RxStrategyProvider,
2928
} from '@rx-angular/cdk/render-strategies';
30-
import {
31-
createListTemplateManager,
32-
RxListManager,
33-
RxListViewComputedContext,
34-
} from '@rx-angular/cdk/template';
29+
import { RxListViewComputedContext } from '@rx-angular/cdk/template';
3530
import {
3631
isObservable,
3732
Observable,
@@ -41,6 +36,7 @@ import {
4136
} from 'rxjs';
4237
import { shareReplay, switchAll } from 'rxjs/operators';
4338
import { RxForViewContext } from './for-view-context';
39+
import { injectReconciler } from './inject-reconciler';
4440

4541
/**
4642
* @description Will be provided through Terser global definitions by Angular CLI
@@ -84,8 +80,6 @@ declare const ngDevMode: boolean;
8480
export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
8581
implements OnInit, DoCheck, OnDestroy
8682
{
87-
/** @internal */
88-
private iterableDiffers = inject(IterableDiffers);
8983
/** @internal */
9084
private cdRef = inject(ChangeDetectorRef);
9185
/** @internal */
@@ -162,7 +156,8 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
162156
* @description
163157
*
164158
* You can change the used `RenderStrategy` by using the `strategy` input of the `*rxFor`. It accepts
165-
* an `Observable<RxStrategyNames>` or [`RxStrategyNames`](https://github.com/rx-angular/rx-angular/blob/b0630f69017cc1871d093e976006066d5f2005b9/libs/cdk/render-strategies/src/lib/model.ts#L52).
159+
* an `Observable<RxStrategyNames>` or
160+
* [`RxStrategyNames`](https://github.com/rx-angular/rx-angular/blob/b0630f69017cc1871d093e976006066d5f2005b9/libs/cdk/render-strategies/src/lib/model.ts#L52).
166161
*
167162
* The default value for strategy is
168163
* [`normal`](https://www.rx-angular.io/docs/template/cdk/render-strategies/strategies/concurrent-strategies).
@@ -215,7 +210,8 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
215210
* - `@ContentChildren`
216211
*
217212
* Read more about this in the
218-
* [official docs](https://www.rx-angular.io/docs/template/rx-for-directive#local-strategies-and-view-content-queries-parent).
213+
* [official
214+
* docs](https://www.rx-angular.io/docs/template/rx-for-directive#local-strategies-and-view-content-queries-parent).
219215
*
220216
* @example
221217
* \@Component({
@@ -240,7 +236,8 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
240236
*
241237
* @param {boolean} renderParent
242238
*
243-
* @deprecated this flag will be dropped soon, as it is no longer required when using signal based view & content queries
239+
* @deprecated this flag will be dropped soon, as it is no longer required when using signal based view & content
240+
* queries
244241
*/
245242
@Input('rxForParent') renderParent = this.strategyProvider.config.parent;
246243

@@ -253,7 +250,8 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
253250
* Event listeners normally trigger zone. Especially high frequently events cause performance issues.
254251
*
255252
* Read more about this in the
256-
* [official docs](https://www.rx-angular.io/docs/template/rx-for-directive#working-with-event-listeners-patchzone).
253+
* [official
254+
* docs](https://www.rx-angular.io/docs/template/rx-for-directive#working-with-event-listeners-patchzone).
257255
*
258256
* @example
259257
* \@Component({
@@ -280,6 +278,8 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
280278
*/
281279
@Input('rxForPatchZone') patchZone = this.strategyProvider.config.patchZone;
282280

281+
private defaultTrackBy: TrackByFunction<unknown> = (i, item) => item;
282+
283283
/**
284284
* @description
285285
* A function or key that defines how to track changes for items in the iterable.
@@ -352,7 +352,7 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
352352
);
353353
}
354354
if (trackByFnOrKey == null) {
355-
this._trackBy = null;
355+
this._trackBy = this.defaultTrackBy;
356356
} else {
357357
this._trackBy =
358358
typeof trackByFnOrKey !== 'function'
@@ -436,47 +436,38 @@ export class RxFor<T, U extends NgIterable<T> = NgIterable<T>>
436436
/** @internal */
437437
private readonly strategy$ = this.strategyInput$.pipe(coerceDistinctWith());
438438

439-
/** @internal */
440-
private listManager: RxListManager<T>;
441-
442439
/** @internal */
443440
private _subscription = new Subscription();
444441

445442
/** @internal */
446-
_trackBy: TrackByFunction<T>;
443+
_trackBy: TrackByFunction<T> = this.defaultTrackBy;
447444
/** @internal */
448445
_distinctBy = (a: T, b: T) => a === b;
449446

447+
private reconciler = injectReconciler();
448+
450449
constructor(
451450
private readonly templateRef: TemplateRef<RxForViewContext<T, U>>,
452451
) {}
453452

454453
/** @internal */
455454
ngOnInit() {
456455
this._subscription.add(this.values$.subscribe((v) => (this.values = v)));
457-
this.listManager = createListTemplateManager<T, RxForViewContext<T>>({
458-
iterableDiffers: this.iterableDiffers,
459-
renderSettings: {
460-
cdRef: this.cdRef,
461-
strategies: this.strategyProvider.strategies as any, // TODO: move strategyProvider
462-
defaultStrategyName: this.strategyProvider.primaryStrategy,
463-
parent: !!this.renderParent,
464-
patchZone: this.patchZone ? this.ngZone : false,
465-
errorHandler: this.errorHandler,
466-
},
467-
templateSettings: {
456+
this._subscription.add(
457+
this.reconciler({
458+
values$: this.values$,
459+
strategy$: this.strategy$,
468460
viewContainerRef: this.viewContainerRef,
469-
templateRef: this.template,
461+
template: this.template,
462+
strategyProvider: this.strategyProvider,
463+
errorHandler: this.errorHandler,
464+
cdRef: this.cdRef,
465+
trackBy: this._trackBy,
470466
createViewContext: this.createViewContext.bind(this),
471467
updateViewContext: this.updateViewContext.bind(this),
472-
},
473-
trackBy: this._trackBy,
474-
});
475-
this.listManager.nextStrategy(this.strategy$);
476-
this._subscription.add(
477-
this.listManager
478-
.render(this.values$)
479-
.subscribe((v) => this._renderCallback?.next(v)),
468+
parent: !!this.renderParent,
469+
patchZone: this.patchZone ? this.ngZone : undefined,
470+
}).subscribe((values) => this._renderCallback?.next(values)),
480471
);
481472
}
482473

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { inject } from '@angular/core';
2+
import { INTERNAL_RX_FOR_RECONCILER_TOKEN } from './for.config';
3+
4+
export function injectReconciler() {
5+
return inject(INTERNAL_RX_FOR_RECONCILER_TOKEN);
6+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { NgIterable, Provider } from '@angular/core';
2+
import { onStrategy } from '@rx-angular/cdk/render-strategies';
3+
import { reconcile, RxLiveCollection } from '@rx-angular/cdk/template';
4+
import { combineLatest, concat, Observable, of } from 'rxjs';
5+
import {
6+
catchError,
7+
ignoreElements,
8+
map,
9+
startWith,
10+
switchMap,
11+
} from 'rxjs/operators';
12+
import { INTERNAL_RX_FOR_RECONCILER_TOKEN } from './for.config';
13+
import { ReconcileFactoryOptions } from './reconcile-factory';
14+
15+
export function provideExperimentalRxForReconciliation(): Provider {
16+
return {
17+
provide: INTERNAL_RX_FOR_RECONCILER_TOKEN,
18+
useFactory:
19+
() =>
20+
<T, U extends NgIterable<T> = NgIterable<T>>(
21+
options: ReconcileFactoryOptions<T, U>,
22+
) => {
23+
const {
24+
values$,
25+
strategy$,
26+
viewContainerRef,
27+
template,
28+
strategyProvider,
29+
errorHandler,
30+
createViewContext,
31+
updateViewContext,
32+
cdRef,
33+
trackBy,
34+
parent,
35+
patchZone,
36+
} = options;
37+
const liveCollection = new RxLiveCollection<T>(
38+
viewContainerRef,
39+
template,
40+
strategyProvider,
41+
createViewContext,
42+
updateViewContext,
43+
);
44+
return combineLatest([
45+
values$,
46+
strategy$.pipe(startWith(strategyProvider.primaryStrategy)),
47+
]).pipe(
48+
switchMap(([iterable, strategyName]) => {
49+
if (iterable == null) {
50+
iterable = <U>[];
51+
}
52+
if (!iterable[Symbol.iterator]) {
53+
throw new Error(
54+
`Error trying to diff '${iterable}'. Only arrays and iterables are allowed`,
55+
);
56+
}
57+
const strategy = strategyProvider.strategies[strategyName]
58+
? strategyName
59+
: strategyProvider.primaryStrategy;
60+
liveCollection.reset();
61+
reconcile(liveCollection, iterable, trackBy);
62+
liveCollection.updateIndexes();
63+
return <Observable<U>>liveCollection.flushQueue(strategy).pipe(
64+
(o$) =>
65+
parent && liveCollection.needHostUpdate
66+
? concat(
67+
o$,
68+
onStrategy(
69+
null,
70+
strategyProvider.strategies[strategy],
71+
(_, work, options) => {
72+
work(cdRef, options.scope);
73+
},
74+
{
75+
scope: (cdRef as any).context ?? cdRef,
76+
ngZone: patchZone,
77+
},
78+
).pipe(ignoreElements()),
79+
)
80+
: o$,
81+
map(() => iterable),
82+
);
83+
}),
84+
catchError((e) => {
85+
errorHandler.handleError(e);
86+
return of(null);
87+
}),
88+
);
89+
},
90+
};
91+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { inject, IterableDiffers, NgIterable, Provider } from '@angular/core';
2+
import {
3+
createListTemplateManager,
4+
RxDefaultListViewContext,
5+
} from '@rx-angular/cdk/template';
6+
import { INTERNAL_RX_FOR_RECONCILER_TOKEN } from './for.config';
7+
import { ReconcileFactoryOptions } from './reconcile-factory';
8+
9+
export const LEGACY_RXFOR_RECONCILIATION_FACTORY = () => {
10+
const iterableDiffers = inject(IterableDiffers);
11+
return <T, U extends NgIterable<T> = NgIterable<T>>(
12+
options: ReconcileFactoryOptions<T, U>,
13+
) => {
14+
const {
15+
values$,
16+
strategy$,
17+
viewContainerRef,
18+
template,
19+
strategyProvider,
20+
errorHandler,
21+
createViewContext,
22+
updateViewContext,
23+
cdRef,
24+
trackBy,
25+
parent,
26+
patchZone,
27+
} = options;
28+
const listManager = createListTemplateManager<
29+
T,
30+
RxDefaultListViewContext<T>
31+
>({
32+
iterableDiffers: iterableDiffers,
33+
renderSettings: {
34+
cdRef: cdRef,
35+
strategies: strategyProvider.strategies as any, // TODO: move strategyProvider
36+
defaultStrategyName: strategyProvider.primaryStrategy,
37+
parent,
38+
patchZone,
39+
errorHandler,
40+
},
41+
templateSettings: {
42+
viewContainerRef,
43+
templateRef: template,
44+
createViewContext,
45+
updateViewContext,
46+
},
47+
trackBy,
48+
});
49+
listManager.nextStrategy(strategy$);
50+
51+
return listManager.render(values$);
52+
};
53+
};
54+
55+
export function provideLegacyRxForReconciliation(): Provider {
56+
return {
57+
provide: INTERNAL_RX_FOR_RECONCILER_TOKEN,
58+
useFactory: LEGACY_RXFOR_RECONCILIATION_FACTORY,
59+
};
60+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
ChangeDetectorRef,
3+
EmbeddedViewRef,
4+
ErrorHandler,
5+
NgIterable,
6+
NgZone,
7+
TemplateRef,
8+
TrackByFunction,
9+
ViewContainerRef,
10+
} from '@angular/core';
11+
import {
12+
RxStrategyNames,
13+
RxStrategyProvider,
14+
} from '@rx-angular/cdk/render-strategies';
15+
import {
16+
RxDefaultListViewContext,
17+
RxListViewComputedContext,
18+
} from '@rx-angular/cdk/template';
19+
import { Observable } from 'rxjs';
20+
21+
export type ReconcileFactoryOptions<
22+
T,
23+
U extends NgIterable<T> = NgIterable<T>,
24+
> = {
25+
values$: Observable<U>;
26+
strategy$: Observable<RxStrategyNames>;
27+
viewContainerRef: ViewContainerRef;
28+
template: TemplateRef<RxDefaultListViewContext<T>>;
29+
strategyProvider: RxStrategyProvider;
30+
errorHandler: ErrorHandler;
31+
cdRef: ChangeDetectorRef;
32+
trackBy: TrackByFunction<T>;
33+
createViewContext: (
34+
item: T,
35+
context: RxListViewComputedContext,
36+
) => RxDefaultListViewContext<T>;
37+
updateViewContext: (
38+
item: T,
39+
view: EmbeddedViewRef<RxDefaultListViewContext<T>>,
40+
context: RxListViewComputedContext,
41+
) => void;
42+
parent?: boolean;
43+
patchZone?: NgZone;
44+
};
45+
46+
export type RxReconcileFactory = <T, U extends NgIterable<T> = NgIterable<T>>(
47+
options: ReconcileFactoryOptions<T, U>,
48+
) => Observable<NgIterable<T>>;

0 commit comments

Comments
 (0)