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

This commit is contained in:
Dennis Hundertmark
2026-03-08 09:50:17 +01:00
parent 2184971175
commit 9d13cc652a
47 changed files with 15272 additions and 14144 deletions
@@ -14,28 +14,32 @@
For two-way binding with `[(value)]` syntax:
```typescript
import { Component, model } from "@angular/core";
import { Component, model } from '@angular/core';
@Component({
selector: "app-slider",
host: {
"(input)": "onInput($event)",
},
template: `
<input type="range" [value]="value()" [min]="min()" [max]="max()" />
<span>{{ value() }}</span>
`,
selector: 'app-slider',
host: {
'(input)': 'onInput($event)',
},
template: `
<input
type="range"
[value]="value()"
[min]="min()"
[max]="max()" />
<span>{{ value() }}</span>
`,
})
export class Slider {
// Model creates both input and output
value = model(0);
min = input(0);
max = input(100);
// Model creates both input and output
value = model(0);
min = input(0);
max = input(100);
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(Number(target.value));
}
onInput(event: Event) {
const target = event.target as HTMLInputElement;
this.value.set(Number(target.value));
}
}
// Usage: <app-slider [(value)]="sliderValue" />
@@ -52,29 +56,31 @@ value = model.required<number>();
Query elements and components in the template:
```typescript
import { Component, viewChild, viewChildren, ElementRef } from "@angular/core";
import { Component, viewChild, viewChildren, ElementRef } from '@angular/core';
@Component({
selector: "app-gallery",
template: `
<div #container class="gallery">
@for (image of images(); track image.id) {
<app-image-card [image]="image" />
}
</div>
`,
selector: 'app-gallery',
template: `
<div
#container
class="gallery">
@for (image of images(); track image.id) {
<app-image-card [image]="image" />
}
</div>
`,
})
export class Gallery {
images = input.required<Image[]>();
images = input.required<Image[]>();
// Query single element
container = viewChild.required<ElementRef<HTMLDivElement>>("container");
// Query single element
container = viewChild.required<ElementRef<HTMLDivElement>>('container');
// Query single component (optional)
firstCard = viewChild(ImageCard);
// Query single component (optional)
firstCard = viewChild(ImageCard);
// Query all matching components
allCards = viewChildren(ImageCard);
// Query all matching components
allCards = viewChildren(ImageCard);
}
```
@@ -83,64 +89,60 @@ export class Gallery {
Query projected content:
```typescript
import {
Component,
contentChild,
contentChildren,
effect,
signal,
} from "@angular/core";
import { Component, contentChild, contentChildren, effect, signal } from '@angular/core';
@Component({
selector: "app-tabs",
template: `
<div class="tab-headers">
@for (tab of tabs(); track tab.label()) {
<button [class.active]="tab === activeTab()" (click)="selectTab(tab)">
{{ tab.label() }}
</button>
}
</div>
<div class="tab-content">
<ng-content />
</div>
`,
selector: 'app-tabs',
template: `
<div class="tab-headers">
@for (tab of tabs(); track tab.label()) {
<button
[class.active]="tab === activeTab()"
(click)="selectTab(tab)">
{{ tab.label() }}
</button>
}
</div>
<div class="tab-content">
<ng-content />
</div>
`,
})
export class Tabs {
// Query all projected Tab children
tabs = contentChildren(Tab);
// Query all projected Tab children
tabs = contentChildren(Tab);
// Query single projected element
header = contentChild("tabHeader");
// Query single projected element
header = contentChild('tabHeader');
activeTab = signal<Tab | undefined>(undefined);
activeTab = signal<Tab | undefined>(undefined);
constructor() {
// Set first tab as active when tabs are available
effect(() => {
const firstTab = this.tabs()[0];
if (firstTab && !this.activeTab()) {
this.activeTab.set(firstTab);
}
});
}
constructor() {
// Set first tab as active when tabs are available
effect(() => {
const firstTab = this.tabs()[0];
if (firstTab && !this.activeTab()) {
this.activeTab.set(firstTab);
}
});
}
selectTab(tab: Tab) {
this.activeTab.set(tab);
}
selectTab(tab: Tab) {
this.activeTab.set(tab);
}
}
@Component({
selector: "app-tab",
template: `<ng-content />`,
host: {
"[class.active]": "isActive()",
"[style.display]": 'isActive() ? "block" : "none"',
},
selector: 'app-tab',
template: `<ng-content />`,
host: {
'[class.active]': 'isActive()',
'[style.display]': 'isActive() ? "block" : "none"',
},
})
export class Tab {
label = input.required<string>();
isActive = input(false);
label = input.required<string>();
isActive = input(false);
}
```
@@ -149,27 +151,27 @@ export class Tab {
Use `inject()` function instead of constructor injection:
```typescript
import { Component, inject } from "@angular/core";
import { Router } from "@angular/router";
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: "app-dashboard",
template: `...`,
selector: 'app-dashboard',
template: `...`,
})
export class Dashboard {
private router = inject(Router);
private userService = inject(User);
private config = inject(APP_CONFIG);
private router = inject(Router);
private userService = inject(User);
private config = inject(APP_CONFIG);
// Optional injection
private analytics = inject(Analytics, { optional: true });
// Optional injection
private analytics = inject(Analytics, { optional: true });
// Self-only injection
private localService = inject(Local, { self: true });
// Self-only injection
private localService = inject(Local, { self: true });
navigateToProfile() {
this.router.navigate(["/profile"]);
}
navigateToProfile() {
this.router.navigate(['/profile']);
}
}
```
@@ -180,18 +182,20 @@ export class Dashboard {
```typescript
// Parent
@Component({
template: `<app-child [data]="parentData()" [config]="config" />`,
template: `<app-child
[data]="parentData()"
[config]="config" />`,
})
export class Parent {
parentData = signal({ name: "Test" });
config = { theme: "dark" };
parentData = signal({ name: 'Test' });
config = { theme: 'dark' };
}
// Child
@Component({ selector: "app-child" })
@Component({ selector: 'app-child' })
export class Child {
data = input.required<Data>();
config = input<Config>();
data = input.required<Data>();
config = input<Config>();
}
```
@@ -200,25 +204,25 @@ export class Child {
```typescript
// Child
@Component({
selector: "app-child",
template: `<button (click)="save()">Save</button>`,
selector: 'app-child',
template: `<button (click)="save()">Save</button>`,
})
export class Child {
saved = output<Data>();
saved = output<Data>();
save() {
this.saved.emit({ id: 1, name: "Item" });
}
save() {
this.saved.emit({ id: 1, name: 'Item' });
}
}
// Parent
@Component({
template: `<app-child (saved)="onSaved($event)" />`,
template: `<app-child (saved)="onSaved($event)" />`,
})
export class Parent {
onSaved(data: Data) {
console.log("Saved:", data);
}
onSaved(data: Data) {
console.log('Saved:', data);
}
}
```
@@ -226,39 +230,37 @@ export class Parent {
```typescript
// Shared state service
@Injectable({ providedIn: "root" })
@Injectable({ providedIn: 'root' })
export class Cart {
private items = signal<CartItem[]>([]);
private items = signal<CartItem[]>([]);
readonly items$ = this.items.asReadonly();
readonly total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0),
);
readonly items$ = this.items.asReadonly();
readonly total = computed(() => this.items().reduce((sum, item) => sum + item.price, 0));
addItem(item: CartItem) {
this.items.update((items) => [...items, item]);
}
addItem(item: CartItem) {
this.items.update((items) => [...items, item]);
}
removeItem(id: string) {
this.items.update((items) => items.filter((i) => i.id !== id));
}
removeItem(id: string) {
this.items.update((items) => items.filter((i) => i.id !== id));
}
}
// Component A
@Component({ template: `<button (click)="add()">Add</button>` })
export class Product {
private cart = inject(Cart);
product = input.required<Product>();
private cart = inject(Cart);
product = input.required<Product>();
add() {
this.cart.addItem({ ...this.product(), quantity: 1 });
}
add() {
this.cart.addItem({ ...this.product(), quantity: 1 });
}
}
// Component B
@Component({ template: `<span>Total: {{ cart.total() }}</span>` })
export class CartSummary {
cart = inject(Cart);
cart = inject(Cart);
}
```
@@ -268,20 +270,20 @@ Using `@defer` for lazy loading:
```typescript
@Component({
template: `
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="chart-placeholder">Loading chart...</div>
} @loading (minimum 500ms) {
<app-spinner />
} @error {
<p>Failed to load chart</p>
}
`,
template: `
@defer (on viewport) {
<app-heavy-chart [data]="chartData()" />
} @placeholder {
<div class="chart-placeholder">Loading chart...</div>
} @loading (minimum 500ms) {
<app-spinner />
} @error {
<p>Failed to load chart</p>
}
`,
})
export class Dashboard {
chartData = input.required<ChartData>();
chartData = input.required<ChartData>();
}
```
@@ -297,16 +299,16 @@ Defer triggers:
```typescript
@Component({
template: `
@defer (on interaction; prefetch on idle) {
<app-comments [postId]="postId()" />
} @placeholder {
<button>Load Comments</button>
}
`,
template: `
@defer (on interaction; prefetch on idle) {
<app-comments [postId]="postId()" />
} @placeholder {
<button>Load Comments</button>
}
`,
})
export class Post {
postId = input.required<string>();
postId = input.required<string>();
}
```
@@ -314,19 +316,19 @@ export class Post {
```typescript
@Directive({
selector: "[appHighlight]",
host: {
"[style.backgroundColor]": "color()",
},
selector: '[appHighlight]',
host: {
'[style.backgroundColor]': 'color()',
},
})
export class Highlight {
color = input("yellow", { alias: "appHighlight" });
color = input('yellow', { alias: 'appHighlight' });
}
// Usage on component
@Component({
imports: [Highlight],
template: `<app-card appHighlight="lightblue" />`,
imports: [Highlight],
template: `<app-card appHighlight="lightblue" />`,
})
export class Page {}
```
@@ -335,24 +337,24 @@ export class Page {}
```typescript
@Component({
selector: "app-error-boundary",
template: `
@if (hasError()) {
<div class="error">
<h3>Something went wrong</h3>
<button (click)="retry()">Retry</button>
</div>
} @else {
<ng-content />
}
`,
selector: 'app-error-boundary',
template: `
@if (hasError()) {
<div class="error">
<h3>Something went wrong</h3>
<button (click)="retry()">Retry</button>
</div>
} @else {
<ng-content />
}
`,
})
export class ErrorBoundary {
hasError = signal(false);
private errorHandler = inject(ErrorHandler);
hasError = signal(false);
private errorHandler = inject(ErrorHandler);
retry() {
this.hasError.set(false);
}
retry() {
this.hasError.set(false);
}
}
```