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:
@@ -16,31 +16,31 @@
|
||||
Combine multiple services into a single API:
|
||||
|
||||
```typescript
|
||||
@Injectable({ providedIn: "root" })
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShopFacade {
|
||||
private productService = inject(Product);
|
||||
private cartService = inject(Cart);
|
||||
private orderService = inject(Order);
|
||||
private productService = inject(Product);
|
||||
private cartService = inject(Cart);
|
||||
private orderService = inject(Order);
|
||||
|
||||
// Expose combined state
|
||||
readonly products = this.productService.products;
|
||||
readonly cart = this.cartService.items;
|
||||
readonly cartTotal = this.cartService.total;
|
||||
// Expose combined state
|
||||
readonly products = this.productService.products;
|
||||
readonly cart = this.cartService.items;
|
||||
readonly cartTotal = this.cartService.total;
|
||||
|
||||
// Unified actions
|
||||
addToCart(productId: string, quantity: number) {
|
||||
const product = this.productService.getById(productId);
|
||||
if (product) {
|
||||
this.cartService.add(product, quantity);
|
||||
// Unified actions
|
||||
addToCart(productId: string, quantity: number) {
|
||||
const product = this.productService.getById(productId);
|
||||
if (product) {
|
||||
this.cartService.add(product, quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkout() {
|
||||
const items = this.cartService.items();
|
||||
const order = await this.orderService.create(items);
|
||||
this.cartService.clear();
|
||||
return order;
|
||||
}
|
||||
async checkout() {
|
||||
const items = this.cartService.items();
|
||||
const order = await this.orderService.create(items);
|
||||
this.cartService.clear();
|
||||
return order;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -48,41 +48,41 @@ export class ShopFacade {
|
||||
|
||||
```typescript
|
||||
interface UserState {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserState {
|
||||
private state = signal<UserState>({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
private state = signal<UserState>({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// Selectors
|
||||
readonly user = computed(() => this.state().user);
|
||||
readonly loading = computed(() => this.state().loading);
|
||||
readonly error = computed(() => this.state().error);
|
||||
readonly isAuthenticated = computed(() => this.state().user !== null);
|
||||
// Selectors
|
||||
readonly user = computed(() => this.state().user);
|
||||
readonly loading = computed(() => this.state().loading);
|
||||
readonly error = computed(() => this.state().error);
|
||||
readonly isAuthenticated = computed(() => this.state().user !== null);
|
||||
|
||||
// Actions
|
||||
setUser(user: User) {
|
||||
this.state.update((s) => ({ ...s, user, loading: false, error: null }));
|
||||
}
|
||||
// Actions
|
||||
setUser(user: User) {
|
||||
this.state.update((s) => ({ ...s, user, loading: false, error: null }));
|
||||
}
|
||||
|
||||
setLoading() {
|
||||
this.state.update((s) => ({ ...s, loading: true, error: null }));
|
||||
}
|
||||
setLoading() {
|
||||
this.state.update((s) => ({ ...s, loading: true, error: null }));
|
||||
}
|
||||
|
||||
setError(error: string) {
|
||||
this.state.update((s) => ({ ...s, loading: false, error }));
|
||||
}
|
||||
setError(error: string) {
|
||||
this.state.update((s) => ({ ...s, loading: false, error }));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.state.set({ user: null, loading: false, error: null });
|
||||
}
|
||||
clear() {
|
||||
this.state.set({ user: null, loading: false, error: null });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -208,36 +208,36 @@ export class User {
|
||||
```typescript
|
||||
// Parent provides service
|
||||
@Component({
|
||||
selector: "app-form-container",
|
||||
providers: [FormState],
|
||||
template: `
|
||||
<app-form-header />
|
||||
<app-form-body />
|
||||
<app-form-footer />
|
||||
`,
|
||||
selector: 'app-form-container',
|
||||
providers: [FormState],
|
||||
template: `
|
||||
<app-form-header />
|
||||
<app-form-body />
|
||||
<app-form-footer />
|
||||
`,
|
||||
})
|
||||
export class FormContainer {
|
||||
private formState = inject(FormState);
|
||||
private formState = inject(FormState);
|
||||
}
|
||||
|
||||
// Children share same instance
|
||||
@Component({
|
||||
selector: "app-form-body",
|
||||
template: `...`,
|
||||
selector: 'app-form-body',
|
||||
template: `...`,
|
||||
})
|
||||
export class FormBody {
|
||||
// Gets same instance as parent
|
||||
private formState = inject(FormState);
|
||||
// Gets same instance as parent
|
||||
private formState = inject(FormState);
|
||||
}
|
||||
|
||||
// Grandchildren also share
|
||||
@Component({
|
||||
selector: "app-form-field",
|
||||
template: `...`,
|
||||
selector: 'app-form-field',
|
||||
template: `...`,
|
||||
})
|
||||
export class FormField {
|
||||
// Gets same instance from ancestor
|
||||
private formState = inject(FormState);
|
||||
// Gets same instance from ancestor
|
||||
private formState = inject(FormState);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -245,20 +245,20 @@ export class FormField {
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: "app-tabs",
|
||||
// providers: Available to component AND content children
|
||||
providers: [TabsSvc],
|
||||
selector: 'app-tabs',
|
||||
// providers: Available to component AND content children
|
||||
providers: [TabsSvc],
|
||||
|
||||
// viewProviders: Available to component AND view children only
|
||||
// NOT available to content children (<ng-content>)
|
||||
viewProviders: [InternalTabs],
|
||||
// viewProviders: Available to component AND view children only
|
||||
// NOT available to content children (<ng-content>)
|
||||
viewProviders: [InternalTabs],
|
||||
|
||||
template: `
|
||||
<div class="tabs">
|
||||
<ng-content />
|
||||
<!-- Content children can't access viewProviders -->
|
||||
</div>
|
||||
`,
|
||||
template: `
|
||||
<div class="tabs">
|
||||
<ng-content />
|
||||
<!-- Content children can't access viewProviders -->
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class Tabs {}
|
||||
```
|
||||
@@ -338,56 +338,56 @@ import { PLATFORM_ID, isPlatformBrowser } from '@angular/common';
|
||||
### Mocking Services
|
||||
|
||||
```typescript
|
||||
describe("UserCmpt", () => {
|
||||
let userServiceSpy: jasmine.SpyObj<User>;
|
||||
describe('UserCmpt', () => {
|
||||
let userServiceSpy: jasmine.SpyObj<User>;
|
||||
|
||||
beforeEach(async () => {
|
||||
userServiceSpy = jasmine.createSpyObj("User", ["getUser", "updateUser"]);
|
||||
userServiceSpy.getUser.and.returnValue(of({ id: "1", name: "Test" }));
|
||||
beforeEach(async () => {
|
||||
userServiceSpy = jasmine.createSpyObj('User', ['getUser', 'updateUser']);
|
||||
userServiceSpy.getUser.and.returnValue(of({ id: '1', name: 'Test' }));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserCmpt],
|
||||
providers: [{ provide: User, useValue: userServiceSpy }],
|
||||
}).compileComponents();
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserCmpt],
|
||||
providers: [{ provide: User, useValue: userServiceSpy }],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it("should load user", () => {
|
||||
const fixture = TestBed.createComponent(UserCmpt);
|
||||
fixture.detectChanges();
|
||||
it('should load user', () => {
|
||||
const fixture = TestBed.createComponent(UserCmpt);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(userServiceSpy.getUser).toHaveBeenCalled();
|
||||
});
|
||||
expect(userServiceSpy.getUser).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Overriding Providers
|
||||
|
||||
```typescript
|
||||
describe("with different config", () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
})
|
||||
.overrideProvider(APP_CONFIG, {
|
||||
useValue: { apiUrl: "http://test-api.com" },
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
describe('with different config', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
})
|
||||
.overrideProvider(APP_CONFIG, {
|
||||
useValue: { apiUrl: 'http://test-api.com' },
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Injection Tokens
|
||||
|
||||
```typescript
|
||||
describe("API_URL token", () => {
|
||||
it("should provide correct URL", () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: API_URL, useValue: "https://api.test.com" }],
|
||||
});
|
||||
describe('API_URL token', () => {
|
||||
it('should provide correct URL', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{ provide: API_URL, useValue: 'https://api.test.com' }],
|
||||
});
|
||||
|
||||
const apiUrl = TestBed.inject(API_URL);
|
||||
expect(apiUrl).toBe("https://api.test.com");
|
||||
});
|
||||
const apiUrl = TestBed.inject(API_URL);
|
||||
expect(apiUrl).toBe('https://api.test.com');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -430,18 +430,18 @@ export class Data {
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class WebSocket {
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private socket: WebSocket | null = null;
|
||||
private destroyRef = inject(DestroyRef);
|
||||
private socket: WebSocket | null = null;
|
||||
|
||||
constructor() {
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.socket?.close();
|
||||
});
|
||||
}
|
||||
constructor() {
|
||||
this.destroyRef.onDestroy(() => {
|
||||
this.socket?.close();
|
||||
});
|
||||
}
|
||||
|
||||
connect(url: string) {
|
||||
this.socket = new WebSocket(url);
|
||||
}
|
||||
connect(url: string) {
|
||||
this.socket = new WebSocket(url);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user