fix(core): Support extending differs from root `NgModule` (#39981)

Differs tries to inject parent differ in order to support extending.
This does not work in the 'root' injector as the provider overrides the
default injector. The fix is to just assume standard set of providers
and extend those instead.

PR close #25015
Issue close #11309 `Can't extend IterableDiffers`
Issue close #18554 `IterableDiffers.extend is not AOT compatible`
  (This is fixed because we no longer have an arrow function in the
  factory but a proper function which can be imported.)

PR Close #39981
This commit is contained in:
Misko Hevery 2020-12-04 16:39:37 -08:00
parent 775394c809
commit 5fc45082ca
9 changed files with 105 additions and 35 deletions

View File

@ -21,7 +21,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 3153, "runtime-es2015": 3153,
"main-es2015": 431696, "main-es2015": 431137,
"polyfills-es2015": 52493 "polyfills-es2015": 52493
} }
} }

View File

@ -3,7 +3,7 @@
"master": { "master": {
"uncompressed": { "uncompressed": {
"runtime-es2015": 1485, "runtime-es2015": 1485,
"main-es2015": 140921, "main-es2015": 140333,
"polyfills-es2015": 36964 "polyfills-es2015": 36964
} }
} }

View File

@ -134,6 +134,10 @@ export interface IterableDifferFactory {
create<V>(trackByFn?: TrackByFunction<V>): IterableDiffer<V>; create<V>(trackByFn?: TrackByFunction<V>): IterableDiffer<V>;
} }
export function defaultIterableDiffersFactory() {
return new IterableDiffers([new DefaultIterableDifferFactory()]);
}
/** /**
* A repository of different iterable diffing strategies used by NgFor, NgClass, and others. * A repository of different iterable diffing strategies used by NgFor, NgClass, and others.
* *
@ -141,11 +145,8 @@ export interface IterableDifferFactory {
*/ */
export class IterableDiffers { export class IterableDiffers {
/** @nocollapse */ /** @nocollapse */
static ɵprov = ɵɵdefineInjectable({ static ɵprov = ɵɵdefineInjectable(
token: IterableDiffers, {token: IterableDiffers, providedIn: 'root', factory: defaultIterableDiffersFactory});
providedIn: 'root',
factory: () => new IterableDiffers([new DefaultIterableDifferFactory()])
});
/** /**
* @deprecated v4.0.0 - Should be private * @deprecated v4.0.0 - Should be private
@ -187,14 +188,11 @@ export class IterableDiffers {
static extend(factories: IterableDifferFactory[]): StaticProvider { static extend(factories: IterableDifferFactory[]): StaticProvider {
return { return {
provide: IterableDiffers, provide: IterableDiffers,
useFactory: (parent: IterableDiffers) => { useFactory: (parent: IterableDiffers|null) => {
if (!parent) { // if parent is null, it means that we are in the root injector and we have just overridden
// Typically would occur when calling IterableDiffers.extend inside of dependencies passed // the default injection mechanism for IterableDiffers, in such a case just assume
// to // `defaultIterableDiffersFactory`.
// bootstrap(), which would override default pipes instead of extending them. return IterableDiffers.create(factories, parent || defaultIterableDiffersFactory());
throw new Error('Cannot extend IterableDiffers without a parent injector');
}
return IterableDiffers.create(factories, parent);
}, },
// Dependency technically isn't optional, but we can provide a better error message this way. // Dependency technically isn't optional, but we can provide a better error message this way.
deps: [[IterableDiffers, new SkipSelf(), new Optional()]] deps: [[IterableDiffers, new SkipSelf(), new Optional()]]

View File

@ -111,6 +111,10 @@ export interface KeyValueDifferFactory {
create<K, V>(): KeyValueDiffer<K, V>; create<K, V>(): KeyValueDiffer<K, V>;
} }
export function defaultKeyValueDiffersFactory() {
return new KeyValueDiffers([new DefaultKeyValueDifferFactory()]);
}
/** /**
* A repository of different Map diffing strategies used by NgClass, NgStyle, and others. * A repository of different Map diffing strategies used by NgClass, NgStyle, and others.
* *
@ -118,11 +122,8 @@ export interface KeyValueDifferFactory {
*/ */
export class KeyValueDiffers { export class KeyValueDiffers {
/** @nocollapse */ /** @nocollapse */
static ɵprov = ɵɵdefineInjectable({ static ɵprov = ɵɵdefineInjectable(
token: KeyValueDiffers, {token: KeyValueDiffers, providedIn: 'root', factory: defaultKeyValueDiffersFactory});
providedIn: 'root',
factory: () => new KeyValueDiffers([new DefaultKeyValueDifferFactory()])
});
/** /**
* @deprecated v4.0.0 - Should be private. * @deprecated v4.0.0 - Should be private.
@ -165,12 +166,10 @@ export class KeyValueDiffers {
return { return {
provide: KeyValueDiffers, provide: KeyValueDiffers,
useFactory: (parent: KeyValueDiffers) => { useFactory: (parent: KeyValueDiffers) => {
if (!parent) { // if parent is null, it means that we are in the root injector and we have just overridden
// Typically would occur when calling KeyValueDiffers.extend inside of dependencies passed // the default injection mechanism for KeyValueDiffers, in such a case just assume
// to bootstrap(), which would override default pipes instead of extending them. // `defaultKeyValueDiffersFactory`.
throw new Error('Cannot extend KeyValueDiffers without a parent injector'); return KeyValueDiffers.create(factories, parent || defaultKeyValueDiffersFactory());
}
return KeyValueDiffers.create(factories, parent);
}, },
// Dependency technically isn't optional, but we can provide a better error message this way. // Dependency technically isn't optional, but we can provide a better error message this way.
deps: [[KeyValueDiffers, new SkipSelf(), new Optional()]] deps: [[KeyValueDiffers, new SkipSelf(), new Optional()]]

View File

@ -854,9 +854,15 @@
{ {
"name": "defaultIterableDiffers" "name": "defaultIterableDiffers"
}, },
{
"name": "defaultIterableDiffersFactory"
},
{ {
"name": "defaultKeyValueDiffers" "name": "defaultKeyValueDiffers"
}, },
{
"name": "defaultKeyValueDiffersFactory"
},
{ {
"name": "defaultScheduler" "name": "defaultScheduler"
}, },

View File

@ -1121,9 +1121,15 @@
{ {
"name": "defaultIterableDiffers" "name": "defaultIterableDiffers"
}, },
{
"name": "defaultIterableDiffersFactory"
},
{ {
"name": "defaultKeyValueDiffers" "name": "defaultKeyValueDiffers"
}, },
{
"name": "defaultKeyValueDiffersFactory"
},
{ {
"name": "defaultMalformedUriErrorHandler" "name": "defaultMalformedUriErrorHandler"
}, },

View File

@ -281,6 +281,9 @@
{ {
"name": "defaultErrorLogger" "name": "defaultErrorLogger"
}, },
{
"name": "defaultIterableDiffersFactory"
},
{ {
"name": "defaultScheduler" "name": "defaultScheduler"
}, },

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injector} from '@angular/core'; import {Injector, IterableDiffer, IterableDifferFactory, IterableDiffers, NgModule, TrackByFunction} from '@angular/core';
import {IterableDiffers} from '@angular/core/src/change_detection/differs/iterable_differs'; import {TestBed} from '@angular/core/testing';
import {SpyIterableDifferFactory} from '../../spies'; import {SpyIterableDifferFactory} from '../../spies';
@ -49,13 +49,6 @@ import {SpyIterableDifferFactory} from '../../spies';
}); });
describe('.extend()', () => { describe('.extend()', () => {
it('should throw if calling extend when creating root injector', () => {
const injector = Injector.create([IterableDiffers.extend([])]);
expect(() => injector.get(IterableDiffers))
.toThrowError(/Cannot extend IterableDiffers without a parent injector/);
});
it('should extend di-inherited differs', () => { it('should extend di-inherited differs', () => {
const parent = new IterableDiffers([factory1]); const parent = new IterableDiffers([factory1]);
const injector = Injector.create([{provide: IterableDiffers, useValue: parent}]); const injector = Injector.create([{provide: IterableDiffers, useValue: parent}]);
@ -66,6 +59,32 @@ import {SpyIterableDifferFactory} from '../../spies';
factory2, factory1 factory2, factory1
]); ]);
}); });
it('should support .extend in root NgModule', () => {
const DIFFER: IterableDiffer<any> = {} as any;
const log: string[] = [];
class MyIterableDifferFactory implements IterableDifferFactory {
supports(objects: any): boolean {
log.push('supports', objects);
return true;
}
create<V>(trackByFn?: TrackByFunction<V>): IterableDiffer<V> {
log.push('create');
return DIFFER;
}
}
@NgModule({providers: [IterableDiffers.extend([new MyIterableDifferFactory()])]})
class MyModule {
}
TestBed.configureTestingModule({imports: [MyModule]});
const differs = TestBed.inject(IterableDiffers);
const differ = differs.find('VALUE').create(null!);
expect(differ).toEqual(DIFFER);
expect(log).toEqual(['supports', 'VALUE', 'create']);
});
}); });
}); });
} }

View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {KeyValueDiffer, KeyValueDifferFactory, KeyValueDiffers, NgModule} from '@angular/core';
import {TestBed} from '@angular/core/testing';
describe('KeyValueDiffers', function() {
it('should support .extend in root NgModule', () => {
const DIFFER: KeyValueDiffer<any, any> = {} as any;
const log: string[] = [];
class MyKeyValueDifferFactory implements KeyValueDifferFactory {
supports(objects: any): boolean {
log.push('supports', objects);
return true;
}
create<K, V>(): KeyValueDiffer<K, V> {
log.push('create');
return DIFFER;
}
}
@NgModule({providers: [KeyValueDiffers.extend([new MyKeyValueDifferFactory()])]})
class MyModule {
}
TestBed.configureTestingModule({imports: [MyModule]});
const differs = TestBed.inject(KeyValueDiffers);
const differ = differs.find('VALUE').create();
expect(differ).toEqual(DIFFER);
expect(log).toEqual(['supports', 'VALUE', 'create']);
});
});