# Angular 17+ Platform Guide

## Contents
- Project scaffolding and inspection
- File structure
- Global styles
- Component generation
- Routing
- API services
- Environment configuration
- Angular-specific rules
- Code patterns

## Project scaffolding and inspection

**Existing project**: Read `app.config.ts`, `app.routes.ts`, and `app.component.html` to understand current state (providers, routes, and layout)

**New project**:
```bash
ng new <name> --style=scss --routing --skip-tests
```

## File structure

Each routed section becomes a directory under `src/app/components/`:

```
src/app/components/<name>/
├── <name>.component.ts
├── <name>.component.html
├── <name>.component.scss
└── <name>.service.ts          # only if OpenAPI provided
```

## Global styles

Generate `:root` CSS custom properties in `src/styles.scss` following [design-tokens-guide.md](design-tokens-guide.md)

## Component generation

For each routed section, the orchestrator creates the `.component.ts` (with service injection and data signals) and `.service.ts` (if OpenAPI provided). The `.component.html` and `.component.scss` are generated by the image-to-component procedure

Update:

- `app.routes.ts` — add lazy-loaded route
- `app.html` — shared layout sections (`<header>`, `<nav>`, `<footer>`, `<aside>`) + `<router-outlet>` where `<main>` was

## Routing

```typescript
export const routes: Routes = [
	{ path: '', redirectTo: 'home', pathMatch: 'full' },
	{ path: 'home', loadComponent: () =>
		import('./components/home/home.component').then(m => m.HomeComponent) },
	{ path: 'items', loadComponent: () =>
		import('./components/items/items.component').then(m => m.ItemsComponent) },
];
```

## API services

### Environment configuration

```typescript
// src/environments/environment.ts
export const environment = {
	apiUrl: '/rest',
};
```

`apiUrl` uses a **relative path** (e.g. `/rest`) so REST calls go through the Angular dev proxy (see [Dev proxy](#dev-proxy) below). Switching between mock and real backend is done by changing the proxy target — no code changes needed

### Register HttpClient provider

**CRITICAL**: When generating services that use `HttpClient`, ensure `provideHttpClient()` is registered in `app.config.ts`. Without it, the app will fail at runtime with `NG0201: No provider found for HttpClient`

```typescript
// src/app/app.config.ts
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
	providers: [
		// … existing providers
		provideHttpClient()
	]
};
```

### Service pattern

```typescript
@Injectable({ providedIn: 'root' })
export class ItemService {
	private http = inject(HttpClient);

	getItems(): Observable<Item[]> {
		return this.http.get<Item[]>(`${environment.apiUrl}/items`).pipe(
			catchError(err => {
				console.error('API error:', err);
				return of([] as Item[]);
			})
		);
	}
}
```

### Dev proxy

Create `proxy.conf.json` at the project root to proxy REST calls to the backend. This avoids CORS issues and allows cookies to be sent on same-origin requests. By default, the target points to the local mock server. Include a commented-out line with the real backend URL so switching is easy

```json
// proxy.conf.json
{
	"/rest": {
		"target": "http://localhost:3001",
		// "target": "https://<real-backend-url>",
		"secure": false,
		"changeOrigin": true
	}
}
```

Add proxy entries for any additional non-REST paths that also need proxying (e.g. chat or streaming endpoints identified in the OpenAPI spec)

Register the proxy in `angular.json` under `serve`:

```json
"serve": {
	"builder": "@angular/build:dev-server",
	"options": {
		"proxyConfig": "proxy.conf.json"
	},
	"configurations": { … }
}
```

### GeneXus backend

Apply when using a GeneXus backend. See [genexus-backend.md](genexus-backend.md) for the rules; below is the Angular implementation

**Image URL resolution**: Add `imageBaseUrl` to environment (absolute URL without `/rest`). Images resolve against `imageBaseUrl`, not `apiUrl`:

```typescript
// In environment.ts, add:
imageBaseUrl: 'https://example.com/MyApp',

// In service:
resolveImageUrl(relativeUrl: string): string {
	if (!relativeUrl) return '';
	if (relativeUrl.startsWith('http')) return relativeUrl;
	return `${environment.imageBaseUrl}/${relativeUrl}`;
}

// Image fallback: use CSS background on image wrapper containers (see SKILL.md Step 7 item 7)
```

**gxid**: Generate per component, pass to every service call:

```typescript
// Component:
private gxid = Math.floor(Math.random() * 32767);
this.service.getItems(this.gxid).subscribe(…)

// Service — accept gxid as parameter:
getItems(gxid: number): Observable<Item[]> {
	return this.http.get<Item[]>(`${environment.apiUrl}/items`, {
		params: { gxid }
	}).pipe(catchError(() => of([])));
}
```

**GX_CLIENT_ID cookie**: Create an interceptor that sets the cookie and enables `withCredentials`:

```typescript
// src/app/gx-client-id.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

function getOrCreateGxClientId(): string {
	const cookieName = 'GX_CLIENT_ID';
	const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${cookieName}=([^;]*)`));
	if (match) return match[1];
	const guid = crypto.randomUUID();
	document.cookie = `${cookieName}=${guid}; path=/; SameSite=Lax`;
	return guid;
}

export const gxClientIdInterceptor: HttpInterceptorFn = (req, next) => {
	getOrCreateGxClientId();
	return next(req.clone({ withCredentials: true }));
};
```

Register: `provideHttpClient(withInterceptors([gxClientIdInterceptor]))` in `app.config.ts`

**Endpoint call order**: Master first, then grids:

```typescript
this.service.getMaster(this.gxid).subscribe(() => {
	this.service.getGrid1(this.gxid).subscribe(…)
	this.service.getGrid2(this.gxid).subscribe(…)
});
```

**Accept header override**: Use `proxy.conf.js` (not `.json`) to override the header:

```javascript
// proxy.conf.js
module.exports = {
	"/rest": {
		target: "http://localhost:3001",        // Mock server
		// target: "https://example.com/MyApp", // Real backend
		secure: false,
		changeOrigin: true,
		onProxyReq: function (proxyReq) {
			proxyReq.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
		}
	}
};
```

Register as `"proxyConfig": "proxy.conf.js"` in `angular.json`

### Mock server

See [mock-server-template.md](mock-server-template.md) for the base template. Customize endpoints to match the OpenAPI spec

### Dev script

Add a `dev` script to `package.json` that runs the mock server and Angular dev server concurrently:

```json
{
	"scripts": {
		"dev": "concurrently \"npx tsx mock-server.ts\" \"ng serve\" --names mock,app",
		"dev:real": "ng serve"
	}
}
```

Install `concurrently` as a dev dependency: `npm install -D concurrently`

### Component with signals

```typescript
@Component({
	standalone: true,
	imports: [RouterLink],
	templateUrl: './items.component.html',
	styleUrl: './items.component.scss'
})
export class ItemsComponent implements OnInit {
	private service = inject(ItemService);
	private destroyRef = inject(DestroyRef);

	items = signal<Item[]>([]);
	loading = signal(true);
	error = signal<string | null>(null);

	ngOnInit() {
		this.service.getItems().pipe(
			takeUntilDestroyed(this.destroyRef)
		).subscribe({
			next: data => { this.items.set(data); this.loading.set(false); },
			error: err => { this.error.set(err.message); this.loading.set(false); }
		});
	}
}
```

## Interaction patterns

- **Optimistic updates**: For user actions with immediate visual feedback (toggles, favorites, quantity changes, likes), update the signal/local state instantly on click, then fire the server call. If the server call fails, revert the signal to its previous value

## Angular-specific rules

- `standalone: true` — never NgModule
- `signal()` / `computed()` / `effect()` for state — never BehaviorSubject
- `inject()` for DI — never constructor injection
- `@if` / `@for` control flow — never `*ngIf` / `*ngFor`
- Typed reactive forms — never ngModel:

```typescript
interface LoginForm {
	email: FormControl<string>;
	password: FormControl<string>;
}

form = new FormGroup<LoginForm>({
	email: new FormControl('', { nonNullable: true }),
	password: new FormControl('', { nonNullable: true })
});
```

- `environment.apiUrl` for REST calls — never hardcoded URLs
