From bc5ddabdcb39e6ebbe2da03dc8ec49bbe26c677d Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 21 Feb 2023 13:51:38 -0800 Subject: [PATCH] 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 --- goldens/public-api/core/index.md | 37 +++++++++++++++++++ packages/core/src/core.ts | 1 + packages/core/src/core_reactivity_export.ts | 12 ++++++ .../src/core_reactivity_export_internal.ts | 9 +++++ packages/core/src/signals/index.ts | 4 +- packages/core/src/signals/src/effect.ts | 2 + .../core/test/signals/non_reactive_spec.ts | 16 ++++---- 7 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/core_reactivity_export.ts create mode 100644 packages/core/src/core_reactivity_export_internal.ts diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md index 1407b9370fb..7dbd111f332 100644 --- a/goldens/public-api/core/index.md +++ b/goldens/public-api/core/index.md @@ -262,6 +262,9 @@ export abstract class ComponentRef { abstract setInput(name: string, value: unknown): void; } +// @public +export function computed(computation: () => T, equal?: ValueEqualityFn): Signal; + // @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 { constructor(nativeElement: T); @@ -787,6 +800,9 @@ export interface InputDecorator { // @public export function isDevMode(): boolean; +// @public +export function isSignal(value: Function): value is Signal; + // @public export function isStandalone(type: Type): boolean; @@ -1300,9 +1316,24 @@ export interface SelfDecorator { new (): Self; } +// @public +export interface SettableSignal extends Signal { + 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) & { + [SIGNAL]: true; +}; + +// @public +export function signal(initialValue: T, equal?: ValueEqualityFn): SettableSignal; + // @public export class SimpleChange { constructor(previousValue: any, currentValue: any, firstChange: boolean); @@ -1421,6 +1452,12 @@ export interface TypeDecorator { export interface TypeProvider extends Type { } +// @public +export function untracked(nonReactiveReadsFn: () => T): T; + +// @public +export type ValueEqualityFn = (a: T, b: T) => boolean; + // @public export interface ValueProvider extends ValueSansProvider { multi?: boolean; diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index ac1fe45cef8..63a514e3beb 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -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'; diff --git a/packages/core/src/core_reactivity_export.ts b/packages/core/src/core_reactivity_export.ts new file mode 100644 index 00000000000..d92bbf77edb --- /dev/null +++ b/packages/core/src/core_reactivity_export.ts @@ -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'; diff --git a/packages/core/src/core_reactivity_export_internal.ts b/packages/core/src/core_reactivity_export_internal.ts new file mode 100644 index 00000000000..b617e178a16 --- /dev/null +++ b/packages/core/src/core_reactivity_export_internal.ts @@ -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'; diff --git a/packages/core/src/signals/index.ts b/packages/core/src/signals/index.ts index 10247bfe699..0bce60ccd75 100644 --- a/packages/core/src/signals/index.ts +++ b/packages/core/src/signals/index.ts @@ -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'; diff --git a/packages/core/src/signals/src/effect.ts b/packages/core/src/signals/src/effect.ts index 5bf1f396d8c..20bc418d252 100644 --- a/packages/core/src/signals/src/effect.ts +++ b/packages/core/src/signals/src/effect.ts @@ -11,6 +11,8 @@ import {Watch} from './watch'; /** * A global reactive effect, which can be manually scheduled or destroyed. + * + * @developerPreview */ export interface Effect { /** diff --git a/packages/core/test/signals/non_reactive_spec.ts b/packages/core/test/signals/non_reactive_spec.ts index 51d5515fe54..7614afa60af 100644 --- a/packages/core/test/signals/non_reactive_spec.ts +++ b/packages/core/test/signals/non_reactive_spec.ts @@ -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