Last active
September 12, 2023 18:23
-
-
Save eneajaho/33a30bcf217c28b89c95517c07b94266 to your computer and use it in GitHub Desktop.
Chau's implementation of computedFrom
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { isSignal, Signal, untracked } from '@angular/core'; | |
import { toObservable, toSignal } from '@angular/core/rxjs-interop'; | |
import { combineLatest, distinctUntilChanged, from, isObservable, ObservableInput, of, OperatorFunction, take } from 'rxjs'; | |
export type ObservableSignalInput<T> = ObservableInput<T> | Signal<T>; | |
/** | |
* So that we can have `fn([Observable<A>, Signal<B>]): Observable<[A, B]>` | |
*/ | |
type ObservableSignalInputTuple<T> = { | |
[K in keyof T]: ObservableSignalInput<T[K]>; | |
}; | |
export function computedFrom<Input extends readonly unknown[], Output = Input>( | |
sources: readonly [...ObservableSignalInputTuple<Input>], | |
operator?: OperatorFunction<Input, Output> | |
): Signal<Output>; | |
export function computedFrom<Input extends object, Output = Input>( | |
sources: ObservableSignalInputTuple<Input>, | |
operator?: OperatorFunction<Input, Output> | |
): Signal<Output>; | |
export function computedFrom( | |
sources: any, | |
operator?: OperatorFunction<any, any> | |
): Signal<any> { | |
let { normalizedSources, initialValues } = Object.entries(sources).reduce( | |
(acc, [keyOrIndex, source]) => { | |
if (isSignal(source)) { | |
acc.normalizedSources[keyOrIndex] = toObservable(source); | |
acc.initialValues[keyOrIndex] = untracked(source); | |
} else if (isObservable(source)) { | |
acc.normalizedSources[keyOrIndex] = source.pipe(distinctUntilChanged()); | |
source.pipe(take(1)).subscribe((attemptedSyncValue) => { | |
if (acc.initialValues[keyOrIndex] !== null) { | |
acc.initialValues[keyOrIndex] = attemptedSyncValue; | |
} | |
}); | |
acc.initialValues[keyOrIndex] ??= null; | |
} else { | |
acc.normalizedSources[keyOrIndex] = from(source as any).pipe( | |
distinctUntilChanged() | |
); | |
acc.initialValues[keyOrIndex] = null; | |
} | |
return acc; | |
}, | |
{ | |
normalizedSources: Array.isArray(sources) ? [] : {}, | |
initialValues: Array.isArray(sources) ? [] : {}, | |
} as { | |
normalizedSources: any; | |
initialValues: any; | |
} | |
); | |
normalizedSources = combineLatest(normalizedSources); | |
if (operator) { | |
normalizedSources = normalizedSources.pipe(operator); | |
operator(of(initialValues)) | |
.pipe(take(1)) | |
.subscribe((newInitialValues) => { | |
initialValues = newInitialValues; | |
}); | |
} | |
return toSignal(normalizedSources, { initialValue: initialValues }); | |
} |
Another issue I found is that a subject which emits the same value as before won't trigger the pipeline.
const subject = new Subject<void>();
// save button click will trigger a API call.
save() {
this.subject.next();
}
$signalResult = computedFrom(subject, switchMapToApiCall)
In html, $signalResult won't be triggered when clicking save button after first click. But using toSignal
won't have any issue. I assume computedFrom won't allow the same emission to be passed to the pipeline.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @eneajaho thanks for sharing this great work. I wonder what
??=
null means in line 40?