From a6b6d74c006cebb24f43593e6400782434266629 Mon Sep 17 00:00:00 2001
From: crisbeto
Date: Thu, 28 Nov 2019 19:42:00 +0100
Subject: [PATCH] fix(common): reflect input type in NgForOf context (#33997)
Fixes `NgForOf` not reflecting the type of its input in the `NgForOfContext`.
PR Close #33997
---
packages/common/src/directives/ng_for_of.ts | 41 ++++---
.../test/ngtsc/template_typecheck_spec.ts | 100 ++++++++++++------
packages/core/test/render3/common_with_def.ts | 4 +-
.../core/test/render3/instructions_spec.ts | 5 +-
tools/public_api_guard/common/common.d.ts | 16 +--
5 files changed, 98 insertions(+), 68 deletions(-)
diff --git a/packages/common/src/directives/ng_for_of.ts b/packages/common/src/directives/ng_for_of.ts
index 680e283d2c..c3d8e1c192 100644
--- a/packages/common/src/directives/ng_for_of.ts
+++ b/packages/common/src/directives/ng_for_of.ts
@@ -6,15 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, TemplateRef, TrackByFunction, ViewContainerRef, forwardRef, isDevMode} from '@angular/core';
+import {Directive, DoCheck, EmbeddedViewRef, Input, IterableChangeRecord, IterableChanges, IterableDiffer, IterableDiffers, NgIterable, TemplateRef, TrackByFunction, ViewContainerRef, isDevMode} from '@angular/core';
/**
* @publicApi
*/
-export class NgForOfContext {
- constructor(
- public $implicit: T, public ngForOf: NgIterable, public index: number,
- public count: number) {}
+export class NgForOfContext> {
+ constructor(public $implicit: T, public ngForOf: U, public index: number, public count: number) {}
get first(): boolean { return this.index === 0; }
@@ -123,13 +121,13 @@ export class NgForOfContext {
* @publicApi
*/
@Directive({selector: '[ngFor][ngForOf]'})
-export class NgForOf implements DoCheck {
+export class NgForOf> implements DoCheck {
/**
* The value of the iterable expression, which can be used as a
* [template input variable](guide/structural-directives#template-input-variable).
*/
@Input()
- set ngForOf(ngForOf: NgIterable|undefined|null) {
+ set ngForOf(ngForOf: U&NgIterable|undefined|null) {
this._ngForOf = ngForOf;
this._ngForOfDirty = true;
}
@@ -165,22 +163,22 @@ export class NgForOf implements DoCheck {
get ngForTrackBy(): TrackByFunction { return this._trackByFn; }
- private _ngForOf: NgIterable|undefined|null = null;
+ private _ngForOf: U|undefined|null = null;
private _ngForOfDirty: boolean = true;
private _differ: IterableDiffer|null = null;
// TODO(issue/24571): remove '!'.
private _trackByFn !: TrackByFunction;
constructor(
- private _viewContainer: ViewContainerRef, private _template: TemplateRef>,
- private _differs: IterableDiffers) {}
+ private _viewContainer: ViewContainerRef,
+ private _template: TemplateRef>, private _differs: IterableDiffers) {}
/**
* A reference to the template that is stamped out for each item in the iterable.
* @see [template reference variable](guide/template-syntax#template-reference-variables--var-)
*/
@Input()
- set ngForTemplate(value: TemplateRef>) {
+ set ngForTemplate(value: TemplateRef>) {
// TODO(TS2.1): make TemplateRef>> once we move to TS v2.1
// The current type is too restrictive; a template that just uses index, for example,
// should be acceptable.
@@ -213,7 +211,7 @@ export class NgForOf implements DoCheck {
}
private _applyChanges(changes: IterableChanges) {
- const insertTuples: RecordViewTuple[] = [];
+ const insertTuples: RecordViewTuple[] = [];
changes.forEachOperation(
(item: IterableChangeRecord, adjustedPreviousIndex: number | null,
currentIndex: number | null) => {
@@ -222,9 +220,9 @@ export class NgForOf implements DoCheck {
// 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(
- this._template, new NgForOfContext(null !, this._ngForOf !, -1, -1),
+ this._template, new NgForOfContext(null !, this._ngForOf !, -1, -1),
currentIndex === null ? undefined : currentIndex);
- const tuple = new RecordViewTuple(item, view);
+ const tuple = new RecordViewTuple(item, view);
insertTuples.push(tuple);
} else if (currentIndex == null) {
this._viewContainer.remove(
@@ -232,7 +230,7 @@ export class NgForOf implements DoCheck {
} else if (adjustedPreviousIndex !== null) {
const view = this._viewContainer.get(adjustedPreviousIndex) !;
this._viewContainer.move(view, currentIndex);
- const tuple = new RecordViewTuple(item, >>view);
+ const tuple = new RecordViewTuple(item, >>view);
insertTuples.push(tuple);
}
});
@@ -242,7 +240,7 @@ export class NgForOf implements DoCheck {
}
for (let i = 0, ilen = this._viewContainer.length; i < ilen; i++) {
- const viewRef = >>this._viewContainer.get(i);
+ const viewRef = >>this._viewContainer.get(i);
viewRef.context.index = i;
viewRef.context.count = ilen;
viewRef.context.ngForOf = this._ngForOf !;
@@ -250,13 +248,13 @@ export class NgForOf implements DoCheck {
changes.forEachIdentityChange((record: any) => {
const viewRef =
- >>this._viewContainer.get(record.currentIndex);
+ >>this._viewContainer.get(record.currentIndex);
viewRef.context.$implicit = record.item;
});
}
private _perViewChange(
- view: EmbeddedViewRef>, record: IterableChangeRecord) {
+ view: EmbeddedViewRef>, record: IterableChangeRecord) {
view.context.$implicit = record.item;
}
@@ -266,13 +264,14 @@ export class NgForOf implements DoCheck {
* The presence of this method is a signal to the Ivy template type-check compiler that the
* `NgForOf` structural directive renders its template with a specific context type.
*/
- static ngTemplateContextGuard(dir: NgForOf, ctx: any): ctx is NgForOfContext {
+ static ngTemplateContextGuard>(dir: NgForOf, ctx: any):
+ ctx is NgForOfContext {
return true;
}
}
-class RecordViewTuple {
- constructor(public record: any, public view: EmbeddedViewRef>) {}
+class RecordViewTuple> {
+ constructor(public record: any, public view: EmbeddedViewRef>) {}
}
function getTypeName(type: any): string {
diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
index d102c204b4..b35ea5cc05 100644
--- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
+++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
@@ -26,15 +26,16 @@ runInEachFileSystem(() => {
env.write('node_modules/@angular/common/index.d.ts', `
import * as i0 from '@angular/core';
-export declare class NgForOfContext {
+export declare class NgForOfContext> {
$implicit: T;
- ngForOf: i0.NgIterable;
- index: number;
count: number;
- readonly first: boolean;
- readonly last: boolean;
readonly even: boolean;
+ readonly first: boolean;
+ index: number;
+ readonly last: boolean;
+ ngForOf: U;
readonly odd: boolean;
+ constructor($implicit: T, ngForOf: U, index: number, count: number);
}
export declare class IndexPipe {
@@ -53,9 +54,13 @@ export declare class SlicePipe {
static ɵpipe: i0.ɵPipeDefWithMeta;
}
-export declare class NgForOf {
- ngForOf: i0.NgIterable;
- static ngTemplateContextGuard(dir: NgForOf, ctx: any): ctx is NgForOfContext;
+export declare class NgForOf> implements DoCheck {
+ ngForOf: (U & i0.NgIterable) | undefined | null;
+ ngForTemplate: TemplateRef>;
+ ngForTrackBy: TrackByFunction;
+ constructor(_viewContainer: ViewContainerRef, _template: TemplateRef>, _differs: IterableDiffers);
+ ngDoCheck(): void;
+ static ngTemplateContextGuard>(dir: NgForOf, ctx: any): ctx is NgForOfContext;
static ɵdir: i0.ɵɵDirectiveDefWithMeta, '[ngFor][ngForOf]', never, {'ngForOf': 'ngForOf'}, {}, never>;
}
@@ -100,18 +105,18 @@ export declare class AnimationEvent {
{fullTemplateTypeCheck: true, strictInputTypes: true, strictAttributeTypes: true});
env.write('test.ts', `
import {Component, Directive, NgModule, Input} from '@angular/core';
-
+
@Component({
selector: 'test',
template: '',
})
class TestCmp {}
-
+
@Directive({selector: '[dir]'})
class TestDir {
@Input() foo: number;
}
-
+
@NgModule({
declarations: [TestCmp, TestDir],
})
@@ -128,7 +133,7 @@ export declare class AnimationEvent {
{fullTemplateTypeCheck: true, strictInputTypes: true, strictOutputEventTypes: true});
env.write('test.ts', `
import {Component, Directive, NgModule, EventEmitter} from '@angular/core';
-
+
@Component({
selector: 'test',
template: '',
@@ -136,7 +141,7 @@ export declare class AnimationEvent {
class TestCmp {
handleEvent(event: number): void {}
}
-
+
@Directive({
selector: '[dir]',
inputs: ['some-input.xs'],
@@ -146,7 +151,7 @@ export declare class AnimationEvent {
'some-input.xs': string;
'some-output': EventEmitter;
}
-
+
@NgModule({
declarations: [TestCmp, TestDir],
})
@@ -164,7 +169,7 @@ export declare class AnimationEvent {
env.tsconfig({fullTemplateTypeCheck: true, strictOutputEventTypes: true});
env.write('test.ts', `
import {Component, Directive, EventEmitter, NgModule, Output} from '@angular/core';
-
+
@Component({
selector: 'test',
template: '',
@@ -172,12 +177,12 @@ export declare class AnimationEvent {
class TestCmp {
update(data: string) {}
}
-
+
@Directive({selector: '[dir]'})
class TestDir {
@Output() update = new EventEmitter();
}
-
+
@NgModule({
declarations: [TestCmp, TestDir],
})
@@ -589,7 +594,7 @@ export declare class AnimationEvent {
beforeEach(() => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
-
+
@Component({
selector: 'test',
template: '',
@@ -597,7 +602,7 @@ export declare class AnimationEvent {
class TestCmp {
update(data: string) {}
}
-
+
@NgModule({
declarations: [TestCmp],
})
@@ -785,6 +790,31 @@ export declare class AnimationEvent {
env.driveMain();
});
+ it('should infer the context of NgFor', () => {
+ env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true});
+ env.write('test.ts', `
+ import {CommonModule} from '@angular/common';
+ import {Component, NgModule} from '@angular/core';
+
+ @Component({
+ selector: 'test',
+ template: '{{all.length}}
',
+ })
+ class TestCmp {
+ users: {name: string}[];
+ }
+
+ @NgModule({
+ declarations: [TestCmp],
+ imports: [CommonModule],
+ })
+ class Module {}
+ `);
+
+ const diags = env.driveDiagnostics();
+ expect(diags.length).toBe(0);
+ });
+
it('should report an error with an unknown local ref target', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
@@ -915,18 +945,18 @@ export declare class AnimationEvent {
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
-
+
@Component({
selector: 'test',
template: \`
- {{foo.name}} of {{foos.length}}
+ {{foo.name}} of {{foos.nonExistingProp}}
\`,
})
export class TestCmp {
foos: {name: string}[];
}
-
+
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
@@ -947,8 +977,8 @@ export declare class AnimationEvent {
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
- expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText)
- .toBe(`Property 'length' does not exist on type 'NgIterable<{ name: string; }>'.`);
+ expect(diags[0].messageText)
+ .toBe(`Property 'nonExistingProp' does not exist on type '{ name: string; }[]'.`);
});
});
@@ -1015,7 +1045,7 @@ export declare class AnimationEvent {
static ɵdir: i0.ɵɵDirectiveDefWithMeta;
}
-
+
export declare class ExternalModule {
static ɵmod: i0.ɵɵNgModuleDefWithMeta;
}
@@ -1024,20 +1054,20 @@ export declare class AnimationEvent {
env.write('test.ts', `
import {Component, Directive, Input, NgModule} from '@angular/core';
import {BaseDir, ExternalModule} from 'external';
-
+
@Directive({
selector: '[child]',
})
class ChildDir extends BaseDir {
@Input() fromChild!: boolean;
}
-
+
@Component({
selector: 'test',
template: '',
})
class TestCmp {}
-
+
@NgModule({
declarations: [TestCmp, ChildDir],
imports: [ExternalModule],
@@ -1260,13 +1290,13 @@ export declare class AnimationEvent {
() => {
env.write('test.ts', `
import {Component, NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
-
+
@Component({
selector: 'blah',
template: 'test',
})
export class FooCmp {}
-
+
@NgModule({
declarations: [FooCmp],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
@@ -1280,13 +1310,13 @@ export declare class AnimationEvent {
it('should not produce diagnostics when using the NO_ERRORS_SCHEMA', () => {
env.write('test.ts', `
import {Component, NgModule, NO_ERRORS_SCHEMA} from '@angular/core';
-
+
@Component({
selector: 'blah',
template: '',
})
export class FooCmp {}
-
+
@NgModule({
declarations: [FooCmp],
schemas: [NO_ERRORS_SCHEMA],
@@ -1314,7 +1344,7 @@ export declare class AnimationEvent {
it('should be correct for direct templates', async() => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
-
+
@Component({
selector: 'test',
template: \`
@@ -1334,7 +1364,7 @@ export declare class AnimationEvent {
it('should be correct for indirect templates', async() => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
-
+
const TEMPLATE = \`
{{user.does_not_exist}}
\`;
@@ -1360,7 +1390,7 @@ export declare class AnimationEvent {
`);
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
-
+
@Component({
selector: 'test',
diff --git a/packages/core/test/render3/common_with_def.ts b/packages/core/test/render3/common_with_def.ts
index 502c22b5d5..0ebf12405a 100644
--- a/packages/core/test/render3/common_with_def.ts
+++ b/packages/core/test/render3/common_with_def.ts
@@ -7,11 +7,11 @@
*/
import {NgForOf as NgForOfDef, NgIf as NgIfDef, NgTemplateOutlet as NgTemplateOutletDef} from '@angular/common';
-import {IterableDiffers, TemplateRef, ViewContainerRef} from '@angular/core';
+import {IterableDiffers, NgIterable, TemplateRef, ViewContainerRef} from '@angular/core';
import {DirectiveType, ɵɵNgOnChangesFeature, ɵɵdefineDirective, ɵɵdirectiveInject} from '../../src/render3/index';
-export const NgForOf: DirectiveType> = NgForOfDef as any;
+export const NgForOf: DirectiveType>> = NgForOfDef as any;
export const NgIf: DirectiveType = NgIfDef as any;
export const NgTemplateOutlet: DirectiveType = NgTemplateOutletDef as any;
diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts
index fbe13c2534..be6f6b5fa2 100644
--- a/packages/core/test/render3/instructions_spec.ts
+++ b/packages/core/test/render3/instructions_spec.ts
@@ -205,7 +205,8 @@ describe('instructions', () => {
describe('performance counters', () => {
it('should create tViews only once for each nested level', () => {
- function ToDoAppComponent_NgForOf_Template_0(rf: RenderFlags, ctx0: NgForOfContext) {
+ function ToDoAppComponent_NgForOf_Template_0(
+ rf: RenderFlags, ctx0: NgForOfContext) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'ul');
ɵɵtemplate(1, ToDoAppComponent_NgForOf_NgForOf_Template_1, 2, 1, 'li', 0);
@@ -219,7 +220,7 @@ describe('instructions', () => {
}
function ToDoAppComponent_NgForOf_NgForOf_Template_1(
- rf: RenderFlags, ctx1: NgForOfContext) {
+ rf: RenderFlags, ctx1: NgForOfContext) {
if (rf & RenderFlags.Create) {
ɵɵelementStart(0, 'li');
ɵɵtext(1);
diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts
index d12af28e6f..2c71a5d748 100644
--- a/tools/public_api_guard/common/common.d.ts
+++ b/tools/public_api_guard/common/common.d.ts
@@ -214,25 +214,25 @@ export declare class NgComponentOutlet implements OnChanges, OnDestroy {
ngOnDestroy(): void;
}
-export declare class NgForOf implements DoCheck {
- ngForOf: NgIterable | undefined | null;
- ngForTemplate: TemplateRef>;
+export declare class NgForOf> implements DoCheck {
+ ngForOf: (U & NgIterable) | undefined | null;
+ ngForTemplate: TemplateRef>;
ngForTrackBy: TrackByFunction;
- constructor(_viewContainer: ViewContainerRef, _template: TemplateRef>, _differs: IterableDiffers);
+ constructor(_viewContainer: ViewContainerRef, _template: TemplateRef>, _differs: IterableDiffers);
ngDoCheck(): void;
- static ngTemplateContextGuard(dir: NgForOf, ctx: any): ctx is NgForOfContext;
+ static ngTemplateContextGuard>(dir: NgForOf, ctx: any): ctx is NgForOfContext;
}
-export declare class NgForOfContext {
+export declare class NgForOfContext> {
$implicit: T;
count: number;
readonly even: boolean;
readonly first: boolean;
index: number;
readonly last: boolean;
- ngForOf: NgIterable;
+ ngForOf: U;
readonly odd: boolean;
- constructor($implicit: T, ngForOf: NgIterable, index: number, count: number);
+ constructor($implicit: T, ngForOf: U, index: number, count: number);
}
export declare class NgIf {