Skip to content

Commit be0455a

Browse files
chintankavathiakirjs
authored andcommitted
fix(elements): return value on signal input getter (angular#62113)
BREAKING CHANGE: Fix signal input getter behavior in custom elements. Before this change, signal inputs in custom elements required function calls to access their values (`elementRef.newInput()`), while decorator inputs were accessed directly (`elementRef.oldInput`). This inconsistency caused confusion and typing difficulties. The getter behavior has been standardized so signal inputs can now be accessed directly, matching the behavior of decorator inputs: Before: - Decorator Input: `elementRef.oldInput` - Signal Input: `elementRef.newInput()` After: - Decorator Input: `elementRef.oldInput` - Signal Input: `elementRef.newInput` closes angular#62097 PR Close angular#62113
1 parent c0e1c41 commit be0455a

File tree

2 files changed

+50
-4
lines changed

2 files changed

+50
-4
lines changed

packages/elements/src/create-custom-element.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Injector, Type} from '@angular/core';
9+
import {Injector, Type, isSignal} from '@angular/core';
1010
import {Subscription} from 'rxjs';
1111

1212
import {ComponentNgElementStrategyFactory} from './component-factory-strategy';
@@ -237,10 +237,11 @@ export function createCustomElement<P>(
237237
}
238238

239239
// Add getters and setters to the prototype for each property input.
240-
inputs.forEach(({propName, transform}) => {
240+
inputs.forEach(({propName, transform, isSignal: _isSignal}) => {
241241
Object.defineProperty(NgElementImpl.prototype, propName, {
242242
get(): any {
243-
return this.ngElementStrategy.getInputValue(propName);
243+
const inputValue = this.ngElementStrategy.getInputValue(propName);
244+
return _isSignal && isSignal(inputValue) ? inputValue() : inputValue;
244245
},
245246
set(newValue: any): void {
246247
this.ngElementStrategy.setInputValue(propName, newValue, transform);

packages/elements/test/create-custom-element_spec.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import {
1414
Injector,
1515
input,
1616
Input,
17+
isSignal,
1718
NgModule,
1819
Output,
20+
signal,
21+
WritableSignal,
1922
} from '@angular/core';
2023
import {BrowserModule, platformBrowser} from '@angular/platform-browser';
2124
import {Subject} from 'rxjs';
@@ -316,6 +319,29 @@ describe('createCustomElement', () => {
316319
expect(strategy.inputs.get('fooSignal')).toBe('value-signal');
317320
});
318321

322+
it('should return value from input getter for input signal', () => {
323+
const {selector, ElementCtor} = createTestCustomElementForSignal();
324+
const element = document.createElement(selector) as HTMLElement & {
325+
fooSignal: string | null;
326+
};
327+
element.setAttribute('foo-signal', 'value-signal');
328+
329+
customElements.define(selector, ElementCtor);
330+
testContainer.appendChild(element);
331+
expect(element.fooSignal).toBe('value-signal');
332+
});
333+
334+
it('should not unpack signal value with input decorator having signal as value', () => {
335+
const {selector, ElementCtor} = createTestCustomElementForSignal();
336+
const element = document.createElement(selector) as HTMLElement & {
337+
fooFoo: WritableSignal<string | null>;
338+
};
339+
340+
customElements.define(selector, ElementCtor);
341+
testContainer.appendChild(element);
342+
expect(isSignal(element.fooFoo)).toBe(true);
343+
});
344+
319345
// Helpers
320346
function createAndRegisterTestCustomElement(strategyFactory: NgElementStrategyFactory) {
321347
const {selector, ElementCtor} = createTestCustomElement(strategyFactory);
@@ -332,6 +358,13 @@ describe('createCustomElement', () => {
332358
};
333359
}
334360

361+
function createTestCustomElementForSignal() {
362+
return {
363+
selector: `test-element-${++selectorUid}`,
364+
ElementCtor: createCustomElement(TestSignalComponent, {injector}),
365+
};
366+
}
367+
335368
@Component({
336369
selector: 'test-component',
337370
template: 'TestComponent|foo({{ fooFoo }})|bar({{ barBar }})',
@@ -349,9 +382,21 @@ describe('createCustomElement', () => {
349382
@Output() bazBaz = new EventEmitter<boolean>();
350383
@Output('quxqux') quxQux = new EventEmitter<Object>();
351384
}
385+
386+
@Component({
387+
selector: 'test-signal-component',
388+
template: 'TestSignalComponent|foo({{ fooFoo() }})|signal({{ fooSignal() }})',
389+
standalone: false,
390+
})
391+
class TestSignalComponent {
392+
@Input() fooFoo = signal<string | null>(null);
393+
// This needs to apply the decorator and pass `isSignal`, because
394+
// the compiler transform doesn't run against JIT tests.
395+
@Input({isSignal: true} as Input) fooSignal = input<string | null>(null);
396+
}
352397
@NgModule({
353398
imports: [BrowserModule],
354-
declarations: [TestComponent],
399+
declarations: [TestComponent, TestSignalComponent],
355400
})
356401
class TestModule implements DoBootstrap {
357402
ngDoBootstrap() {}

0 commit comments

Comments
 (0)