Skip to content

Commit 1685164

Browse files
alxhubpkozlowski-opensource
authored andcommitted
feat(core): support default value in resource() (#59655)
Before `resource()` resolves, its value is in an unknown state. By default it returns `undefined` in these scenarios, so the type of `.value()` includes `undefined`. This commit adds a `defaultValue` option to `resource()` and `rxResource()` which overrides this default. When provided, an unresolved resource will return this value instead of `undefined`, which simplifies the typing of `.value()`. PR Close #59655
1 parent edb8407 commit 1685164

File tree

6 files changed

+73
-2
lines changed

6 files changed

+73
-2
lines changed

goldens/public-api/core/index.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export interface AttributeDecorator {
186186

187187
// @public
188188
export interface BaseResourceOptions<T, R> {
189+
defaultValue?: NoInfer<T>;
189190
equal?: ValueEqualityFn<T>;
190191
injector?: Injector;
191192
request?: () => R;
@@ -1604,6 +1605,11 @@ export interface Resource<T> {
16041605
readonly value: Signal<T>;
16051606
}
16061607

1608+
// @public
1609+
export function resource<T, R>(options: ResourceOptions<T, R> & {
1610+
defaultValue: NoInfer<T>;
1611+
}): ResourceRef<T>;
1612+
16071613
// @public
16081614
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined>;
16091615

goldens/public-api/core/rxjs-interop/index.api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export function outputToObservable<T>(ref: OutputRef<T>): Observable<T>;
2626
// @public
2727
export function pendingUntilEvent<T>(injector?: Injector): MonoTypeOperatorFunction<T>;
2828

29+
// @public
30+
export function rxResource<T, R>(opts: RxResourceOptions<T, R> & {
31+
defaultValue: NoInfer<T>;
32+
}): ResourceRef<T>;
33+
2934
// @public
3035
export function rxResource<T, R>(opts: RxResourceOptions<T, R>): ResourceRef<T | undefined>;
3136

packages/core/rxjs-interop/src/rx_resource.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,21 @@ export interface RxResourceOptions<T, R> extends BaseResourceOptions<T, R> {
2828

2929
/**
3030
* Like `resource` but uses an RxJS based `loader` which maps the request to an `Observable` of the
31-
* resource's value. Like `firstValueFrom`, only the first emission of the Observable is considered.
31+
* resource's value.
3232
*
3333
* @experimental
3434
*/
35+
export function rxResource<T, R>(
36+
opts: RxResourceOptions<T, R> & {defaultValue: NoInfer<T>},
37+
): ResourceRef<T>;
38+
39+
/**
40+
* Like `resource` but uses an RxJS based `loader` which maps the request to an `Observable` of the
41+
* resource's value.
42+
*
43+
* @experimental
44+
*/
45+
export function rxResource<T, R>(opts: RxResourceOptions<T, R>): ResourceRef<T | undefined>;
3546
export function rxResource<T, R>(opts: RxResourceOptions<T, R>): ResourceRef<T | undefined> {
3647
opts?.injector || assertInInjectionContext(rxResource);
3748
return resource<T, R>({

packages/core/src/resource/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ export interface BaseResourceOptions<T, R> {
183183
*/
184184
request?: () => R;
185185

186+
/**
187+
* The value which will be returned from the resource when a server value is unavailable, such as
188+
* when the resource is still loading, or in an error state.
189+
*/
190+
defaultValue?: NoInfer<T>;
191+
186192
/**
187193
* Equality function used to compare the return value of the loader.
188194
*/

packages/core/src/resource/resource.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,28 @@ import {DestroyRef} from '../linker/destroy_ref';
4040
*
4141
* @experimental
4242
*/
43+
export function resource<T, R>(
44+
options: ResourceOptions<T, R> & {defaultValue: NoInfer<T>},
45+
): ResourceRef<T>;
46+
47+
/**
48+
* Constructs a `Resource` that projects a reactive request to an asynchronous operation defined by
49+
* a loader function, which exposes the result of the loading operation via signals.
50+
*
51+
* Note that `resource` is intended for _read_ operations, not operations which perform mutations.
52+
* `resource` will cancel in-progress loads via the `AbortSignal` when destroyed or when a new
53+
* request object becomes available, which could prematurely abort mutations.
54+
*
55+
* @experimental
56+
*/
57+
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined>;
4358
export function resource<T, R>(options: ResourceOptions<T, R>): ResourceRef<T | undefined> {
4459
options?.injector || assertInInjectionContext(resource);
4560
const request = (options.request ?? (() => null)) as () => R;
4661
return new ResourceImpl<T | undefined, R>(
4762
request,
4863
getLoader(options),
49-
undefined,
64+
options.defaultValue,
5065
options.equal ? wrapEqualityFn(options.equal) : undefined,
5166
options.injector ?? inject(Injector),
5267
);

packages/core/test/resource/resource_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,34 @@ describe('resource', () => {
161161
expect(echoResource.error()).toEqual(Error('KO'));
162162
});
163163

164+
it('should return a default value if provided', async () => {
165+
const DEFAULT: string[] = [];
166+
const request = signal(0);
167+
const res = resource({
168+
request,
169+
loader: async ({request}) => {
170+
if (request === 2) {
171+
throw new Error('err');
172+
}
173+
return ['data'];
174+
},
175+
defaultValue: DEFAULT,
176+
injector: TestBed.inject(Injector),
177+
});
178+
expect(res.value()).toBe(DEFAULT);
179+
180+
await TestBed.inject(ApplicationRef).whenStable();
181+
expect(res.value()).not.toBe(DEFAULT);
182+
183+
request.set(1);
184+
expect(res.value()).toBe(DEFAULT);
185+
186+
request.set(2);
187+
await TestBed.inject(ApplicationRef).whenStable();
188+
expect(res.error()).not.toBeUndefined();
189+
expect(res.value()).toBe(DEFAULT);
190+
});
191+
164192
it('should _not_ load if the request resolves to undefined', () => {
165193
const counter = signal(0);
166194
const backend = new MockEchoBackend();

0 commit comments

Comments
 (0)