Skip to content

Commit

Permalink
feat(core): expose new input API for signal-based inputs (#53872)
Browse files Browse the repository at this point in the history
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 #53872
  • Loading branch information
devversion authored and atscott committed Jan 10, 2024
1 parent b2066d4 commit 863be4b
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 469 deletions.
45 changes: 30 additions & 15 deletions goldens/public-api/core/index.md
Expand Up @@ -867,13 +867,43 @@ export interface Input {
// @public (undocumented)
export const Input: InputDecorator;

// @public
export const input: InputFunction;

// @public (undocumented)
export interface InputDecorator {
(arg?: string | Input): any;
// (undocumented)
new (arg?: string | Input): any;
}

// @public
export interface InputFunction {
<ReadT>(): InputSignal<ReadT | undefined>;
// (undocumented)
<ReadT>(initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
// (undocumented)
<ReadT, WriteT>(initialValue: ReadT, opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
required: {
<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
<ReadT, WriteT>(opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
};
}

// @public
export interface InputOptions<ReadT, WriteT> {
alias?: string;
transform?: (v: WriteT) => ReadT;
}

// @public
export type InputOptionsWithoutTransform<ReadT> = Omit<InputOptions<ReadT, ReadT>, 'transform'> & {
transform?: undefined;
};

// @public
export type InputOptionsWithTransform<ReadT, WriteT> = Required<Pick<InputOptions<ReadT, WriteT>, 'transform'>> & InputOptions<ReadT, WriteT>;

// @public
export interface InputSignal<ReadT, WriteT = ReadT> extends Signal<ReadT> {
// (undocumented)
Expand Down Expand Up @@ -1647,21 +1677,6 @@ export interface WritableSignal<T> extends Signal<T> {
update(updateFn: (value: T) => T): void;
}

// @public
export function ɵinputFunctionForApiGuard<ReadT>(): InputSignal<ReadT | undefined>;

// @public (undocumented)
export function ɵinputFunctionForApiGuard<ReadT>(initialValue: ReadT, opts?: ɵInputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;

// @public (undocumented)
export function ɵinputFunctionForApiGuard<ReadT, WriteT>(initialValue: ReadT, opts: ɵInputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;

// @public
export function ɵinputFunctionRequiredForApiGuard<ReadT>(opts?: ɵInputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;

// @public (undocumented)
export function ɵinputFunctionRequiredForApiGuard<ReadT, WriteT>(opts: ɵInputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;

// (No @packageDocumentation comment for this package)

```
6 changes: 3 additions & 3 deletions integration/cli-signal-inputs/src/app/greet.component.ts
@@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, Component, Input, ɵinput} from '@angular/core';
import {ChangeDetectionStrategy, Component, Input, input} from '@angular/core';

@Component({
selector: 'greet',
Expand All @@ -8,8 +8,8 @@ import {ChangeDetectionStrategy, Component, Input, ɵinput} from '@angular/core'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GreetComponent {
firstName = ɵinput.required<string>();
lastName = ɵinput(undefined, {
firstName = input.required<string>();
lastName = input(undefined, {
transform: (v?: string) => v === undefined ? 'transformed-fallback' : `ng-${v}`,
});

Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -151,7 +151,7 @@
"@actions/core": "^1.10.0",
"@angular-devkit/architect-cli": "^0.1701.0-rc",
"@angular/animations": "^17.1.0-next",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#6e00fd0afb199cc491e800986970ee6df67d8226",
"@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#7c85318b6d0157568391df9c84ddbe80933315bd",
"@angular/docs": "https://github.com/angular/dev-infra-private-docs-builds.git#95e269cfa6eaee59bea8ce83dedf7c5dc7e9b55c",
"@angular/ng-dev": "https://github.com/angular/dev-infra-private-ng-dev-builds.git#0515382207bd7112504e41e758878c0aaa911f3c",
"@babel/helper-remap-async-to-generator": "^7.18.9",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/authoring.ts
Expand Up @@ -9,6 +9,6 @@
// Note: `input` is exported in `core.ts` due to:
// https://docs.google.com/document/d/1RXb1wYwsbJotO1KBgSDsAtKpduGmIHod9ADxuXcAvV4/edit?tab=t.0.

export {InputFunction as ɵInputFunction, inputFunction as ɵinputFunctionForApiGuard, inputRequiredFunction as ɵinputFunctionRequiredForApiGuard} from './authoring/input';
export {InputOptions as ɵInputOptions, InputOptionsWithoutTransform as ɵInputOptionsWithoutTransform, InputOptionsWithTransform as ɵInputOptionsWithTransform, InputSignal, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from './authoring/input_signal';
export {InputFunction} from './authoring/input';
export {InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE} from './authoring/input_signal';
export {ɵUnwrapDirectiveSignalInputs} from './authoring/input_type_checking';
92 changes: 47 additions & 45 deletions packages/core/src/authoring/input.ts
Expand Up @@ -9,45 +9,28 @@
import {createInputSignal, InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal} from './input_signal';
import {REQUIRED_UNSET_VALUE} from './input_signal_node';

/**
* Initializes an input with an initial value. If no explicit value
* is specified, Angular will use `undefined`.
*
* Consider using `input.required` for inputs that don't need an
* initial value.
*
* @usageNotes
* Initialize an input in your directive or component by declaring a
* class field and initializing it with the `input()` function.
*
* ```ts
* @Directive({..})
* export class MyDir {
* firstName = input<string>(); // string|undefined
* lastName = input.required<string>(); // string
* age = input(0); // number
* }
* ```
*/
export function inputFunction<ReadT>(): InputSignal<ReadT|undefined>;
export function inputFunction<ReadT>(
initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
export function inputFunction<ReadT, WriteT>(
initialValue: ReadT,
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
export function inputFunction<ReadT, WriteT>(
initialValue?: ReadT,
opts?: InputOptions<ReadT, WriteT>): InputSignal<ReadT|undefined, WriteT> {
return createInputSignal(initialValue, opts);
}

export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
InputSignal<ReadT, WriteT> {
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
}

/**
* Initializes a required input. Users of your directive/component,
* need to bind to this input, otherwise they will see errors.
* *
* The `input` function allows declaration of inputs in directives and
* components.
*
* The function exposes an API for also declaring required inputs via the
* `input.required` function.
*
* @usageNotes
* Initialize an input in your directive or component by declaring a
* class field and initializing it with the `input()` function.
* class field and initializing it with the `input()` or `input.required()`
* function.
*
* ```ts
* @Directive({..})
Expand All @@ -57,25 +40,42 @@ export function inputFunction<ReadT, WriteT>(
* age = input(0); // number
* }
* ```
*
* @developerPreview
*/
export function inputRequiredFunction<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>):
InputSignal<ReadT>;
export function inputRequiredFunction<ReadT, WriteT>(
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
InputSignal<ReadT, WriteT> {
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
export interface InputFunction {
/**
* Initializes an input with an initial value. If no explicit value
* is specified, Angular will use `undefined`.
*
* Consider using `input.required` for inputs that don't need an
* initial value.
*
* @developerPreview
*/
<ReadT>(): InputSignal<ReadT|undefined>;
<ReadT>(initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
<ReadT, WriteT>(initialValue: ReadT, opts: InputOptionsWithTransform<ReadT, WriteT>):
InputSignal<ReadT, WriteT>;

/**
* Initializes a required input.
*
* Users of your directive/component need to bind to this
* input. If unset, a compile time error will be reported.
*
* @developerPreview
*/
required: {
<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
<ReadT, WriteT>(opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
};
}

/**
* Type of the `input` function.
* The `input` function allows declaration of inputs in directives and
* components.
*
* The input function is a special function that also provides access to
* required inputs via the `.required` property.
*/
export type InputFunction = typeof inputFunction&{required: typeof inputRequiredFunction};

/**
* Initializes an input with an initial value. If no explicit value
* is specified, Angular will use `undefined`.
*
Expand All @@ -94,11 +94,13 @@ export type InputFunction = typeof inputFunction&{required: typeof inputRequired
* age = input(0); // number
* }
* ```
*
* @developerPreview
*/
export const input: InputFunction = (() => {
// Note: This may be considered a side-effect, but nothing will depend on
// this assignment, unless this `input` constant export is accessed. It's a
// self-contained side effect that is local to the user facing`input` export.
(inputFunction as any).required = inputRequiredFunction;
return inputFunction as InputFunction;
return inputFunction as (typeof inputFunction&{required: typeof inputRequiredFunction});
})();
18 changes: 15 additions & 3 deletions packages/core/src/authoring/input_signal.ts
Expand Up @@ -14,6 +14,8 @@ import {Signal} from '../render3/reactivity/api';
import {INPUT_SIGNAL_NODE, InputSignalNode, REQUIRED_UNSET_VALUE} from './input_signal_node';

/**
* @developerPreview
*
* Options for signal inputs.
*/
export interface InputOptions<ReadT, WriteT> {
Expand All @@ -32,11 +34,19 @@ export interface InputOptions<ReadT, WriteT> {
transform?: (v: WriteT) => ReadT;
}

/** Signal input options without the transform option. */
/**
* Signal input options without the transform option.
*
* @developerPreview
*/
export type InputOptionsWithoutTransform<ReadT> =
// Note: We still keep a notion of `transform` for auto-completion.
Omit<InputOptions<ReadT, ReadT>, 'transform'>&{transform?: undefined};
/** Signal input options with the transform option required. */
/**
* Signal input options with the transform option required.
*
* @developerPreview
*/
export type InputOptionsWithTransform<ReadT, WriteT> =
Required<Pick<InputOptions<ReadT, WriteT>, 'transform'>>&InputOptions<ReadT, WriteT>;

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

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/core.ts
Expand Up @@ -16,7 +16,7 @@ export * from './authoring';
// Input is exported separately as this file is exempted from JSCompiler's
// conformance requirement for inferred const exports.
// See: https://docs.google.com/document/d/1RXb1wYwsbJotO1KBgSDsAtKpduGmIHod9ADxuXcAvV4/edit?tab=t.0
export {input as ɵinput} from './authoring/input';
export {input} from './authoring/input';

export * from './metadata';
export * from './version';
Expand Down
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

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

describe('signal inputs', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/authoring/input_signal_spec.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Component, computed, effect, ɵinput as input} from '@angular/core';
import {Component, computed, effect, input} from '@angular/core';
import {SIGNAL} from '@angular/core/primitives/signals';
import {TestBed} from '@angular/core/testing';

Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/playground/zone-signal-input/index.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

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

@Component({
Expand Down

0 comments on commit 863be4b

Please sign in to comment.