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:
parent
2fc5ae561b
commit
89b44d1900
@ -144,8 +144,9 @@ export function createCustomElement<P>(
|
|||||||
//
|
//
|
||||||
// TODO(andrewseguin): Add e2e tests that cover cases where the constructor isn't called. For
|
// 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.
|
// now this is tested using a Google internal test suite.
|
||||||
if (this._ngElementStrategy === null) {
|
if (!this._ngElementStrategy) {
|
||||||
const strategy = this._ngElementStrategy = strategyFactory.create(this.injector);
|
const strategy = this._ngElementStrategy =
|
||||||
|
strategyFactory.create(this.injector || config.injector);
|
||||||
|
|
||||||
// Collect pre-existing values on the element to re-apply through the strategy.
|
// Collect pre-existing values on the element to re-apply through the strategy.
|
||||||
const preExistingValues =
|
const preExistingValues =
|
||||||
@ -173,12 +174,10 @@ export function createCustomElement<P>(
|
|||||||
return this._ngElementStrategy!;
|
return this._ngElementStrategy!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly injector: Injector;
|
private _ngElementStrategy?: NgElementStrategy;
|
||||||
private _ngElementStrategy: NgElementStrategy|null = null;
|
|
||||||
|
|
||||||
constructor(injector?: Injector) {
|
constructor(private readonly injector?: Injector) {
|
||||||
super();
|
super();
|
||||||
this.injector = injector || config.injector;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(
|
attributeChangedCallback(
|
||||||
|
@ -73,6 +73,26 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
expect(strategy.getInputValue('barBar')).toBe('value-barbar');
|
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', () => {
|
it('should listen to output events after connected', () => {
|
||||||
const element = new NgElementCtor(injector);
|
const element = new NgElementCtor(injector);
|
||||||
element.connectedCallback();
|
element.connectedCallback();
|
||||||
@ -260,7 +280,14 @@ if (browserDetection.supportsCustomElements) {
|
|||||||
class TestStrategyFactory implements NgElementStrategyFactory {
|
class TestStrategyFactory implements NgElementStrategyFactory {
|
||||||
testStrategy = new TestStrategy();
|
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;
|
return this.testStrategy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user