Skip to content

Commit

Permalink
refactor(core): introduce runtime InputSignal implementation (angul…
Browse files Browse the repository at this point in the history
…ar#53571)

This commit introduces the runtime `InputSignal` implementation.
Input initializers using `input` or `input.required` will result in
an instance of `InputSignal` to be created.

An input signal extends the signal primtive, with a couple of small
differences:

 - it's a readonly signal. There is no public `set` or `update`.
 - equality is non-configurable. As per CD semantics, the value is
   guaranteed to be different when the `property` instruction attempts
   to update an input signal.
 - we support a `transform` function, that allows transforming input
   values. The transform is called whenever the input is set. An
   alternative could have been to follow computed-semantics and call the
   transform upon accessing, if dirty.

In the future, we might change this to extend the computed reactive
node, so that we can support computed inputs that do not rely on
continious bound value assignments. See signal based components RFC.

PR Close angular#53571
  • Loading branch information
devversion authored and amilamen committed Jan 26, 2024
1 parent 75646da commit f21657f
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 10 deletions.
9 changes: 7 additions & 2 deletions goldens/public-api/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { Observable } from 'rxjs';
import { SIGNAL } from '@angular/core/primitives/signals';
import { SignalNode } from '@angular/core/primitives/signals';
import { Subject } from 'rxjs';
import { Subscription } from 'rxjs';

Expand Down Expand Up @@ -874,10 +875,14 @@ export interface InputDecorator {
}

// @public
export type InputSignal<ReadT, WriteT = ReadT> = Signal<ReadT> & {
export interface InputSignal<ReadT, WriteT = ReadT> extends Signal<ReadT> {
// (undocumented)
INPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
// (undocumented)
INPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
};
// (undocumented)
[SIGNAL]: InputSignalNode<ReadT, WriteT>;
}

// @public
export function isDevMode(): boolean;
Expand Down
13 changes: 7 additions & 6 deletions packages/core/src/authoring/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/

import {InputOptions, InputOptionsWithoutTransform, InputOptionsWithTransform, InputSignal} from './input_signal';
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
Expand Down Expand Up @@ -35,9 +36,9 @@ 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> {
throw new Error('TODO');
initialValue?: ReadT,
opts?: InputOptions<ReadT, WriteT>): InputSignal<ReadT|undefined, WriteT> {
return createInputSignal(initialValue, opts);
}

/**
Expand All @@ -61,9 +62,9 @@ export function inputRequiredFunction<ReadT>(opts?: InputOptionsWithoutTransform
InputSignal<ReadT>;
export function inputRequiredFunction<ReadT, WriteT>(
opts: InputOptionsWithTransform<ReadT, WriteT>): InputSignal<ReadT, WriteT>;
export function inputRequiredFunction<ReadT, WriteT>(_opts?: InputOptions<ReadT, WriteT>):
export function inputRequiredFunction<ReadT, WriteT>(opts?: InputOptions<ReadT, WriteT>):
InputSignal<ReadT, WriteT> {
throw new Error('TODO');
return createInputSignal(REQUIRED_UNSET_VALUE as never, opts);
}

/**
Expand Down
42 changes: 40 additions & 2 deletions packages/core/src/authoring/input_signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/

import {producerAccessed, SIGNAL} from '@angular/core/primitives/signals';

import {Signal} from '../render3/reactivity/api';

import {INPUT_SIGNAL_NODE, InputSignalNode, REQUIRED_UNSET_VALUE} from './input_signal_node';

/**
* Options for signal inputs.
*/
Expand Down Expand Up @@ -45,7 +49,41 @@ export const ɵINPUT_SIGNAL_BRAND_WRITE_TYPE = /* @__PURE__ */ Symbol();
* carries additional type-information for transforms, and that Angular internally
* updates the signal whenever a new value is bound.
*/
export type InputSignal<ReadT, WriteT = ReadT> = Signal<ReadT>&{
export interface InputSignal<ReadT, WriteT = ReadT> extends Signal<ReadT> {
[ɵINPUT_SIGNAL_BRAND_READ_TYPE]: ReadT;
[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]: WriteT;
};
[SIGNAL]: InputSignalNode<ReadT, WriteT>;
}

/**
* Creates an input signal.
*
* @param initialValue The initial value.
* 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>): InputSignal<ReadT, WriteT> {
const node: InputSignalNode<ReadT, WriteT> = Object.create(INPUT_SIGNAL_NODE);

node.value = initialValue;

// Perf note: Always set `transformFn` here to ensure that `node` always
// has the same v8 class shape, allowing monomorphic reads on input signals.
node.transformFn = options?.transform;

function inputValueFn() {
// Record that someone looked at this signal.
producerAccessed(node);

if (node.value === REQUIRED_UNSET_VALUE) {
// TODO: Use a runtime error w/ error guide.
throw new Error('Input is required, but no value set yet.');
}

return node.value;
}

(inputValueFn as any)[SIGNAL] = node;
return inputValueFn as InputSignal<ReadT, WriteT>;
}
48 changes: 48 additions & 0 deletions packages/core/src/authoring/input_signal_node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @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 {SIGNAL_NODE, SignalNode, signalSetFn} from '@angular/core/primitives/signals';

export const REQUIRED_UNSET_VALUE = /* @__PURE__ */ Symbol('InputSignalNode#UNSET');

/**
* 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|typeof REQUIRED_UNSET_VALUE> {
/**
* User-configured transform that will run whenever a new value is applied
* to the input signal node.
*/
transformFn: ((value: WriteT) => ReadT)|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.
*/
applyValueToInputSignal<ReadT, WriteT>(node: InputSignalNode<ReadT, WriteT>, value: ReadT): void;
}

// Note: Using an IIFE here to ensure that the spread assignment is not considered
// a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
// TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
export const INPUT_SIGNAL_NODE: InputSignalNode<unknown, unknown> = /* @__PURE__ */ (() => {
return {
...SIGNAL_NODE,
transformFn: undefined,

applyValueToInputSignal<ReadT, WriteT>(node: InputSignalNode<ReadT, WriteT>, value: ReadT) {
signalSetFn(node, value);
}
};
})();
3 changes: 3 additions & 0 deletions packages/core/test/bundling/defer/bundle.golden_symbols.json
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,9 @@
{
"name": "init_input_signal"
},
{
"name": "init_input_signal_node"
},
{
"name": "init_input_transforms_feature"
},
Expand Down

0 comments on commit f21657f

Please sign in to comment.