Skip to content

Commit 1d6b3a8

Browse files
committed
feat: omit need to unwrap query ref
feat: add `withMutation` feature Release-As: 0.2.0
1 parent 1cb2edb commit 1d6b3a8

35 files changed

+9353
-4165
lines changed

.github/workflows/pr.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,10 @@ jobs:
2222
- name: Install dependencies
2323
uses: ./.github/actions/install-npm-deps
2424

25+
- name: Lint
26+
run: npx --no -- ng lint
27+
2528
- name: Build
26-
run: npx --no -- ng build ngx-signal-store-query
29+
run: |
30+
npx --no -- ng build ngx-signal-store-query --configuration=production
31+
npx --no -- ng build demo --configuration=production --progress=false --verbose

.github/workflows/release-please.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
- name: Build
5252
if: ${{ steps.release.outputs.release_created }}
5353
shell: bash
54-
run: npx --no -- ng build ngx-signal-store-query
54+
run: npx --no -- ng build ngx-signal-store-query --configuration=production
5555

5656
- name: Copy extra files
5757
if: ${{ steps.release.outputs.release_created }}

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ import { ExampleStore } from './example.store.ts';
4949
selector: 'app-example',
5050
template: `
5151
<pre>
52-
Loading: {{ store.exampleQuery().isLoading() }}
53-
Fetching: {{ store.exampleQuery().isFetching() }}
52+
Loading: {{ store.exampleQuery.isLoading() }}
53+
Fetching: {{ store.exampleQuery.isFetching() }}
5454
Data:
55-
{{ store.exampleQuery().data() | json }}
55+
{{ store.exampleQuery.data() | json }}
5656
</pre>
5757
`,
5858
changeDetection: ChangeDetectionStrategy.OnPush,

apps/.gitkeep

Whitespace-only changes.

apps/demo/public/assets/.gitkeep

Whitespace-only changes.

apps/demo/public/assets/data/gh-org-repos-angular.json

Lines changed: 3326 additions & 0 deletions
Large diffs are not rendered by default.

apps/demo/public/assets/data/gh-org-repos-google.json

Lines changed: 3332 additions & 0 deletions
Large diffs are not rendered by default.

apps/demo/src/app/app.component.html

Lines changed: 5 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,6 @@
1-
@if (isFetching() || isLoading()) {
2-
<mat-progress-bar class="busy-indicator" mode="indeterminate" />
3-
}
4-
5-
<mat-list class="list">
6-
<div mat-subheader>{{ organization() | titlecase }} Repositories</div>
7-
8-
@if (isFetching() || isLoading()) {
9-
<ng-container *ngTemplateOutlet="skeleton" />
10-
<ng-container *ngTemplateOutlet="skeleton" />
11-
<ng-container *ngTemplateOutlet="skeleton" />
12-
} @else {
13-
@for (repo of data(); track repo) {
14-
<mat-list-item class="list-item">
15-
<mat-icon class="list-item__icon" matListItemIcon>folder</mat-icon>
16-
<div class="list-item__title" matListItemTitle>
17-
<a class="list-item__link" [href]="repo.html_url" target="_blank" rel="external nofollow noopener noreferrer">
18-
{{ repo.name }}
19-
</a>
20-
<span class="list-item__pill">{{ repo.private ? 'Private' : 'Public' }}</span>
21-
</div>
22-
<div class="list-item__description" matListItemLine>
23-
{{ repo.description }}
24-
</div>
25-
</mat-list-item>
26-
} @empty {
27-
<div class="no-records">No repositories found</div>
28-
}
29-
}
30-
</mat-list>
1+
<ssq-counter />
2+
<ssq-github-repos />
313

32-
<ng-template #skeleton>
33-
<mat-list-item class="list-item skeleton">
34-
<mat-icon class="list-item__icon" matListItemIcon>folder</mat-icon>
35-
<div class="list-item__title" matListItemTitle>
36-
<ssq-skeleton-text />
37-
</div>
38-
<div class="list-item__description" matListItemLine>
39-
<ssq-skeleton-text inlineSize="12rem" />
40-
</div>
41-
</mat-list-item>
42-
</ng-template>
43-
44-
<angular-query-devtools />
4+
@defer (when isDevMode) {
5+
<angular-query-devtools />
6+
}

apps/demo/src/app/app.component.scss

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,7 @@
1-
@use '@angular/material' as mat;
2-
@use 'theme' as *;
3-
4-
@mixin apply-theme {
5-
--ssq-chip-border-color: #{mat.get-theme-color($light-theme, on-surface)};
6-
7-
@media (prefers-color-scheme: dark) {
8-
--ssq-chip-border-color: #{mat.get-theme-color($dark-theme, on-surface)};
9-
}
10-
}
11-
121
:host {
13-
@include apply-theme;
14-
2+
margin-inline: auto;
3+
padding: 2rem;
4+
inline-size: clamp(20rem, 100%, 64rem);
5+
min-block-size: 100%;
156
display: block;
16-
17-
.busy-indicator {
18-
inset: 0 auto 0;
19-
position: fixed;
20-
}
21-
22-
.list-item {
23-
:is(&__pill) {
24-
padding: 0.05rem 0.375rem;
25-
translate: 0.125rem -0.25rem;
26-
display: inline-grid;
27-
align-items: center;
28-
border: 0.05rem solid hsl(from var(--ssq-chip-border-color) h s l / 50%);
29-
border-radius: 2em;
30-
font-size: 0.75rem;
31-
}
32-
}
33-
34-
.no-records {
35-
display: block;
36-
place-content: center;
37-
text-align: center;
38-
opacity: 0.5;
39-
}
407
}

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

Lines changed: 6 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,19 @@
1-
import { NgTemplateOutlet, TitleCasePipe } from '@angular/common';
2-
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
3-
4-
import { MatIconModule } from '@angular/material/icon';
5-
import { MatListModule } from '@angular/material/list';
6-
import { MatProgressBarModule } from '@angular/material/progress-bar';
1+
import { ChangeDetectionStrategy, Component, isDevMode } from '@angular/core';
72

83
import { AngularQueryDevtools } from '@tanstack/angular-query-devtools-experimental';
94

10-
import { AppStore } from './app.store';
11-
import { SkeletonTextComponent } from './skeleton-text.component';
5+
import { CounterComponent } from './counter';
6+
import { GithubReposComponent } from './gh-repos';
7+
import { SkeletonTextComponent } from './skeleton-text';
128

139
@Component({
1410
standalone: true,
1511
selector: 'ssq-app',
1612
templateUrl: './app.component.html',
1713
styleUrl: './app.component.scss',
1814
changeDetection: ChangeDetectionStrategy.OnPush,
19-
providers: [AppStore],
20-
imports: [
21-
TitleCasePipe,
22-
MatIconModule,
23-
MatListModule,
24-
MatProgressBarModule,
25-
AngularQueryDevtools,
26-
SkeletonTextComponent,
27-
NgTemplateOutlet,
28-
],
15+
imports: [AngularQueryDevtools, CounterComponent, GithubReposComponent, SkeletonTextComponent],
2916
})
3017
export class AppComponent {
31-
protected readonly store = inject(AppStore);
32-
33-
protected organization = this.store.organization;
34-
35-
protected githubQuery = this.store.githubQuery;
36-
37-
protected isFetching = computed(() => {
38-
const githubQuery = this.githubQuery();
39-
40-
return githubQuery.isFetching();
41-
});
42-
43-
protected isLoading = computed(() => {
44-
const githubQuery = this.githubQuery();
45-
46-
return githubQuery.isLoading();
47-
});
48-
49-
protected data = computed(() => {
50-
const githubQuery = this.githubQuery();
51-
52-
return githubQuery.data() ?? [];
53-
});
18+
protected readonly isDevMode = isDevMode();
5419
}

apps/demo/src/app/app.config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import { provideHttpClient, withFetch } from '@angular/common/http';
1+
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
22
import { type ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
33
import { provideClientHydration } from '@angular/platform-browser';
44
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
55

66
import { provideAngularQuery, QueryClient } from '@tanstack/angular-query-experimental';
77

8+
import { githubApiInterceptor } from './gh-repos';
9+
810
export const appConfig: ApplicationConfig = {
911
providers: [
1012
provideZoneChangeDetection({ eventCoalescing: true }),
1113
provideClientHydration(),
1214
provideAnimationsAsync(),
13-
provideHttpClient(withFetch()),
15+
provideHttpClient(withFetch(), withInterceptors([githubApiInterceptor])),
1416
provideAngularQuery(
1517
new QueryClient({
1618
defaultOptions: {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<output aria-label="Current count">{{ value() }}</output>
2+
3+
<button mat-fab type="button" aria-label="Increase" title="Increase" [disabled]="isBusy()" (click)="increase()">
4+
<mat-icon fontIcon="exposure_plus_1" />
5+
</button>
6+
7+
<button mat-fab type="button" aria-label="Decrease" title="Decrease" [disabled]="isBusy()" (click)="decrease()">
8+
<mat-icon fontIcon="exposure_neg_1" />
9+
</button>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@use '@angular/material' as mat;
2+
3+
@use 'theme' as *;
4+
5+
:host {
6+
margin-inline: auto;
7+
min-inline-size: 6rem;
8+
inline-size: fit-content;
9+
display: grid;
10+
grid-template-columns: 1fr 1fr;
11+
grid-template-rows: auto 3rem;
12+
justify-items: center;
13+
gap: 0.5rem;
14+
15+
output {
16+
grid-column: 1 / 3;
17+
display: block;
18+
align-content: center;
19+
text-align: center;
20+
font: #{mat.get-theme-typography($light-theme, headline-large, font)};
21+
}
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2+
3+
import { MatButtonModule } from '@angular/material/button';
4+
import { MatIconModule } from '@angular/material/icon';
5+
6+
import { CounterStore } from './counter.store';
7+
8+
@Component({
9+
standalone: true,
10+
selector: 'ssq-counter',
11+
templateUrl: './counter.component.html',
12+
styleUrl: './counter.component.scss',
13+
changeDetection: ChangeDetectionStrategy.OnPush,
14+
providers: [CounterStore],
15+
imports: [MatButtonModule, MatIconModule],
16+
})
17+
export class CounterComponent {
18+
private readonly store = inject(CounterStore);
19+
20+
protected readonly value = this.store.count;
21+
22+
public readonly isBusy = computed(() => {
23+
const increase = this.store.increaseMutation.isPending();
24+
const decrease = this.store.decreaseMutation.isPending();
25+
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);
35+
}
36+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { DestroyRef, inject } from '@angular/core';
2+
3+
import { MatSnackBar } from '@angular/material/snack-bar';
4+
5+
import { patchState, signalStore, withState } from '@ngrx/signals';
6+
import { withMutation } from '@ngx-signal-store-query/core';
7+
8+
export const CounterStore = signalStore(
9+
withState({ count: 0 }),
10+
withMutation('increase', (store) => () => {
11+
const destroyRef = inject(DestroyRef);
12+
const snackBar = inject(MatSnackBar);
13+
14+
let timer: ReturnType<typeof setTimeout> | null = null;
15+
16+
destroyRef.onDestroy(() => timer != null && clearTimeout(timer));
17+
18+
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 to big'));
25+
}
26+
27+
timer = setTimeout(() => resolve({ count: count + amount }), 250);
28+
});
29+
},
30+
onSuccess({ count }: CounterResponse): void {
31+
return patchState(store, { count });
32+
},
33+
onError(error: Error): void {
34+
snackBar.open(error.message, '', {
35+
panelClass: 'popover-error',
36+
duration: 5000,
37+
});
38+
},
39+
};
40+
}),
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 to 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+
}),
72+
);
73+
74+
interface CounterResponse {
75+
count: number;
76+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './counter.component';

0 commit comments

Comments
 (0)