feat: Implement tasks feature using NGRX signals and remove the old counter store, alongside general project configuration and skill documentation updates.
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
This commit is contained in:
@@ -12,34 +12,34 @@ Fetch data in Angular using signal-based `resource()`, `httpResource()`, and the
|
||||
`httpResource()` wraps HttpClient with signal-based state management:
|
||||
|
||||
```typescript
|
||||
import { Component, signal } from "@angular/core";
|
||||
import { httpResource } from "@angular/common/http";
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { httpResource } from '@angular/common/http';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-user-profile",
|
||||
template: `
|
||||
@if (userResource.isLoading()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (userResource.error()) {
|
||||
<p>Error: {{ userResource.error()?.message }}</p>
|
||||
<button (click)="userResource.reload()">Retry</button>
|
||||
} @else if (userResource.hasValue()) {
|
||||
<h1>{{ userResource.value().name }}</h1>
|
||||
<p>{{ userResource.value().email }}</p>
|
||||
}
|
||||
`,
|
||||
selector: 'app-user-profile',
|
||||
template: `
|
||||
@if (userResource.isLoading()) {
|
||||
<p>Loading...</p>
|
||||
} @else if (userResource.error()) {
|
||||
<p>Error: {{ userResource.error()?.message }}</p>
|
||||
<button (click)="userResource.reload()">Retry</button>
|
||||
} @else if (userResource.hasValue()) {
|
||||
<h1>{{ userResource.value().name }}</h1>
|
||||
<p>{{ userResource.value().email }}</p>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class UserProfile {
|
||||
userId = signal("123");
|
||||
userId = signal('123');
|
||||
|
||||
// Reactive HTTP resource - refetches when userId changes
|
||||
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
|
||||
// Reactive HTTP resource - refetches when userId changes
|
||||
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,21 +51,21 @@ userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
|
||||
|
||||
// With full request options
|
||||
userResource = httpResource<User>(() => ({
|
||||
url: `/api/users/${this.userId()}`,
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${this.token()}` },
|
||||
params: { include: "profile" },
|
||||
url: `/api/users/${this.userId()}`,
|
||||
method: 'GET',
|
||||
headers: { Authorization: `Bearer ${this.token()}` },
|
||||
params: { include: 'profile' },
|
||||
}));
|
||||
|
||||
// With default value
|
||||
usersResource = httpResource<User[]>(() => "/api/users", {
|
||||
defaultValue: [],
|
||||
usersResource = httpResource<User[]>(() => '/api/users', {
|
||||
defaultValue: [],
|
||||
});
|
||||
|
||||
// Skip request when params undefined
|
||||
userResource = httpResource<User>(() => {
|
||||
const id = this.userId();
|
||||
return id ? `/api/users/${id}` : undefined;
|
||||
const id = this.userId();
|
||||
return id ? `/api/users/${id}` : undefined;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -117,12 +117,12 @@ export class Search {
|
||||
|
||||
```typescript
|
||||
todosResource = resource({
|
||||
defaultValue: [] as Todo[],
|
||||
params: () => ({ filter: this.filter() }),
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/todos?filter=${params.filter}`);
|
||||
return res.json();
|
||||
},
|
||||
defaultValue: [] as Todo[],
|
||||
params: () => ({ filter: this.filter() }),
|
||||
loader: async ({ params }) => {
|
||||
const res = await fetch(`/api/todos?filter=${params.filter}`);
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
// value() returns Todo[] (never undefined)
|
||||
@@ -134,14 +134,14 @@ todosResource = resource({
|
||||
const userId = signal<string | null>(null);
|
||||
|
||||
userResource = resource({
|
||||
params: () => {
|
||||
const id = userId();
|
||||
// Return undefined to skip loading
|
||||
return id ? { id } : undefined;
|
||||
},
|
||||
loader: async ({ params }) => {
|
||||
return fetch(`/api/users/${params.id}`).then((r) => r.json());
|
||||
},
|
||||
params: () => {
|
||||
const id = userId();
|
||||
// Return undefined to skip loading
|
||||
return id ? { id } : undefined;
|
||||
},
|
||||
loader: async ({ params }) => {
|
||||
return fetch(`/api/users/${params.id}`).then((r) => r.json());
|
||||
},
|
||||
});
|
||||
// Status is 'idle' when params returns undefined
|
||||
```
|
||||
@@ -204,18 +204,18 @@ deleteUser(id: string) {
|
||||
### Request Options
|
||||
|
||||
```typescript
|
||||
this.http.get<User[]>("/api/users", {
|
||||
headers: {
|
||||
Authorization: "Bearer token",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
params: {
|
||||
page: "1",
|
||||
limit: "10",
|
||||
sort: "name",
|
||||
},
|
||||
observe: "response", // Get full HttpResponse
|
||||
responseType: "json",
|
||||
this.http.get<User[]>('/api/users', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer token',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
params: {
|
||||
page: '1',
|
||||
limit: '10',
|
||||
sort: 'name',
|
||||
},
|
||||
observe: 'response', // Get full HttpResponse
|
||||
responseType: 'json',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -225,44 +225,43 @@ this.http.get<User[]>("/api/users", {
|
||||
|
||||
```typescript
|
||||
// auth.interceptor.ts
|
||||
import { HttpInterceptorFn } from "@angular/common/http";
|
||||
import { inject } from "@angular/core";
|
||||
import { HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const authService = inject(Auth);
|
||||
const token = authService.token();
|
||||
const authService = inject(Auth);
|
||||
const token = authService.token();
|
||||
|
||||
if (token) {
|
||||
req = req.clone({
|
||||
setHeaders: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
if (token) {
|
||||
req = req.clone({
|
||||
setHeaders: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
|
||||
return next(req);
|
||||
return next(req);
|
||||
};
|
||||
|
||||
// error.interceptor.ts
|
||||
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
return next(req).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status === 401) {
|
||||
inject(Router).navigate(["/login"]);
|
||||
}
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
return next(req).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status === 401) {
|
||||
inject(Router).navigate(['/login']);
|
||||
}
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// logging.interceptor.ts
|
||||
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const started = Date.now();
|
||||
return next(req).pipe(
|
||||
tap({
|
||||
next: () =>
|
||||
console.log(`${req.method} ${req.url} - ${Date.now() - started}ms`),
|
||||
error: (err) => console.error(`${req.method} ${req.url} failed`, err),
|
||||
}),
|
||||
);
|
||||
const started = Date.now();
|
||||
return next(req).pipe(
|
||||
tap({
|
||||
next: () => console.log(`${req.method} ${req.url} - ${Date.now() - started}ms`),
|
||||
error: (err) => console.error(`${req.method} ${req.url} failed`, err),
|
||||
}),
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
@@ -270,14 +269,10 @@ export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
|
||||
```typescript
|
||||
// app.config.ts
|
||||
import { provideHttpClient, withInterceptors } from "@angular/common/http";
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(
|
||||
withInterceptors([authInterceptor, errorInterceptor, loggingInterceptor]),
|
||||
),
|
||||
],
|
||||
providers: [provideHttpClient(withInterceptors([authInterceptor, errorInterceptor, loggingInterceptor]))],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -287,26 +282,24 @@ export const appConfig: ApplicationConfig = {
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
template: `
|
||||
@if (userResource.error(); as error) {
|
||||
<div class="error">
|
||||
<p>{{ getErrorMessage(error) }}</p>
|
||||
<button (click)="userResource.reload()">Retry</button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
@if (userResource.error(); as error) {
|
||||
<div class="error">
|
||||
<p>{{ getErrorMessage(error) }}</p>
|
||||
<button (click)="userResource.reload()">Retry</button>
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class UserCmpt {
|
||||
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
|
||||
userResource = httpResource<User>(() => `/api/users/${this.userId()}`);
|
||||
|
||||
getErrorMessage(error: unknown): string {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
return (
|
||||
error.error?.message || `Error ${error.status}: ${error.statusText}`
|
||||
);
|
||||
getErrorMessage(error: unknown): string {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
return error.error?.message || `Error ${error.status}: ${error.statusText}`;
|
||||
}
|
||||
return 'An unexpected error occurred';
|
||||
}
|
||||
return "An unexpected error occurred";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -330,35 +323,32 @@ getUser(id: string) {
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
template: `
|
||||
@switch (dataResource.status()) {
|
||||
@case ("idle") {
|
||||
<p>Enter a search term</p>
|
||||
}
|
||||
@case ("loading") {
|
||||
<app-spinner />
|
||||
}
|
||||
@case ("reloading") {
|
||||
<app-data [data]="dataResource.value()" />
|
||||
<app-spinner size="small" />
|
||||
}
|
||||
@case ("resolved") {
|
||||
<app-data [data]="dataResource.value()" />
|
||||
}
|
||||
@case ("error") {
|
||||
<app-error
|
||||
[error]="dataResource.error()"
|
||||
(retry)="dataResource.reload()"
|
||||
/>
|
||||
}
|
||||
}
|
||||
`,
|
||||
template: `
|
||||
@switch (dataResource.status()) {
|
||||
@case ('idle') {
|
||||
<p>Enter a search term</p>
|
||||
}
|
||||
@case ('loading') {
|
||||
<app-spinner />
|
||||
}
|
||||
@case ('reloading') {
|
||||
<app-data [data]="dataResource.value()" />
|
||||
<app-spinner size="small" />
|
||||
}
|
||||
@case ('resolved') {
|
||||
<app-data [data]="dataResource.value()" />
|
||||
}
|
||||
@case ('error') {
|
||||
<app-error
|
||||
[error]="dataResource.error()"
|
||||
(retry)="dataResource.reload()" />
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
export class Data {
|
||||
query = signal("");
|
||||
dataResource = httpResource<Data[]>(() =>
|
||||
this.query() ? `/api/search?q=${this.query()}` : undefined,
|
||||
);
|
||||
query = signal('');
|
||||
dataResource = httpResource<Data[]>(() => (this.query() ? `/api/search?q=${this.query()}` : undefined));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user