diff --git a/packages/elements/src/create-custom-element.ts b/packages/elements/src/create-custom-element.ts
index 7c2e222cd5..2e1069840f 100644
--- a/packages/elements/src/create-custom-element.ts
+++ b/packages/elements/src/create-custom-element.ts
@@ -145,9 +145,32 @@ export function createCustomElement
(
// 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) {
- this._ngElementStrategy = strategyFactory.create(this.injector);
+ const strategy = this._ngElementStrategy = strategyFactory.create(this.injector);
+
+ // Collect pre-existing values on the element to re-apply through the strategy.
+ const preExistingValues =
+ inputs.filter(({propName}) => this.hasOwnProperty(propName)).map(({propName}): [
+ string, any
+ ] => [propName, (this as any)[propName]]);
+
+ // In some browsers (e.g. IE10), `Object.setPrototypeOf()` (which is required by some Custom
+ // Elements polyfills) is not defined and is thus polyfilled in a way that does not preserve
+ // the prototype chain. In such cases, `this` will not be an instance of `NgElementImpl` and
+ // thus not have the component input getters/setters defined on `NgElementImpl.prototype`.
+ if (!(this instanceof NgElementImpl)) {
+ // Add getters and setters to the instance itself for each property input.
+ defineInputGettersSetters(inputs, this);
+ } else {
+ // Delete the property from the instance, so that it can go through the getters/setters
+ // set on `NgElementImpl.prototype`.
+ preExistingValues.forEach(([propName]) => delete (this as any)[propName]);
+ }
+
+ // Re-apply pre-existing values through the strategy.
+ preExistingValues.forEach(([propName, value]) => strategy.setInputValue(propName, value));
}
- return this._ngElementStrategy;
+
+ return this._ngElementStrategy!;
}
private readonly injector: Injector;
@@ -193,16 +216,26 @@ export function createCustomElement
(
// Update the property descriptor of `NgElementImpl#ngElementStrategy` to make it enumerable.
Object.defineProperty(NgElementImpl.prototype, 'ngElementStrategy', {enumerable: true});
- // Add getters and setters to the prototype for each property input. If the config does not
- // contain property inputs, use all inputs by default.
- inputs.map(({propName}) => propName).forEach(property => {
- Object.defineProperty(NgElementImpl.prototype, property, {
- get: function() { return this.ngElementStrategy.getInputValue(property); },
- set: function(newValue: any) { this.ngElementStrategy.setInputValue(property, newValue); },
+ // Add getters and setters to the prototype for each property input.
+ defineInputGettersSetters(inputs, NgElementImpl.prototype);
+
+ return (NgElementImpl as any) as NgElementConstructor
;
+}
+
+// Helpers
+function defineInputGettersSetters(
+ inputs: {propName: string, templateName: string}[], target: object): void {
+ // Add getters and setters for each property input.
+ inputs.forEach(({propName}) => {
+ Object.defineProperty(target, propName, {
+ get(): any {
+ return this.ngElementStrategy.getInputValue(propName);
+ },
+ set(newValue: any): void {
+ this.ngElementStrategy.setInputValue(propName, newValue);
+ },
configurable: true,
enumerable: true,
});
});
-
- return (NgElementImpl as any) as NgElementConstructor
;
}
diff --git a/packages/elements/test/create-custom-element_spec.ts b/packages/elements/test/create-custom-element_spec.ts
index 7df5327569..3551084d72 100644
--- a/packages/elements/test/create-custom-element_spec.ts
+++ b/packages/elements/test/create-custom-element_spec.ts
@@ -22,12 +22,16 @@ type WithFooBar = {
if (browserDetection.supportsCustomElements) {
describe('createCustomElement', () => {
+ let selectorUid = 0;
+ let testContainer: HTMLDivElement;
let NgElementCtor: NgElementConstructor;
let strategy: TestStrategy;
let strategyFactory: TestStrategyFactory;
let injector: Injector;
beforeAll(done => {
+ testContainer = document.createElement('div');
+ document.body.appendChild(testContainer);
destroyPlatform();
platformBrowserDynamic()
.bootstrapModule(TestModule)
@@ -36,18 +40,23 @@ if (browserDetection.supportsCustomElements) {
strategyFactory = new TestStrategyFactory();
strategy = strategyFactory.testStrategy;
- NgElementCtor = createCustomElement(TestComponent, {injector, strategyFactory});
+ const {selector, ElementCtor} = createTestCustomElement();
+ NgElementCtor = ElementCtor;
// The `@webcomponents/custom-elements/src/native-shim.js` polyfill allows us to create
// new instances of the NgElement which extends HTMLElement, as long as we define it.
- customElements.define('test-element', NgElementCtor);
+ customElements.define(selector, NgElementCtor);
})
.then(done, done.fail);
});
afterEach(() => strategy.reset());
- afterAll(() => destroyPlatform());
+ afterAll(() => {
+ destroyPlatform();
+ document.body.removeChild(testContainer);
+ (testContainer as any) = null;
+ });
it('should use a default strategy for converting component inputs', () => {
expect(NgElementCtor.observedAttributes).toEqual(['foo-foo', 'barbar']);
@@ -113,7 +122,90 @@ if (browserDetection.supportsCustomElements) {
expect(strategy.inputs.get('barBar')).toBe('barBar-value');
});
+ it('should capture properties set before upgrading the element', () => {
+ // Create a regular element and set properties on it.
+ const {selector, ElementCtor} = createTestCustomElement();
+ const element = Object.assign(document.createElement(selector), {
+ fooFoo: 'foo-prop-value',
+ barBar: 'bar-prop-value',
+ });
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-prop-value');
+
+ // Upgrade the element to a Custom Element and insert it into the DOM.
+ customElements.define(selector, ElementCtor);
+ testContainer.appendChild(element);
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-prop-value');
+
+ expect(strategy.inputs.get('fooFoo')).toBe('foo-prop-value');
+ expect(strategy.inputs.get('barBar')).toBe('bar-prop-value');
+ });
+
+ it('should capture properties set after upgrading the element but before inserting it into the DOM',
+ () => {
+ // Create a regular element and set properties on it.
+ const {selector, ElementCtor} = createTestCustomElement();
+ const element = Object.assign(document.createElement(selector), {
+ fooFoo: 'foo-prop-value',
+ barBar: 'bar-prop-value',
+ });
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-prop-value');
+
+ // Upgrade the element to a Custom Element (without inserting it into the DOM) and update a
+ // property.
+ customElements.define(selector, ElementCtor);
+ customElements.upgrade(element);
+ element.barBar = 'bar-prop-value-2';
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-prop-value-2');
+
+ // Insert the element into the DOM.
+ testContainer.appendChild(element);
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-prop-value-2');
+
+ expect(strategy.inputs.get('fooFoo')).toBe('foo-prop-value');
+ expect(strategy.inputs.get('barBar')).toBe('bar-prop-value-2');
+ });
+
+ it('should allow overwriting properties with attributes after upgrading the element but before inserting it into the DOM',
+ () => {
+ // Create a regular element and set properties on it.
+ const {selector, ElementCtor} = createTestCustomElement();
+ const element = Object.assign(document.createElement(selector), {
+ fooFoo: 'foo-prop-value',
+ barBar: 'bar-prop-value',
+ });
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-prop-value');
+
+ // Upgrade the element to a Custom Element (without inserting it into the DOM) and set an
+ // attribute.
+ customElements.define(selector, ElementCtor);
+ customElements.upgrade(element);
+ element.setAttribute('barbar', 'bar-attr-value');
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-attr-value');
+
+ // Insert the element into the DOM.
+ testContainer.appendChild(element);
+ expect(element.fooFoo).toBe('foo-prop-value');
+ expect(element.barBar).toBe('bar-attr-value');
+
+ expect(strategy.inputs.get('fooFoo')).toBe('foo-prop-value');
+ expect(strategy.inputs.get('barBar')).toBe('bar-attr-value');
+ });
+
// Helpers
+ function createTestCustomElement() {
+ return {
+ selector: `test-element-${++selectorUid}`,
+ ElementCtor: createCustomElement(TestComponent, {injector, strategyFactory}),
+ };
+ }
+
@Component({
selector: 'test-component',
template: 'TestComponent|foo({{ fooFoo }})|bar({{ barBar }})',