11

Lab 11 - Angular Frontend (Part 1)

Partea 3 din 4

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

Parte 3 — Componente, routing, module

Avem modelele și serviciile gata. Acum scriem componentele care le consumă: LoginComponent (formular reactiv) și ArticleListComponent (consum API). Le legăm prin routing și le declarăm în AppModule.

Generarea componentelor

Angular CLI scaffold-ează componente cu structura corectă (TS + HTML + CSS + spec), le declară automat în AppModule și updatează app.module.ts.

# In news-portal-app/
ng generate component features/auth/login
ng generate component features/articles/article-list

# Comenzile pentru exercitii:
# ng generate component features/auth/register
# ng generate component features/articles/article-detail
# ng generate component shared/components/header

Forma scurtă: ng g c features/auth/login. Fiecare comandă creează:

features/auth/login/
├── login.component.ts
├── login.component.html
├── login.component.css
└── login.component.spec.ts   (test, il puteti sterge daca nu il folositi)

și adaugă LoginComponent în array-ul declarations din AppModule.

LoginComponent — reactive form + apel AuthService

Reactive forms = formulare definite în TS, cu validare declarativă. Alternativa e template-driven forms (definite în HTML cu ngModel) — folositoare pentru cazuri simple, dar reactive forms scalează mai bine la validări custom și logică complexă.

src/app/features/auth/login/login.component.ts:

import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from '../../../core/services/auth';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrl: './login.component.css'
})
export class LoginComponent {
  loginForm: FormGroup;
  loading = false;
  submitted = false;
  error: string | null = null;

  constructor(
    private fb: FormBuilder,
    private router: Router,
    private route: ActivatedRoute,
    private authService: AuthService
  ) {
    this.loginForm = this.fb.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required, Validators.minLength(6)]]
    });
  }

  get f() { return this.loginForm.controls; }

  onSubmit(): void {
    this.submitted = true;
    if (this.loginForm.invalid) return;

    this.loading = true;
    this.error = null;

    this.authService.login(this.f['email'].value, this.f['password'].value).subscribe({
      next: () => {
        const returnUrl = this.route.snapshot.queryParams['returnUrl'] ?? '/articles';
        this.router.navigateByUrl(returnUrl);
      },
      error: err => {
        this.error = err.error?.message ?? 'Email sau parola incorecta';
        this.loading = false;
      }
    });
  }
}

src/app/features/auth/login/login.component.html:

<div class="container mt-5" style="max-width: 480px;">
  <h2 class="mb-4">Autentificare</h2>

  <div *ngIf="error" class="alert alert-danger">{{ error }}</div>

  <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
    <div class="mb-3">
      <label for="email" class="form-label">Email</label>
      <input id="email" type="email" class="form-control"
             formControlName="email"
             [class.is-invalid]="submitted && f['email'].invalid">
      <div *ngIf="submitted && f['email'].invalid" class="invalid-feedback">
        Email obligatoriu si valid.
      </div>
    </div>

    <div class="mb-3">
      <label for="password" class="form-label">Parola</label>
      <input id="password" type="password" class="form-control"
             formControlName="password"
             [class.is-invalid]="submitted && f['password'].invalid">
      <div *ngIf="submitted && f['password'].invalid" class="invalid-feedback">
        Parola obligatorie (min. 6 caractere).
      </div>
    </div>

    <button type="submit" class="btn btn-primary w-100" [disabled]="loading">
      {{ loading ? 'Se conecteaza...' : 'Conectare' }}
    </button>
  </form>

  <p class="mt-3 text-center">
    Nu ai cont? <a routerLink="/register">Inregistreaza-te</a>
  </p>
</div>

Sintaxa Angular pe scurt

Sintaxa Ce face
[formGroup]="loginForm" property binding — leagă form-ul HTML de FormGroup-ul TS
formControlName="email" leagă input-ul de control-ul email din grup
(ngSubmit)="onSubmit()" event binding — apelează metoda la submit
*ngIf="cond" structural directive — randează doar dacă cond e truthy
[class.is-invalid]="cond" adaugă/scoate clasa CSS conditionat
[disabled]="loading" property binding pentru atribute boolean
{{ expression }} interpolation — afișează rezultatul în text
routerLink="/login" directivă din RouterModule — navigare client-side

ArticleListComponent — consum API read-only

src/app/features/articles/article-list/article-list.component.ts:

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { ArticleService } from '../../../core/services/article';
import { Article } from '../../../shared/models/article';

@Component({
  selector: 'app-article-list',
  templateUrl: './article-list.component.html',
  styleUrl: './article-list.component.css'
})
export class ArticleListComponent implements OnInit {
  articles: Article[] = [];
  loading = true;
  error: string | null = null;

  constructor(
    private articleService: ArticleService,
    private router: Router
  ) {}

  ngOnInit(): void {
    this.articleService.getAll().subscribe({
      next: articles => {
        this.articles = articles;
        this.loading = false;
      },
      error: err => {
        this.error = 'Nu s-au putut incarca articolele';
        this.loading = false;
      }
    });
  }

  viewArticle(id: number): void {
    this.router.navigate(['/articles', id]);
  }
}

src/app/features/articles/article-list/article-list.component.html:

<div class="container mt-5">
  <h1 class="mb-4">Articole</h1>

  <div *ngIf="loading" class="alert alert-info">Se incarca...</div>
  <div *ngIf="error" class="alert alert-danger">{{ error }}</div>

  <div *ngIf="!loading && articles.length === 0" class="alert alert-warning">
    Nu sunt articole disponibile.
  </div>

  <div *ngIf="!loading" class="row">
    <div *ngFor="let article of articles" class="col-md-6 col-lg-4 mb-4">
      <div class="card h-100">
        <div class="card-body">
          <h5 class="card-title">{{ article.title }}</h5>
          <p class="text-muted mb-2">{{ article.categoryName }}</p>
          <p class="card-text">{{ article.content | slice:0:120 }}...</p>
          <small class="text-muted d-block mb-2">
            Autor: {{ article.authorName }} ·
            {{ article.publishedAt | date:'dd MMM yyyy' }}
          </small>
          <button class="btn btn-sm btn-primary" (click)="viewArticle(article.id)">
            Detalii
          </button>
        </div>
      </div>
    </div>
  </div>
</div>

Lifecycle hook OnInit

ngOnInit rulează o singură dată, după ce Angular a inițializat componenta și a injectat dependențele. E locul corect pentru apeluri HTTP de inițializare.

De ce nu în constructor? Constructor-ul rulează înainte ca Angular să fi terminat de bind-uit @Input()-urile și de injectat dependențele. Convenția e: constructor doar pentru DI; logică de inițializare în ngOnInit.

Pipe-uri folosite

  • slice:0:120 — taie textul la primele 120 de caractere
  • date:'dd MMM yyyy' — formatează publishedAt (string ISO) ca 18 mai 2026

Pipe-urile se pot înlănțui: {{ value | filter1 | filter2 }}.

Routing

src/app/app-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './features/auth/login/login.component';
import { ArticleListComponent } from './features/articles/article-list/article-list.component';

const routes: Routes = [
  { path: '', redirectTo: '/articles', pathMatch: 'full' },
  { path: 'login', component: LoginComponent },
  // { path: 'register', component: RegisterComponent },         <- exercitiu Ex 2
  { path: 'articles', component: ArticleListComponent },
  // { path: 'articles/:id', component: ArticleDetailComponent }, <- exercitiu Ex 3
  // Lab 12: { path: 'articles/new', ..., canActivate: [AuthGuard] }
  { path: '**', redirectTo: '/articles' }   // catch-all - 404 friendly
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}
Detaliu Explicație
path: '' + redirectTo: '/articles' rădăcina aplicației redirectează la lista de articole
pathMatch: 'full' redirect-ul se aplică doar dacă URL-ul e exact /, nu și pentru /articles care începe cu /
:id parametru de rută — /articles/5id = '5'. Detalii la Ex 3.
path: '**' catch-all pentru rute inexistente. Întotdeauna ultima.
RouterModule.forRoot(routes) înregistrează ruterul la nivelul aplicației. Modulele lazy-loaded folosesc .forChild.

AppModule — declarații + provider pentru interceptor

src/app/app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './features/auth/login/login.component';
import { ArticleListComponent } from './features/articles/article-list/article-list.component';
import { HttpConfigInterceptor } from './core/services/http-config.interceptor';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    ArticleListComponent
    // RegisterComponent       <- adaugat la Ex 2
    // ArticleDetailComponent  <- adaugat la Ex 3
    // HeaderComponent         <- adaugat la Ex 4
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ReactiveFormsModule,
    FormsModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpConfigInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

ng generate component ... adaugă automat declarațiile aici — nu trebuie să le scrieți manual decât pentru componente create din afara CLI-ului.

Modul / Provider Pentru ce
BrowserModule obligatoriu pentru orice aplicație Angular care rulează în browser
HttpClientModule activează HttpClient (folosit în AuthService și ArticleService)
ReactiveFormsModule activează [formGroup] și formControlName
FormsModule activează [(ngModel)] (template-driven, util pentru formulare simple)
HTTP_INTERCEPTORS cu multi: true înregistrează interceptor-ul; multi permite mai multe interceptors în lanț

AppComponent — shell-ul aplicației

src/app/app.component.html (înlocuiți tot conținutul default cu):

<router-outlet></router-outlet>

Atât. <router-outlet> e marker-ul unde routerul randează componenta corespunzătoare URL-ului curent. La Ex 4 adăugați <app-header> deasupra.

Notă: dacă vreți rapid un look stilat fără să scrieți CSS, adăugați Bootstrap în src/index.html:

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3/dist/css/bootstrap.min.css" rel="stylesheet">

Componentele de mai sus folosesc deja clase Bootstrap (container, btn, card, etc.).

Test rapid al setup-ului

Cu backend-ul dotnet run și frontend-ul ng serve pornite:

  1. http://localhost:4200/articles → lista de articole din BD (cele seedate în Lab 7+)
  2. http://localhost:4200/login → formularul de login. Folosiți credențialele admin seedate în Data/SeedData.cs (default: admin@newsportal.com / Admin123!).
  3. După login, în DevTools → tab ApplicationLocal Storage → există token cu un JWT.
  4. La refresh, AuthService.loadUserFromToken() recitește token-ul și state-ul rămâne autentificat (până la logout() sau ștergerea manuală).

Dacă Login dă 401, verificați în log-urile dotnet run: e fie email greșit, fie parolă greșită. Pentru 403 / CORS errors, recitiți Partea 1.


Exerciții (3p)

Exercițiul 2 (1p) — RegisterComponent cu reactive form

Scrieți componenta de înregistrare după modelul LoginComponent.

Cerințe

  1. ng generate component features/auth/register
  2. Câmpuri în formular: email, fullName, password, confirmPassword
  3. Validări:
    • email — required + email format
    • fullName — required + minLength(3)
    • password — required + minLength(6)
    • confirmPassword — required + trebuie să fie egal cu password
  4. La submit valid: authService.register(email, fullName, password). La succes, redirect la /articles (utilizatorul e deja logat — backend-ul returnează JWT direct).
  5. La eroare (de obicei 400 Bad Request cu mesaj sau lista de erori), afișați mesajul de la API într-un alert-danger.
  6. Decomentați ruta /register din AppRoutingModule.

Hint pentru validatorul de potrivire

Validators.required și Validators.email se aplică per câmp. Pentru “confirm password = password” e nevoie de un validator la nivel de grup:

this.registerForm = this.fb.group({
  email: ['', [Validators.required, Validators.email]],
  fullName: ['', [Validators.required, Validators.minLength(3)]],
  password: ['', [Validators.required, Validators.minLength(6)]],
  confirmPassword: ['', Validators.required]
}, { validators: passwordsMatch });

function passwordsMatch(group: AbstractControl): ValidationErrors | null {
  const pwd = group.get('password')?.value;
  const cpwd = group.get('confirmPassword')?.value;
  return pwd === cpwd ? null : { passwordsMismatch: true };
}

În template afișați eroarea cu *ngIf="submitted && registerForm.errors?.['passwordsMismatch']".

Exercițiul 3 (1p) — ArticleDetailComponent cu parametru de rută

Pagină dedicată pentru un articol — accesibilă la /articles/:id.

Cerințe

  1. ng generate component features/articles/article-detail

  2. Citiți id din rută folosind ActivatedRoute:

    constructor(private route: ActivatedRoute, private articleService: ArticleService) {}
    
    ngOnInit(): void {
      const id = Number(this.route.snapshot.paramMap.get('id'));
      this.articleService.getById(id).subscribe(...);
    }
    
  3. Afișați toate câmpurile modelului Article: title, content (integral, nu trunchiat), categoryName, authorName, publishedAt (cu pipe date).

  4. Tratați 404: dacă getById() întoarce eroare (HTTP 404), afișați Articolul nu a fost găsit și un buton de întoarcere la listă.

  5. Decomentați ruta articles/:id în AppRoutingModule.

  6. Verificați că butonul Detalii din ArticleListComponent (deja scris) navighează corect.

Bonus: dacă articolul are tags?: string[], afișați-le ca badge-uri Bootstrap (<span class="badge bg-secondary">{{ tag }}</span>). În Lab 11 e opțional — în Lab 10 am adăugat tag-uri la backend; dacă endpoint-ul /api/articles/{id} nu le include în DTO, ignorați partea cu tags.

Exercițiul 4 (1p) — HeaderComponent cu navigare reactivă

Bară de navigare afișată pe toate paginile, care se schimbă după autentificare.

Cerințe

  1. ng generate component shared/components/header

  2. Link-uri: Articole (/articles), Login (/login), Register (/register).

  3. State reactiv: folosiți AuthService.currentUser$ cu | async pipe în template:

    <ng-container *ngIf="currentUser$ | async as user; else anon">
      <span class="navbar-text">{{ user.name }}</span>
      <a class="nav-link" (click)="logout()" style="cursor:pointer">Deconectare</a>
    </ng-container>
    <ng-template #anon>
      <a class="nav-link" routerLink="/login">Login</a>
      <a class="nav-link" routerLink="/register">Register</a>
    </ng-template>
    
  4. Când utilizatorul e logat: afișați numele + buton Deconectare (apelează authService.logout(), apoi navighează la /login).

  5. Când nu e logat: afișați Login + Register.

  6. Adăugați <app-header></app-header> deasupra <router-outlet> în app.component.html.

Despre | async pipe

async se abonează automat la observabil, livrează ultima valoare și se dezabonează când componenta e distrusă — gestionează corect lifecycle-ul fără cod manual de subscribe/unsubscribe. E pattern-ul recomandat în Angular pentru observabile expuse în template.

Atenție la inițializarea currentUser$ în clasă: NU scrieți currentUser$ = this.authService.currentUser$ direct pe linia declarației — cu useDefineForClassFields activ (default Angular 16+), property initializers rulează înainte de constructor, deci this.authService e încă undefined. Soluție: declarați currentUser$; și inițializați-l în corpul constructorului (this.currentUser$ = this.authService.currentUser$;).

Sumar: la finalul Lab 11 aveți o aplicație Angular care listează articolele din backend, login + register cu JWT, navigare condiționată în header. Operațiile de scriere și UI bazat pe roluri ajung în Lab 12.