feat(core): add Angular Signals to the public API (#49150)

This commit exposes `signal`, `computed`, `effect` and various helpers from
the `@angular/core` entrypoint.

These APIs are marked as `@developerPreview` and are still prototypes in
active development. Their final shapes will be subject to our internal
design reviews as well as one or more community RFCs. We're exporting them
now to allow for experimentation using 16.0.0 next and RC releases.

PR Close #49150
This commit is contained in:
Alex Rickabaugh 2023-02-21 13:51:38 -08:00
parent 5dce2a5a3a
commit bc5ddabdcb
7 changed files with 71 additions and 10 deletions

View file

@ -262,6 +262,9 @@ export abstract class ComponentRef<C> {
abstract setInput(name: string, value: unknown): void;
}
// @public
export function computed<T>(computation: () => T, equal?: ValueEqualityFn<T>): Signal<T>;
// @public
export interface ConstructorProvider extends ConstructorSansProvider {
multi?: boolean;
@ -475,6 +478,16 @@ export interface DoCheck {
ngDoCheck(): void;
}
// @public
export interface Effect {
readonly consumer: Consumer;
destroy(): void;
schedule(): void;
}
// @public
export function effect(effectFn: () => void): Effect;
// @public
export class ElementRef<T = any> {
constructor(nativeElement: T);
@ -787,6 +800,9 @@ export interface InputDecorator {
// @public
export function isDevMode(): boolean;
// @public
export function isSignal(value: Function): value is Signal<unknown>;
// @public
export function isStandalone(type: Type<unknown>): boolean;
@ -1300,9 +1316,24 @@ export interface SelfDecorator {
new (): Self;
}
// @public
export interface SettableSignal<T> extends Signal<T> {
mutate(mutatorFn: (value: T) => void): void;
set(value: T): void;
update(updateFn: (value: T) => T): void;
}
// @public
export function setTestabilityGetter(getter: GetTestability): void;
// @public
export type Signal<T> = (() => T) & {
[SIGNAL]: true;
};
// @public
export function signal<T>(initialValue: T, equal?: ValueEqualityFn<T>): SettableSignal<T>;
// @public
export class SimpleChange {
constructor(previousValue: any, currentValue: any, firstChange: boolean);
@ -1421,6 +1452,12 @@ export interface TypeDecorator {
export interface TypeProvider extends Type<any> {
}
// @public
export function untracked<T>(nonReactiveReadsFn: () => T): T;
// @public
export type ValueEqualityFn<T> = (a: T, b: T) => boolean;
// @public
export interface ValueProvider extends ValueSansProvider {
multi?: boolean;

View file

@ -34,6 +34,7 @@ export {EventEmitter} from './event_emitter';
export {ErrorHandler} from './error_handler';
export * from './core_private_export';
export * from './core_render3_private_export';
export * from './core_reactivity_export';
export {SecurityContext} from './sanitization/security';
export {Sanitizer} from './sanitization/sanitizer';
export {createNgModule, createNgModuleRef, createEnvironmentInjector} from './render3/ng_module_ref';

View file

@ -0,0 +1,12 @@
/**
* @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
*/
// This file exists to allow the set of reactivity exports to be modified in g3, as these APIs are
// only "beta" for the time being.
export * from './core_reactivity_export_internal';

View file

@ -0,0 +1,9 @@
/**
* @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
*/
export {computed, effect, Effect, isSignal, SettableSignal, Signal, signal, untracked, ValueEqualityFn} from './signals';

View file

@ -8,8 +8,8 @@
export {isSignal, Signal, ValueEqualityFn} from './src/api';
export {computed} from './src/computed';
export {effect} from './src/effect';
export {effect, Effect} from './src/effect';
export {setActiveConsumer} from './src/graph';
export {SettableSignal, signal} from './src/signal';
export {untracked as untrack} from './src/untracked';
export {untracked} from './src/untracked';
export {Watch} from './src/watch';

View file

@ -11,6 +11,8 @@ import {Watch} from './watch';
/**
* A global reactive effect, which can be manually scheduled or destroyed.
*
* @developerPreview
*/
export interface Effect {
/**

View file

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {computed, signal, untrack} from '@angular/core/src/signals';
import {computed, signal, untracked} from '@angular/core/src/signals';
import {effect, effectsDone as flush, resetEffects} from '@angular/core/src/signals/src/effect';
describe('non-reactive reads', () => {
@ -17,15 +17,15 @@ describe('non-reactive reads', () => {
it('should read the latest value from signal', () => {
const counter = signal(0);
expect(untrack(counter)).toEqual(0);
expect(untracked(counter)).toEqual(0);
counter.set(1);
expect(untrack(counter)).toEqual(1);
expect(untracked(counter)).toEqual(1);
});
it('should not add dependencies to computed when reading a value from a signal', () => {
const counter = signal(0);
const double = computed(() => untrack(counter) * 2);
const double = computed(() => untracked(counter) * 2);
expect(double()).toEqual(0);
@ -37,10 +37,10 @@ describe('non-reactive reads', () => {
const counter = signal(0);
const double = computed(() => counter() * 2);
expect(untrack(double)).toEqual(0);
expect(untracked(double)).toEqual(0);
counter.set(2);
expect(untrack(double)).toEqual(4);
expect(untracked(double)).toEqual(4);
});
it('should not make surrounding effect depend on the signal', async () => {
@ -48,7 +48,7 @@ describe('non-reactive reads', () => {
const runLog: number[] = [];
effect(() => {
runLog.push(untrack(s));
runLog.push(untracked(s));
});
// an effect will run at least once
@ -89,7 +89,7 @@ describe('non-reactive reads', () => {
let runLog: string[] = [];
const effectRef = effect(() => {
untrack(() => runLog.push(`${first()} ${last()}`));
untracked(() => runLog.push(`${first()} ${last()}`));
});
// effects run at least once