Skip to content

Commit 863be4b

Browse files
devversionatscott
authored andcommitted
feat(core): expose new input API for signal-based inputs (angular#53872)
Enables signal inputs for existing Zone based components. This is a next step we are taking to bring signal inputs earlier to the Angular community. The goal is to enable early access for the ecosystem to signal inputs, while we are continuing development of full signal components as outlined in the RFC. This will allow the ecosystem to start integrating signals more deeply, prepare for future migrations, and improves code quality and DX for existing components (especially for OnPush). Based on our work on full signal components, we've gathered more information and learned new things. We've improved the API by introducing a way to intuitively declare required inputs, as well as improved the API around initial values. We even support non-primitive initial values as the first argument to the `input` function now. ```ts @directive({..}) export class MyDir { firstName = input<string>(); // string|undefined lastName = input.required<string>(); // string age = input(0); // number ``` PR Close angular#53872
1 parent b2066d4 commit 863be4b

File tree

11 files changed

+124
-469
lines changed

11 files changed

+124
-469
lines changed

goldens/public-api/core/index.md

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -867,13 +867,43 @@ export interface Input {
867867
// @public (undocumented)
868868
export const Input: InputDecorator;
869869

870+
// @public
871+
export const input: InputFunction;
872+
870873
// @public (undocumented)
871874
export interface InputDecorator {
872875
(arg?: string | Input): any;
873876
// (undocumented)
874877
new (arg?: string | Input): any;
875878
}
876879

880+
// @public
881+
export interface InputFunction {
882+
<ReadT>(): InputSignal<ReadT | undefined>;
883+
// (undocumented)
884+
<ReadT>(initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
885+
// (undocumented)
886+
<ReadT, WriteT>(initialValue: ReadT, opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
887+
required: {
888+
<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
889+
<ReadT, WriteT>(opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
890+
};
891+
}
892+
893+
// @public
894+
export interface InputOptions<ReadT, WriteT> {
895+
alias?: string;
896+
transform?: (v: WriteT) => ReadT;
897+
}
898+
899+
// @public
900+
export type InputOptionsWithoutTransform<ReadT> = Omit<InputOptions<ReadT, ReadT>, 'transform'> & {
901+
transform?: undefined;
902+
};
903+
904+
// @public
905+
export type InputOptionsWithTransform<ReadT, WriteT> = Required<Pick<InputOptions<ReadT, WriteT>, 'transform'>> & InputOptions<ReadT, WriteT>;
906+
877907
// @public
878908
export interface InputSignal<ReadT, WriteT = ReadT> extends Signal<ReadT> {
879909
// (undocumented)
@@ -1647,21 +1677,6 @@ export interface WritableSignal<T> extends Signal<T> {
16471677
update(updateFn: (value: T) => T): void;
16481678
}
16491679

1650-
// @public
1651-
export function ɵinputFunctionForApiGuard<ReadT>(): InputSignal<ReadT | undefined>;
1652-
1653-
// @public (undocumented)
1654-
export function ɵinputFunctionForApiGuard<ReadT>(initialValue: ReadT, opts?: ɵInputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
1655-
1656-
// @public (undocumented)
1657-
export function ɵinputFunctionForApiGuard<ReadT, WriteT>(initialValue: ReadT, opts: ɵInputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
1658-
1659-
// @public
1660-
export function ɵinputFunctionRequiredForApiGuard<ReadT>(opts?: ɵInputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
1661-
1662-
// @public (undocumented)
1663-
export function ɵinputFunctionRequiredForApiGuard<ReadT, WriteT>(opts: ɵInputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
1664-
16651680
// (No @packageDocumentation comment for this package)
16661681

16671682
```

integration/cli-signal-inputs/src/app/greet.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {ChangeDetectionStrategy, Component, Input, ɵinput} from '@angular/core';
1+
import {ChangeDetectionStrategy, Component, Input, input} from '@angular/core';
22

33
@Component({
44
selector: 'greet',
@@ -8,8 +8,8 @@ import {ChangeDetectionStrategy, Component, Input, ɵinput} from '@angular/core'
88
changeDetection: ChangeDetectionStrategy.OnPush,
99
})
1010
export class GreetComponent {
11-
firstName = ɵinput.required<string>();
12-
lastName = ɵinput(undefined, {
11+
firstName = input.required<string>();
12+
lastName = input(undefined, {
1313
transform: (v?: string) => v === undefined ? 'transformed-fallback' : `ng-${v}`,
1414
});
1515

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"@actions/core": "^1.10.0",
152152
"@angular-devkit/architect-cli": "^0.1701.0-rc",
153153
"@angular/animations": "^17.1.0-next",
154-
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#6e00fd0afb199cc491e800986970ee6df67d8226",
154+
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c85318b6d0157568391df9c84ddbe80933315bd",
155155
"@angular/docs": "https://github.com/angular/dev-infra-private-docs-builds.git#95e269cfa6eaee59bea8ce83dedf7c5dc7e9b55c",
156156
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#0515382207bd7112504e41e758878c0aaa911f3c",
157157
"@babel/helper-remap-async-to-generator": "^7.18.9",

packages/core/src/authoring.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
// Note: `input` is exported in `core.ts` due to:
1010
// https://docs.google.com/document/d/1RXb1wYwsbJotO1KBgSDsAtKpduGmIHod9ADxuXcAvV4/edit?tab=t.0.
1111

12-
export {InputFunction as ɵInputFunction, inputFunction as ɵinputFunctionForApiGuard, inputRequiredFunction as ɵinputFunctionRequiredForApiGuard} from './authoring/input';
13-
export {InputOptions as ɵInputOptions, InputOptionsWithoutTransform as ɵInputOptionsWithoutTransform, InputOptionsWithTransform as ɵInputOptionsWithTransform, InputSignal, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from './authoring/input_signal';
12+
export {InputFunction} from './authoring/input';
13+
export {InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from './authoring/input_signal';
1414
export {ɵUnwrapDirectiveSignalInputs} from './authoring/input_type_checking';

packages/core/src/authoring/input.ts

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,28 @@
99
import {createInputSignal, InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal} from './input_signal';
1010
import {REQUIRED_UNSET_VALUE} from './input_signal_node';
1111

12-
/**
13-
* Initializes an input with an initial value. If no explicit value
14-
* is specified, Angular will use `undefined`.
15-
*
16-
* Consider using `input.required` for inputs that don't need an
17-
* initial value.
18-
*
19-
* @usageNotes
20-
* Initialize an input in your directive or component by declaring a
21-
* class field and initializing it with the `input()` function.
22-
*
23-
* ```ts
24-
* @Directive({..})
25-
* export class MyDir {
26-
* firstName = input<string>(); // string|undefined
27-
* lastName = input.required<string>(); // string
28-
* age = input(0); // number
29-
* }
30-
* ```
31-
*/
32-
export function inputFunction<ReadT>(): InputSignal<ReadT|undefined>;
33-
export function inputFunction<ReadT>(
34-
initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
35-
export function inputFunction<ReadT, WriteT>(
36-
initialValue: ReadT,
37-
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
3812
export function inputFunction<ReadT, WriteT>(
3913
initialValue?: ReadT,
4014
opts?: InputOptions<ReadT, WriteT>): InputSignal<ReadT|undefined, WriteT> {
4115
return createInputSignal(initialValue, opts);
4216
}
4317

18+
export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
19+
InputSignal<ReadT, WriteT> {
20+
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
21+
}
22+
4423
/**
45-
* Initializes a required input. Users of your directive/component,
46-
* need to bind to this input, otherwise they will see errors.
47-
* *
24+
* The `input` function allows declaration of inputs in directives and
25+
* components.
26+
*
27+
* The function exposes an API for also declaring required inputs via the
28+
* `input.required` function.
29+
*
4830
* @usageNotes
4931
* Initialize an input in your directive or component by declaring a
50-
* class field and initializing it with the `input()` function.
32+
* class field and initializing it with the `input()` or `input.required()`
33+
* function.
5134
*
5235
* ```ts
5336
* @Directive({..})
@@ -57,25 +40,42 @@ export function inputFunction<ReadT, WriteT>(
5740
* age = input(0); // number
5841
* }
5942
* ```
43+
*
44+
* @developerPreview
6045
*/
61-
export function inputRequiredFunction<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>):
62-
InputSignal<ReadT>;
63-
export function inputRequiredFunction<ReadT, WriteT>(
64-
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
65-
export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
66-
InputSignal<ReadT, WriteT> {
67-
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
46+
export interface InputFunction {
47+
/**
48+
* Initializes an input with an initial value. If no explicit value
49+
* is specified, Angular will use `undefined`.
50+
*
51+
* Consider using `input.required` for inputs that don't need an
52+
* initial value.
53+
*
54+
* @developerPreview
55+
*/
56+
<ReadT>(): InputSignal<ReadT|undefined>;
57+
<ReadT>(initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
58+
<ReadT, WriteT>(initialValue: ReadT, opts: InputOptionsWithTransform<ReadT, WriteT>):
59+
InputSignal<ReadT, WriteT>;
60+
61+
/**
62+
* Initializes a required input.
63+
*
64+
* Users of your directive/component need to bind to this
65+
* input. If unset, a compile time error will be reported.
66+
*
67+
* @developerPreview
68+
*/
69+
required: {
70+
<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
71+
<ReadT, WriteT>(opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
72+
};
6873
}
6974

7075
/**
71-
* Type of the `input` function.
76+
* The `input` function allows declaration of inputs in directives and
77+
* components.
7278
*
73-
* The input function is a special function that also provides access to
74-
* required inputs via the `.required` property.
75-
*/
76-
export type InputFunction = typeof inputFunction&{required: typeof inputRequiredFunction};
77-
78-
/**
7979
* Initializes an input with an initial value. If no explicit value
8080
* is specified, Angular will use `undefined`.
8181
*
@@ -94,11 +94,13 @@ export type InputFunction = typeof inputFunction&{required: typeof inputRequired
9494
* age = input(0); // number
9595
* }
9696
* ```
97+
*
98+
* @developerPreview
9799
*/
98100
export const input: InputFunction = (() => {
99101
// Note: This may be considered a side-effect, but nothing will depend on
100102
// this assignment, unless this `input` constant export is accessed. It's a
101103
// self-contained side effect that is local to the user facing`input` export.
102104
(inputFunction as any).required = inputRequiredFunction;
103-
return inputFunction as InputFunction;
105+
return inputFunction as (typeof inputFunction&{required: typeof inputRequiredFunction});
104106
})();

packages/core/src/authoring/input_signal.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {Signal} from '../render3/reactivity/api';
1414
import {INPUT_SIGNAL_NODE, InputSignalNode, REQUIRED_UNSET_VALUE} from './input_signal_node';
1515

1616
/**
17+
* @developerPreview
18+
*
1719
* Options for signal inputs.
1820
*/
1921
export interface InputOptions<ReadT, WriteT> {
@@ -32,11 +34,19 @@ export interface InputOptions<ReadT, WriteT> {
3234
transform?: (v: WriteT) => ReadT;
3335
}
3436

35-
/** Signal input options without the transform option. */
37+
/**
38+
* Signal input options without the transform option.
39+
*
40+
* @developerPreview
41+
*/
3642
export type InputOptionsWithoutTransform<ReadT> =
3743
// Note: We still keep a notion of `transform` for auto-completion.
3844
Omit<InputOptions<ReadT, ReadT>, 'transform'>&{transform?: undefined};
39-
/** Signal input options with the transform option required. */
45+
/**
46+
* Signal input options with the transform option required.
47+
*
48+
* @developerPreview
49+
*/
4050
export type InputOptionsWithTransform<ReadT, WriteT> =
4151
Required<Pick<InputOptions<ReadT, WriteT>, 'transform'>>&InputOptions<ReadT, WriteT>;
4252

@@ -49,11 +59,13 @@ export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE = /* @__PURE__ */ Symbol();
4959
* An input signal is similar to a non-writable signal except that it also
5060
* carries additional type-information for transforms, and that Angular internally
5161
* updates the signal whenever a new value is bound.
62+
*
63+
* @developerPreview
5264
*/
5365
export interface InputSignal<ReadT, WriteT = ReadT> extends Signal<ReadT> {
66+
[SIGNAL]: InputSignalNode<ReadT, WriteT>;
5467
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
5568
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
56-
[SIGNAL]: InputSignalNode<ReadT, WriteT>;
5769
}
5870

5971
/**

packages/core/src/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export * from './authoring';
1616
// Input is exported separately as this file is exempted from JSCompiler's
1717
// conformance requirement for inferred const exports.
1818
// See: https://docs.google.com/document/d/1RXb1wYwsbJotO1KBgSDsAtKpduGmIHod9ADxuXcAvV4/edit?tab=t.0
19-
export {input as ɵinput} from './authoring/input';
19+
export {input} from './authoring/input';
2020

2121
export * from './metadata';
2222
export * from './version';

packages/core/test/acceptance/input_signals/signal_inputs_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, computed, Directive, effect, ɵinput as input} from '@angular/core';
9+
import {Component, computed, Directive, effect, input} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
1111

1212
describe('signal inputs', () => {

packages/core/test/authoring/input_signal_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, computed, effect, ɵinput as input} from '@angular/core';
9+
import {Component, computed, effect, input} from '@angular/core';
1010
import {SIGNAL} from '@angular/core/primitives/signals';
1111
import {TestBed} from '@angular/core/testing';
1212

packages/core/test/playground/zone-signal-input/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, EventEmitter, Input, Output, signal, ɵinput as input} from '@angular/core';
9+
import {Component, EventEmitter, Input, input, Output, signal} from '@angular/core';
1010
import {bootstrapApplication} from '@angular/platform-browser';
1111

1212
@Component({

0 commit comments

Comments
 (0)