From a9bac6d52bd304bc42add79b52375faf1a13bcb7 Mon Sep 17 00:00:00 2001 From: Luk Date: Sun, 27 Jul 2025 18:37:10 +0200 Subject: [PATCH 01/43] Add Firebase configuration files and Docker setup for Angular SSR application and LLMs files --- .firebaserc | 17 + Dockerfile | 35 + eslintrc.cjs | 102 + firebase.json | 38 + llms.txt | 129 - llms/app-llm.txt | 354 + llms/architecture.txt | 322 + llms/guidelines-copilot.md | 576 ++ llms/llm-full.txt | 14281 +++++++++++++++++++++++++++++ llms/prompt-copilot-refactor.txt | 116 + 10 files changed, 15841 insertions(+), 129 deletions(-) create mode 100644 .firebaserc create mode 100644 Dockerfile create mode 100644 eslintrc.cjs delete mode 100644 llms.txt create mode 100644 llms/app-llm.txt create mode 100644 llms/architecture.txt create mode 100644 llms/guidelines-copilot.md create mode 100644 llms/llm-full.txt create mode 100644 llms/prompt-copilot-refactor.txt diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..a5a06c0 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,17 @@ +{ + "projects": { + "default": "your-firebase-project-id" + }, + "targets": { + "your-firebase-project-id": { + "hosting": { + "admin": [ + "admin-site-id" + ], + "samples": [ + "samples-site-id" + ] + } + } + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2824339 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Dockerfile for Angular SSR (blog-ssg) on Cloud Run +# Multi-stage: build Angular bundles, then run the SSR server. +# Cloud Run expects the app to listen on PORT env var (default 8080). + +# ---------- Build stage ---------- +FROM node:20-bullseye AS builder +WORKDIR /workspace + +# Install deps with pnpm (recommended for speed). Fallback to npm if needed. +COPY package.json* pnpm-lock.yaml* package-lock.json* .npmrc* ./ +RUN corepack enable && corepack prepare pnpm@latest --activate || true +RUN if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else pnpm install; fi + +COPY . . + +# Build browser, server (SSR). Prerender can be done in CI before building the image. +RUN npx ng build blog-ssg --configuration=production +RUN npx ng run blog-ssg:server:production +# Optional: +# RUN npx ng run blog-ssg:prerender --routes=apps/blog-ssg/routes.txt + +# ---------- Runtime stage ---------- +FROM node:20-bullseye-slim AS runtime +ENV NODE_ENV=production +WORKDIR /srv + +# Copy built artifacts +COPY --from=builder /workspace/dist/apps/blog-ssg /srv/dist/apps/blog-ssg + +# Cloud Run provides PORT env. Angular SSR server must bind to this port. +ENV PORT=8080 +EXPOSE 8080 + +# Adjust path to the server entry if your project differs. +CMD ["node", "dist/apps/blog-ssg/server/server.mjs"] diff --git a/eslintrc.cjs b/eslintrc.cjs new file mode 100644 index 0000000..a13e10e --- /dev/null +++ b/eslintrc.cjs @@ -0,0 +1,102 @@ +/* .eslintrc.cjs — boundaries for angular.fun monorepo + * Layers per app: core, layout, ui, pattern, data-access, feature (lazy) + * Shared libs: shared/ui, shared/pattern, shared/data-access + */ +/* eslint-env node */ +module.exports = { + root: true, + ignorePatterns: ['**/*'], + overrides: [ + { + files: ['*.ts'], + parserOptions: { + project: ['tsconfig.json'], + sourceType: 'module', + }, + extends: [ + 'plugin:@angular-eslint/recommended', + 'plugin:@angular-eslint/template/process-inline-templates', + ], + plugins: ['boundaries'], + settings: { + 'boundaries/elements': [ + // ---- Shared libs ---- + { type: 'shared-ui', pattern: 'libs/shared/ui/**' }, + { type: 'shared-pattern', pattern: 'libs/shared/pattern/**' }, + { type: 'shared-data', pattern: 'libs/shared/data-access/**' }, + + // ---- Apps: blog-ssg ---- + { type: 'blog-core', pattern: 'apps/blog-ssg/src/app/core/**' }, + { type: 'blog-layout', pattern: 'apps/blog-ssg/src/app/layout/**' }, + { type: 'blog-ui', pattern: 'apps/blog-ssg/src/app/ui/**' }, + { type: 'blog-pattern', pattern: 'apps/blog-ssg/src/app/pattern/**' }, + { type: 'blog-data', pattern: 'apps/blog-ssg/src/app/data-access/**' }, + { type: 'blog-feature', pattern: 'apps/blog-ssg/src/app/feature/**' }, + + // ---- Apps: admin-spa ---- + { type: 'admin-core', pattern: 'apps/admin-spa/src/app/core/**' }, + { type: 'admin-layout', pattern: 'apps/admin-spa/src/app/layout/**' }, + { type: 'admin-ui', pattern: 'apps/admin-spa/src/app/ui/**' }, + { type: 'admin-pattern', pattern: 'apps/admin-spa/src/app/pattern/**' }, + { type: 'admin-data', pattern: 'apps/admin-spa/src/app/data-access/**' }, + { type: 'admin-feature', pattern: 'apps/admin-spa/src/app/feature/**' }, + + // ---- Apps: code-samples-mfe ---- + { type: 'samples-core', pattern: 'apps/code-samples-mfe/src/app/core/**' }, + { type: 'samples-layout', pattern: 'apps/code-samples-mfe/src/app/layout/**' }, + { type: 'samples-ui', pattern: 'apps/code-samples-mfe/src/app/ui/**' }, + { type: 'samples-pattern', pattern: 'apps/code-samples-mfe/src/app/pattern/**' }, + { type: 'samples-data', pattern: 'apps/code-samples-mfe/src/app/data-access/**' }, + { type: 'samples-feature', pattern: 'apps/code-samples-mfe/src/app/feature/**' } + ], + }, + rules: { + // Prevent unknown files from bypassing the rules + 'boundaries/no-unknown-files': 'error', + + // Forbid private imports across elements + 'boundaries/no-private': 'error', + + // Allowed import graph + 'boundaries/allowed-types': ['error', [ + // Shared layers + { from: ['shared-ui'], allow: [] }, + { from: ['shared-pattern'], allow: ['shared-ui'] }, + { from: ['shared-data'], allow: [] }, + + // blog-ssg + { from: ['blog-core'], allow: ['shared-data'] }, + { from: ['blog-layout'], allow: ['blog-core', 'shared-ui', 'shared-pattern'] }, + { from: ['blog-ui'], allow: ['shared-ui'] }, + { from: ['blog-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] }, + { from: ['blog-data'], allow: ['shared-data'] }, + { from: ['blog-feature'], allow: ['blog-core', 'blog-layout', 'blog-ui', 'blog-pattern', 'blog-data', 'shared-ui', 'shared-pattern', 'shared-data'] }, + + // admin-spa + { from: ['admin-core'], allow: ['shared-data'] }, + { from: ['admin-layout'], allow: ['admin-core', 'shared-ui', 'shared-pattern'] }, + { from: ['admin-ui'], allow: ['shared-ui'] }, + { from: ['admin-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] }, + { from: ['admin-data'], allow: ['shared-data'] }, + { from: ['admin-feature'], allow: ['admin-core', 'admin-layout', 'admin-ui', 'admin-pattern', 'admin-data', 'shared-ui', 'shared-pattern', 'shared-data'] }, + + // samples-mfe + { from: ['samples-core'], allow: ['shared-data'] }, + { from: ['samples-layout'], allow: ['samples-core', 'shared-ui', 'shared-pattern'] }, + { from: ['samples-ui'], allow: ['shared-ui'] }, + { from: ['samples-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] }, + { from: ['samples-data'], allow: ['shared-data'] }, + { from: ['samples-feature'], allow: ['samples-core', 'samples-layout', 'samples-ui', 'samples-pattern', 'samples-data', 'shared-ui', 'shared-pattern', 'shared-data'] } + ]], + + // Enforce public APIs (optional): only import from directories' public entry points + // 'boundaries/entry-point': ['error', [{ target: 'always', from: ['shared-ui', 'shared-pattern', 'shared-data'] }]] + }, + }, + { + files: ['*.html'], + extends: ['plugin:@angular-eslint/template/recommended'], + rules: {} + } + ] +}; diff --git a/firebase.json b/firebase.json index e69de29..3285260 100644 --- a/firebase.json +++ b/firebase.json @@ -0,0 +1,38 @@ +{ + "hosting": [ + { + "target": "admin", + "public": "dist/apps/admin-spa", + "ignore": ["**/.*", "**/node_modules/**"], + "rewrites": [{ "source": "**", "destination": "/index.html" }], + "headers": [ + { + "source": "**/*.@(js|css)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + } + ] + }, + { + "target": "samples", + "public": "dist/apps/code-samples-mfe", + "ignore": ["**/.*", "**/node_modules/**"], + "rewrites": [{ "source": "**", "destination": "/index.html" }], + "headers": [ + { + "source": "**/remoteEntry.@(js|mjs)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + }, + { + "source": "**/*.@(js|css)", + "headers": [ + { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } + ] + } + ] + } + ] +} diff --git a/llms.txt b/llms.txt deleted file mode 100644 index 47c724d..0000000 --- a/llms.txt +++ /dev/null @@ -1,129 +0,0 @@ -# Angular - -Angular — Deliver web apps with confidence 🚀 - -## Table of Contents - -- [What is Angular](https://angular.dev/overview) -- [Installation guide](https://angular.dev/installation) -- [Style Guide](https://next.angular.dev/style-guide) - -## Components - -- [What is a component](https://angular.dev/guide/components) -- [Component selectors](https://angular.dev/guide/components/selectors) -- [Styling components](https://angular.dev/guide/components/styling) -- [Accepting data with input properties](https://angular.dev/guide/components/inputs) -- [Custom events with output](https://angular.dev/guide/components/outputs) -- [Content projection](https://angular.dev/guide/components/content-projection) -- [Component lifecycle](https://angular.dev/guide/components/lifecycle) - -## Templates guides - -- [Template Overview](https://angular.dev/guide/templates) -- [Adding event listeners](https://angular.dev/guide/templates/event-listeners) -- [Binding text, properties and attributes](https://angular.dev/guide/templates/binding) -- [Control Flow](https://angular.dev/guide/templates/control-flow) -- [Template variable declaration](https://angular.dev/guide/templates/variables) -- [Deferred loading of components](https://angular.dev/guide/templates/defer) -- [Expression syntax](https://angular.dev/guide/templates/expression-syntax) - -## Directives - -- [Directives overview](https://angular.dev/guide/directives) -- [Attribute directives](https://angular.dev/guide/directives/attribute-directives) -- [Structural directives](https://angular.dev/guide/directives/structural-directives) -- [Directive composition](https://angular.dev/guide/directives/directive-composition-api) -- [Optimizing images](https://angular.dev/guide/image-optimization) - -## Signals - -- [Signals overview](https://angular.dev/guide/signals) -- [Dependent state with linkedSignal](https://angular.dev/guide/signals/linked-signal) -- [Async reactivity with resources](https://angular.dev/guide/signals/resource) - -## Dependency injection (DI) - -- [Dependency Injection overview](https://angular.dev/guide/di) -- [Understanding Dependency injection](https://angular.dev/guide/di/dependency-injection) -- [Creating an injectable service](https://angular.dev/guide/di/creating-injectable-service) -- [Configuring dependency providers](https://angular.dev/guide/di/dependency-injection-providers) -- [Injection context](https://angular.dev/guide/di/dependency-injection-context) -- [Hierarchical injectors](https://angular.dev/guide/di/hierarchical-dependency-injection) -- [Optimizing Injection tokens](https://angular.dev/guide/di/lightweight-injection-tokens) - -## RxJS - -- [RxJS interop with Angular signals](https://angular.dev/ecosystem/rxjs-interop) -- [Component output interop](https://angular.dev/ecosystem/rxjs-interop/output-interop) - -## Loading Data - -- [HttpClient overview](https://angular.dev/guide/http) -- [Setting up the HttpClient](https://angular.dev/guide/http/setup) -- [Making requests](https://angular.dev/guide/http/making-requests) -- [Intercepting requests](https://angular.dev/guide/http/interceptors) -- [Testing](https://angular.dev/guide/http/testing) - -## Forms -- [Forms overview](https://angular.dev/guide/forms) -- [Reactive Forms](https://angular.dev/guide/forms/reactive-forms) -- [Strictly types forms](https://angular.dev/guide/forms/typed-forms) -- [Template driven forms](https://angular.dev/guide/forms/template-driven-forms) -- [Validate forms input](https://angular.dev/guide/forms/form-validation) -- [Building dynamic forms](https://angular.dev/guide/forms/dynamic-forms) - -## Routing -- [Routing overview](https://angular.dev/guide/routing) -- [Common routing tasks](https://angular.dev/guide/routing/common-router-tasks) -- [Routing in an SPA](https://angular.dev/guide/routing/router-tutorial) -- [Creating custom route matches](https://angular.dev/guide/routing/routing-with-urlmatcher) - -## Server Side Rendering (SSR) - -- [SSR Overview](https://angular.dev/guide/performance) -- [SSR with Angular](https://angular.dev/guide/ssr) -- [Build-time prerendering (SSG)](https://angular.dev/guide/prerendering) -- [Hybrid rendering with server routing](https://angular.dev/guide/hybrid-rendering) -- [Hydration](https://angular.dev/guide/hydration) -- [Incremental Hydration](https://angular.dev/guide/incremental-hydration) - -# CLI -[Angular CLI Overview](https://angular.dev/tools/cli) - -## Testing - -- [Testing overview](https://angular.dev/guide/testing) -- [Testing coverage](https://angular.dev/guide/testing/code-coverage) -- [Testing services](https://angular.dev/guide/testing/services) -- [Basics of component testing](https://angular.dev/guide/testing/components-basics) -- [Component testing scenarios](https://angular.dev/guide/testing/components-scenarios) -- [Testing attribute directives](https://angular.dev/guide/testing/attribute-directives -- [Testing pipes](https://angular.dev/guide/testing/pipes -- [Debugging tests](https://angular.dev/guide/testing/debugging) -- [Testing utility apis](https://angular.dev/guide/testing/utility-apis) -- [Component harness overview](https://angular.dev/guide/testing/component-harnesses-overview) -- [Using component harness in tests](https://angular.dev/guide/testing/using-component-harnesses) -- [Creating a component harness for your components](https://angular.dev/guide/testing/creating-component-harnesses) - -## Animations -- [Animations your content](https://angular.dev/guide/animations/css) -- [Route transition animation](https://angular.dev/guide/animations/route-animations) -- [Migrating to native CSS animations](https://next.angular.dev/guide/animations/migration) - -## APIs -- [API reference](https://angular.dev/api) -- [CLI command reference](https://angular.dev/cli) - - -## Others - -- [Zoneless](https://angular.dev/guide/experimental/zoneless) -- [Error encyclopedia](https://angular.dev/errors) -- [Extended diagnostics](https://angular.dev/extended-diagnostics) -- [Update guide](https://angular.dev/update-guide) -- [Contribute to Angular](https://github.com/angular/angular/blob/main/CONTRIBUTING.md) -- [Angular's Roadmap](https://angular.dev/roadmap) -- [Keeping your projects up-to-date](https://angular.dev/update) -- [Security](https://angular.dev/best-practices/security) -- [Internationalization (i18n)](https://angular.dev/guide/i18n) diff --git a/llms/app-llm.txt b/llms/app-llm.txt new file mode 100644 index 0000000..a9daebf --- /dev/null +++ b/llms/app-llm.txt @@ -0,0 +1,354 @@ +# app-llm.txt — Copilot / LLM brief for angular.fun (Angular 20+) + +## Goal +Rebuild **angular.fun** as a clean Angular 20+ multi‑project workspace (no Nx) with three independently deployed apps and shared libraries: + +- **blog-ssg** (public): Hybrid rendering (**SSG for articles**, **SSR for home**; CSR where needed). Deployed to **Google Cloud Run** from source using buildpacks. SEO and Core Web Vitals are top priority. [docs: hybrid rendering, route‑level render mode, Cloud Run deploy from source] +- **admin-spa**: Pure CSR admin panel. Deployed to **Firebase Hosting**. +- **code-samples-mfe**: Micro‑frontend for heavy examples, loaded lazily from the blog via **Native Federation** (Angular Architects). Deployed to **Firebase Hosting**. +- **libs/shared**: UI (presentational), pattern (composable UI + injectables), data‑access (REST clients, mappers, Supabase helpers). + +**State**: Signals for local state; **NgRx SignalStore** for shared/domain state and collections. + +**Data**: Supabase (Postgres + Auth + Storage). Public content is fetched via **PostgREST REST endpoints** in SSG/SSR to avoid initializing `supabase-js`. Admin/MFE can use `@supabase/supabase-js`. Public assets come from **Supabase Storage CDN**. + +**A11y & Performance**: Aim for **100/100/100/100 Lighthouse** on the home page. Enforce with Lighthouse CI in GitHub Actions. Add Playwright + axe accessibility checks. + +--- + +## Projects & folders + +**Workspace (Angular CLI):** +- `apps/blog-ssg` — public reader app +- `apps/admin-spa` — admin +- `apps/code-samples-mfe` — remote +- `libs/shared/ui` — presentational, stateless standalone components +- `libs/shared/pattern` — reusable building blocks (standalone + service) +- `libs/shared/data-access` — REST clients, DTOs, adapters, Supabase REST helper + +**Inside each app:** +- `core/` — `provideCore({ routes })`, Http interceptors, error handling, analytics +- `layout/` — top‑level shells and frames +- `ui/` — purely presentational standalone components +- `pattern/` — configurable composites (can inject services, but keep DI stable) +- `feature/` — **always lazy** features per route +- Enforce one‑way dependency flow with **eslint-plugin-boundaries**. + +Rule of three: if a piece of functionality repeats ≥3 times, extract to `ui`, `pattern` or `data-access`. + +--- + +## Rendering model + +### blog-ssg (public) +- **Routes** + - `/` → **SSR** (freshness, future personalization) + - `/posts/:slug` → **SSG** (prerendered) + - others → choose CSR/SSR case‑by‑case via **route‑level render mode**. +- **Hydration** + - Enable hydration by default. + - For incompatible widgets, wrap with a component using `host: { ngSkipHydration: 'true' }`. +- **Deferrable views** + - Wrap heavy, non‑critical UI in `@defer` with placeholders and `on viewport/idle` triggers. +- **Images** + - Use `NgOptimizedImage`. LCP image must be priority. Use Supabase Storage public URLs as the origin; consider a thin loader function for width/format parameters if using the image resizing gateway. + +### admin-spa +- CSR only. Quill/editor packages lazy‑loaded here. No SSR/SSG or SEO work required. + +### code-samples-mfe +- Use **Native Federation**. Expose a routes module. Loaded lazily from blog with `loadRemoteModule('samples', './routes')`. Share Angular/RxJS as singletons. No SSR/SSG. + +--- + +## Data access + +- **Public read (blog-ssg)**: call **PostgREST** endpoints with the anon key and RLS‑safe policies. Prefer server‑side requests during prerender/SSR to avoid leaking keys to clients. For SSG, the build script generates `routes.txt` by fetching `id`/`slug` of published posts. +- **Admin/MFE**: use `@supabase/supabase-js` browser client. +- **Auth**: already implemented with Supabase. When SSR needs session, use `@supabase/ssr` and `createServerClient` with cookies (Cloud Run). +- **Storage**: serve images from Supabase **CDN** (public bucket). Control cache with versioned filenames or signed URLs if you need cache‑busting. + +--- + +## State management (strict) + +- Local component state: `signal`, `computed`, `linkedSignal`. +- Shared/domain state: **NgRx SignalStore** (`@ngrx/signals`): + - Structure with `withState`, `withComputed`, `withMethods`, `withHooks`. + - Immutable updates via **`patchState`** (no mutations). + - Collections: `withEntities` + updaters (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, …). + - RxJS interop: `rxMethod` for effectful pipelines; use Angular `toSignal`/`toObservable` where needed. + +--- + +## Tooling & versions + +- **Angular 20+**. +- **Node.js LTS**: **20.19+** (Angular 20 drops Node 18). Configure Actions runners to Node 20. +- **Package manager**: `pnpm` recommended (fast, content‑addressable store). Use `actions/setup-node` caching with `cache: 'pnpm'`. + +--- + +## CI/CD + +### Overview +- Separate jobs for each app. Common `test` job first. +- **blog-ssg**: build (`ng build` + `ng run blog-ssg:server` + prerender), then **deploy to Cloud Run**. Prefer **Workload Identity Federation** instead of JSON key when possible. +- **admin-spa** and **code-samples-mfe**: build and **deploy to Firebase Hosting** (each to its own site/target). Use `firebase init hosting:github` to scaffold. +- **Lighthouse CI**: run on the preview/live URL of the **home page** only. Thresholds set to 1.00 for all categories; fail PRs on regressions. +- **Accessibility**: Playwright + `@axe-core/playwright` scan of the home page gate in CI. + +### Monorepo workflow (sketch) +See `.github/workflows/ci.yml` below for a consolidated workflow. + +--- + +## Example snippets + +### 1) Route-level render mode (blog-ssg) +```ts +import type { ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { path: '', renderMode: 'ssr' }, // home + { path: 'posts/:slug', renderMode: 'ssg' }, // articles + { path: 'search', renderMode: 'csr' }, // client-only +]; +``` + +### 2) Native Federation lazy route (blog → samples) +```ts +import { Routes } from '@angular/router'; +import { loadRemoteModule } from '@angular-architects/native-federation'; + +export const routes: Routes = [ + { + path: 'examples', + loadChildren: () => loadRemoteModule('samples', './routes').then(m => m.routes), + }, +]; +``` + +### 3) SignalStore skeleton +```ts +import { signalStore, withState, withComputed, withMethods, withHooks, patchState } from '@ngrx/signals'; +import { computed, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +interface Post { id: string; slug: string; title: string; html: string; } +interface State { loading: boolean; error: string | null; items: Post[]; selectedId: string | null; } +const initial: State = { loading: false, error: null, items: [], selectedId: null }; + +export const PostsStore = signalStore( + { providedIn: 'root' }, + withState(initial), + withComputed(({ items, selectedId }) => ({ + count: computed(() => items().length), + selected: computed(() => items().find(p => p.id === selectedId())), + })), + withMethods((store, http = inject(HttpClient)) => ({ + async loadAll() { + patchState(store, { loading: true, error: null }); + try { + const data = await http.get('/api/posts').toPromise(); + patchState(store, { items: data ?? [], loading: false }); + } catch (e: any) { + patchState(store, { loading: false, error: String(e?.message ?? e) }); + } + }, + select(id: string | null) { patchState(store, { selectedId: id }); }, + })), + withHooks({ onInit() {/* optional prefetch */} }), +); +``` + +### 4) `routes.txt` generator (Node script using PostgREST) +```ts +// scripts/generate-routes.ts +// Usage: node scripts/generate-routes.mjs > apps/blog-ssg/routes.txt +import fs from 'node:fs/promises'; +import process from 'node:process'; + +const REST_URL = process.env.SUPABASE_REST_URL; // https://.supabase.co/rest/v1 +const ANON_KEY = process.env.SUPABASE_ANON_KEY; +if (!REST_URL || !ANON_KEY) { + console.error('Missing SUPABASE_REST_URL or SUPABASE_ANON_KEY'); process.exit(1); +} + +const headers = { + apikey: ANON_KEY, + Authorization: `Bearer ${ANON_KEY}`, + Prefer: 'count=exact', +}; + +// Query only published posts and select slug +const url = new URL('/rest/v1/posts', REST_URL); +url.searchParams.set('select', 'slug'); +url.searchParams.set('published', 'eq.true'); +url.searchParams.set('order', 'id.asc'); +url.searchParams.set('limit', '10000'); + +const res = await fetch(url, { headers }); +if (!res.ok) { + console.error('PostgREST error', res.status, await res.text()); process.exit(1); +} +const rows = await res.json(); +const routes = ['/', ...rows.map((r) => `/posts/${r.slug}`)]; +await fs.writeFile('apps/blog-ssg/routes.txt', routes.join('\n'), 'utf8'); +console.log(`Generated ${routes.length} routes.`); +``` + +### 5) Playwright + axe accessibility test (home page) +```ts +// e2e/a11y.spec.ts +import { test, expect } from '@playwright/test'; +import { AxeBuilder } from '@axe-core/playwright'; + +test('home has no critical a11y violations', async ({ page }) => { + await page.goto(process.env.E2E_BASE_URL ?? 'https://angular.fun/'); + const accessibilityScanResults = await new AxeBuilder({ page }) + .withTags(['wcag2a', 'wcag2aa']) + .analyze(); + expect(accessibilityScanResults.violations).toEqual([]); +}); +``` + +### 6) GitHub Actions (monorepo, Node 20, pnpm, Cloud Run + Firebase + LHCI) +```yaml +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + NODE_VERSION: '20.19.0' # Angular 20 requires Node >= 20.19.x + PROJECT_ID: your-gcp-project + REGION: europe-west4 + CLOUD_RUN_SERVICE: angular-blog + BLOG_URL: https://angular.fun/ + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + - uses: pnpm/action-setup@v4 + with: { run_install: false } + - run: corepack enable + - run: pnpm install --frozen-lockfile + - run: pnpm run lint + - run: pnpm run test -- --watch=false --browsers=ChromeHeadless + + blog: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + - uses: pnpm/action-setup@v4 + with: { run_install: false } + - run: corepack enable + - run: pnpm install --frozen-lockfile + - name: Generate prerender routes + run: node scripts/generate-routes.mjs + env: + SUPABASE_REST_URL: ${{ secrets.SUPABASE_REST_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + - name: Build blog (browser + server + prerender) + run: | + pnpm ng build blog-ssg --configuration=production + pnpm ng run blog-ssg:server:production + pnpm ng run blog-ssg:prerender --routes=apps/blog-ssg/routes.txt + - uses: google-github-actions/auth@v2 + with: + # Recommend switching to Workload Identity Federation later + credentials_json: ${{ secrets.GCP_SA_KEY }} + - name: Deploy to Cloud Run (buildpacks from source) + uses: google-github-actions/deploy-cloudrun@v2 + with: + service: ${{ env.CLOUD_RUN_SERVICE }} + region: ${{ env.REGION }} + source: . + # Optionally pass environment variables to the container if using start script + - name: Lighthouse CI + run: | + pnpm dlx @lhci/cli autorun --collect.url=${{ env.BLOG_URL }} --assert.assertions.categories:performance=">=1" --assert.assertions.categories:accessibility=">=1" --assert.assertions.categories:best-practices=">=1" --assert.assertions.categories:seo=">=1" + + admin: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + - uses: pnpm/action-setup@v4 + with: { run_install: false } + - run: corepack enable + - run: pnpm install --frozen-lockfile + - run: pnpm ng build admin-spa --configuration=production + - name: Deploy to Firebase Hosting (admin) + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} + channelId: live + projectId: your-firebase-project + target: admin + + samples: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + - uses: pnpm/action-setup@v4 + with: { run_install: false } + - run: corepack enable + - run: pnpm install --frozen-lockfile + - run: pnpm ng build code-samples-mfe --configuration=production + - name: Deploy to Firebase Hosting (samples) + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} + channelId: live + projectId: your-firebase-project + target: samples +``` + +> **Note**: Cloud Run deploys from source using buildpacks. Ensure `package.json` has a `start` script that launches the SSR server (e.g. `node dist/apps/blog-ssg/server/server.mjs`). Alternatively, switch to a **Dockerfile** with an explicit build and run command if you need more control. + +--- + +## i18n roadmap + +- Start English‑only. Next locale: **pl**. +- Use Angular i18n (`@angular/localize`) and extraction. Configure localized builds and route prefixes (`/en/...`, `/pl/...`). The dev server can run **one locale at a time**; use `ng serve --configuration=pl` when needed. +- For SSG, generate routes per locale (e.g. `apps/blog-ssg/routes.en.txt`, `routes.pl.txt`) and run prerender for both. Route‑level render mode still applies per locale. + +--- + +## SEO decisions + +- Generate server‑side meta (title, description, canonical, Open Graph, Twitter). For `og:image`, generate images at publish time and serve from Supabase Storage CDN. Consider adding a small Cloud Run job/Function to generate OG images on demand if needed. + +--- + +## Styling +- **Tailwind + DaisyUI**. Provide design tokens (colors, spacing, font scale). Set CSS budget limits in `angular.json`. Monitor CSS size in CI. + diff --git a/llms/architecture.txt b/llms/architecture.txt new file mode 100644 index 0000000..733d38a --- /dev/null +++ b/llms/architecture.txt @@ -0,0 +1,322 @@ +This architecture organizes an Angular 19+ workspace around strict isolation, a one‑way dependency graph, and explicit template contexts provided by standalone components. Features are always lazy and treated as black boxes: they expose UI through routes (or “drop‑in” pattern components) but never leak implementation. Shared logic follows the “extract one level up” rule into core, ui, or pattern, preserving independence and preventing cycles. Injector hierarchy is used deliberately: singletons are provided in the eager root; feature‑scoped providers live on each lazy EnvironmentInjector; element‑level providers are reserved for special cases. + +The stack relies on the standalone APIs (`provideRouter`, `provideHttpClient`, functional guards/interceptors) and esbuild builders for fast feedback. Lazy routing (`loadChildren`) and `@defer` split bundles where it matters; UI standalones are “cherry‑picked,” keeping eager bundles small. Automated validation with `eslint-plugin-boundaries` encodes the architecture so dependency mistakes fail CI, removing “hope‑based architecture.” Esbuild provides significantly faster builds and enables even sizable apps to remain on a standard Angular CLI workspace. + +────────────────────────────────────────────────────────────────────────────── +Workspace & folder tree (with purposes) + +Note: The same rules apply whether the workspace uses /projects or /apps + /libs. This plan uses /apps and /libs as requested. + +/apps ........................................ root for runnable applications (browser, admin, etc.). + /my-app .................................... primary Angular SPA. + /src + /app ................................... application code organized by architecture types (core, layout, ui, pattern, feature). + /core ................................ application-wide singletons, DI setup, domain services, state, and initialization logic. + /auth .............................. domain folder: auth state/services, tokens, functional guards. + auth.service.ts + auth.guard.ts .................... functional guard; inject AuthService, Router. + auth.tokens.ts + index.ts + /user .............................. domain: user profile clients, selectors, mappers. + /http .............................. cross-cutting HTTP interceptors/options. Functional interceptors via provideHttpClient. + logging.interceptor.ts + auth.interceptor.ts + /utils ............................. pure functions (dates, query params, parsing). + core.ts ............................ provideCore({ routes }) — central app providers. + index.ts + /layout .............................. eager shell(s) that frame routed features; consumes core services & ui standalones. + /main + main-layout.component.ts + main-layout.component.html + index.ts + /auth + auth-layout.component.ts + auth-layout.component.html + index.ts + /ui .................................. generic, state‑free standalones (components/directives/pipes) using only @Input/@Output; no services. + /avatar + avatar.component.ts + index.ts + /button + button.component.ts + /table + data-table.component.ts + /form + form-field.component.ts + /pattern ............................. “drop‑in” reusable patterns: packaged standalones + injectables, consumed by features/layouts. + /document-manager + document-manager.component.ts .... main drop‑in entry; heavy parts can be deferred in template. + document.service.ts + index.ts + /approval-process + approval.component.ts + approval.service.ts + /change-history + change-history.component.ts + change-history.service.ts + /notes + notes.component.ts + notes.service.ts + /feature ............................. lazy‑loaded business capabilities, fully isolated; may contain nested lazy sub‑features. + /home .............................. example initial feature — still lazy to keep a single mental model. + home.routes.ts ................... export default Routes with component + children. + home.component.ts + index.ts + /orders ............................ domain feature (lazy). + orders.routes.ts ................. route config; providers:[] for feature-scoped services. + /data ............................ feature-scoped services, repositories, adapters (provided via routes). + order.api.ts + order.service.ts + tokens.ts + index.ts + /ui .............................. feature-local standalones (not generic enough for /ui). Extract to /ui after 3+ usages. + order-list.component.ts + order-details.component.ts + /sub-feature ..................... nested lazy sub-features for second-level navigation. + /details + details.routes.ts + details.component.ts + index.ts + /customers + customers.routes.ts + /data + /ui + /sub-feature + /shared-detail ..................... shared lazy feature (reused via route config, not direct TS imports). + detail.routes.ts + detail.component.ts + app.component.ts ..................... either or . + app.routes.ts ........................ top-level routes using layouts & lazy features. + app.config.ts ........................ provideCore({ routes }) + provideHttpClient(...). + /assets ................................ static assets. + /environments .......................... env files for build-time replacements. + /e2e ..................................... end‑to‑end tests for this app (Playwright/Cypress). + +/libs ........................................ organization libraries when the workspace grows; consumed via public APIs. + /data-access ............................... shareable backend SDKs or adapters, exported via public-api.ts. + /public-api.ts + /src/lib/... + /ui ........................................ cross‑app generic UI library if needed (mirror /apps/*/ui rules). + /pattern ................................... cross‑app patterns; consumed by features/layouts. + +/tools ....................................... tooling scripts & configs (madge graph, budgets, schematics, ESLint config helpers). + /madge + madge.config.mjs ........................ settings to visualize dependency graphs. + /schematics + README.md ................................ notes for ng-morph/schematics to generate features, guards, routes. + /.eslintrc.js .............................. central ESLint + boundaries config (see below). + +/e2e ......................................... optional global E2E project when testing multiple apps. + +/.eslintignore +/.eslintrc.js ................................ alternative location for ESLint config. +/tsconfig.json +/angular.json ............................... use esbuild builders and budgets. +/package.json + +Routing & lazy loading examples + +Top-level routes with two layouts and lazy features: + +/apps/my-app/src/app/app.routes.ts +---------------------------------------------------------------- +export const routes: Routes = [ + { + path: '', + component: AuthLayoutComponent, + children: [ + { path: 'login', loadChildren: () => import('./feature/home/home.routes') }, + // signup, recovery... + ], + }, + { + path: 'app', + component: MainLayoutComponent, + children: [ + { path: 'orders', loadChildren: () => import('./feature/orders/orders.routes') }, + { path: 'customers', loadChildren: () => import('./feature/customers/customers.routes') }, + ], + }, + { path: '', pathMatch: 'full', redirectTo: 'app/orders' }, + { path: '**', redirectTo: '' }, +]; + +Feature route with feature‑scoped providers (isolation): + +/apps/my-app/src/app/feature/orders/orders.routes.ts +---------------------------------------------------------------- +import { Routes } from '@angular/router'; +import { provideOrders } from './orders.providers'; + +export default [ + { + path: '', + providers: [provideOrders()], + loadComponent: () => import('./ui/order-list.component').then(m => m.OrderListComponent), + children: [ + { path: ':id', loadChildren: () => import('../shared-detail/detail.routes') }, + ], + }, +] as Routes; + +@defer for heavy widgets inside a feature: + +apps/my-app/src/app/feature/orders/ui/order-details.component.html +---------------------------------------------------------------- +@defer (on viewport) { + +} @placeholder { + +} + +Application bootstrap & providers + +Centralized core provider: + +apps/my-app/src/app/core/core.ts +---------------------------------------------------------------- +import { Routes, provideRouter, withComponentInputBinding, withEnabledBlockingInitialNavigation, withInMemoryScrolling, withRouterConfig } from '@angular/router'; +import { ENVIRONMENT_INITIALIZER, importProvidersFrom } from '@angular/core'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { authInterceptor } from './auth/auth.interceptor'; + +export interface CoreOptions { routes: Routes; } + +export function provideCore({ routes }: CoreOptions) { + return [ + provideAnimationsAsync(), + provideRouter( + routes, + withRouterConfig({ onSameUrlNavigation: 'reload' }), + withComponentInputBinding(), + withEnabledBlockingInitialNavigation(), + withInMemoryScrolling({ anchorScrolling: 'enabled', scrollPositionRestoration: 'enabled' }), + ), + provideHttpClient(withInterceptors([authInterceptor])), + importProvidersFrom(/* third‑party NgModules if unavoidable */), + { + provide: ENVIRONMENT_INITIALIZER, + multi: true, + useValue() { + // kick-start processes, app metrics, warm-ups… + }, + }, + ]; +} + +Bootstrapping: + +apps/my-app/src/app/app.config.ts +---------------------------------------------------------------- +import { ApplicationConfig } from '@angular/core'; +import { routes } from './app.routes'; +import { provideCore } from './core/core'; + +export const appConfig: ApplicationConfig = { + providers: [provideCore({ routes })], +}; + +Testing conventions + +• Unit tests: colocate *.spec.ts next to implementation in each folder. Feature tests cover only that feature; changes in one feature must not break others. +• E2E: per‑app E2E in /apps/my-app/e2e or a central /e2e when scenarios span multiple apps. Prefer deep links for nested routes. + +Heuristics & rules of thumb + +• Every feature is lazy; even the first one. +• If feature A needs logic from feature B, extract one level up to core (injectables/state), ui (generic standalones), or pattern (configurable drop‑in). Never import a sibling feature. +• UI in /ui is standalone‑only (no services). If a component needs data, the parent feature provides it via inputs/outputs; if coupling grows, convert to a pattern. +• Prefer function‑based guards/interceptors/services using inject(); reserve element providers for rare per‑component instances. +• Prioritize isolation over DRY. Wait for at least 3 real repetitions before extracting. +• Use esbuild builders; analyze bundles with budgets and visualize imports with madge to ensure a clean one‑way dependency flow. + +ESLint — eslint-plugin-boundaries config stub +(Place in /.eslintrc.js or /tools/.eslintrc.js) + +```js +// .eslintrc.js — boundaries config stub for /apps + /libs layout +/** @type {import('eslint').Linter.Config} */ +module.exports = { + overrides: [ + { + files: ['*.ts'], + plugins: ['boundaries'], + extends: [ + // ...your standard Angular/TS presets, + 'plugin:boundaries/strict', // all files must belong to a known type + ], + settings: { + 'import/resolver': { typescript: { alwaysTryTypes: true } }, + 'boundaries/dependency-nodes': ['import', 'dynamic-import'], + 'boundaries/ignore': [ + '**/jest*.ts', + '**/*.spec.ts', + '**/e2e/**', + ], + 'boundaries/elements': [ + { type: 'main', mode: 'file', pattern: 'main.ts', basePattern: 'apps/**/src', baseCapture: ['app'] }, + { type: 'app', mode: 'file', pattern: 'app(.*).ts', basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + + { type: 'core', pattern: 'core', basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + { type: 'ui', pattern: 'ui', basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + { type: 'layout', pattern: 'layout', basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + { type: 'pattern', pattern: 'pattern', basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + + { type: 'feature-routes', mode: 'file', pattern: 'feature/*/*.routes.ts', capture: ['feature'], basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + { type: 'feature', pattern: 'feature/*', capture: ['feature'], basePattern: 'apps/**/src/app', baseCapture: ['app'] }, + + { type: 'lib-api', mode: 'file', pattern: 'libs/*/public-api.ts', capture: ['lib'] }, + { type: 'lib', pattern: 'libs/*/src/lib', capture: ['lib'] }, + ], + }, + rules: { + 'boundaries/element-types': ['error', { + default: 'disallow', + rules: [ + { from: 'main', allow: [['app', { app: '${from.app}' }]] }, + + { from: 'core', allow: [['core', { app: '${from.app}' }], ['lib-api']] }, + { from: 'ui', allow: [['ui', { app: '${from.app}' }], ['lib-api']] }, + { from: 'layout', allow: [ + ['core', { app: '${from.app}' }], + ['ui', { app: '${from.app}' }], + ['pattern', { app: '${from.app}' }], + ['lib-api'], + ]}, + { from: 'app', allow: [ + ['app', { app: '${from.app}' }], + ['core', { app: '${from.app}' }], + ['layout', { app: '${from.app}' }], + ['feature-routes', { app: '${from.app}' }], + ['lib-api'], + ]}, + { from: 'pattern', allow: [ + ['core', { app: '${from.app}' }], + ['ui', { app: '${from.app}' }], + ['pattern', { app: '${from.app}' }], + ['lib-api'], + ]}, + { from: 'feature', allow: [ + ['core', { app: '${from.app}' }], + ['ui', { app: '${from.app}' }], + ['pattern', { app: '${from.app}' }], + ['lib-api'], + ]}, + { from: 'feature-routes', allow: [ + ['core', { app: '${from.app}' }], + ['pattern', { app: '${from.app}' }], + ['feature', { app: '${from.app}', feature: '${from.feature}' }], + ['feature-routes', { app: '${from.app}', feature: '!${from.feature}' }], + ['lib-api'], + ]}, + + { from: 'lib-api', allow: [['lib', { lib: '${from.lib}' }]] }, + { from: 'lib', allow: [['lib', { lib: '${from.lib}' }]] }, + ], + }], + }, + }, + ], +}; +``` diff --git a/llms/guidelines-copilot.md b/llms/guidelines-copilot.md new file mode 100644 index 0000000..71a2b12 --- /dev/null +++ b/llms/guidelines-copilot.md @@ -0,0 +1,576 @@ +# Persona + +You are a dedicated Angular developer who thrives on leveraging the absolute latest features of the framework to build cutting-edge applications. You are currently immersed in Angular v20+, passionately adopting signals for reactive state management, embracing standalone components for streamlined architecture, and utilizing the new control flow for more intuitive template logic. Performance is paramount to you, who constantly seeks to optimize change detection and improve user experience through these modern Angular paradigms. When prompted, assume You are familiar with all the newest APIs and best practices, valuing clean, efficient, and maintainable code. + +## Examples + +These are modern examples of how to write an Angular 20 component with signals + +```ts +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; + + +@Component({ + selector: '{{tag-name}}-root', + templateUrl: '{{tag-name}}.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class { { + ClassName +} +} +{ +protected readonly + isServerRunning = signal(true); + toggleServerStatus() + { + this.isServerRunning.update(isServerRunning => !isServerRunning); + } +} +``` + +```css +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + + button { + margin-top: 10px; + } +} +``` + +```html + +
+ @if (isServerRunning()) { + Yes, the server is running + } @else { + No, the server is not running + } + +
+``` + +When you update a component, be sure to put the logic in the ts file, the styles in the css file and the html template in the html file. + +## Resources + +Here are some links to the essentials for building Angular applications. Use these to get an understanding of how some of the core functionality works +https://angular.dev/essentials/components +https://angular.dev/essentials/signals +https://angular.dev/essentials/templates +https://angular.dev/essentials/dependency-injection + +## Best practices & Style guide + +Here are the best practices and the style guide information. + +### Coding Style guide + +Here is a link to the most recent Angular style guide https://angular.dev/style-guide + +### TypeScript Best Practices + +- Use strict type checking +- Prefer type inference when the type is obvious +- Avoid the `any` type; use `unknown` when type is uncertain + +### Angular Best Practices + +- Always use standalone components over `NgModules` +- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators +- Use signals for state management +- Implement lazy loading for feature routes +- Use `NgOptimizedImage` for all static images. +- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead + +### Components + +- Keep components small and focused on a single responsibility +- Use `input()` signal instead of decorators, learn more here https://angular.dev/guide/components/inputs +- Use `output()` function instead of decorators, learn more here https://angular.dev/guide/components/outputs +- Use `computed()` for derived state learn more about signals here https://angular.dev/guide/signals. +- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator +- Prefer inline templates for small components +- Prefer Reactive forms instead of Template-driven ones +- Do NOT use `ngClass`, use `class` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings +- DO NOT use `ngStyle`, use `style` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings + +### State Management + +- Use signals for local component state +- Use `computed()` for derived state +- Keep state transformations pure and predictable +- Do NOT use `mutate` on signals, use `update` or `set` instead + +### Templates + +- Keep templates simple and avoid complex logic +- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch` +- Use the async pipe to handle observables +- Use built in pipes and import pipes when being used in a template, learn more https://angular.dev/guide/templates/pipes# + +### Services + +- Design services around a single responsibility +- Use the `providedIn: 'root'` option for singleton services +- Use the `inject()` function instead of constructor injection + +# guidelines-copilot.md — uzupełnienia + +## Workspace + +- Monorepo Angular CLI (bez Nx). Projekty: `apps/blog-ssg` (SSR/SSG), `apps/admin-spa` (SPA), `apps/code-samples-mfe` (MFE), biblioteki w `libs/`. +- Architektura według `architecture.txt`: `core/`, `layout/`, `ui/`, `pattern/`, `feature/` (lazy). Granice wymusza `eslint-plugin-boundaries`. Feature’y nie importują się nawzajem. + +## Rendering + +- Blog: hybrydowo (SSR + SSG). Generuj `routes.txt` lub `getPrerenderParams`. Ciężkie widgety pod `@defer`. W razie problemów z hydracją: `host: { ngSkipHydration: 'true' }`. +- Admin: wyłącznie CSR. +- Code‑samples: mikro‑frontend ładowany przez Module/Native Federation; brak prerenderingu. + +## State Management — **używaj NgRx SignalStore** + +**Zasady ogólne** + +- Drobny stan lokalny w komponentach: `signal`, `computed`, `linkedSignal`. +- Stan domenowy współdzielony: **`@ngrx/signals`**. +- Transformacje są czyste; **bez mutacji**. Aktualizacje przez `patchState(store, partial)`. +- Obliczenia wtórne przez `withComputed`. Logika imperatywna i side‑effects w `withMethods` (serwisy HTTP, router itp.). +- Inicjalizacja/cleanup przez `withHooks({ onInit, onDestroy })`. +- Dla kolekcji encji używaj `withEntities` i updaterów (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, itd.). +- Asynchroniczność: + - Proste pobrania: `async/await` + `firstValueFrom` wewnątrz metod store lub + - **`rxMethod`** (z `@ngrx/signals/rxjs-interop`) do reakcji na zmiany sygnałów/zdarzeń z operatorskim pipeline (debounce, switchMap, cancelation) albo + - **`resource`** (Angular) + `withProps` do odpornego na race conditions ładowania. +- Interoperacyjność: `toSignal(observable)`, `toObservable(signal)` z `@angular/core/rxjs-interop`. + +**Minimalny szablon Store** + +```ts +import {inject, computed} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import { + signalStore, withState, withComputed, withMethods, withHooks, patchState, +} from '@ngrx/signals'; + +interface State { + loading: boolean; + error: string | null; + items: Item[]; + selectedId: string | null; +} + +const initialState: State = {loading: false, error: null, items: [], selectedId: null}; + +export const ExampleStore = signalStore( + {providedIn: 'root'}, + withState(initialState), + withComputed(({items, selectedId}) => ({ + selected: computed(() => items().find(i => i.id === selectedId())), + count: computed(() => items().length), + })), + withMethods((store, http = inject(HttpClient)) => ({ + async load() { + patchState(store, {loading: true, error: null}); + try { + const data = await http.get('/api/items').toPromise(); + patchState(store, {items: data ?? [], loading: false}); + } catch (e: any) { + patchState(store, {loading: false, error: String(e?.message ?? e)}); + } + }, + select(id: string | null) { + patchState(store, {selectedId: id}); + }, + upsert(item: Item) { + const list = store.items(); + const i = list.findIndex(x => x.id === item.id); + patchState(store, {items: i === -1 ? [...list, item] : list.map(x => x.id === item.id ? item : x)}); + }, + })), + withHooks({ + onInit() {/* opcjonalnie */ + } + }), +); +``` + +**rxMethod — reagowanie na zmiany sygnałów** + +```ts +import {rxMethod} from '@ngrx/signals/rxjs-interop'; +import {debounceTime, distinctUntilChanged, switchMap, tap, catchError, of} from 'rxjs'; + +export const SearchStore = signalStore( + withState({query: '', results: [] as Result[], loading: false, error: null as string | null}), + withMethods((store, svc = inject(SearchService)) => { + const search = rxMethod(pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => patchState(store, {loading: true, error: null})), + switchMap(q => svc.search(q).pipe( + tap(results => patchState(store, {results, loading: false})), + catchError(err => { + patchState(store, {error: String(err), loading: false}); + return of([]); + }), + )), + )); + return { + setQuery(q: string) { + patchState(store, {query: q}); + search(q); + }, + }; + }), +); +``` + +**Entity management** + +```ts +import {withEntities, addEntities, setEntities, updateEntity, removeEntity} from '@ngrx/signals/entities'; + +type Id = string; + +interface Todo { + id: Id; + title: string; + done: boolean; +} + +export const TodosStore = signalStore( + withState({loading: false}), + withEntities(), + withMethods((store) => ({ + add(todo: Todo) { + patchState(store, addEntities(todo)); + }, + set(list: Todo[]) { + patchState(store, setEntities(list)); + }, + toggle(id: Id) { + patchState(store, updateEntity({id, changes: (t) => ({...t, done: !t.done})})); + }, + remove(id: Id) { + patchState(store, removeEntity({id})); + }, + })), +); +``` + +**Checklist projektowy (ważne)** + +- Używaj **signals** w komponentach. Do globalnego stanu i przepływów domenowych używaj **SignalStore**. +- Nigdy nie mutuj obiektów i tablic w stanie. Zawsze twórz nowe referencje. +- Oddziel pobieranie danych (metody store/serwisy) od renderowania UI. +- Efekty uboczne inicjuj w `withMethods` lub `withHooks`, nie w `computed()`. +- W SSR pamiętaj o bezpiecznym dostępie do `window/document`. +- Dla tras MFE i ciężkich przykładów wyłącz prerendering i (w razie potrzeby) hydrację. + +## Workspace & Projects + +- CLI multi‑project workspace (no Nx). Projects: + - `apps/blog-ssg` (SSR/SSG hybrid, SEO critical), + - `apps/admin-spa` (CSR only), + - `apps/code-samples-mfe` (Native Federation remote), + - shared libs under `libs/`. +- All **features are lazy**. Features never import other features directly. Reuse via `ui`, `pattern`, or routing. Enforce boundaries in ESLint. fileciteturn3file0 + +## Rendering + +- Use **route‑level render mode** (Angular v19+) to choose SSR/SSG/CSR per route. Home = SSR, article pages = SSG, others case‑by‑case. citeturn2search2turn0search0 +- Hydration by default. Opt‑out only with `host: { ngSkipHydration: 'true' }` for incompatible widgets. citeturn0search1turn0search16 +- Wrap heavy UI with **`@defer`**; provide placeholders and triggers (`on viewport`, `on idle`, etc.). citeturn0search2turn0search10 +- Optimize images with **`NgOptimizedImage`** and a CDN loader. Prioritize LCP. citeturn2search1turn2search7 + +## Micro‑frontend + +- Prefer **Native Federation** (Angular Architects) for the **code‑samples** remote — minimal setup, integrates with the CLI’s esbuild ApplicationBuilder, future‑proof. Shell: blog. Remote: samples. Load via `loadRemoteModule('samples', './routes')`. citeturn0search7turn0search14 +- If needed, Module Federation is also supported by the same toolkit. citeturn0search6 + +## Data access (Supabase) + +- **Public reader app** should fetch article data over **REST (PostgREST)** during SSG/SSR to avoid supabase-js initialization complexities. Keep requests RLS‑safe or fetch via a server token on Cloud Run. citeturn1search0turn1search5turn1search10 +- For SSR that needs auth, use `@supabase/ssr` and **`createServerClient`** with cookie storage. For browsers (Admin/MFE), use `createBrowserClient`. citeturn1search1turn1search6turn1search16 + +## **State Management — Use NgRx SignalStore** + +### Principles + +- Local state: **signals** — `signal`, `computed`, `linkedSignal`. Keep computations pure. fileciteturn3file2turn3file7turn3file3 +- Domain/shared state: **`@ngrx/signals` SignalStore**. Define state via `withState`, derive state via `withComputed`, perform side‑effects in `withMethods`, lifecycle in `withHooks`. Update state immutably with **`patchState`**. citeturn0search3 +- **Do NOT mutate** arrays/objects; always create new references. +- Async: + - Simple fetches: `async/await` + `firstValueFrom`. + - Reactive workflows: **`rxMethod`** with RxJS operators. + - Interop: `toSignal` / `toObservable` from `@angular/rxjs-interop`. citeturn0search5turn0search20 +- Entities: use `withEntities` + updaters (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, etc.). Expose selectors via computed signals. citeturn0search4turn0search11 + +### Store template + +```ts +import {computed, inject} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import { + signalStore, withState, withComputed, withMethods, withHooks, patchState, +} from '@ngrx/signals'; + +interface Item { + id: string; + title: string; +} + +interface State { + loading: boolean; + error: string | null; + items: Item[]; + selectedId: string | null; +} + +const initial: State = {loading: false, error: null, items: [], selectedId: null}; + +export const ItemsStore = signalStore( + {providedIn: 'root'}, + withState(initial), + withComputed(({items, selectedId}) => ({ + count: computed(() => items().length), + selected: computed(() => items().find(i => i.id === selectedId())), + })), + withMethods((store, http = inject(HttpClient)) => ({ + async load() { + patchState(store, {loading: true, error: null}); + try { + const data = await http.get('/api/items').toPromise(); + patchState(store, {items: data ?? [], loading: false}); + } catch (e: any) { + patchState(store, {loading: false, error: String(e?.message ?? e)}); + } + }, + select(id: string | null) { + patchState(store, {selectedId: id}); + }, + })), + withHooks({ + onInit() {/* optional */ + } + }), +); +``` + +Reference: NgRx SignalStore docs. citeturn0search3 + +### Entities example + +```ts +import {signalStore, withMethods, patchState} from '@ngrx/signals'; +import {withEntities, addEntities, setEntities, updateEntity, removeEntity} from '@ngrx/signals/entities'; + +type Id = string; + +interface Todo { + id: Id; + title: string; + done: boolean; +} + +export const TodosStore = signalStore( + withEntities(), + withMethods((store) => ({ + add(todo: Todo) { + patchState(store, addEntities(todo)); + }, + set(all: Todo[]) { + patchState(store, setEntities(all)); + }, + toggle(id: Id) { + patchState(store, updateEntity({id, changes: (t) => ({...t, done: !t.done})})); + }, + remove(id: Id) { + patchState(store, removeEntity({id})); + }, + })), +); +``` + +Reference: NgRx entities plugin docs. citeturn0search4turn0search11 + +### rxMethod example + +```ts +import {rxMethod} from '@ngrx/signals/rxjs-interop'; +import {debounceTime, distinctUntilChanged, switchMap, tap, catchError, of} from 'rxjs'; + +export const SearchStore = signalStore( + withState({query: '', results: [] as Result[], loading: false, error: null as string | null}), + withMethods((store, svc = inject(SearchService)) => { + const search = rxMethod(pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => patchState(store, {loading: true, error: null})), + switchMap(q => svc.search(q).pipe( + tap(results => patchState(store, {results, loading: false})), + catchError(err => { + patchState(store, {error: String(err), loading: false}); + return of([]); + }), + )), + )); + return { + setQuery(q: string) { + patchState(store, {query: q}); + search(q); + }, + }; + }), +); +``` + +Reference: NgRx rxMethod docs. citeturn0search5 + +## Performance & Web Vitals + +- Target **100/100/100/100** Lighthouse in CI. Use **Lighthouse CI** GitHub Action and fail PRs on regressions. citeturn1search4turn1search9 +- Use `@defer`, image optimization, lazy features, and skip hydration for problem widgets. Monitor LCP/INP/CLS. citeturn0search2turn2search1 + +## Accessibility & i18n + +- Add a11y linting and automated checks (axe). +- Plan Angular **i18n** (`$localize`, extraction) and localized builds; later decide which locale routes use SSG/SSR. citeturn2search0turn2search19turn2search2 + +## Projects + +- `apps/blog-ssg` (SSR/SSG hybrid, SEO critical) → Google Cloud Run. +- `apps/admin-spa` (CSR) → Firebase Hosting. +- `apps/code-samples-mfe` (Native Federation remote) → Firebase Hosting. +- `libs/shared/*` for reusable UI, pattern, data-access. + +## Rendering + +- Use **route‑level render mode** to select SSR/SSG/CSR per route. Home = SSR, `/posts/:slug` = SSG. Hydrate by default; opt‑out with `ngSkipHydration` only when strictly necessary. Use `@defer` to reduce initial JS and improve LCP/INP. Optimize images using `NgOptimizedImage`. + +## Micro‑frontend + +- Prefer **Native Federation**. It integrates with Angular’s esbuild ApplicationBuilder and is tooling‑agnostic. Blog is the shell; Samples is the remote. Expose routes; load with `loadRemoteModule('samples', './routes')`. Share Angular and RxJS as singletons. + +## Supabase + +- Public content: fetch via **PostgREST REST** in SSG/SSR (RLS‑safe anon role). Avoid initializing `supabase-js` for prerenders. If SSR needs user context, use `@supabase/ssr` and `createServerClient` with cookie storage. +- Storage: serve images from **Supabase Storage CDN**. For cache‑busting, prefer versioned filenames or signed URLs. + +## State Management — **NgRx SignalStore** + +- Local state: `signal`, `computed`, `linkedSignal`. +- Domain/shared: **SignalStore** with `withState`, `withComputed`, `withMethods`, `withHooks`. Updates via **`patchState`** only. **Do not mutate**. +- Entities: `withEntities` + updaters (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, …). +- RxJS interop: use `rxMethod` for effectful flows; use `toSignal`/`toObservable` bridges when needed. + +## Performance & CI + +- Target **100/100/100/100** Lighthouse on the home page. Enforce with **Lighthouse CI** in GitHub Actions (fail PRs on regressions). +- Add **Playwright + axe** accessibility scans on the home page. +- Node **20.19+** on all runners. +- Prefer **Workload Identity Federation** for Cloud Run auth when ready. + +## i18n + +- Plan localized builds (`localize` config) with route prefixes per locale. The dev server supports a **single locale per run**; use `--configuration=`. +- For SSG, generate `routes..txt` and prerender per locale. + +### Api calls + +- For SSG/SSR, use this api-url-builder.ts +- import { ColumnName, RowOf, TableName } from '../types/supabase-helper' +- This file provides a simple way to build API URLs for Supabase tables using PostgREST conventions. + +```ts +//https://postgrest.org/en/stable/references/api/tables_views.html#operators +type Op = + | 'eq' + | 'gt' + | 'gte' + | 'lt' + | 'lte' + | 'neq' + | 'like' + | 'ilike' + | 'match' + | 'imatch' + | 'in' + | 'is' + | 'isdistinct' + | 'fts' + | 'plfts' + | 'phfts' + | 'wfts' + | 'cs' + | 'cd' + | 'ov' + | 'sl' + | 'sr' + | 'nxr' + | 'nxl' + | 'adj' + | 'not' + | 'or' + | 'and' + | 'all' + | 'any'; + +export class UB { + private selects = new Set(); + private filters: string[] = []; + private orders: string[] = []; + private lim?: number; + private off?: number; + + constructor(private readonly table: T) { + } + + select(...cols: string[]) { + cols.forEach((c) => this.selects.add(c)); + return this; + } + + where>(col: K, op: Op, value: RowOf[K]) { + this.filters.push( + `${encodeURIComponent(String(col))}=${op}.${encodeURIComponent(String(value))}`, + ); + return this; + } + + orderBy>( + col: K, + dir: 'asc' | 'desc' = 'asc', + nulls?: 'first' | 'last', + ) { + this.orders.push(`${String(col)}.${dir}${nulls ? `.nulls${nulls}` : ''}`); + return this; + } + + range(from: number, to: number) { + this.off = from; + this.lim = to - from + 1; + return this; + } + + build(): string { + const p: string[] = []; + if (this.selects.size) + p.push(`select=${encodeURIComponent([...this.selects].join(','))}`); + if (this.filters.length) p.push(...this.filters); + if (this.orders.length) + p.push(`order=${encodeURIComponent(this.orders.join(','))}`); + if (this.lim !== undefined) p.push(`limit=${this.lim}`); + if (this.off !== undefined) p.push(`offset=${this.off}`); + return `${this.table}?${p.join('&')}`; + } +} + +export const createApiUrl = (table: T) => new UB(table); +``` diff --git a/llms/llm-full.txt b/llms/llm-full.txt new file mode 100644 index 0000000..0e43cde --- /dev/null +++ b/llms/llm-full.txt @@ -0,0 +1,14281 @@ + + + +Angular is a web framework that empowers developers to build fast, reliable applications. + + +Maintained by a dedicated team at Google, Angular provides a broad suite of tools, APIs, and +libraries to simplify and streamline your development workflow. Angular gives you +a solid platform on which to build fast, reliable applications that scale with both the size of +your team and the size of your codebase. + +**Want to see some code?** Jump over to our [Essentials](essentials) for a quick overview of +what it's like to use Angular, or get started in the [Tutorial](tutorials/learn-angular) if you +prefer following step-by-step instructions. + +## Features that power your development +## Develop applications faster than ever +## Ship with confidence +## Works at any scale +## Open-source first +## A thriving community +Get started with Angular quickly with online starters or locally with your terminal. + +## Play Online + +If you just want to play around with Angular in your browser without setting up a project, you can use our online sandbox: +## Set up a new project locally + +If you're starting a new project, you'll most likely want to create a local project so that you can use tooling such as Git. + +### Prerequisites + +- **Node.js** - [v20.11.1 or newer](/reference/versions) +- **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/) +- **Terminal** - Required for running Angular CLI commands +- **Development Tool** - To improve your development workflow, we recommend the [Angular Language Service](/tools/language-service) + +### Instructions + +The following guide will walk you through setting up a local Angular project. + +#### Install Angular CLI + +Open a terminal (if you're using [Visual Studio Code](https://code.visualstudio.com/), you can open an [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)) and run the following command: + +``` +// npm +npm install -g @angular/cli +``` +``` +// pnpm +pnpm install -g @angular/cli +``` +``` +// yarn +yarn global add @angular/cli +``` +``` +// bun +bun install -g @angular/cli +``` +If you are having issues running this command in Windows or Unix, check out the [CLI docs](/tools/cli/setup-local#install-the-angular-cli) for more info. + +#### Create a new project + +In your terminal, run the CLI command `ng new` with the desired project name. In the following examples, we'll be using the example project name of `my-first-angular-app`. + +```shell +ng new +``` +You will be presented with some configuration options for your project. Use the arrow and enter keys to navigate and select which options you desire. + +If you don't have any preferences, just hit the enter key to take the default options and continue with the setup. + +After you select the configuration options and the CLI runs through the setup, you should see the following message: + +```shell +✔ Packages installed successfully. + Successfully initialized git. +``` + +At this point, you're now ready to run your project locally! + +#### Running your new project locally + +In your terminal, switch to your new Angular project. + +```shell +cd my-first-angular-app +``` +All of your dependencies should be installed at this point (which you can verify by checking for the existent for a `node_modules` folder in your project), so you can start your project by running the command: + +```shell +npm start +``` +If everything is successful, you should see a similar confirmation message in your terminal: + +```shell +Watch mode enabled. Watching for file changes... +NOTE: Raw file sizes do not reflect development server per-request transformations. + ➜ Local: http://localhost:4200/ + ➜ press h + enter to show help +``` + +And now you can visit the path in `Local` (e.g., `http://localhost:4200`) to see your application. Happy coding! 🎉 + +### Using AI for Development + +To get started with building in your preferred AI powered IDE, [check out Angular prompt rules and best practices](/ai/develop-with-ai). + +## Next steps + +Now that you've created your Angular project, you can learn more about Angular in our [Essentials guide](/essentials) or choose a topic in our in-depth guides! +# Angular coding style guide + +## Introduction + +This guide covers a range of style conventions for Angular application code. These recommendations +are not required for Angular to work, but instead establish a set of coding practices that promote +consistency across the Angular ecosystem. A consistent set of practices makes it easier to share +code and move between projects. + +This guide does _not_ cover TypeScript or general coding practices unrelated to Angular. For +TypeScript, check +out [Google's TypeScript style guide](https://google.github.io/styleguide/tsguide.html). + +### When in doubt, prefer consistency + +Whenever you encounter a situation in which these rules contradict the style of a particular file, +prioritize maintaining consistency within a file. Mixing different style conventions in a single +file creates more confusion than diverging from the recommendations in this guide. + +## Naming + +### Separate words in file names with hyphens + +Separate words within a file name with hyphens (`-`). For example, a component named `UserProfile` +has a file name `user-profile.ts`. + +### Use the same name for a file's tests with `.spec` at the end + +For unit tests, end file names with `.spec.ts`. For example, the unit test file for +the `UserProfile` component has the file name `user-profile.spec.ts`. + +### Match file names to the TypeScript identifier within + +File names should generally describe the contents of the code in the file. When the file contains a +TypeScript class, the file name should reflect that class name. For example, a file containing a +component named `UserProfile` has the name `user-profile.ts`. + +If the file contains more than one primary namable identifier, choose a name that describes the +common theme to the code within. If the code in a file does not fit within a common theme or feature +area, consider breaking the code up into different files. Avoid overly generic file names +like `helpers.ts`, `utils.ts`, or `common.ts`. + +### Use the same file name for a component's TypeScript, template, and styles + +Components typically consist of one TypeScript file, one template file, and one style file. These +files should share the same name with different file extensions. For example, a `UserProfile` +component can have the files `user-profile.ts`, `user-profile.html`, and `user-profile.css`. + +If a component has more than one style file, append the name with additional words that describe the +styles specific to that file. For example, `UserProfile` might have style +files `user-profile-settings.css` and `user-profile-subscription.css`. + +## Project structure + +### All the application's code goes in a directory named `src` + +All of your Angular UI code (TypeScript, HTML, and styles) should live inside a directory +named `src`. Code that's not related to UI, such as configuration files or scripts, should live +outside the `src` directory. + +This keeps the root application directory consistent between different Angular projects and creates +a clear separation between UI code and other code in your project. + +### Bootstrap your application in a file named `main.ts` directly inside `src` + +The code to start up, or **bootstrap**, an Angular application should always live in a file +named `main.ts`. This represents the primary entry point to the application. + +### Group closely related files together in the same directory + +Angular components consist of a TypeScript file and, optionally, a template and one or more style +files. You should group these together in the same directory. + +Unit tests should live in the same directory as the code-under-test. Avoid collecting unrelated +tests into a single `tests` directory. + +### Organize your project by feature areas + +Organize your project into subdirectories based on the features of your application or common themes +to the code in those directories. For example, the project structure for a movie theater site, +MovieReel, might look like this: + +``` +src/ +├─ movie-reel/ +│ ├─ show-times/ +│ │ ├─ film-calendar/ +│ │ ├─ film-details/ +│ ├─ reserve-tickets/ +│ │ ├─ payment-info/ +│ │ ├─ purchase-confirmation/ +``` + +Avoid creating subdirectories based on the type of code that lives in those directories. For +example, avoid creating directories like `components`, `directives`, and `services`. + +Avoid putting so many files into one directory that it becomes hard to read or navigate. As the +number of files in a directory grows, consider splitting further into additional sub-directories. + +### One concept per file + +Prefer focusing source files on a single _concept_. For Angular classes specifically, this usually +means one component, directive, or service per file. However, it's okay if a file contains more than +one component or directive if your classes are relatively small and they tie together as part of a +single concept. + +When in doubt, go with the approach that leads to smaller files. + +## Dependency injection + +### Prefer the `inject` function over constructor parameter injection + +Prefer using the `inject` function over injecting constructor parameters. The `inject` function works the same way as constructor parameter injection, but offers several style advantages: + +* `inject` is generally more readable, especially when a class injects many dependencies. +* It's more syntactically straightforward to add comments to injected dependencies +* `inject` offers better type inference. +* When targeting ES2022+ with [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig/#useDefineForClassFields), you can avoid separating field declaration and initialization when fields read on injected dependencies. + +[You can refactor existing code to `inject` with an automatic tool](reference/migrations/inject-function). + +## Components and directives + +### Choosing component selectors + +See +the [Components guide for details on choosing component selectors](guide/components/selectors#choosing-a-selector). + +### Naming component and directive members + +See the Components guide for details +on [naming input properties](guide/components/inputs#choosing-input-names) +and [naming output properties](guide/components/outputs#choosing-event-names). + +### Choosing directive selectors + +Directives should use the +same [application-specific prefix](guide/components/selectors#selector-prefixes) +as your components. + +When using an attribute selector for a directive, use a camelCase attribute name. For example, if +your application is named "MovieReel" and you build a directive that adds a tooltip to an element, +you might use the selector `[mrTooltip]`. + +### Group Angular-specific properties before methods + +Components and directives should group Angular-specific properties together, typically near the top +of the class declaration. This includes injected dependencies, inputs, outputs, and queries. Define +these and other properties before the class's methods. + +This practice makes it easier to find the class's template APIs and dependencies. + +### Keep components and directives focused on presentation + +Code inside your components and directives should generally relate to the UI shown on the page. For +code that makes sense on its own, decoupled from the UI, prefer refactoring to other files. For +example, you can factor form validation rules or data transformations into separate functions or +classes. + +### Avoid overly complex logic in templates + +Angular templates are designed to +accommodate [JavaScript-like expressions](guide/templates/expression-syntax). +You should take advantage of these expressions to capture relatively straightforward logic directly +in template expressions. + +When the code in a template gets too complex, though, refactor logic into the TypeScript code (typically with a [computed](guide/signals#computed-signals)). + +There's no one hard-and-fast rule that determines what constitutes "complex". Use your best +judgement. + +### Use `protected` on class members that are only used by a component's template + +A component class's public members intrinsically define a public API that's accessible via +dependency injection and [queries](guide/components/queries). Prefer `protected` +access for any members that are meant to be read from the component's template. + +```ts +@Component({ + ..., + template: `

{{ fullName() }}

`, +}) +export class UserProfile { + firstName = input(); + lastName = input(); + +// `fullName` is not part of the component's public API, but is used in the template. + protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`); +} +``` + +### Use `readonly` on properties that are initialized by Angular + +Mark component and directive properties initialized by Angular as `readonly`. This includes +properties initialized by `input`, `model`, `output`, and queries. The readonly access modifier +ensures that the value set by Angular is not overwritten. + +```ts +@Component({/* ... */}) +export class UserProfile { + readonly userId = input(); + readonly userSaved = output(); +} +``` + +For components and directives that use the decorator-based `@Input`, `@Output`, and query APIs, this +advice applies to output properties and queries, but not input properties. + +```ts +@Component({/* ... */}) +export class UserProfile { + @Output() readonly userSaved = new EventEmitter(); + @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList; +} +``` + +### Prefer `class` and `style` over `ngClass` and `ngStyle` + +Prefer `class` and `style` bindings over using the [`NgClass`](/api/common/NgClass) and [`NgStyle`](/api/common/NgStyle) directives. + +```html + +
+ +
+ +
+``` + +Both `class` and `style` bindings use a more straightforward syntax that aligns closely with +standard HTML attributes. This makes your templates easier to read and understand, especially for +developers familiar with basic HTML. + +Additionally, the `NgClass` and `NgStyle` directives incur an additional performance cost compared +to the built-in `class` and `style` binding syntax. + +For more details, refer to the [bindings guide](/guide/templates/binding#css-class-and-style-property-bindings) + +### Name event handlers for what they _do_, not for the triggering event + +Prefer naming event handlers for the action they perform rather than for the triggering event: + +```html + + + + + +``` + +Using meaningful names like this makes it easier to tell what an event does from reading the +template. + +For keyboard events, you can use Angular's key event modifiers with specific handler names: + +```html +