fix(common): expand type for "ngForOf" input to work with strict null checks (#31371)

Currently the `ngForOf` input accepts `null` or `undefined` as valid
values. Although when using strict template input type checking
(which will be supported by `ngtsc`), passing `null` or `undefined`
with strict null checks enabled causes a type check failure because
the type for the `ngForOf` input becomes too strict if strict null checks
are enabled. The type of the input needs to be expanded to also accept
`null` or `undefined` to behave consistently regardless of the
`strictNullChecks` flag.

This is necessary because whenever strict input type checking is enabled
by default, most of the Angular projects that use `*ngFor` with the async pipe
will either need to disable template type checking or strict null checks
because the `async` pipe returns `null` if the observable hasn't been
emitted yet.

See for example how this affects the `angular/components` repository and
how much bloat the workaround involves: https://github.com/angular/components/pull/16373/files#r296942696.

PR Close #31371
This commit is contained in:
Paul Gschwendtner 2019-10-03 18:31:28 +02:00 committed by Alex Rickabaugh
parent fee28e20bb
commit c1bb88603e
5 changed files with 12 additions and 10 deletions

View File

@ -129,7 +129,7 @@ export class NgForOf<T> implements DoCheck {
* [template input variable](guide/structural-directives#template-input-variable). * [template input variable](guide/structural-directives#template-input-variable).
*/ */
@Input() @Input()
set ngForOf(ngForOf: NgIterable<T>) { set ngForOf(ngForOf: NgIterable<T>|undefined|null) {
this._ngForOf = ngForOf; this._ngForOf = ngForOf;
this._ngForOfDirty = true; this._ngForOfDirty = true;
} }
@ -165,8 +165,7 @@ export class NgForOf<T> implements DoCheck {
get ngForTrackBy(): TrackByFunction<T> { return this._trackByFn; } get ngForTrackBy(): TrackByFunction<T> { return this._trackByFn; }
// TODO(issue/24571): remove '!'. private _ngForOf: NgIterable<T>|undefined|null = null;
private _ngForOf !: NgIterable<T>;
private _ngForOfDirty: boolean = true; private _ngForOfDirty: boolean = true;
private _differ: IterableDiffer<T>|null = null; private _differ: IterableDiffer<T>|null = null;
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@ -219,8 +218,11 @@ export class NgForOf<T> implements DoCheck {
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number | null, (item: IterableChangeRecord<any>, adjustedPreviousIndex: number | null,
currentIndex: number | null) => { currentIndex: number | null) => {
if (item.previousIndex == null) { if (item.previousIndex == null) {
// NgForOf is never "null" or "undefined" here because the differ detected
// that a new item needs to be inserted from the iterable. This implies that
// there is an iterable value for "_ngForOf".
const view = this._viewContainer.createEmbeddedView( const view = this._viewContainer.createEmbeddedView(
this._template, new NgForOfContext<T>(null !, this._ngForOf, -1, -1), this._template, new NgForOfContext<T>(null !, this._ngForOf !, -1, -1),
currentIndex === null ? undefined : currentIndex); currentIndex === null ? undefined : currentIndex);
const tuple = new RecordViewTuple<T>(item, view); const tuple = new RecordViewTuple<T>(item, view);
insertTuples.push(tuple); insertTuples.push(tuple);
@ -243,7 +245,7 @@ export class NgForOf<T> implements DoCheck {
const viewRef = <EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(i); const viewRef = <EmbeddedViewRef<NgForOfContext<T>>>this._viewContainer.get(i);
viewRef.context.index = i; viewRef.context.index = i;
viewRef.context.count = ilen; viewRef.context.count = ilen;
viewRef.context.ngForOf = this._ngForOf; viewRef.context.ngForOf = this._ngForOf !;
} }
changes.forEachIdentityChange((record: any) => { changes.forEachIdentityChange((record: any) => {

View File

@ -146,7 +146,7 @@ export class DefaultIterableDiffer<V> implements IterableDiffer<V>, IterableChan
} }
} }
diff(collection: NgIterable<V>): DefaultIterableDiffer<V>|null { diff(collection: NgIterable<V>|null|undefined): DefaultIterableDiffer<V>|null {
if (collection == null) collection = []; if (collection == null) collection = [];
if (!isListLikeIterable(collection)) { if (!isListLikeIterable(collection)) {
throw new Error( throw new Error(

View File

@ -34,7 +34,7 @@ export interface IterableDiffer<V> {
* @returns an object describing the difference. The return value is only valid until the next * @returns an object describing the difference. The return value is only valid until the next
* `diff()` invocation. * `diff()` invocation.
*/ */
diff(object: NgIterable<V>): IterableChanges<V>|null; diff(object: NgIterable<V>|undefined|null): IterableChanges<V>|null;
} }
/** /**

View File

@ -239,7 +239,7 @@ export declare class NgComponentOutlet implements OnChanges, OnDestroy {
} }
export declare class NgForOf<T> implements DoCheck { export declare class NgForOf<T> implements DoCheck {
ngForOf: NgIterable<T>; ngForOf: NgIterable<T> | undefined | null;
ngForTemplate: TemplateRef<NgForOfContext<T>>; ngForTemplate: TemplateRef<NgForOfContext<T>>;
ngForTrackBy: TrackByFunction<T>; ngForTrackBy: TrackByFunction<T>;
constructor(_viewContainer: ViewContainerRef, _template: TemplateRef<NgForOfContext<T>>, _differs: IterableDiffers); constructor(_viewContainer: ViewContainerRef, _template: TemplateRef<NgForOfContext<T>>, _differs: IterableDiffers);

View File

@ -263,7 +263,7 @@ export declare class DefaultIterableDiffer<V> implements IterableDiffer<V>, Iter
readonly length: number; readonly length: number;
constructor(trackByFn?: TrackByFunction<V>); constructor(trackByFn?: TrackByFunction<V>);
check(collection: NgIterable<V>): boolean; check(collection: NgIterable<V>): boolean;
diff(collection: NgIterable<V>): DefaultIterableDiffer<V> | null; diff(collection: NgIterable<V> | null | undefined): DefaultIterableDiffer<V> | null;
forEachAddedItem(fn: (record: IterableChangeRecord_<V>) => void): void; forEachAddedItem(fn: (record: IterableChangeRecord_<V>) => void): void;
forEachIdentityChange(fn: (record: IterableChangeRecord_<V>) => void): void; forEachIdentityChange(fn: (record: IterableChangeRecord_<V>) => void): void;
forEachItem(fn: (record: IterableChangeRecord_<V>) => void): void; forEachItem(fn: (record: IterableChangeRecord_<V>) => void): void;
@ -505,7 +505,7 @@ export interface IterableChanges<V> {
} }
export interface IterableDiffer<V> { export interface IterableDiffer<V> {
diff(object: NgIterable<V>): IterableChanges<V> | null; diff(object: NgIterable<V> | undefined | null): IterableChanges<V> | null;
} }
export interface IterableDifferFactory { export interface IterableDifferFactory {