Skip to content

Commit

Permalink
fix(core): make QueryList implement Iterable in the type system
Browse files Browse the repository at this point in the history
Originally, QueryList implemented Iterable and provided a Symbol.iterator
on its prototype. This caused issues with tree-shaking, so QueryList was
refactored and the Symbol.iterator added in its constructor instead. As
part of this change, QueryList no longer implemented Iterable directly.

Unfortunately, this meant that QueryList was no longer assignable to
Iterable or, consequently, NgIterable. NgIterable is used for NgFor's input,
so this meant that QueryList was not usable (in a type sense) for NgFor
iteration. View Engine's template type checking would not catch this, but
Ivy's did.

As a fix, this commit adds the declaration (but not the implementation) of
the Symbol.iterator function back to QueryList. This has no runtime effect,
so it doesn't affect tree-shaking of QueryList, but it ensures that
QueryList is assignable to NgIterable and thus usable with NgFor.

Fixes angular#29842
  • Loading branch information
alxhub committed Nov 18, 2019
1 parent ce30888 commit 70a0098
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 4 deletions.
4 changes: 4 additions & 0 deletions packages/compiler-cli/test/ngtsc/fake_core/index.ts
Expand Up @@ -85,3 +85,7 @@ export const NO_ERRORS_SCHEMA: any = false;
export class EventEmitter<T> {
subscribe(generatorOrNext?: any, error?: any, complete?: any): unknown { return null; }
}

export interface QueryList<T>/* implements Iterable<T> */ { [Symbol.iterator]: () => Iterator<T>; }

export type NgIterable<T> = Array<T>| Iterable<T>;
28 changes: 26 additions & 2 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Expand Up @@ -28,7 +28,7 @@ import * as i0 from '@angular/core';
export declare class NgForOfContext<T> {
$implicit: T;
ngForOf: T[];
ngForOf: i0.NgIterable<T>;
index: number;
count: number;
readonly first: boolean;
Expand All @@ -44,7 +44,7 @@ export declare class IndexPipe {
}
export declare class NgForOf<T> {
ngForOf: T[];
ngForOf: i0.NgIterable<T>;
static ngTemplateContextGuard<T>(dir: NgForOf<T>, ctx: any): ctx is NgForOfContext<T>;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<NgForOf<any>, '[ngFor][ngForOf]', never, {'ngForOf': 'ngForOf'}, {}, never>;
}
Expand Down Expand Up @@ -714,6 +714,30 @@ export declare class AnimationEvent {
env.driveMain();
});

it('should accept NgFor iteration over a QueryList', () => {
env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true});
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule, QueryList} from '@angular/core';
@Component({
selector: 'test',
template: '<div *ngFor="let user of users">{{user.name}}</div>',
})
class TestCmp {
users!: QueryList<{name: string}>;
}
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
})
class Module {}
`);

env.driveMain();
});

it('should report an error with an unknown local ref target', () => {
env.write('test.ts', `
import {Component, NgModule} from '@angular/core';
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/linker/query_list.ts
Expand Up @@ -42,7 +42,7 @@ function symbolIterator<T>(this: QueryList<T>): Iterator<T> {
*
* @publicApi
*/
export class QueryList<T>/* implements Iterable<T> */ {
export class QueryList<T> implements Iterable<T> {
public readonly dirty = true;
private _results: Array<T> = [];
public readonly changes: Observable<any> = new EventEmitter();
Expand Down Expand Up @@ -142,4 +142,11 @@ export class QueryList<T>/* implements Iterable<T> */ {
(this.changes as EventEmitter<any>).complete();
(this.changes as EventEmitter<any>).unsubscribe();
}

// The implementation of `Symbol.iterator` should be declared here, but this would cause
// tree-shaking issues with `QueryList. So instead, it's added in the constructor (see comments
// there) and this declaration is left here to ensure that TypeScript considers QueryList to
// implement the Iterable interface. This is required for template type-checking of NgFor loops
// over QueryLists to work correctly, since QueryList must be assignable to NgIterable.
[Symbol.iterator] !: () => Iterator<T>;
}
16 changes: 16 additions & 0 deletions packages/core/test/linker/query_list_spec.ts
Expand Up @@ -135,6 +135,22 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin
expect(queryList.some(item => item === 'four')).toEqual(false);
});

it('should be iterable', () => {
const data = ['one', 'two', 'three'];
queryList.reset([...data]);

// The type here is load-bearing: it asserts that queryList is considered assignable to
// Iterable<string> in TypeScript. This is important for template type-checking of *ngFor
// when looping over query results.
const queryListAsIterable: Iterable<string> = queryList;

// For loops use the iteration protocol.
for (const value of queryListAsIterable) {
expect(value).toBe(data.shift() !);
}
expect(data.length).toBe(0);
});

if (getDOM().supportsDOMEvents()) {
describe('simple observable interface', () => {
it('should fire callbacks on change', fakeAsync(() => {
Expand Down
3 changes: 2 additions & 1 deletion tools/public_api_guard/core/core.d.ts
Expand Up @@ -1135,7 +1135,8 @@ export interface Query {
export declare abstract class Query {
}

export declare class QueryList<T> {
export declare class QueryList<T> implements Iterable<T> {
[Symbol.iterator]: () => Iterator<T>;
readonly changes: Observable<any>;
readonly dirty = true;
readonly first: T;
Expand Down

0 comments on commit 70a0098

Please sign in to comment.