Skip to content

Commit

Permalink
refactor(core): improve API documentation for input after angular.d…
Browse files Browse the repository at this point in the history
…ev support (angular#54925)

This commit improves the API documentation for `input` after
we added support for initializer APIs in angular.dev docs generation.

Changes:

- Rename `ReadT` to `T`. This conceptually makes it easy to talk about
  inputs of type `T` if there is no transform involved. The common case.
- Rename `WriteT` to `TransformT`. This makes it clear that this is the
  type that the "transform" needs to handle.
- Improves the "overall" description of the input function so that it
  can be shown as a general overview for the API site.
- Improves usage notes to be a little more helpful, yielding more useful
  content in the API docs usage notes section.
- Add short JSDoc description for each individual overload.

PR Close angular#54925
  • Loading branch information
devversion authored and ilirbeqirii committed Apr 6, 2024
1 parent e3061ba commit 4ea4afb
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 84 deletions.
36 changes: 18 additions & 18 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -937,43 +937,41 @@ export interface InputDecorator {

// @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>): InputSignalWithTransform<ReadT, WriteT>;
<T>(): InputSignal<T | undefined>;
<T>(initialValue: T, opts?: InputOptionsWithoutTransform<T>): InputSignal<T>;
<T, TransformT>(initialValue: T, opts: InputOptionsWithTransform<T, TransformT>): InputSignalWithTransform<T, TransformT>;
required: {
<ReadT>(opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
<ReadT, WriteT>(opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignalWithTransform<ReadT, WriteT>;
<T>(opts?: InputOptionsWithoutTransform<T>): InputSignal<T>;
<T, TransformT>(opts: InputOptionsWithTransform<T, TransformT>): InputSignalWithTransform<T, TransformT>;
};
}

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

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

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

// @public
export interface InputSignal<ReadT> extends InputSignalWithTransform<ReadT, ReadT> {
export interface InputSignal<T> extends InputSignalWithTransform<T, T> {
}

// @public
export interface InputSignalWithTransform<ReadT, WriteT> extends Signal<ReadT> {
export interface InputSignalWithTransform<T, TransformT> extends Signal<T> {
// (undocumented)
INPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
INPUT_SIGNAL_BRAND_READ_TYPE]: T;
// (undocumented)
INPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
INPUT_SIGNAL_BRAND_WRITE_TYPE]: TransformT;
// (undocumented)
[SIGNAL]: InputSignalNode<ReadT, WriteT>;
[SIGNAL]: InputSignalNode<T, TransformT>;
}

// @public
Expand Down Expand Up @@ -1101,9 +1099,11 @@ export const model: ModelFunction;
// @public
export interface ModelFunction {
<T>(): ModelSignal<T | undefined>;
// (undocumented)
<T>(initialValue: T, opts?: ModelOptions): ModelSignal<T>;
required<T>(opts?: ModelOptions): ModelSignal<T>;
// (undocumented)
required: {
<T>(opts?: ModelOptions): ModelSignal<T>;
};
}

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ class TcbDirectiveInputsOp extends TcbOp {
// For signal inputs, a `transformType` will never be set as we do not capture
// the transform in the compiler metadata. Signal inputs incorporate their
// transform write type into their member type, and we extract it below when
// unwrapping the `InputSignal<ReadT, WriteT>`.
// setting the `WriteT` of such `InputSignalWithTransform<_, WriteT>`.

if (this.dir.coercedInputFields.has(fieldName)) {
let type: ts.TypeNode;
Expand Down
104 changes: 61 additions & 43 deletions packages/core/src/authoring/input/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function inputFunction<ReadT, WriteT>(
return createInputSignal(initialValue, opts);
}

export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
export function inputRequiredFunction<ReadT, WriteT = ReadT>(opts?: InputOptions<ReadT, WriteT>):
InputSignalWithTransform<ReadT, WriteT> {
ngDevMode && assertInInjectionContext(input);
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
Expand All @@ -31,77 +31,95 @@ export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT,
* 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()` or `input.required()`
* function.
*
* ```ts
* @Directive({..})
* export class MyDir {
* firstName = input<string>(); // string|undefined
* lastName = input.required<string>(); // string
* age = input(0); // number
* }
* ```
*
* @developerPreview
*/
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.
* Initializes an input of type `T` with an initial value of `undefined`.
* Angular will implicitly use `undefined` as initial value.
*/
<T>(): InputSignal<T|undefined>;
/** Declares an input of type `T` with an explicit initial value. */
<T>(initialValue: T, opts?: InputOptionsWithoutTransform<T>): InputSignal<T>;
/**
* Declares an input of type `T` with an initial value and a transform
* function.
*
* @developerPreview
* The input accepts values of type `TransformT` and the given
* transform function will transform the value to type `T`.
*/
<ReadT>(): InputSignal<ReadT|undefined>;
<ReadT>(initialValue: ReadT, opts?: InputOptionsWithoutTransform<ReadT>): InputSignal<ReadT>;
<ReadT, WriteT>(initialValue: ReadT, opts: InputOptionsWithTransform<ReadT, WriteT>):
InputSignalWithTransform<ReadT, WriteT>;
<T, TransformT>(initialValue: T, opts: InputOptionsWithTransform<T, TransformT>):
InputSignalWithTransform<T, TransformT>;

/**
* Initializes a required input.
*
* Users of your directive/component need to bind to this
* Consumers 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>):
InputSignalWithTransform<ReadT, WriteT>;
/** Declares a required input of type `T`. */
<T>(opts?: InputOptionsWithoutTransform<T>): InputSignal<T>;
/**
* Declares a required input of type `T` with a transform function.
*
* The input accepts values of type `TransformT` and the given
* transform function will transform the value to type `T`.
*/
<T, TransformT>(opts: InputOptionsWithTransform<T, TransformT>):
InputSignalWithTransform<T, TransformT>;
};
}

/**
* The `input` function allows declaration of inputs in directives and
* components.
* The `input` function allows declaration of Angular inputs in directives
* and components.
*
* There are two variants of inputs that can be declared:
*
* Initializes an input with an initial value. If no explicit value
* is specified, Angular will use `undefined`.
* 1. **Optional inputs** with an initial value.
* 2. **Required inputs** that consumers need to set.
*
* Consider using `input.required` for inputs that don't need an
* initial value.
* By default, the `input` function will declare optional inputs that
* always have an initial value. Required inputs can be declared
* using the `input.required()` function.
*
* Inputs are signals. The values of an input are exposed as a `Signal`.
* The signal always holds the latest value of the input that is bound
* from the parent.
*
* @usageNotes
* Initialize an input in your directive or component by declaring a
* class field and initializing it with the `input()` function.
* To use signal-based inputs, import `input` from `@angular/core`.
*
* ```
* import {input} from '@angular/core`;
* ```
*
* Inside your component, introduce a new class member and initialize
* it with a call to `input` or `input.required`.
*
* ```ts
* @Directive({..})
* export class MyDir {
* firstName = input<string>(); // string|undefined
* lastName = input.required<string>(); // string
* age = input(0); // number
* @Component({
* ...
* })
* export class UserProfileComponent {
* firstName = input<string>(); // Signal<string|undefined>
* lastName = input.required<string>(); // Signal<string>
* age = input(0) // Signal<number>
* }
* ```
*
* Inside your component template, you can display values of the inputs
* by calling the signal.
*
* ```html
* <span>{{firstName()}}</span>
* ```
*
* @developerPreview
* @initializerApiFunction
*/
export const input: InputFunction = (() => {
// Note: This may be considered a side-effect, but nothing will depend on
Expand Down
32 changes: 16 additions & 16 deletions packages/core/src/authoring/input/input_signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {INPUT_SIGNAL_NODE, InputSignalNode, REQUIRED_UNSET_VALUE} from './input_
*
* Options for signal inputs.
*/
export interface InputOptions<ReadT, WriteT> {
export interface InputOptions<T, TransformT> {
/** Optional public name for the input. By default, the class field name is used. */
alias?: string;
/**
Expand All @@ -31,24 +31,24 @@ export interface InputOptions<ReadT, WriteT> {
* attribute form to bind to the input via `<my-dir input>`. A transform can then
* handle such string values and convert them to `boolean`. See: {@link booleanAttribute}.
*/
transform?: (v: WriteT) => ReadT;
transform?: (v: TransformT) => T;
}

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

export const ɵINPUT_SIGNAL_BRAND_READ_TYPE = /* @__PURE__ */ Symbol();
export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE = /* @__PURE__ */ Symbol();
Expand Down Expand Up @@ -77,10 +77,10 @@ export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE = /* @__PURE__ */ Symbol();
*
* @developerPreview
*/
export interface InputSignalWithTransform<ReadT, WriteT> extends Signal<ReadT> {
[SIGNAL]: InputSignalNode<ReadT, WriteT>;
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
export interface InputSignalWithTransform<T, TransformT> extends Signal<T> {
[SIGNAL]: InputSignalNode<T, TransformT>;
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: T;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: TransformT;
}

/**
Expand All @@ -94,7 +94,7 @@ export interface InputSignalWithTransform<ReadT, WriteT> extends Signal<ReadT> {
*
* @developerPreview
*/
export interface InputSignal<ReadT> extends InputSignalWithTransform<ReadT, ReadT> {}
export interface InputSignal<T> extends InputSignalWithTransform<T, T> {}

/**
* Creates an input signal.
Expand All @@ -103,10 +103,10 @@ export interface InputSignal<ReadT> extends InputSignalWithTransform<ReadT, Read
* Can be set to {@link REQUIRED_UNSET_VALUE} for required inputs.
* @param options Additional options for the input. e.g. a transform, or an alias.
*/
export function createInputSignal<ReadT, WriteT>(
initialValue: ReadT,
options?: InputOptions<ReadT, WriteT>): InputSignalWithTransform<ReadT, WriteT> {
const node: InputSignalNode<ReadT, WriteT> = Object.create(INPUT_SIGNAL_NODE);
export function createInputSignal<T, TransformT>(
initialValue: T,
options?: InputOptions<T, TransformT>): InputSignalWithTransform<T, TransformT> {
const node: InputSignalNode<T, TransformT> = Object.create(INPUT_SIGNAL_NODE);

node.value = initialValue;

Expand All @@ -133,5 +133,5 @@ export function createInputSignal<ReadT, WriteT>(
inputValueFn.toString = () => `[Input Signal: ${inputValueFn()}]`;
}

return inputValueFn as InputSignalWithTransform<ReadT, WriteT>;
return inputValueFn as InputSignalWithTransform<T, TransformT>;
}
10 changes: 5 additions & 5 deletions packages/core/src/authoring/input/input_signal_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@ export const REQUIRED_UNSET_VALUE = /* @__PURE__ */ Symbol('InputSignalNode#UNSE
* Reactive node type for an input signal. An input signal extends a signal.
* There are special properties to enable transforms and required inputs.
*/
export interface InputSignalNode<ReadT, WriteT> extends SignalNode<ReadT> {
export interface InputSignalNode<T, TransformT> extends SignalNode<T> {
/**
* User-configured transform that will run whenever a new value is applied
* to the input signal node.
*/
transformFn: ((value: WriteT) => ReadT)|undefined;
transformFn: ((value: TransformT) => T)|undefined;

/**
* Applies a new value to the input signal. Expects transforms to be run
* manually before.
*
* This function is called by the framework runtime code whenever a binding
* changes. The value can in practice be anything at runtime, but for typing
* purposes we assume it's a valid `ReadT` value. Type-checking will enforce that.
* purposes we assume it's a valid `T` value. Type-checking will enforce that.
*/
applyValueToInputSignal<ReadT, WriteT>(node: InputSignalNode<ReadT, WriteT>, value: ReadT): void;
applyValueToInputSignal<T, TransformT>(node: InputSignalNode<T, TransformT>, value: T): void;
}

// Note: Using an IIFE here to ensure that the spread assignment is not considered
Expand All @@ -40,7 +40,7 @@ export const INPUT_SIGNAL_NODE: InputSignalNode<unknown, unknown> = /* @__PURE__
...SIGNAL_NODE,
transformFn: undefined,

applyValueToInputSignal<ReadT, WriteT>(node: InputSignalNode<ReadT, WriteT>, value: ReadT) {
applyValueToInputSignal<T, TransformT>(node: InputSignalNode<T, TransformT>, value: T) {
signalSetFn(node, value);
}
};
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/authoring/input/input_type_checking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {InputSignalWithTransform} from './input_signal';

/** Retrieves the `WriteT` of an `InputSignal` and `InputSignalWithTransform`. */
/** Retrieves the write type of an `InputSignal` and `InputSignalWithTransform`. */
export type ɵUnwrapInputSignalWriteType<Field> =
Field extends InputSignalWithTransform<any, infer WriteT>? WriteT : never;

Expand Down

0 comments on commit 4ea4afb

Please sign in to comment.