Skip to content

Commit a220e66

Browse files
committed
refactor(core): implement upgradeSignalToWritable utility
This function implements a common pattern/building block seen in other signal APIs, where a base `computed` or other derived signal is made writable by patching on a `.set` and `.update` method.
1 parent a7a48ac commit a220e66

File tree

3 files changed

+55
-1
lines changed

3 files changed

+55
-1
lines changed

packages/core/src/core_reactivity_export_internal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export {
2323
signal,
2424
WritableSignal,
2525
ɵunwrapWritableSignal,
26+
upgradeSignalToWritable as ɵupgradeSignalToWritable,
2627
} from './render3/reactivity/signal';
2728
export {linkedSignal} from './render3/reactivity/linked_signal';
2829
export {untracked} from './render3/reactivity/untracked';

packages/core/src/render3/reactivity/signal.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../../../primitives/signals';
1717

1818
import {isSignal, Signal, ValueEqualityFn} from './api';
19+
import {untracked} from './untracked';
1920

2021
/** Symbol used distinguish `WritableSignal` from other non-writable signals and functions. */
2122
export const ɵWRITABLE_SIGNAL: unique symbol = /* @__PURE__ */ Symbol('WRITABLE_SIGNAL');
@@ -109,3 +110,34 @@ export function signalAsReadonlyFn<T>(this: SignalGetter<T>): Signal<T> {
109110
export function isWritableSignal(value: unknown): value is WritableSignal<unknown> {
110111
return isSignal(value) && typeof (value as any).set === 'function';
111112
}
113+
114+
/**
115+
* Upgrade the `read` signal to be a `WritableSignal`, using the `write` function to set the value.
116+
*
117+
* Callers must ensure that `write` updates the value of `read` in a synchronous way.
118+
*
119+
* It is an error to call `upgradeSignalToWritable` on a `WritableSignal`.
120+
*/
121+
export function upgradeSignalToWritable<T>(
122+
read: WritableSignal<T>,
123+
write: (value: T) => void,
124+
): never;
125+
export function upgradeSignalToWritable<T>(
126+
read: Signal<T>,
127+
write: (value: T) => void,
128+
): asserts read is WritableSignal<T>;
129+
export function upgradeSignalToWritable<T>(
130+
read: Signal<T>,
131+
write: (value: T) => void,
132+
): asserts read is WritableSignal<T> {
133+
const getter = read as WritableSignal<T>;
134+
if (ngDevMode && (getter as {set?: unknown}).set) {
135+
throw new Error('Cannot upgrade an already-writable signal.');
136+
}
137+
138+
getter.set = write;
139+
getter.update = (fn: (value: T) => T) => {
140+
write(fn(untracked(read)));
141+
};
142+
getter.asReadonly = signalAsReadonlyFn;
143+
}

packages/core/test/signals/signal_spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {computed, signal} from '../../src/core';
9+
import {
10+
computed,
11+
signal,
12+
ɵupgradeSignalToWritable as upgradeSignalToWritable,
13+
} from '../../src/core';
1014
import {
1115
ReactiveHookFn,
1216
ReactiveNode,
@@ -224,4 +228,21 @@ describe('signals', () => {
224228
expect(producerKindsCreated).toEqual(['signal']);
225229
setPostProducerCreatedFn(prev);
226230
});
231+
232+
describe('upgradeSignalToWritable', () => {
233+
it('should upgrade a signal to a writable signal', () => {
234+
const celsius = signal(-40);
235+
const fahrenheit = computed(() => celsius() * (9 / 5) + 32);
236+
upgradeSignalToWritable(fahrenheit, (valueF) => celsius.set((valueF - 32) * (5 / 9)));
237+
238+
expect(celsius()).toBe(-40);
239+
expect(fahrenheit()).toBe(-40);
240+
241+
fahrenheit.set(212);
242+
expect(celsius()).toBe(0);
243+
244+
celsius.set(0);
245+
expect(fahrenheit()).toBe(32);
246+
});
247+
});
227248
});

0 commit comments

Comments
 (0)