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

@ -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;
}
}