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 înngOnInit.
Pipe-uri folosite
slice:0:120— taie textul la primele 120 de caracteredate:'dd MMM yyyy'— formateazăpublishedAt(string ISO) ca18 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/5 → id = '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:
http://localhost:4200/articles→ lista de articole din BD (cele seedate în Lab 7+)http://localhost:4200/login→ formularul de login. Folosiți credențialele admin seedate înData/SeedData.cs(default:admin@newsportal.com/Admin123!).- După login, în DevTools → tab
Application→Local Storage→ existătokencu un JWT. - La refresh,
AuthService.loadUserFromToken()recitește token-ul și state-ul rămâne autentificat (până lalogout()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
ng generate component features/auth/register- Câmpuri în formular:
email,fullName,password,confirmPassword - Validări:
email— required + email formatfullName— required + minLength(3)password— required + minLength(6)confirmPassword— required + trebuie să fie egal cupassword
- La submit valid:
authService.register(email, fullName, password). La succes, redirect la/articles(utilizatorul e deja logat — backend-ul returnează JWT direct). - La eroare (de obicei
400 Bad Requestcu mesaj sau lista de erori), afișați mesajul de la API într-unalert-danger. - Decomentați ruta
/registerdinAppRoutingModule.
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
-
ng generate component features/articles/article-detail -
Citiți
iddin rută folosindActivatedRoute:constructor(private route: ActivatedRoute, private articleService: ArticleService) {} ngOnInit(): void { const id = Number(this.route.snapshot.paramMap.get('id')); this.articleService.getById(id).subscribe(...); } -
Afișați toate câmpurile modelului
Article:title,content(integral, nu trunchiat),categoryName,authorName,publishedAt(cu pipedate). -
Tratați 404: dacă
getById()întoarce eroare (HTTP 404), afișațiArticolul nu a fost găsitși un buton de întoarcere la listă. -
Decomentați ruta
articles/:idînAppRoutingModule. -
Verificați că butonul
DetaliidinArticleListComponent(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
-
ng generate component shared/components/header -
Link-uri:
Articole(/articles),Login(/login),Register(/register). -
State reactiv: folosiți
AuthService.currentUser$cu| asyncpipe î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> -
Când utilizatorul e logat: afișați numele + buton Deconectare (apelează
authService.logout(), apoi navighează la/login). -
Când nu e logat: afișați
Login+Register. -
Adăugați
<app-header></app-header>deasupra<router-outlet>înapp.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țicurrentUser$ = this.authService.currentUser$direct pe linia declarației — cuuseDefineForClassFieldsactiv (default Angular 16+), property initializers rulează înainte de constructor, decithis.authServicee încăundefined. Soluție: declarațicurrentUser$;ș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.