12

Lab 12 - Angular Frontend (Part 2) - CRUD, Reactive Forms, Role-based UI

Partea 5 din 5

Actualizat 2026-05-17Sursă pe GitHub ↗Cod start

Parte 5 — Cheatsheet

Referință rapidă pentru ce-ați văzut în Lab 12. Pentru Angular CLI / CORS / template syntax / routing de bază, vedeți cheatsheet-ul Lab 11.

Pornire mediu (recapitulare)

# Terminal 1: backend
cd Lab12_start/backend
dotnet run

# Terminal 2: frontend
cd Lab12_start/news-portal-app
npm install              # doar prima data
ng serve                 # http://localhost:4200

Login dev: admin@newsportal.com / Admin@123 → rolul Admin în JWT.

HttpClient — metode CRUD complete

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

private http = inject(HttpClient);

// GET — listă (200 OK + array JSON)
this.http.get<Article[]>(url): Observable<Article[]>

// GET — detaliu (200 OK + obiect JSON)
this.http.get<Article>(`${url}/${id}`): Observable<Article>

// POST — create (201 Created + resursa creată)
this.http.post<Article>(url, dto): Observable<Article>

// PUT — update (204 No Content)
this.http.put<void>(`${url}/${id}`, dto): Observable<void>

// DELETE — remove (204 No Content)
this.http.delete<void>(`${url}/${id}`): Observable<void>

Pattern de consum standard:

this.service.method(...).subscribe({
  next: data => /* succes */,
  error: err => /* HttpErrorResponse */
});

HttpErrorResponse are .status (cod HTTP), .error (body de la server), .message.

Reactive Forms — pattern complet

import { FormBuilder, FormGroup, Validators } from '@angular/forms';

private fb = inject(FormBuilder);
form!: FormGroup;

ngOnInit(): void {
  this.form = this.fb.group({
    title:      ['', [Validators.required, Validators.minLength(5)]],
    content:    ['', [Validators.required, Validators.minLength(20)]],
    categoryId: [null, Validators.required]
  });
}

onSubmit(): void {
  if (this.form.invalid) {
    this.form.markAllAsTouched();
    return;
  }
  const dto = this.form.value;
  this.service.create(dto).subscribe(...);
}

Template:

<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <input formControlName="title" class="form-control">
  <div *ngIf="form.get('title')?.touched && form.get('title')?.errors as e">
    <span *ngIf="e['required']">Obligatoriu</span>
    <span *ngIf="e['minlength']">Minim {{ e['minlength'].requiredLength }} caractere</span>
  </div>
  <button [disabled]="form.invalid">Submit</button>
</form>

Componenta standalone trebuie să aibă ReactiveFormsModule în array-ul imports al @Component.

Validatori built-in

Validator Eroare key Note
Validators.required required valoare ne-vidă
Validators.requiredTrue required doar pentru checkbox-uri „terms accepted"
Validators.email email regex RFC 5322 simplificat
Validators.minLength(n) minlength string sau array
Validators.maxLength(n) maxlength string sau array
Validators.min(n) min numeric
Validators.max(n) max numeric
Validators.pattern(re) pattern string sau RegExp

Combinare: [Validators.required, Validators.minLength(5)] — ambele se aplică, errors agregă.

Stări control + form

Property Sens
valid / invalid trec / nu trec validarea
pristine / dirty nu a fost modificat / a fost modificat
touched / untouched s-a făcut blur / nu s-a interacționat
pending validare async în curs
value valoarea curentă
errors obiect { keyValidator: details } sau null

Tipic afișezi erori cu *ngIf="ctrl.touched && ctrl.invalid" — nu speria user-ul cu erori la load.

patchValue vs setValue

// patchValue - relaxat, accepta lipsa unor chei
this.form.patchValue({ title: 'X', content: 'Y' });
// categoryId ramane neschimbat

// setValue - strict, cere toate cheile
this.form.setValue({ title: 'X', content: 'Y', categoryId: 1 });
// fara una -> eroare runtime

Pentru load din API folosiți patchValue — DTO-ul poate avea câmpuri extra (id, publishedAt) care nu-s în form.

Routing pentru CRUD

// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './core/guards/auth-guard';

export const routes: Routes = [
  { path: 'articles',              component: ArticleList },
  { path: 'articles/new',          component: ArticleForm,   canActivate: [authGuard] },
  { path: 'articles/:id/edit',     component: ArticleForm,   canActivate: [authGuard] },
  { path: 'articles/:id',          component: ArticleDetail }
];

Reguli ordine:

  • articles/new înainte de articles/:id — altfel :id prinde 'new'.
  • articles/:id/edit și articles/:id se distinge prin segment count, ordinea lor nu contează.

ActivatedRoute — citire param

private route = inject(ActivatedRoute);

ngOnInit() {
  // Snapshot - o data, la init
  const id = this.route.snapshot.paramMap.get('id');   // string | null

  // Reactiv - la fiecare schimbare (utile cand path e identic, doar param-ul difera)
  this.route.paramMap.subscribe(map => {
    const id = map.get('id');
  });

  // Query params: /x?returnUrl=/y
  const ret = this.route.snapshot.queryParams['returnUrl'];
}

Pentru :id/edit în Lab 12 e suficient snapshot — componenta se distruge la navigate-out.

Role-based UI

currentUser = signal<CurrentUser | null>(null);

ngOnInit(): void {
  this.authService.currentUser$.subscribe(u => this.currentUser.set(u));
}

canModify(article: Article): boolean {
  const u = this.currentUser();
  if (!u) return false;
  return u.roles.includes('Admin') || article.authorId === u.id;
}

Template:

<ng-container *ngIf="canModify(article)">
  <button (click)="editArticle(article.id, $event)">Edit</button>
  <button (click)="deleteArticle(article.id, $event)">Delete</button>
</ng-container>

<ng-container> = wrapper logic fără DOM. Ideal pentru *ngIf peste mai multe elemente.

Delete cu confirm + refresh local

deleteArticle(id: number, event: Event): void {
  event.stopPropagation();

  if (!confirm('Sigur stergeti?')) return;

  this.articleService.delete(id).subscribe({
    next: () => this.articles.update(arr => arr.filter(a => a.id !== id)),
    error: () => this.error.set('Eroare la stergere')
  });
}

Status code-uri HTTP la CRUD

Status Cand Ce face frontend
200 OK GET success citește body
201 Created POST success citește body (resursa nouă)
204 No Content PUT / DELETE success nu citește body
400 Bad Request validare eșuată afișează err.error.errors[] (ModelState)
401 Unauthorized fără token / token invalid redirect la /login (interceptor de erori)
403 Forbidden autentificat dar fără permisiuni mesaj „nu aveți acces"
404 Not Found resursa nu există mesaj „nu a fost găsit"
500 Server Error excepție backend (prinsă de middleware) mesaj generic, nu detaliile

Refactor backend pe care l-ați primit (recapitulare)

Toate au fost pre-aplicate în Lab12_start/backend/ — nu trebuie să le scrieți, dar e bine să știți unde să vă uitați:

Pattern Fișier Înlocuiește
Mapping centralizat Article → ViewModel Mappings/ArticleViewModelMappings.cs mapping inline din HomeController + ArticlesController (5 acțiuni)
Ownership check refolosibil Authorization/ClaimsPrincipalExtensions.cs (User.CanModifyArticle(article)) IsOwnerOrAdmin privat duplicat în 2 controllere
JWT generation extracted Services/IJwtService.cs + JwtService.cs 28 linii din AuthApiController.GenerateJwtAsync
Categories endpoint nou Controllers/Api/CategoriesApiController.cs (lipsea — necesar pentru dropdown)

Probleme comune Lab 12

Problemă Cauză Fix
TODO Lab 12: implementati ... aruncat la subscribe service-ul are stub, nu apel real înlocuiți throw new Error(...) cu return this.http.<verb>(...)
Submit nu face nimic form.invalid true, dar fără markAllAsTouched() adăugați call-ul în guard
Dropdown-ul de categorii e gol categoryService.getAll() aruncă TODO sau nu se apelează în ngOnInit recheck Ex 1 + ngOnInit
<select> arată valoarea ca string, dar form-ul cere number folosiți [ngValue]="c.id", nu [value]="c.id" înlocuiți
Buton Edit/Delete vizibil pentru oricine canModify returnează true neglijent implementați conform Ex 4
403 Forbidden la DELETE chiar pentru Admin rolul Admin lipsește din JWT (cont creat fără seedRoles) recreate user via Register sau verificați seedul
currentUser e null chiar după login subscribe la currentUser$ lipsește în ngOnInit adăugați-l
cannot find module '...' la build path greșit la import (relative) folosiți '../../../shared/...' din core/services/
Cannot read properties of null (reading 'errors') template accesează form.get('x') înainte ca form-ul să existe form!: FormGroup + construire în ngOnInit; folosiți ?. în template
404 la /api/categories uitat să fie pornit backend-ul, sau port greșit în environment recheck apiUrl

Linkuri rapide