Skip to content

Commit 0ae6d81

Browse files
cexbrayatmattrbeck
authored andcommitted
fix(core): preserve explicit input transform write type
If a directive has an input declared as `dismissible = input<boolean>(true, {transform: booleanAttribute});` then the following templates were not compiling: ``` <div directiveName dismissible="true"></div> <div directiveName dismissible></div> ``` This commit fixes the issue, without breaking contravariant consumers.
1 parent 359fb50 commit 0ae6d81

5 files changed

Lines changed: 90 additions & 10 deletions

File tree

goldens/public-api/core/index.api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,8 +1029,8 @@ export interface InputFunction {
10291029
<T>(initialValue: undefined, opts: InputOptionsWithoutTransform<T>): InputSignal<T | undefined>;
10301030
<T, TransformT>(initialValue: T, opts: InputOptionsWithTransform<T, TransformT>): InputSignalWithTransform<T, TransformT>;
10311031
<T, TransformT>(initialValue: undefined, opts: InputOptionsWithTransform<T | undefined, TransformT>): InputSignalWithTransform<T | undefined, TransformT>;
1032-
<T>(initialValue: T, opts: InputOptionsWithTransform<T, unknown>): InputSignalWithTransform<T, T>;
1033-
<T>(initialValue: undefined, opts: InputOptionsWithTransform<T | undefined, unknown>): InputSignalWithTransform<T | undefined, T | undefined>;
1032+
<T>(initialValue: T, opts: InputOptionsWithTransform<T, unknown>): InputSignalWithTransform<T, unknown>;
1033+
<T>(initialValue: undefined, opts: InputOptionsWithTransform<T | undefined, unknown>): InputSignalWithTransform<T | undefined, unknown>;
10341034
required: {
10351035
<T>(opts?: InputOptionsWithoutTransform<T>): InputSignal<T>;
10361036
<T, TransformT>(opts: InputOptionsWithTransform<T, TransformT>): InputSignalWithTransform<T, TransformT>;

packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,64 @@ runInEachFileSystem(() => {
320320
);
321321
});
322322

323+
it('should allow text attributes for explicit signal inputs with booleanAttribute transform', () => {
324+
env.write(
325+
'test.ts',
326+
`
327+
import {booleanAttribute, Component, Directive, input} from '@angular/core';
328+
329+
@Directive({
330+
selector: '[directiveName]',
331+
})
332+
export class TestDir {
333+
dismissible = input<boolean>(true, {transform: booleanAttribute});
334+
}
335+
336+
@Component({
337+
template: \`
338+
<div directiveName [dismissible]="true"></div>
339+
<div directiveName dismissible="true"></div>
340+
<div directiveName dismissible></div>
341+
\`,
342+
imports: [TestDir],
343+
})
344+
export class TestComp {}
345+
`,
346+
);
347+
348+
const diagnostics = env.driveDiagnostics();
349+
expect(diagnostics).toEqual([]);
350+
});
351+
352+
it('should allow text attributes for explicit signal inputs with numberAttribute transform', () => {
353+
env.write(
354+
'test.ts',
355+
`
356+
import {Component, Directive, input, numberAttribute} from '@angular/core';
357+
358+
@Directive({
359+
selector: '[directiveName]',
360+
})
361+
export class TestDir {
362+
count = input<number>(0, {transform: numberAttribute});
363+
}
364+
365+
@Component({
366+
template: \`
367+
<div directiveName [count]="1"></div>
368+
<div directiveName count="1"></div>
369+
<div directiveName count=""></div>
370+
\`,
371+
imports: [TestDir],
372+
})
373+
export class TestComp {}
374+
`,
375+
);
376+
377+
const diagnostics = env.driveDiagnostics();
378+
expect(diagnostics).toEqual([]);
379+
});
380+
323381
it('should report unset required inputs', () => {
324382
env.write(
325383
'test.ts',

packages/core/src/authoring/input/input.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,17 +79,20 @@ export interface InputFunction {
7979
): InputSignalWithTransform<T | undefined, TransformT>;
8080
/**
8181
* Declares an input of type `T` with an initial value and a transform function
82-
* that accepts values of the same type.
82+
* that accepts values of type `unknown`.
8383
*/
84-
<T>(initialValue: T, opts: InputOptionsWithTransform<T, unknown>): InputSignalWithTransform<T, T>;
84+
<T>(
85+
initialValue: T,
86+
opts: InputOptionsWithTransform<T, unknown>,
87+
): InputSignalWithTransform<T, unknown>;
8588
/**
8689
* Declares an input of type `T|undefined` without an initial value and with a transform
87-
* function that accepts values of the same type.
90+
* function that accepts values of type `unknown`.
8891
*/
8992
<T>(
9093
initialValue: undefined,
9194
opts: InputOptionsWithTransform<T | undefined, unknown>,
92-
): InputSignalWithTransform<T | undefined, T | undefined>;
95+
): InputSignalWithTransform<T | undefined, unknown>;
9396

9497
/**
9598
* Initializes a required input.

packages/core/test/authoring/signal_input_signature_test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,15 @@ export class InputSignatureTest {
102102
transform: (v: string | boolean) => '',
103103
});
104104

105-
/** boolean, boolean */
105+
/** boolean, unknown */
106106
explicitReadWithBooleanAttributeTransform = input<boolean>(false, {transform: booleanAttribute});
107-
/** number, number */
107+
/** number, unknown */
108108
explicitReadWithNumberAttributeTransform = input<number>(0, {transform: numberAttribute});
109-
/** boolean | undefined, boolean | undefined */
109+
/** boolean | undefined, unknown */
110110
explicitReadWithUndefinedInitialBooleanAttributeTransform = input<boolean>(undefined, {
111111
transform: booleanAttribute,
112112
});
113-
/** number | undefined, number | undefined */
113+
/** number | undefined, unknown */
114114
explicitReadWithUndefinedInitialNumberAttributeTransform = input<number>(undefined, {
115115
transform: numberAttribute,
116116
});

packages/forms/signals/test/web/form_field.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3978,6 +3978,25 @@ describe('field directive', () => {
39783978
});
39793979

39803980
describe('input transforms', () => {
3981+
it('should accept an explicit read type with InputSignalWithTransform', () => {
3982+
@Component({selector: 'custom-control', template: ``})
3983+
class CustomControl implements FormValueControl<string> {
3984+
readonly value = model('');
3985+
readonly disabled = input<boolean>(false, {transform: booleanAttribute});
3986+
}
3987+
3988+
@Component({
3989+
imports: [FormField, CustomControl],
3990+
template: `<custom-control [formField]="f" />`,
3991+
})
3992+
class TestCmp {
3993+
readonly f = form(signal(''));
3994+
}
3995+
3996+
const fixture = act(() => TestBed.createComponent(TestCmp));
3997+
expect(fixture.componentInstance).toBeDefined();
3998+
});
3999+
39814000
it('should accept InputSignal without transform', () => {
39824001
@Component({selector: 'custom-control', template: ``})
39834002
class CustomControl implements FormValueControl<string> {

0 commit comments

Comments
 (0)