Skip to content

Commit aaa287e

Browse files
authored
Local variables poc (rx-angular#325)
1 parent b027e9b commit aaa287e

14 files changed

+840
-65
lines changed

apps/experiments/src/app/cd-embedded-view/03/parent.component.ts

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,8 @@
11
import { Component } from '@angular/core';
22
import { environment } from '../../../environments/environment';
3-
import { merge, Observable, Subject } from 'rxjs';
4-
import { map, scan, share } from 'rxjs/operators';
5-
6-
const rand = (n: number = 2): number => {
7-
// tslint:disable-next-line:no-bitwise
8-
return ~~(Math.random() * n);
9-
};
10-
const immutableIncArr = (n: number = 10) => (o$: Observable<number>) => o$.pipe(
11-
scan((a, i, idx) => {
12-
if(i === 1) {
13-
a[idx % n] = { id: idx % n, value: rand() };
14-
} else if(i === 0) {
15-
const id = rand(a.length);
16-
a[id] = { id, value: rand() };
17-
} else {
18-
a.splice(idx % n, 1);
19-
}
20-
return a;
21-
}, [])
22-
);
23-
const mutableIncArr = (n: number = 10) => {
24-
return (o$: Observable<number>) => o$.pipe(
25-
scan((a, i, idx) => {
26-
a[idx % n].value = rand();
27-
return a;
28-
}, [])
29-
);
30-
}
31-
32-
const immutableArr = (n: number = 10) => (o$: Observable<number>) => o$.pipe(
33-
map(v => Array(n).fill(0).map((_, idx) => ({ id: idx % n, value: rand() })))
34-
);
35-
36-
const mutableArr = (n: number = 10) => {
37-
const arr = Array(n);
38-
return (o$: Observable<number>) => o$.pipe(
39-
map(v => arr.forEach((i, idx) => i.value = rand()))
40-
);
41-
}
3+
import { merge, Subject } from 'rxjs';
4+
import { share } from 'rxjs/operators';
5+
import { immutableArr, immutableIncArr } from '../utils';
426

437
@Component({
448
selector: 'app-cd-embedded-view-parent03',
@@ -131,7 +95,7 @@ export class CdEmbeddedViewParent03Component {
13195

13296
array$ = merge(
13397
this.changeOneClick$.pipe(immutableIncArr()),
134-
this.changeAllClick$.pipe(immutableArr()),
98+
this.changeAllClick$.pipe(immutableArr())
13599
).pipe(
136100
share()
137101
);

apps/experiments/src/app/cd-embedded-view/03/poc3-for.directive.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,9 @@ import { distinctUntilChanged, filter, groupBy, map, mergeAll, mergeMap, switchA
1919

2020
export class PocForViewContext<T, U extends NgIterable<T> = NgIterable<T>> {
2121

22-
constructor(public $implicit: T, public pocLet: U, public index: number, public count: number) {}
22+
$implicit: T;
23+
public pocLet: U;
2324

24-
get first(): boolean {
25-
return this.index === 0;
26-
}
27-
28-
get last(): boolean {
29-
return this.index === this.count - 1;
30-
}
31-
32-
get even(): boolean {
33-
return this.index % 2 === 0;
34-
}
35-
36-
get odd(): boolean {
37-
return !this.even;
38-
}
3925
}
4026
interface RecordViewTuple<T, U extends NgIterable<T>> {
4127
record: any;
@@ -125,7 +111,10 @@ export class PocForIterable<T, U extends NgIterable<T> = NgIterable<T>> implemen
125111
// create the embedded view for each value with default values
126112
const view = this.viewContainerRef.createEmbeddedView(
127113
this.nextTemplateRef,
128-
new PocForViewContext<T, U>(null, this.values, -1, -1),
114+
{
115+
$implicit: this.values as any,
116+
pocLet: this.values,
117+
},
129118
currentIndex === null ? undefined : currentIndex
130119
);
131120
insertTuples.push({
@@ -155,8 +144,6 @@ export class PocForIterable<T, U extends NgIterable<T> = NgIterable<T>> implemen
155144

156145
for (let i = 0, ilen = this.viewContainerRef.length; i < ilen; i++) {
157146
const viewRef = <EmbeddedViewRef<PocForViewContext<T, U>>>this.viewContainerRef.get(i);
158-
viewRef.context.index = i;
159-
viewRef.context.count = ilen;
160147
viewRef.context.pocLet = this.values;
161148
}
162149

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Component, ViewEncapsulation } from '@angular/core';
2+
import { environment } from '../../../environments/environment';
3+
import { merge, Subject } from 'rxjs';
4+
import { share } from 'rxjs/operators';
5+
import { immutableArr, immutableIncArr } from '../utils';
6+
7+
@Component({
8+
selector: 'app-cd-embedded-view-parent05',
9+
template: `
10+
<h2>
11+
CD EmbeddedView 05
12+
<small>Local Variables</small>
13+
<renders></renders>
14+
</h2>
15+
16+
<button mat-raised-button [unpatch] (click)="changeOneClick$.next(1)">
17+
update
18+
</button>
19+
<button mat-raised-button [unpatch] (click)="changeAllClick$.next(10)">
20+
Change all
21+
</button>
22+
23+
<!-- <pre>{{array$ | push | json}}</pre> -->
24+
<mat-checkbox></mat-checkbox>
25+
<div class="row">
26+
<div class="col">
27+
<h2>Static Custom Variables</h2>
28+
<ng-container
29+
*poc5LocV="array$;
30+
let value;
31+
localVariableProjections: localVariableProjections;
32+
let index = index;
33+
let first = first;
34+
let last = last;
35+
let customVariable = customVariable;
36+
">
37+
id: {{value.id}},
38+
index: {{index}},
39+
first: {{first}},
40+
last: {{last}}, <br/>
41+
customVariable: {{customVariable | json}}<br/>
42+
<b>Item: </b><renders></renders>
43+
<ng-container *ngFor="let item of value.arr; trackBy: trackById">
44+
<renders [radius]="10"></renders> child: {{item.value}}
45+
</ng-container>
46+
<br/>
47+
</ng-container>
48+
</div>
49+
</div>
50+
`,
51+
changeDetection: environment.changeDetection,
52+
encapsulation: ViewEncapsulation.None,
53+
styles: [`
54+
.row {
55+
display: flex;
56+
}
57+
58+
.col {
59+
width: 49%;
60+
}
61+
`]
62+
})
63+
export class CdEmbeddedViewParent05Component {
64+
65+
66+
localVariableProjections = {
67+
test: (a) => {
68+
// tslint:disable-next-line:no-bitwise
69+
return ~~a.$implicit;
70+
}
71+
};
72+
73+
changeOneClick$ = new Subject<number>();
74+
changeAllClick$ = new Subject<number>();
75+
76+
array$ = merge(
77+
this.changeOneClick$.pipe(immutableIncArr()),
78+
this.changeAllClick$.pipe(immutableArr())
79+
).pipe(
80+
share()
81+
);
82+
83+
trackById = (i) => i.id;
84+
85+
}
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import {
2+
ChangeDetectorRef,
3+
Directive,
4+
EmbeddedViewRef,
5+
Input,
6+
IterableChangeRecord,
7+
IterableChanges,
8+
IterableDiffer,
9+
IterableDiffers,
10+
NgIterable,
11+
OnDestroy,
12+
OnInit,
13+
TemplateRef,
14+
TrackByFunction,
15+
ViewContainerRef
16+
} from '@angular/core';
17+
18+
import { Observable, ObservableInput, ReplaySubject, Subscription, Unsubscribable } from 'rxjs';
19+
import { distinctUntilChanged, filter, map, switchAll, tap } from 'rxjs/operators';
20+
21+
interface RecordViewTuple<T, U extends NgIterable<T>> {
22+
record: any;
23+
view: EmbeddedViewRef<Poc5Locv5ViewContext<T, U>>;
24+
}
25+
26+
27+
export class Poc5Locv5ViewContext<T, U extends NgIterable<T> = NgIterable<T>> {
28+
29+
localVariableProjections: CustomVariablesProjectors = {};
30+
31+
constructor(public $implicit: T, public pocLet: U, public index: number, public count: number) {
32+
}
33+
34+
get first(): boolean {
35+
return this.index === 0;
36+
}
37+
38+
get last(): boolean {
39+
return this.index === this.count - 1;
40+
}
41+
42+
get even(): boolean {
43+
return this.index % 2 === 0;
44+
}
45+
46+
get odd(): boolean {
47+
return !this.even;
48+
}
49+
50+
get customVariable(): unknown {
51+
return Object.entries(this.localVariableProjections)
52+
.reduce((acc, [name, fn]) => {
53+
return { ...acc, [name]: fn(this) };
54+
}, {});
55+
}
56+
}
57+
58+
interface CustomVariablesProjectors {
59+
[variableName: string]: (context) => unknown;
60+
}
61+
62+
@Directive({
63+
// tslint:disable-next-line:directive-selector
64+
selector: '[poc5LocV]'
65+
})
66+
export class Poc5Locv5<T, U extends NgIterable<T> = NgIterable<T>> implements OnInit, OnDestroy {
67+
private differ: IterableDiffer<T> | null = null;
68+
private subscription: Unsubscribable = new Subscription();
69+
70+
observables$ = new ReplaySubject<ObservableInput<U & NgIterable<T>>>(1);
71+
values: U & NgIterable<T>;
72+
values$ = this.observables$
73+
.pipe(
74+
distinctUntilChanged(),
75+
switchAll()
76+
);
77+
78+
private _trackByFn: TrackByFunction<T>;
79+
80+
_localVariableProjections;
81+
@Input()
82+
set poc5LocVLocalVariableProjections(o: CustomVariablesProjectors) {
83+
this._localVariableProjections = o;
84+
}
85+
@Input()
86+
set poc5LocVIterableTrackBy(fn: TrackByFunction<T>) {
87+
this._trackByFn = fn;
88+
}
89+
90+
@Input()
91+
set poc5LocV(potentialObservable: ObservableInput<U & NgIterable<T>> | null | undefined) {
92+
this.observables$.next(potentialObservable);
93+
}
94+
95+
constructor(
96+
private cdRef: ChangeDetectorRef,
97+
private readonly nextTemplateRef: TemplateRef<Poc5Locv5ViewContext<T, U>>,
98+
private readonly viewContainerRef: ViewContainerRef,
99+
private iterableDiffers: IterableDiffers
100+
) {
101+
102+
}
103+
104+
ngOnInit() {
105+
this.subscription = this.values$.pipe(
106+
// the actual values arrive here
107+
tap(value => {
108+
// set helper variable for applyChanges method
109+
this.values = value;
110+
// set new differ if there is none yet
111+
if (!this.differ && value) {
112+
this.differ = this.iterableDiffers.find(value).create(this._trackByFn);
113+
}
114+
}),
115+
// if there is no differ, we don't need to apply changes
116+
filter(() => !!this.differ),
117+
// apply differ -> return changes
118+
map(value => this.differ.diff(value)),
119+
// filter out no changes
120+
filter(changes => !!changes)
121+
)
122+
.subscribe(
123+
changes => {
124+
this.applyChanges(changes);
125+
}
126+
);
127+
}
128+
129+
ngOnDestroy() {
130+
this.subscription.unsubscribe();
131+
}
132+
133+
private applyChanges(changes: IterableChanges<T>) {
134+
// behavior like *ngFor
135+
const insertTuples: RecordViewTuple<T, U>[] = [];
136+
// TODO: dig into `IterableDiffer`
137+
changes.forEachOperation(
138+
(
139+
changeRecord: IterableChangeRecord<T>,
140+
previousIndex: number | null,
141+
currentIndex: number | null
142+
) => {
143+
if (changeRecord.previousIndex == null) {
144+
// this is basically the first run
145+
// create the embedded view for each value with default values
146+
const view = this.viewContainerRef.createEmbeddedView(
147+
this.nextTemplateRef,
148+
new Poc5Locv5ViewContext<T, U>(null, this.values, -1, -1),
149+
currentIndex === null ? undefined : currentIndex
150+
);
151+
insertTuples.push({
152+
view,
153+
record: changeRecord
154+
});
155+
156+
} else if (currentIndex == null) {
157+
158+
this.viewContainerRef.remove(
159+
previousIndex === null ? undefined : previousIndex);
160+
161+
} else if (previousIndex !== null) {
162+
163+
const view = <EmbeddedViewRef<Poc5Locv5ViewContext<T, U>>>this.viewContainerRef.get(previousIndex);
164+
this.viewContainerRef.move(view, currentIndex);
165+
insertTuples.push({
166+
view,
167+
record: changeRecord
168+
});
169+
}
170+
});
171+
172+
for (let i = 0; i < insertTuples.length; i++) {
173+
this._perViewChange(insertTuples[i].view, insertTuples[i].record);
174+
}
175+
176+
for (let i = 0, ilen = this.viewContainerRef.length; i < ilen; i++) {
177+
const viewRef = <EmbeddedViewRef<Poc5Locv5ViewContext<T, U>>>this.viewContainerRef.get(i);
178+
viewRef.context.localVariableProjections = this._localVariableProjections;
179+
180+
viewRef.context.index = i;
181+
viewRef.context.count = ilen;
182+
viewRef.context.pocLet = this.values;
183+
}
184+
185+
changes.forEachIdentityChange((record: IterableChangeRecord<T>) => {
186+
const viewRef =
187+
<EmbeddedViewRef<Poc5Locv5ViewContext<T, U>>>this.viewContainerRef.get(record.currentIndex);
188+
viewRef.context.$implicit = record.item;
189+
viewRef.detectChanges();
190+
});
191+
}
192+
193+
private _perViewChange(
194+
view: EmbeddedViewRef<Poc5Locv5ViewContext<T, U>>, record: IterableChangeRecord<T>) {
195+
view.context.$implicit = record.item;
196+
view.detectChanges();
197+
}
198+
199+
}

0 commit comments

Comments
 (0)