Skip to content

Commit 25615c8

Browse files
committed
feat: simplify types
feat: only infer types of inputs and outputs Release-As: 0.3.0
1 parent b44810d commit 25615c8

File tree

11 files changed

+118
-128
lines changed

11 files changed

+118
-128
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { type Observable, of, throwError } from 'rxjs';
2+
import { delay } from 'rxjs/operators';
3+
4+
export class CounterApiService {
5+
public increaseBy$(amount: number, count: number): Observable<CounterApiResponse> {
6+
if (amount > 0 && count >= 5) {
7+
return throwError(() => new RangeError('Count is too high'));
8+
}
9+
10+
if (amount < 0 && count <= 0) {
11+
return throwError(() => new RangeError('Count is too low'));
12+
}
13+
14+
return of({ count: count + amount }).pipe(delay(250));
15+
}
16+
}
17+
18+
export interface CounterApiResponse {
19+
count: number;
20+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<output aria-label="Current count">{{ value() }}</output>
22

3-
<button mat-fab type="button" aria-label="Increase" title="Increase" [disabled]="isBusy()" (click)="increase()">
3+
<button mat-fab type="button" aria-label="Increase" title="Increase" [disabled]="isBusy()" (click)="increaseBy(1)">
44
<mat-icon fontIcon="exposure_plus_1" />
55
</button>
66

7-
<button mat-fab type="button" aria-label="Decrease" title="Decrease" [disabled]="isBusy()" (click)="decrease()">
7+
<button mat-fab type="button" aria-label="Decrease" title="Decrease" [disabled]="isBusy()" (click)="increaseBy(-1)">
88
<mat-icon fontIcon="exposure_neg_1" />
99
</button>

apps/demo/src/app/counter/counter.component.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/c
33
import { MatButtonModule } from '@angular/material/button';
44
import { MatIconModule } from '@angular/material/icon';
55

6+
import { CounterApiService } from './counter-api.service';
67
import { CounterStore } from './counter.store';
78

89
@Component({
@@ -11,26 +12,17 @@ import { CounterStore } from './counter.store';
1112
templateUrl: './counter.component.html',
1213
styleUrl: './counter.component.scss',
1314
changeDetection: ChangeDetectionStrategy.OnPush,
14-
providers: [CounterStore],
15+
providers: [CounterApiService, CounterStore],
1516
imports: [MatButtonModule, MatIconModule],
1617
})
1718
export class CounterComponent {
1819
private readonly store = inject(CounterStore);
1920

20-
protected readonly value = this.store.count;
21+
protected readonly value = computed(() => this.store.count());
2122

22-
public readonly isBusy = computed(() => {
23-
const increase = this.store.increaseMutation.isPending();
24-
const decrease = this.store.decreaseMutation.isPending();
23+
protected readonly isBusy = computed(() => this.store.counterMutation.isPending());
2524

26-
return increase || decrease;
27-
});
28-
29-
public increase(): void {
30-
this.store.increaseMutation.mutate(1);
31-
}
32-
33-
public decrease(): void {
34-
this.store.decreaseMutation.mutate(1);
25+
protected increaseBy(amount: number): void {
26+
this.store.counterMutation.mutate(amount);
3527
}
3628
}
Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
11
import { DestroyRef, inject } from '@angular/core';
2+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
23

34
import { MatSnackBar } from '@angular/material/snack-bar';
45

56
import { patchState, signalStore, withState } from '@ngrx/signals';
67
import { withMutation } from '@ngx-signal-store-query/core';
8+
import { lastValueFrom } from 'rxjs';
9+
10+
import { CounterApiService } from './counter-api.service';
711

812
export const CounterStore = signalStore(
913
withState({ count: 0 }),
10-
withMutation('increase', (store) => () => {
14+
withMutation('counter', (store) => () => {
1115
const destroyRef = inject(DestroyRef);
1216
const snackBar = inject(MatSnackBar);
13-
14-
let timer: ReturnType<typeof setTimeout> | null = null;
15-
16-
destroyRef.onDestroy(() => timer != null && clearTimeout(timer));
17+
const api = inject(CounterApiService);
1718

1819
return {
19-
mutationFn(amount: number): Promise<CounterResponse> {
20-
const count = store.count();
21-
22-
return new Promise((resolve, reject) => {
23-
if (count >= 5) {
24-
return reject(new RangeError('Count is too high'));
25-
}
26-
27-
timer = setTimeout(() => resolve({ count: count + amount }), 250);
28-
});
20+
mutationFn(amount: number) {
21+
return lastValueFrom(api.increaseBy$(amount, store.count()).pipe(takeUntilDestroyed(destroyRef)));
2922
},
30-
onSuccess({ count }: CounterResponse): void {
23+
onSuccess({ count }): void {
3124
return patchState(store, { count });
3225
},
3326
onError(error: Error): void {
@@ -38,39 +31,4 @@ export const CounterStore = signalStore(
3831
},
3932
};
4033
}),
41-
withMutation('decrease', (store) => () => {
42-
const destroyRef = inject(DestroyRef);
43-
const snackBar = inject(MatSnackBar);
44-
45-
let timer: ReturnType<typeof setTimeout> | null = null;
46-
47-
destroyRef.onDestroy(() => timer != null && clearTimeout(timer));
48-
49-
return {
50-
mutationFn: (amount: number): Promise<CounterResponse> => {
51-
const count = store.count();
52-
53-
return new Promise((resolve, reject) => {
54-
if (count <= 0) {
55-
return reject(new RangeError('Count is too low'));
56-
}
57-
58-
timer = setTimeout(() => resolve({ count: count - amount }), 250);
59-
});
60-
},
61-
onSuccess: ({ count }: CounterResponse): void => {
62-
return patchState(store, { count });
63-
},
64-
onError: (error: Error): void => {
65-
snackBar.open(error.message, '', {
66-
panelClass: 'popover-error',
67-
duration: 5000,
68-
});
69-
},
70-
};
71-
}),
7234
);
73-
74-
interface CounterResponse {
75-
count: number;
76-
}

apps/demo/src/app/gh-repos/github-repos.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
@if (isFetching() || isLoading()) {
1+
@if (isBusy()) {
22
<mat-progress-bar class="busy-indicator" mode="indeterminate" />
33
}
44

55
<mat-list class="list">
66
<div mat-subheader>{{ organization() | titlecase }} Repositories</div>
77

8-
@if (isFetching() || isLoading()) {
8+
@if (isBusy()) {
99
<ng-container *ngTemplateOutlet="skeleton" />
1010
<ng-container *ngTemplateOutlet="skeleton" />
1111
<ng-container *ngTemplateOutlet="skeleton" />

apps/demo/src/app/gh-repos/github-repos.component.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { NgTemplateOutlet, TitleCasePipe } from '@angular/common';
2-
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
1+
import { isPlatformBrowser, NgTemplateOutlet, TitleCasePipe } from '@angular/common';
2+
import {
3+
ChangeDetectionStrategy,
4+
Component,
5+
computed,
6+
DestroyRef,
7+
inject,
8+
type OnInit,
9+
PLATFORM_ID,
10+
} from '@angular/core';
311

412
import { MatIconModule } from '@angular/material/icon';
513
import { MatListModule } from '@angular/material/list';
@@ -19,14 +27,31 @@ import { GithubStore } from './github.store';
1927
providers: [GithubApiService, GithubStore],
2028
imports: [NgTemplateOutlet, TitleCasePipe, MatIconModule, MatListModule, MatProgressBarModule, SkeletonTextComponent],
2129
})
22-
export class GithubReposComponent {
30+
export class GithubReposComponent implements OnInit {
31+
private readonly destroyRef = inject(DestroyRef);
32+
33+
private readonly platformId = inject(PLATFORM_ID);
34+
2335
private readonly store = inject(GithubStore);
2436

25-
protected readonly organization = this.store.organization;
37+
protected readonly organization = computed(() => this.store.organization());
2638

27-
protected readonly isFetching = this.store.githubQuery.isFetching;
39+
protected readonly isBusy = computed(() => {
40+
const isFetching = this.store.githubQuery.isFetching();
41+
const isLoading = this.store.githubQuery.isLoading();
2842

29-
protected readonly isLoading = this.store.githubQuery.isLoading;
43+
return isFetching || isLoading;
44+
});
3045

3146
protected readonly data = computed(() => this.store.githubQuery.data() ?? []);
47+
48+
public ngOnInit(): void {
49+
if (!isPlatformBrowser(this.platformId)) {
50+
return;
51+
}
52+
53+
const timer = setTimeout(() => this.store.changeOrganization('angular'), this.store.delay() + 3000);
54+
55+
this.destroyRef.onDestroy(() => clearTimeout(timer));
56+
}
3257
}
Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { isPlatformBrowser } from '@angular/common';
22
import { type HttpErrorResponse } from '@angular/common/http';
33
import { DestroyRef, inject, PLATFORM_ID } from '@angular/core';
4+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
45

56
import { MatSnackBar } from '@angular/material/snack-bar';
67

7-
import { patchState, signalStore, withHooks, withState } from '@ngrx/signals';
8+
import { patchState, signalStore, withMethods, withState } from '@ngrx/signals';
89
import { withQuery } from '@ngx-signal-store-query/core';
910
import { lastValueFrom } from 'rxjs';
1011

@@ -19,7 +20,13 @@ export const GithubStore = signalStore(
1920
delay: isPlatformBrowser(platformId) ? 2000 : 0,
2021
};
2122
}),
23+
withMethods((store) => ({
24+
changeOrganization(organization: string) {
25+
return patchState(store, { organization });
26+
},
27+
})),
2228
withQuery('github', (store) => {
29+
const destroyRef = inject(DestroyRef);
2330
const snackBar = inject(MatSnackBar);
2431
const api = inject(GithubApiService);
2532

@@ -31,34 +38,18 @@ export const GithubStore = signalStore(
3138
enabled: !!organization,
3239
queryKey: ['github', 'orgs', { organization }, 'repos'],
3340
queryFn: () =>
34-
lastValueFrom(api.fetchOrganizationRepositoryList$(organization, delay)).catch(
35-
(error: HttpErrorResponse | Error) => {
36-
snackBar.open(error.message, '', {
37-
panelClass: 'popover-error',
38-
duration: 5000,
39-
});
40-
41-
return [];
42-
},
43-
),
41+
lastValueFrom(
42+
api.fetchOrganizationRepositoryList$(organization, delay).pipe(takeUntilDestroyed(destroyRef)),
43+
).catch((error: HttpErrorResponse | Error) => {
44+
snackBar.open(error.message, '', {
45+
panelClass: 'popover-error',
46+
duration: 5000,
47+
});
48+
49+
return [];
50+
}),
4451
staleTime: 5 * 60 * 1000,
4552
};
4653
};
4754
}),
48-
withHooks((store) => {
49-
const destroyRef = inject(DestroyRef);
50-
const platformId = inject(PLATFORM_ID);
51-
52-
return {
53-
onInit() {
54-
if (!isPlatformBrowser(platformId)) {
55-
return;
56-
}
57-
58-
const timer = setTimeout(() => patchState(store, { organization: 'angular' }), store.delay() + 3000);
59-
60-
destroyRef.onDestroy(() => clearTimeout(timer));
61-
},
62-
};
63-
}),
6455
);

libs/ngx-signal-store-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngx-signal-store-query/core",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "Signal Store feature that bridges with Angular Query",
55
"keywords": [
66
"Angular",

libs/ngx-signal-store-query/src/types.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,27 @@ import {
77
import {
88
type CreateMutationOptions,
99
type CreateMutationResult,
10+
type CreateQueryOptions,
1011
type CreateQueryResult,
11-
type injectQuery,
1212
} from '@tanstack/angular-query-experimental';
13+
import { type QueryClient } from '@tanstack/query-core';
1314

14-
import type { QueryClient } from '@tanstack/query-core';
15+
export type QueryStore<Input extends SignalStoreFeatureResult> = Prettify<
16+
StateSignals<Input['state']> & Input['computed'] & Input['methods'] & WritableStateSource<Prettify<Input['state']>>
17+
>;
1518

1619
export type CreateQueryFn<
1720
TDataFn = unknown,
18-
TData = TDataFn,
1921
TError = Error,
22+
TData = TDataFn,
2023
Input extends SignalStoreFeatureResult = SignalStoreFeatureResult,
21-
> = (store: QueryStore<Input>) => QueryFactory<TDataFn, TData, TError>;
24+
> = (store: QueryStore<Input>) => (client: QueryClient) => CreateQueryOptions<TDataFn, TError, TData>;
2225

2326
export type QueryProp<Name extends string> = `${Uncapitalize<Name>}Query`;
2427

2528
export type QueryMethod<TData = unknown, TError = Error> = (() => CreateQueryResult<TData, TError>) &
2629
CreateQueryResult<TData, TError>;
2730

28-
export type QueryStore<Input extends SignalStoreFeatureResult> = Prettify<
29-
StateSignals<Input['state']> & Input['computed'] & Input['methods'] & WritableStateSource<Prettify<Input['state']>>
30-
>;
31-
32-
export type QueryFactory<TDataFn = unknown, TData = TDataFn, TError = Error> = Parameters<
33-
typeof injectQuery<TDataFn, TError, TData>
34-
>[0];
35-
3631
export type CreateMutationFn<
3732
TData = unknown,
3833
TError = Error,

libs/ngx-signal-store-query/src/with-mutation.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { inject, Injector } from '@angular/core';
2-
31
import {
42
type EmptyFeatureResult,
53
signalStoreFeature,
@@ -21,23 +19,31 @@ export const withMutation = <
2119
Input extends SignalStoreFeatureResult = SignalStoreFeatureResult,
2220
>(
2321
name: Name,
24-
createMutationFn: CreateMutationFn<TData, TError, TVariables, TContext, Input>,
22+
createMutationFn: CreateMutationFn<TData, TError, TVariables, TContext, NoInfer<Input>>,
2523
): SignalStoreFeature<
2624
Input,
27-
EmptyFeatureResult & { methods: Record<MutationProp<Name>, MutationMethod<TData, TError, TVariables, TContext>> }
25+
EmptyFeatureResult & {
26+
methods: Record<
27+
MutationProp<NoInfer<Name>>,
28+
MutationMethod<NoInfer<TData>, NoInfer<TError>, NoInfer<TVariables>, NoInfer<TContext>>
29+
>;
30+
}
2831
> => {
29-
const prop: MutationProp<Name> = `${lowerFirst(name)}Mutation`;
32+
const prop: MutationProp<NoInfer<Name>> = `${lowerFirst(name)}Mutation`;
3033

3134
return signalStoreFeature(
3235
withMethods((store) => {
33-
const mutation = injectMutation(createMutationFn(store as QueryStore<Input>), inject(Injector));
36+
const mutation = injectMutation(createMutationFn(store as QueryStore<NoInfer<Input>>));
3437

3538
return {
3639
[prop]: new Proxy(() => mutation, {
3740
get: (_, prop) => Reflect.get(mutation, prop),
3841
has: (_, prop) => Reflect.has(mutation, prop),
3942
}),
40-
} as Record<MutationProp<Name>, MutationMethod<TData, TError, TVariables, TContext>>;
43+
} as Record<
44+
MutationProp<NoInfer<Name>>,
45+
MutationMethod<NoInfer<TData>, NoInfer<TError>, NoInfer<TVariables>, NoInfer<TContext>>
46+
>;
4147
}),
4248
);
4349
};

0 commit comments

Comments
 (0)