Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1 Initial PR for signal inputs #53521

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 14 additions & 9 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,12 @@ export interface InputDecorator {
new (arg?: string | Input): any;
}

// @public
export type InputSignal<ReadT, WriteT = ReadT> = Signal<ReadT> & {
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
};

// @public
export function isDevMode(): boolean;

Expand Down Expand Up @@ -1637,20 +1643,19 @@ export interface WritableSignal<T> extends Signal<T> {
}

// @public
export function ɵɵdefineInjectable<T>(opts: {
token: unknown;
providedIn?: Type<any> | 'root' | 'platform' | 'any' | 'environment' | null;
factory: () => T;
}): unknown;
export function ɵinputFunctionForApiGuard<ReadT>(): InputSignal<ReadT | undefined>;

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

// @public (undocumented)
export function ɵɵinject<T>(token: ProviderToken<T>, flags?: InjectFlags): T | null;
export function ɵinputFunctionForApiGuard<ReadT, WriteT>(initialValue: ReadT, opts: ɵInputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;

// @public
export function ɵɵinjectAttribute(attrNameToInject: string): string | null;
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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ function toInputMapping<TExpression>(
classPropertyName: key,
required: false,
transformFunction: null,
// TODO: Handle `isSignal` information for inputs.
isSignal: false,
};
}

Expand All @@ -103,6 +105,8 @@ function toInputMapping<TExpression>(
classPropertyName: values[1].getString(),
transformFunction: values.length > 2 ? values[2].getOpaque() : null,
required: false,
// TODO: Handle `isSignal` information for inputs.
isSignal: false,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function compileInputTransformFields(inputs: ClassPropertyMapping<InputMa
const extraFields: CompileResult[] = [];

for (const input of inputs) {
// Note: Signal inputs capture their transform `WriteT` as part of the `InputSignal`.
// Such inputs will not have a `transform` captured and not generate coercion members.

if (input.transform) {
extraFields.push({
name: `ngAcceptInputType_${input.classPropertyName}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import ts from 'typescript';

import {ClassMember, ReflectionHost} from '../../../reflection';

/** Metadata describing an input declared via the `input` function. */
export interface InputMemberMetadata {
/** Node referring to the call expression. */
inputCall: ts.CallExpression;
/** Node referring to the options expression, if specified. */
optionsNode: ts.Expression|undefined;
/** Whether the input is required or not. i.e. `input.required` was used. */
isRequired: boolean;
}

/**
* Attempts to identify and parse an Angular input that is declared
* as a class member using the `input`/`input.required` functions.
*/
export function tryParseInputInitializerAndOptions(
member: ClassMember, reflector: ReflectionHost,
coreModule: string|undefined): InputMemberMetadata|null {
if (member.value === null || !ts.isCallExpression(member.value)) {
return null;
}
const call = member.value;

// Extract target. Either:
// - `[input]`
// - `core.[input]`
// - `input.[required]`
// - `core.input.[required]`.
let target = extractPropertyTarget(call.expression);
if (target === null) {
return null;
}

// Case 1: No `required`.
// TODO(signal-input-public): Clean this up.
if (target.text === 'input' || target.text === 'ɵinput') {
if (!isReferenceToInputFunction(target, coreModule, reflector)) {
return null;
}
const optionsNode: ts.Expression|undefined = call.arguments[1];
return {inputCall: call, optionsNode, isRequired: false};
}

// Case 2: Using `required.
// Ensure there is a property access to `[input].required` or `[core.input].required`.
if (target.text !== 'required' || !ts.isPropertyAccessExpression(call.expression)) {
return null;
}

const inputCall = call.expression;
target = extractPropertyTarget(inputCall.expression);
if (target === null || !isReferenceToInputFunction(target, coreModule, reflector)) {
// Ensure the call refers to the real `input` function from Angular core.
return null;
}

const optionsNode: ts.Expression|undefined = call.arguments[0];

return {
inputCall: call,
optionsNode,
isRequired: true,
};
}

/**
* Extracts the identifier property target of a expression, supporting
* one level deep property accesses.
*
* e.g. `input.required` will return `input`.
* e.g. `input` will return `input`.
*
*/
function extractPropertyTarget(node: ts.Expression): ts.Identifier|null {
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) {
return node.name;
} else if (ts.isIdentifier(node)) {
return node;
}
return null;
}

/**
* Verifies that the given identifier resolves to the `input` expression from
* Angular core.
*/
function isReferenceToInputFunction(
devversion marked this conversation as resolved.
Show resolved Hide resolved
target: ts.Identifier, coreModule: string|undefined, reflector: ReflectionHost): boolean {
const decl = reflector.getDeclarationOfIdentifier(target);
if (decl === null || !ts.isVariableDeclaration(decl.node) || decl.node.name === undefined ||
!ts.isIdentifier(decl.node.name)) {
// The initializer isn't a declared, identifier named variable declaration.
return false;
}
if (coreModule !== undefined && decl.viaModule !== coreModule) {
// The initializer is matching so far, but in the wrong module.
return false;
}
// TODO(signal-input-public): Clean this up.
return decl.node.name.text === 'input' || decl.node.name.text === 'ɵinput';
}