Skip to content

Commit

Permalink
fix(elements): do not break when the constructor of an Angular Elemen…
Browse files Browse the repository at this point in the history
…t is not called (#36114) (#37226)

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

PR Close #37226
  • Loading branch information
gkalpak authored and kara committed May 20, 2020
1 parent a33cb2d commit 1465372
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 7 deletions.
11 changes: 5 additions & 6 deletions packages/elements/src/create-custom-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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(
Expand Down
29 changes: 28 additions & 1 deletion packages/elements/test/create-custom-element_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
Expand Down

0 comments on commit 1465372

Please sign in to comment.