I've been working on using signals more often than not and have run into a use case where I can't find a reasonable answer.
Many of the examples and guides online show creating a signal with a single value and updating it through .set() or .update(). For example:
public value = signal<int>(0); value.set(1);
Which makes perfect sense with simple inputs, but what happens when we want to use a signal with an object that has multiple values? Or we want multiple inputs? For example, if I have a User object
export interface User { firstName: string, lastName: string }
And that object is inside of a signal,
public currentUser = signal<User>({firstName: 'Doug', lastName: 'The Cat'});
And I want to have a form for users to update their name, how would I update the individual values without completely overwriting the existing user object?
I've tried creating a shallow copy of the signals output to then attach the form to and then calling set on the signal to update the properties but that feels off since I'm just overwriting what's there.
I also thought about turning each value, firstName, lastName into their own signals, but that would get out of hand really and inflexible fast as values get added to the object like streetAddress, poBox, etc.
Using the update method to patch the value of a signal is going to be your best bet. In your use case it would work like something below:
this.currentUser.update(x => ({ ...x, firstName: 'Danny' }));
Using the spread operator to create a new object and setting only the patched properties is a common practice and has many benefits over updating the property directly. One such benefit that is specific to signals, is that it won't cause any issues when using the signal as part of a computed signal. If you were to update the property directly then the computed signal will not update since the object reference was never changed.
If you wanted, you could also develop your own version of writable signals that work specifically with objects and has its own patch method. This would have the benefit of being usable in templates.
export type PatchableSignal<T extends {}> = Signal<T> & WritableSignal<T> &
{
/** Updates properties on an object */
patch(value: Partial<T>): void;
};
export function patchableSignal<T extends {}>(initialValue: T, options?: CreateSignalOptions<T>): PatchableSignal<T> {
const internal = signal<T>(initialValue) as SignalGetter<T> & WritableSignal<T>;
const node = internal[SIGNAL];
return Object.assign(internal, {
patch: (value: Partial<T>) => signalUpdateFn(node, (x) => ({ ...x, ...value })),
});
}
<button (click)="currentUser.patch({ firstName: 'New Name' })">Click Me</button>