Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/elements/src/create-custom-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ export function createCustomElement<P>(
// Re-apply pre-existing input values (set as properties on the element) through the
// strategy.
// TODO(alxhub): why are we doing this? this makes no sense.
inputs.forEach(({propName, transform, isSignal}) => {
if (!this.hasOwnProperty(propName) || isSignal) {
// No pre-existing value for `propName`, or a signal input.
inputs.forEach(({propName, transform}) => {
if (!this.hasOwnProperty(propName)) {
// No pre-existing value for `propName`.
return;
}

// Delete the property from the instance and re-apply it through the strategy.
// Delete the property from the DOM node and re-apply it through the strategy.
const value = (this as any)[propName];
delete (this as any)[propName];
strategy.setInputValue(propName, value, transform);
Expand Down
36 changes: 35 additions & 1 deletion packages/elements/test/create-custom-element_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
DoBootstrap,
EventEmitter,
Injector,
input,
Input,
NgModule,
Output,
Expand All @@ -31,6 +32,7 @@ interface WithFooBar {
fooFoo: string;
barBar: string;
fooTransformed: unknown;
fooSignal: string | null;
}

describe('createCustomElement', () => {
Expand Down Expand Up @@ -66,20 +68,27 @@ describe('createCustomElement', () => {
});

it('should use a default strategy for converting component inputs', () => {
expect(NgElementCtor.observedAttributes).toEqual(['foo-foo', 'barbar', 'foo-transformed']);
expect(NgElementCtor.observedAttributes).toEqual([
'foo-foo',
'barbar',
'foo-transformed',
'foo-signal',
]);
});

it('should send input values from attributes when connected', () => {
const element = new NgElementCtor(injector);
element.setAttribute('foo-foo', 'value-foo-foo');
element.setAttribute('barbar', 'value-barbar');
element.setAttribute('foo-transformed', 'truthy');
element.setAttribute('foo-signal', 'value-signal');
element.connectedCallback();
expect(strategy.connectedElement).toBe(element);

expect(strategy.getInputValue('fooFoo')).toBe('value-foo-foo');
expect(strategy.getInputValue('barBar')).toBe('value-barbar');
expect(strategy.getInputValue('fooTransformed')).toBe(true);
expect(strategy.getInputValue('fooSignal')).toBe('value-signal');
});

it('should work even if the constructor is not called (due to polyfill)', () => {
Expand All @@ -95,12 +104,14 @@ describe('createCustomElement', () => {
element.setAttribute('foo-foo', 'value-foo-foo');
element.setAttribute('barbar', 'value-barbar');
element.setAttribute('foo-transformed', 'truthy');
element.setAttribute('foo-signal', 'value-signal');
element.connectedCallback();

expect(strategy.connectedElement).toBe(element);
expect(strategy.getInputValue('fooFoo')).toBe('value-foo-foo');
expect(strategy.getInputValue('barBar')).toBe('value-barbar');
expect(strategy.getInputValue('fooTransformed')).toBe(true);
expect(strategy.getInputValue('fooSignal')).toBe('value-signal');
});

it('should listen to output events after connected', () => {
Expand Down Expand Up @@ -174,10 +185,12 @@ describe('createCustomElement', () => {
element.fooFoo = 'foo-foo-value';
element.barBar = 'barBar-value';
element.fooTransformed = 'truthy';
element.fooSignal = 'value-signal';

expect(strategy.inputs.get('fooFoo')).toBe('foo-foo-value');
expect(strategy.inputs.get('barBar')).toBe('barBar-value');
expect(strategy.inputs.get('fooTransformed')).toBe(true);
expect(strategy.inputs.get('fooSignal')).toBe('value-signal');
});

it('should properly handle getting/setting properties on the element even if the constructor is not called', () => {
Expand All @@ -191,10 +204,12 @@ describe('createCustomElement', () => {
element.fooFoo = 'foo-foo-value';
element.barBar = 'barBar-value';
element.fooTransformed = 'truthy';
element.fooSignal = 'value-signal';

expect(strategy.inputs.get('fooFoo')).toBe('foo-foo-value');
expect(strategy.inputs.get('barBar')).toBe('barBar-value');
expect(strategy.inputs.get('fooTransformed')).toBe(true);
expect(strategy.inputs.get('fooSignal')).toBe('value-signal');
});

it('should capture properties set before upgrading the element', () => {
Expand All @@ -204,21 +219,25 @@ describe('createCustomElement', () => {
fooFoo: 'foo-prop-value',
barBar: 'bar-prop-value',
fooTransformed: 'truthy' as unknown,
fooSignal: 'value-signal',
});
expect(element.fooFoo).toBe('foo-prop-value');
expect(element.barBar).toBe('bar-prop-value');
expect(element.fooTransformed).toBe('truthy');
expect(element.fooSignal).toBe('value-signal');

// 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(element.fooTransformed).toBe(true);
expect(element.fooSignal).toBe('value-signal');

expect(strategy.inputs.get('fooFoo')).toBe('foo-prop-value');
expect(strategy.inputs.get('barBar')).toBe('bar-prop-value');
expect(strategy.inputs.get('fooTransformed')).toBe(true);
expect(strategy.inputs.get('fooSignal')).toBe('value-signal');
});

it('should capture properties set after upgrading the element but before inserting it into the DOM', () => {
Expand All @@ -228,30 +247,36 @@ describe('createCustomElement', () => {
fooFoo: 'foo-prop-value',
barBar: 'bar-prop-value',
fooTransformed: 'truthy' as unknown,
fooSignal: 'value-signal',
});
expect(element.fooFoo).toBe('foo-prop-value');
expect(element.barBar).toBe('bar-prop-value');
expect(element.fooTransformed).toBe('truthy');
expect(element.fooSignal).toBe('value-signal');

// 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';
element.fooTransformed = '';
element.fooSignal = 'value-signal-changed';
expect(element.fooFoo).toBe('foo-prop-value');
expect(element.barBar).toBe('bar-prop-value-2');
expect(element.fooTransformed).toBe('');
expect(element.fooSignal).toBe('value-signal-changed');

// 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(element.fooTransformed).toBe(false);
expect(element.fooSignal).toBe('value-signal-changed');

expect(strategy.inputs.get('fooFoo')).toBe('foo-prop-value');
expect(strategy.inputs.get('barBar')).toBe('bar-prop-value-2');
expect(strategy.inputs.get('fooTransformed')).toBe(false);
expect(strategy.inputs.get('fooSignal')).toBe('value-signal-changed');
});

it('should allow overwriting properties with attributes after upgrading the element but before inserting it into the DOM', () => {
Expand All @@ -261,10 +286,12 @@ describe('createCustomElement', () => {
fooFoo: 'foo-prop-value',
barBar: 'bar-prop-value',
fooTransformed: 'truthy' as unknown,
fooSignal: 'value-signal',
});
expect(element.fooFoo).toBe('foo-prop-value');
expect(element.barBar).toBe('bar-prop-value');
expect(element.fooTransformed).toBe('truthy');
expect(element.fooSignal).toBe('value-signal');

// Upgrade the element to a Custom Element (without inserting it into the DOM) and set an
// attribute.
Expand All @@ -275,16 +302,19 @@ describe('createCustomElement', () => {
expect(element.fooFoo).toBe('foo-prop-value');
expect(element.barBar).toBe('bar-attr-value');
expect(element.fooTransformed).toBe(false);
expect(element.fooSignal).toBe('value-signal');

// Insert the element into the DOM.
testContainer.appendChild(element);
expect(element.fooFoo).toBe('foo-prop-value');
expect(element.barBar).toBe('bar-attr-value');
expect(element.fooTransformed).toBe(false);
expect(element.fooSignal).toBe('value-signal');

expect(strategy.inputs.get('fooFoo')).toBe('foo-prop-value');
expect(strategy.inputs.get('barBar')).toBe('bar-attr-value');
expect(strategy.inputs.get('fooTransformed')).toBe(false);
expect(strategy.inputs.get('fooSignal')).toBe('value-signal');
});

// Helpers
Expand Down Expand Up @@ -313,6 +343,10 @@ describe('createCustomElement', () => {
@Input('barbar') barBar!: string;
@Input({transform: (value: unknown) => !!value}) fooTransformed!: boolean;

// This needs to apply the decorator and pass `isSignal`, because
// the compiler transform doesn't run against JIT tests.
@Input({isSignal: true} as Input) fooSignal = input<string | null>(null);

@Output() bazBaz = new EventEmitter<boolean>();
@Output('quxqux') quxQux = new EventEmitter<Object>();
}
Expand Down