fix(elements): do not break when the constructor of an Angular Element is not called (#36114)

Previously, the correct behavior of Angular custom elements relied on
the constructor being called (and thus the `injector` property being
initialized). However, some polyfills (e.g. `document-register-element`)
do not call the constructor of custom elements, which resulted in the
`injector` property being undefined and the `NgElementStrategy` failing
to be instantiated.

This commit fixes it by being tolerant to the `injector` property being
undefined and falling back to the injector passed to the
`createCustomElement()` config.

NOTE:
We don't have proper tests exercising the situation where the
constructor is not called. For now this is tested using a Google
internal test suite (which is how this issue was caught).
This commit also adds a rudimentary unit test to emulate this situation.

PR Close #36114
This commit is contained in:
George Kalpakas 2020-05-20 11:10:27 +03:00 committed by Kara Erickson
parent 2fc5ae561b
commit 89b44d1900
2 changed files with 33 additions and 7 deletions

View File

@ -144,8 +144,9 @@ export function createCustomElement<P>(
//
// TODO(andrewseguin): Add e2e tests that cover cases where the constructor isn't called. For
// now this is tested using a Google internal test suite.
if (this._ngElementStrategy === null) {
const strategy = this._ngElementStrategy = strategyFactory.create(this.injector);
if (!this._ngElementStrategy) {
const strategy = this._ngElementStrategy =
strategyFactory.create(this.injector || config.injector);
// Collect pre-existing values on the element to re-apply through the strategy.
const preExistingValues =
@ -173,12 +174,10 @@ export function createCustomElement<P>(
return this._ngElementStrategy!;
}
private readonly injector: Injector;
private _ngElementStrategy: NgElementStrategy|null = null;
private _ngElementStrategy?: NgElementStrategy;
constructor(injector?: Injector) {
constructor(private readonly injector?: Injector) {
super();
this.injector = injector || config.injector;
}
attributeChangedCallback(

View File

@ -73,6 +73,26 @@ if (browserDetection.supportsCustomElements) {
expect(strategy.getInputValue('barBar')).toBe('value-barbar');
});
it('should work even if when the constructor is not called (due to polyfill)', () => {
// Some polyfills (e.g. `document-register-element`) do not call the constructor of custom
// elements. Currently, all the constructor does is initialize the `injector` property. This
// test simulates not having called the constructor by "unsetting" the property.
//
// NOTE:
// If the constructor implementation changes in the future, this test needs to be adjusted
// accordingly.
const element = new NgElementCtor(injector);
delete (element as any).injector;
element.setAttribute('foo-foo', 'value-foo-foo');
element.setAttribute('barbar', 'value-barbar');
element.connectedCallback();
expect(strategy.connectedElement).toBe(element);
expect(strategy.getInputValue('fooFoo')).toBe('value-foo-foo');
expect(strategy.getInputValue('barBar')).toBe('value-barbar');
});
it('should listen to output events after connected', () => {
const element = new NgElementCtor(injector);
element.connectedCallback();
@ -260,7 +280,14 @@ if (browserDetection.supportsCustomElements) {
class TestStrategyFactory implements NgElementStrategyFactory {
testStrategy = new TestStrategy();
create(): NgElementStrategy {
create(injector: Injector): NgElementStrategy {
// Although not used by the `TestStrategy`, verify that the injector is provided.
if (!injector) {
throw new Error(
'Expected injector to be passed to `TestStrategyFactory#create()`, but received ' +
`value of type ${typeof injector}: ${injector}`);
}
return this.testStrategy;
}
}