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
+128 -128
View File
@@ -10,58 +10,54 @@ Create standalone components for Angular v20+. Components are standalone by defa
## Component Structure
```typescript
import {
Component,
ChangeDetectionStrategy,
input,
output,
computed,
} from "@angular/core";
import { Component, ChangeDetectionStrategy, input, output, computed } from '@angular/core';
@Component({
selector: "app-user-card",
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: "user-card",
"[class.active]": "isActive()",
"(click)": "handleClick()",
},
template: `
<img [src]="avatarUrl()" [alt]="name() + ' avatar'" />
<h2>{{ name() }}</h2>
@if (showEmail()) {
<p>{{ email() }}</p>
}
`,
styles: `
:host {
display: block;
}
:host.active {
border: 2px solid blue;
}
`,
selector: 'app-user-card',
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'class': 'user-card',
'[class.active]': 'isActive()',
'(click)': 'handleClick()',
},
template: `
<img
[src]="avatarUrl()"
[alt]="name() + ' avatar'" />
<h2>{{ name() }}</h2>
@if (showEmail()) {
<p>{{ email() }}</p>
}
`,
styles: `
:host {
display: block;
}
:host.active {
border: 2px solid blue;
}
`,
})
export class UserCard {
// Required input
name = input.required<string>();
// Required input
name = input.required<string>();
// Optional input with default
email = input<string>("");
showEmail = input(false);
// Optional input with default
email = input<string>('');
showEmail = input(false);
// Input with transform
isActive = input(false, { transform: booleanAttribute });
// Input with transform
isActive = input(false, { transform: booleanAttribute });
// Computed from inputs
avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
// Computed from inputs
avatarUrl = computed(() => `https://api.example.com/avatar/${this.name()}`);
// Output
selected = output<string>();
// Output
selected = output<string>();
handleClick() {
this.selected.emit(this.name());
}
handleClick() {
this.selected.emit(this.name());
}
}
```
@@ -78,7 +74,7 @@ count = input(0);
label = input<string>();
// With alias for template binding
size = input("medium", { alias: "buttonSize" });
size = input('medium', { alias: 'buttonSize' });
// With transform function
disabled = input(false, { transform: booleanAttribute });
@@ -88,14 +84,14 @@ value = input(0, { transform: numberAttribute });
## Signal Outputs
```typescript
import { output, outputFromObservable } from "@angular/core";
import { output, outputFromObservable } from '@angular/core';
// Basic output
clicked = output<void>();
selected = output<Item>();
// With alias
valueChange = output<number>({ alias: "change" });
valueChange = output<number>({ alias: 'change' });
// From Observable (for RxJS interop)
scroll$ = new Subject<number>();
@@ -112,41 +108,41 @@ Use the `host` object in `@Component`—do NOT use `@HostBinding` or `@HostListe
```typescript
@Component({
selector: "app-button",
host: {
// Static attributes
role: "button",
selector: 'app-button',
host: {
// Static attributes
'role': 'button',
// Dynamic class bindings
"[class.primary]": 'variant() === "primary"',
"[class.disabled]": "disabled()",
// Dynamic class bindings
'[class.primary]': 'variant() === "primary"',
'[class.disabled]': 'disabled()',
// Dynamic style bindings
"[style.--btn-color]": "color()",
// Dynamic style bindings
'[style.--btn-color]': 'color()',
// Attribute bindings
"[attr.aria-disabled]": "disabled()",
"[attr.tabindex]": "disabled() ? -1 : 0",
// Attribute bindings
'[attr.aria-disabled]': 'disabled()',
'[attr.tabindex]': 'disabled() ? -1 : 0',
// Event listeners
"(click)": "onClick($event)",
"(keydown.enter)": "onClick($event)",
"(keydown.space)": "onClick($event)",
},
template: `<ng-content />`,
// Event listeners
'(click)': 'onClick($event)',
'(keydown.enter)': 'onClick($event)',
'(keydown.space)': 'onClick($event)',
},
template: `<ng-content />`,
})
export class Button {
variant = input<"primary" | "secondary">("primary");
disabled = input(false, { transform: booleanAttribute });
color = input("#007bff");
variant = input<'primary' | 'secondary'>('primary');
disabled = input(false, { transform: booleanAttribute });
color = input('#007bff');
clicked = output<void>();
clicked = output<void>();
onClick(event: Event) {
if (!this.disabled()) {
this.clicked.emit();
onClick(event: Event) {
if (!this.disabled()) {
this.clicked.emit();
}
}
}
}
```
@@ -154,18 +150,18 @@ export class Button {
```typescript
@Component({
selector: "app-card",
template: `
<header>
<ng-content select="[card-header]" />
</header>
<main>
<ng-content />
</main>
<footer>
<ng-content select="[card-footer]" />
</footer>
`,
selector: 'app-card',
template: `
<header>
<ng-content select="[card-header]" />
</header>
<main>
<ng-content />
</main>
<footer>
<ng-content select="[card-footer]" />
</footer>
`,
})
export class Card {}
@@ -180,26 +176,26 @@ export class Card {}
## Lifecycle Hooks
```typescript
import { OnDestroy, OnInit, afterNextRender, afterRender } from "@angular/core";
import { OnDestroy, OnInit, afterNextRender, afterRender } from '@angular/core';
export class My implements OnInit, OnDestroy {
constructor() {
// For DOM manipulation after render (SSR-safe)
afterNextRender(() => {
// Runs once after first render
});
constructor() {
// For DOM manipulation after render (SSR-safe)
afterNextRender(() => {
// Runs once after first render
});
afterRender(() => {
// Runs after every render
});
}
afterRender(() => {
// Runs after every render
});
}
ngOnInit() {
/* Component initialized */
}
ngOnDestroy() {
/* Cleanup */
}
ngOnInit() {
/* Component initialized */
}
ngOnDestroy() {
/* Cleanup */
}
}
```
@@ -215,28 +211,26 @@ Components MUST:
```typescript
@Component({
selector: "app-toggle",
host: {
role: "switch",
"[attr.aria-checked]": "checked()",
"[attr.aria-label]": "label()",
tabindex: "0",
"(click)": "toggle()",
"(keydown.enter)": "toggle()",
"(keydown.space)": "toggle(); $event.preventDefault()",
},
template: `<span class="toggle-track"
><span class="toggle-thumb"></span
></span>`,
selector: 'app-toggle',
host: {
'role': 'switch',
'[attr.aria-checked]': 'checked()',
'[attr.aria-label]': 'label()',
'tabindex': '0',
'(click)': 'toggle()',
'(keydown.enter)': 'toggle()',
'(keydown.space)': 'toggle(); $event.preventDefault()',
},
template: `<span class="toggle-track"><span class="toggle-thumb"></span></span>`,
})
export class Toggle {
label = input.required<string>();
checked = input(false, { transform: booleanAttribute });
checkedChange = output<boolean>();
label = input.required<string>();
checked = input(false, { transform: booleanAttribute });
checkedChange = output<boolean>();
toggle() {
this.checkedChange.emit(!this.checked());
}
toggle() {
this.checkedChange.emit(!this.checked());
}
}
```
@@ -262,8 +256,7 @@ Use native control flow—do NOT use `*ngIf`, `*ngFor`, `*ngSwitch`.
}
<!-- Switch -->
@switch (status()) { @case ('pending') { <span>Pending</span> } @case ('active')
{ <span>Active</span> } @default { <span>Unknown</span> } }
@switch (status()) { @case ('pending') { <span>Pending</span> } @case ('active') { <span>Active</span> } @default { <span>Unknown</span> } }
```
## Class and Style Bindings
@@ -285,17 +278,24 @@ Do NOT use `ngClass` or `ngStyle`. Use direct bindings:
Use `NgOptimizedImage` for static images:
```typescript
import { NgOptimizedImage } from "@angular/common";
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
<img ngSrc="/assets/hero.jpg" width="800" height="600" priority />
<img [ngSrc]="imageUrl()" width="200" height="200" />
`,
imports: [NgOptimizedImage],
template: `
<img
ngSrc="/assets/hero.jpg"
width="800"
height="600"
priority />
<img
width="200"
height="200"
[ngSrc]="imageUrl()" />
`,
})
export class Hero {
imageUrl = input.required<string>();
imageUrl = input.required<string>();
}
```