<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Udhëzues Angular How-to: Tailwind CSS, chart.js, sinjalet, ngrx dhe format reaktive të avancuara

Udhëzues Angular How-To: Tailwind CSS, Chart.js, Sinjalet, NgRx dhe Format Reaktive të Avancuara

Angular How-To Guides: How-To Integrimet, Menaxhimi i Shtetit dhe Format Reaktive të Avancuara

Udhëzuesit hap pas hapi janë disa nga përmbajtjet më të dobishme për zhvilluesit, sepse ato zgjidhin probleme reale: instaloni një bibliotekë, konfiguroni saktë një projekt, zgjidhni një strategji të menaxhimit të shtetit, krijoni forma komplekse dhe parandaloni gabimet e vështira për t'u zbuluar.

Në këtë artikull do të shohim një rrugë praktike për të punuar më mirë me Angular moderne: Integrimi i Tailwind CSS, duke përdorur Chart.js, animacione, menaxhim të gjendjes me Sinjalet, NgRx dhe Akita, dhe ndërtimi i formave të avancuara të përgjegjshme me vërtetime sinkrone dhe asinkrone.

1. Parakushtet e projektit këndor

Përpara se të integroni bibliotekat ose të zbatoni arkitektura komplekse, është mirë të filloni nga një themel i pastër. Ne do të përdorim një projekt modern Angular, mundësisht me komponentë të pavarur, Forma Reaktive dhe TypeScript.

Krijimi i projektit

npm install -g @angular/cli

ng new angular-how-to-guide
cd angular-how-to-guide

ng serve

Nëse jeni duke punuar në një projekt ekzistues, të paktën sigurohuni që të keni:

  • Angular u përditësua në një version të fundit.
  • Node.js në një version të pajtueshëm me Angular.
  • TypeScript është konfiguruar saktë.
  • Një strukturë e qartë për komponentët, shërbimet, modelet dhe dyqanet.

Struktura e rekomanduar

src/
  app/
    core/
      services/
      guards/
      interceptors/
    shared/
      components/
      pipes/
      validators/
    features/
      dashboard/
      products/
      profile/
    state/
      signals/
      ngrx/
    app.config.ts
    app.routes.ts

Një strukturë e rregullt ndihmon veçanërisht kur projekti rritet. Ndarja e bazës, e përbashkët, veçorive dhe gjendjes ju lejon të shmangni varësitë konfuze dhe komponentët tepër të mëdhenj.

2. Si të integroni Tailwind CSS në Angular

Tailwind CSS është një bibliotekë e parë e shërbimeve që ju lejon të krijoni ndërfaqe moderne pa shkruar sasi të mëdha e CSS me porosi. Në Angular është shumë i dobishëm për panelet e kontrollit, panelet e administratorit, faqet e uljes dhe komponentët e ripërdorshëm.

Instalimi

npm install tailwindcss @tailwindcss/postcss postcss --save-dev

Krijoni ose përditësoni skedarin e konfigurimit PostCSS.

{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

Pastaj importoni Tailwind në skedarin e stileve globale, për shembullsrc/styles.css.

@import "tailwindcss";

Shembull i komponentit këndor me Tailwind

import { Component } from '@angular/core';

@Component({
  selector: 'app-pricing-card',
  standalone: true,
  template: `
    <section class="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
      <p class="text-sm font-medium uppercase tracking-wide text-indigo-600">Piano Pro</p>
      <h2 class="mt-2 text-3xl font-bold text-slate-900">€29/mese</h2>
      <p class="mt-3 text-slate-600">
        Ideale per team che vogliono creare applicazioni Angular più velocemente.
      </p>

      <ul class="mt-6 space-y-2 text-sm text-slate-700">
        <li>Componenti riutilizzabili</li>
        <li>Dashboard responsive</li>
        <li>Supporto a form complessi</li>
      </ul>

      <button class="mt-6 w-full rounded-xl bg-indigo-600 px-4 py-3 font-semibold text-white hover:bg-indigo-700">
        Inizia ora
      </button>
    </section>
  `
})
export class PricingCardComponent {}

Praktikat më të mira me Tailwind në Angular

  • Shmangni shabllonet e mëdha plot me klasa të përsëritura.
  • Nxjerrja e komponentëve të ripërdorshëm kur një UI përsëritet.
  • Përdorni klasa me kusht me[emri i klasës]ose[ngClass].
  • Përcaktoni modele të qëndrueshme të shenjave për ngjyrat, hapësirat dhe tipografinë.
  • Mos e përdorni Tailwind si një justifikim për të injoruar aksesin.

Shembull me klasa me kusht

<button
  type="button"
  class="rounded-lg px-4 py-2 font-semibold transition"
  [class.bg-green-600]="isActive"
  [class.text-white]="isActive"
  [class.bg-slate-200]="!isActive"
  [class.text-slate-700]="!isActive">
  Stato elemento
</button>

Kjo teknikë është e dobishme për skedat, butonat aktivë, filtrat, artikujt e zgjedhur dhe gjendjet e vlefshmërisë.

3. Si të integroni Chart.js në Angular

Chart.js është një bibliotekë e përdorur gjerësisht për grafikët e linjave, shiritave, radarëve, donutëve dhe tabelave statistikore. Në Angular mund ta përdorni drejtpërdrejt ose përmes mbështjellësve të dedikuar. Këtu shohim një integrim të drejtpërdrejtë, të thjeshta dhe të kontrollueshme.

Instalimi

npm install chart.js

Krijimi i komponentit grafik

ng generate component features/dashboard/revenue-chart --standalone

Kodi TypeScript i komponentit

import {
  AfterViewInit,
  Component,
  ElementRef,
  OnDestroy,
  ViewChild
} from '@angular/core';

import {
  Chart,
  ChartConfiguration,
  registerables
} from 'chart.js';

Chart.register(...registerables);

@Component({
  selector: 'app-revenue-chart',
  standalone: true,
  template: `
    <div class="h-80 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
      <h2 class="mb-4 text-lg font-semibold text-slate-900">Ricavi mensili</h2>
      <canvas #canvas></canvas>
    </div>
  `
})
export class RevenueChartComponent implements AfterViewInit, OnDestroy {
  @ViewChild('canvas')
  canvas!: ElementRef<HTMLCanvasElement>;

  private chart?: Chart;

  ngAfterViewInit(): void {
    const config: ChartConfiguration = {
      type: 'line',
      data: {
        labels: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu'],
        datasets: [
          {
            label: 'Ricavi',
            data: [12000, 15000, 14000, 21000, 26000, 31000],
            borderColor: '#4f46e5',
            backgroundColor: 'rgba(79, 70, 229, 0.15)',
            tension: 0.35,
            fill: true
          }
        ]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: {
            display: true
          }
        },
        scales: {
          y: {
            beginAtZero: true
          }
        }
      }
    };

    this.chart = new Chart(this.canvas.nativeElement, config);
  }

  ngOnDestroy(): void {
    this.chart?.destroy();
  }
}

Pse të shkatërrohet grafiku?

Kur një komponent Angular hiqet nga faqja, Chart.js mund të lërë dëgjues ose objekte në kujtesë. Thirrnishkatërroj ()ngOnDestroy()shmangni rrjedhjet e memories, veçanërisht në tabelat me shumë grafikë.

Përditësoni të dhënat në mënyrë dinamike

updateChart(values: number[]): void {
  if (!this.chart) {
    return;
  }

  this.chart.data.datasets[0].data = values;
  this.chart.update();
}

Në një projekt real, vlerat shpesh vijnë nga një shërbim HTTP, një dyqan global ose një sinjal.

Këshilla praktike

  • Përdorni komponentë të veçantë për çdo grafik kompleks.
  • Mos inicializoni Chart.js përpara se kanavasi të jetë i disponueshëm.
  • Gjithmonë shkatërroni grafikun brendangOnDestroy().
  • Për SSR ose Angular Universal, ekzekutoni Chart.js vetëm nga ana e shfletuesit.
  • Normalizoni të dhënat përpara se t'i kaloni në grafik.

4. Si të shtoni animacione në Angular

Animacionet përmirësojnë perceptimin e ndërfaqes, por duhet të përdoren me maturi. Qëllimi nuk është të "zbukurosh" faqen, por të ndihmosh përdoruesin të kuptojë se çfarë po ndodh: hapja e paneleve, shfaqja e njoftimeve, kalimet ndërmjet gjendjeve dhe reagimet mbi formularët.

Instalimi i paketës Angular Animations

npm install @angular/animations

Konfigurimi në app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimations()
  ]
};

Shembull i animacionit fade + rrëshqitje

import { Component } from '@angular/core';
import {
  animate,
  style,
  transition,
  trigger
} from '@angular/animations';

@Component({
  selector: 'app-notification',
  standalone: true,
  animations: [
    trigger('fadeSlide', [
      transition(':enter', [
        style({
          opacity: 0,
          transform: 'translateY(12px)'
        }),
        animate(
          '180ms ease-out',
          style({
            opacity: 1,
            transform: 'translateY(0)'
          })
        )
      ]),
      transition(':leave', [
        animate(
          '140ms ease-in',
          style({
            opacity: 0,
            transform: 'translateY(12px)'
          })
        )
      ])
    ])
  ],
  template: `
    @if (visible) {
      <div
        @fadeSlide
        class="rounded-xl border border-green-200 bg-green-50 p-4 text-green-800">
        Operazione completata con successo.
      </div>
    }

    <button
      class="mt-4 rounded-lg bg-green-600 px-4 py-2 text-white"
      type="button"
      (click)="visible = !visible">
      Mostra/Nascondi
    </button>
  `
})
export class NotificationComponent {
  visible = true;
}

Kur të përdorni një bibliotekë të jashtme si GSAP

Animacionet këndore janë të mjaftueshme për shumë ndërfaqe aplikacionesh. Një bibliotekë si GSAP bëhet e dobishme kur keni nevojë për afate komplekse kohore, animacione të koordinuara, faqet e uljes shumë dinamike ose mikro-ndërveprime të sofistikuara.

npm install gsap

Rregull i rëndësishëm: Shmangni animimin e pronave të rënda sigjerësia,lartësiaosekryekur mund të përdornitransformojDheerrësirë, të cilat në përgjithësi janë më efikase.

5. Menaxhimi i shtetit në Angular: Sinjalet, NgRx dhe Akita

Menaxhimi i shtetit është një nga temat më të rëndësishme në Angular. Në fillim një shërbim me disa variabla mund të duket i mjaftueshëm, por ndërsa aplikacioni rritet, ai bëhet i nevojshëm zgjidhni një qasje më të strukturuar.

Krahasim i shpejtë

Zgjidhje Kur ta përdorni Avantazhet Disavantazhet
Sinjalet Shtet i thjeshtë/mesatar lokal ose i përbashkët Sintaksë moderne, reagim i thjeshtë, më pak boilerplate Kërkon disiplinë arkitekturore në projekte të mëdha
NgRx Aplikacionet e ndërmarrjeve, flukset komplekse, auditueshmëria Model i parashikueshëm, DevTools, efekte, përzgjedhës Më shumë kazan dhe kurbë mësimi
Akita Dyqane më të dobëta, dyqane entitetesh, ekipe që duan më pak bojlerplate API pragmatike, menaxhimi i përshtatshëm i entitetit Më pak ekosistem qendror se NgRx

Rregulli i madh

  • Përdorni Sinjalet për gjendjen lokale, gjendjen e ndërfaqes së përdoruesit dhe veçoritë e vogla dhe të mesme.
  • Përdorni NgRx kur keni shumë transmetime asinkrone, role, leje, caching dhe korrigjime të avancuara.
  • Përdorni Akita nëse dëshironi një dyqan të strukturuar, por më pak të gjerë se NgRx.
  • Mos prezantoni një dyqan global nëse problemi mund të zgjidhet me një shërbim të thjeshtë.

6. Udhëzues praktik: ruajeni me sinjale këndore

Sinjalet ju lejojnë të përfaqësoni vlerat reaktive në një mënyrë të thjeshtë. Ato janë veçanërisht të përshtatshme për krijimin e dyqaneve të vogla të veçorive pa futur biblioteka të jashtme.

Le të krijojmë një dyqan Todo

import { Injectable, computed, signal } from '@angular/core';

export interface Todo {
  id: number;
  title: string;
  done: boolean;
}

export type TodoFilter = 'all' | 'active' | 'done';

@Injectable({
  providedIn: 'root'
})
export class TodoStore {
  private readonly todosState = signal<Todo[]>([]);

  readonly filter = signal<TodoFilter>('all');

  readonly todos = this.todosState.asReadonly();

  readonly activeCount = computed(() => {
    return this.todosState().filter(todo => !todo.done).length;
  });

  readonly completedCount = computed(() => {
    return this.todosState().filter(todo => todo.done).length;
  });

  readonly visibleTodos = computed(() => {
    const filter = this.filter();
    const todos = this.todosState();

    if (filter === 'active') {
      return todos.filter(todo => !todo.done);
    }

    if (filter === 'done') {
      return todos.filter(todo => todo.done);
    }

    return todos;
  });

  addTodo(title: string): void {
    const cleanTitle = title.trim();

    if (!cleanTitle) {
      return;
    }

    const todo: Todo = {
      id: Date.now(),
      title: cleanTitle,
      done: false
    };

    this.todosState.update(current => [todo, ...current]);
  }

  toggleTodo(id: number): void {
    this.todosState.update(current => {
      return current.map(todo => {
        if (todo.id !== id) {
          return todo;
        }

        return {
          ...todo,
          done: !todo.done
        };
      });
    });
  }

  removeTodo(id: number): void {
    this.todosState.update(current => {
      return current.filter(todo => todo.id !== id);
    });
  }

  clearCompleted(): void {
    this.todosState.update(current => {
      return current.filter(todo => !todo.done);
    });
  }
}

Komponenti që përdor dyqanin

import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TodoStore } from './todo.store';

@Component({
  selector: 'app-todos',
  standalone: true,
  imports: [FormsModule],
  template: `
    <section class="mx-auto max-w-2xl rounded-2xl bg-white p-6 shadow-sm">
      <h2 class="text-2xl font-bold text-slate-900">Todo con Signals</h2>

      <div class="mt-4 flex gap-2">
        <input
          class="flex-1 rounded-lg border border-slate-300 px-3 py-2"
          placeholder="Nuova attività"
          [(ngModel)]="title">

        <button
          type="button"
          class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
          (click)="add()">
          Aggiungi
        </button>
      </div>

      <div class="mt-4 flex gap-2">
        <button type="button" (click)="store.filter.set('all')">Tutti</button>
        <button type="button" (click)="store.filter.set('active')">Attivi</button>
        <button type="button" (click)="store.filter.set('done')">Completati</button>
      </div>

      <ul class="mt-6 space-y-2">
        @for (todo of store.visibleTodos(); track todo.id) {
          <li class="flex items-center justify-between rounded-lg border border-slate-200 p-3">
            <label class="flex items-center gap-2">
              <input
                type="checkbox"
                [checked]="todo.done"
                (change)="store.toggleTodo(todo.id)">

              <span [class.line-through]="todo.done">
                {{ todo.title }}
              </span>
            </label>

            <button
              type="button"
              class="text-red-600"
              (click)="store.removeTodo(todo.id)">
              Elimina
            </button>
          </li>
        }
      </ul>

      <p class="mt-4 text-sm text-slate-600">
        Attivi: {{ store.activeCount() }} -
        Completati: {{ store.completedCount() }}
      </p>
    </section>
  `
})
export class TodosComponent {
  readonly store = inject(TodoStore);

  title = '';

  add(): void {
    this.store.addTodo(this.title);
    this.title = '';
  }
}

Përparësitë e qasjes së sinjaleve

  • Kodi është i lexueshëm.
  • Ju nuk keni nevojë për veprime, reduktues apo efekte për raste të thjeshta.
  • Vlerat e përftuara mei llogaritur ()janë shumë komode.
  • Shteti mbetet i kapsuluar në shërbim.
  • Është e lehtë të testosh metodat e dyqanit.

Kufiri për të mbajtur mend

Sinjalet nuk do të thotë "arkitekturë automatike". Nëse projekti rritet shumë, ju ende duhet të përcaktoni konventat: emërtimi, ndarja e shërbimeve, qëndrueshmëria, ruajtja në memorie, trajtimi i gabimeve dhe komunikimi me API-të.

7. Tutorial praktik: menaxhimi i shtetit me NgRx

NgRx zbaton një model të frymëzuar nga Redux. Është i përshtatshëm kur dëshironi një gjendje të parashikueshme, të gjurmueshme dhe lehtësisht të korrigjueshme. Përdoret gjerësisht në aplikacionet e ndërmarrjeve.

Instalimi

ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools

Përkufizimi i modelit

export interface Product {
  id: number;
  name: string;
  price: number;
}

export interface ProductsState {
  items: Product[];
  loading: boolean;
  error: string | null;
}

export const initialProductsState: ProductsState = {
  items: [],
  loading: false,
  error: null
};

Veprimet

import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Product } from './product.model';

export const ProductsActions = createActionGroup({
  source: 'Products',
  events: {
    'Load Products': emptyProps(),
    'Load Products Success': props<{ products: Product[] }>(),
    'Load Products Failure': props<{ error: string }>()
  }
});

Reduktues

import { createReducer, on } from '@ngrx/store';
import { ProductsActions } from './products.actions';
import { initialProductsState } from './product.model';

export const productsReducer = createReducer(
  initialProductsState,

  on(ProductsActions.loadProducts, state => ({
    ...state,
    loading: true,
    error: null
  })),

  on(ProductsActions.loadProductsSuccess, (state, { products }) => ({
    ...state,
    items: products,
    loading: false
  })),

  on(ProductsActions.loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  }))
);

Përzgjedhës

import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductsState } from './product.model';

export const selectProductsState =
  createFeatureSelector<ProductsState>('products');

export const selectProducts =
  createSelector(
    selectProductsState,
    state => state.items
  );

export const selectProductsLoading =
  createSelector(
    selectProductsState,
    state => state.loading
  );

export const selectProductsError =
  createSelector(
    selectProductsState,
    state => state.error
  );

Efektet

import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
import { ProductsActions } from './products.actions';
import { ProductsApiService } from './products-api.service';

@Injectable()
export class ProductsEffects {
  private readonly actions$ = inject(Actions);
  private readonly api = inject(ProductsApiService);

  loadProducts$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(ProductsActions.loadProducts),
      switchMap(() => {
        return this.api.getProducts().pipe(
          map(products => ProductsActions.loadProductsSuccess({ products })),
          catchError(() => {
            return of(
              ProductsActions.loadProductsFailure({
                error: 'Errore durante il caricamento dei prodotti.'
              })
            );
          })
        );
      })
    );
  });
}

Konfigurimi i dyqanit

import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productsReducer } from './state/products.reducer';
import { ProductsEffects } from './state/products.effects';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore({
      products: productsReducer
    }),
    provideEffects([
      ProductsEffects
    ]),
    provideStoreDevtools({
      maxAge: 25
    })
  ]
};

Komponent këndor me NgRx

import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { ProductsActions } from './state/products.actions';
import {
  selectProducts,
  selectProductsError,
  selectProductsLoading
} from './state/products.selectors';

@Component({
  selector: 'app-products-page',
  standalone: true,
  template: `
    <section class="space-y-4">
      <h2 class="text-2xl font-bold">Prodotti</h2>

      @if (loading()) {
        <p>Caricamento...</p>
      }

      @if (error()) {
        <p class="text-red-600">{{ error() }}</p>
      }

      <ul class="grid gap-4 md:grid-cols-3">
        @for (product of products(); track product.id) {
          <li class="rounded-xl border border-slate-200 p-4">
            <h3 class="font-semibold">{{ product.name }}</h3>
            <p>€ {{ product.price }}</p>
          </li>
        }
      </ul>
    </section>
  `
})
export class ProductsPageComponent {
  private readonly store = inject(Store);

  readonly products = this.store.selectSignal(selectProducts);
  readonly loading = this.store.selectSignal(selectProductsLoading);
  readonly error = this.store.selectSignal(selectProductsError);

  constructor() {
    this.store.dispatch(ProductsActions.loadProducts());
  }
}

Kur NgRx është zgjedhja e duhur

  • Ju keni shumë veçori që ndajnë të dhëna.
  • Ju keni rrjedha komplekse asinkrone.
  • Ju duhet DevTools dhe korrigjimi i udhëtimit në kohë.
  • Ekipi duhet të ndjekë modele shumë të qarta.
  • Shteti duhet të jetë i parashikueshëm dhe i centralizuar.

8. Kur duhet përdorur Akita

Akita është një zgjidhje më pragmatike dhe më pak e folur për menaxhimin e shtetit sesa NgRx. Është e dobishme kur dëshironi një dyqan të strukturuar, por pa shkruar shumë veprime dhe reduktues.

Instalimi

npm install @datorama/akita

Shembull konceptual i një dyqani

import { Injectable } from '@angular/core';
import {
  EntityState,
  EntityStore,
  StoreConfig
} from '@datorama/akita';

export interface CartItem {
  id: number;
  name: string;
  quantity: number;
}

export interface CartState extends EntityState<CartItem, number> {}

@Injectable({
  providedIn: 'root'
})
@StoreConfig({
  name: 'cart'
})
export class CartStore extends EntityStore<CartState> {
  constructor() {
    super();
  }
}

Shërbimi për të ndryshuar gjendjen

import { Injectable, inject } from '@angular/core';
import { CartStore, CartItem } from './cart.store';

@Injectable({
  providedIn: 'root'
})
export class CartService {
  private readonly cartStore = inject(CartStore);

  addItem(item: CartItem): void {
    this.cartStore.add(item);
  }

  updateQuantity(id: number, quantity: number): void {
    this.cartStore.update(id, {
      quantity
    });
  }

  removeItem(id: number): void {
    this.cartStore.remove(id);
  }

  clearCart(): void {
    this.cartStore.reset();
  }
}

Zgjedhje praktike

Nëse ekipi juaj tashmë është i njohur me NgRx dhe punon në aplikacionet e ndërmarrjeve, NgRx mbetet një zgjedhje shumë solide. Sidoqoftë, nëse dëshironi një dyqan që zbatohet më shpejt dhe më pak ceremonial, Akita mund të jetë një alternativë e mirë. Për aplikacione të reja dhe të vogla dhe të mesme, konsideroni fillimisht Sinjalet.

9. Format e avancuara reaktive në Angular

Format Reaktive janë një nga mjetet më të fuqishme të Angular. Ato ju lejojnë të ndërtoni forma komplekse, të vërtetoni hyrjet, të menaxhoni gjendjet dinamike, të krijoni FormArrays, vlefshmëritë ndërmjet fushave dhe kontrollet asinkrone të lidhura me API-të.

Skenar praktik

Le të krijojmë një formë profili me:

  • të dhënat e llogarisë;
  • email me vërtetim asinkron;
  • fjalëkalim me vleftësues të personalizuar;
  • konfirmoni fjalëkalimin;
  • listë dinamike e aftësive meFormArray;
  • pastroni mesazhet e gabimit në shabllon.

Vlerësues të personalizuar

import {
  AbstractControl,
  AsyncValidatorFn,
  ValidationErrors,
  ValidatorFn
} from '@angular/forms';

import {
  catchError,
  map,
  of,
  switchMap,
  timer
} from 'rxjs';

import { UsersApiService } from './users-api.service';

export function passwordStrengthValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const value = String(control.value || '');

    const hasMinLength = value.length >= 8;
    const hasUppercase = /[A-Z]/.test(value);
    const hasNumber = /[0-9]/.test(value);
    const hasSpecial = /[^A-Za-z0-9]/.test(value);

    const valid =
      hasMinLength &&
      hasUppercase &&
      hasNumber &&
      hasSpecial;

    if (valid) {
      return null;
    }

    return {
      weakPassword: {
        hasMinLength,
        hasUppercase,
        hasNumber,
        hasSpecial
      }
    };
  };
}

export function passwordsMatchValidator(
  passwordKey: string,
  confirmPasswordKey: string
): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const password = group.get(passwordKey)?.value;
    const confirmPassword = group.get(confirmPasswordKey)?.value;

    if (password === confirmPassword) {
      return null;
    }

    return {
      passwordsMismatch: true
    };
  };
}

export function uniqueEmailValidator(
  usersApi: UsersApiService
): AsyncValidatorFn {
  return (control: AbstractControl) => {
    const email = String(control.value || '').trim();

    if (!email) {
      return of(null);
    }

    return timer(400).pipe(
      switchMap(() => usersApi.emailExists(email)),
      map(exists => {
        return exists ? { emailTaken: true } : null;
      }),
      catchError(() => {
        return of(null);
      })
    );
  };
}

Shembull i shërbimit API

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class UsersApiService {
  private readonly http = inject(HttpClient);

  emailExists(email: string): Observable<boolean> {
    return this.http.get<boolean>(
      `/api/users/email-exists?email=${encodeURIComponent(email)}`
    );
  }
}

Komponent me formë komplekse reaktive

import { Component, inject } from '@angular/core';
import {
  FormArray,
  NonNullableFormBuilder,
  ReactiveFormsModule,
  Validators
} from '@angular/forms';

import {
  passwordStrengthValidator,
  passwordsMatchValidator,
  uniqueEmailValidator
} from './profile.validators';

import { UsersApiService } from './users-api.service';

@Component({
  selector: 'app-profile-form',
  standalone: true,
  imports: [
    ReactiveFormsModule
  ],
  templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
  private readonly fb = inject(NonNullableFormBuilder);
  private readonly usersApi = inject(UsersApiService);

  readonly form = this.fb.group({
    account: this.fb.group(
      {
        name: [
          '',
          [
            Validators.required,
            Validators.minLength(2)
          ]
        ],
        email: [
          '',
          {
            validators: [
              Validators.required,
              Validators.email
            ],
            asyncValidators: [
              uniqueEmailValidator(this.usersApi)
            ],
            updateOn: 'blur'
          }
        ],
        password: [
          '',
          [
            Validators.required,
            passwordStrengthValidator()
          ]
        ],
        confirmPassword: [
          '',
          [
            Validators.required
          ]
        ]
      },
      {
        validators: [
          passwordsMatchValidator('password', 'confirmPassword')
        ]
      }
    ),
    profile: this.fb.group({
      role: ['frontend'],
      bio: ['']
    }),
    skills: this.fb.array([
      this.createSkill()
    ])
  });

  get skills(): FormArray {
    return this.form.controls.skills;
  }

  createSkill() {
    return this.fb.group({
      name: [
        '',
        [
          Validators.required
        ]
      ],
      years: [
        0,
        [
          Validators.required,
          Validators.min(0),
          Validators.max(40)
        ]
      ]
    });
  }

  addSkill(): void {
    this.skills.push(this.createSkill());
  }

  removeSkill(index: number): void {
    if (this.skills.length === 1) {
      return;
    }

    this.skills.removeAt(index);
  }

  submit(): void {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }

    console.log('Payload form:', this.form.getRawValue());
  }
}

Shablloni HTML i formularit

<form
  class="mx-auto max-w-3xl space-y-8 rounded-2xl bg-white p-6 shadow-sm"
  [formGroup]="form"
  (ngSubmit)="submit()">

  <section formGroupName="account" class="space-y-4">
    <h2 class="text-xl font-bold text-slate-900">Account</h2>

    <div>
      <label class="block text-sm font-medium text-slate-700">
        Nome
      </label>

      <input
        class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
        type="text"
        formControlName="name">

      @if (form.controls.account.controls.name.touched &&
           form.controls.account.controls.name.hasError('required')) {
        <p class="mt-1 text-sm text-red-600">
          Il nome è obbligatorio.
        </p>
      }
    </div>

    <div>
      <label class="block text-sm font-medium text-slate-700">
        Email
      </label>

      <input
        class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
        type="email"
        formControlName="email">

      @if (form.controls.account.controls.email.pending) {
        <p class="mt-1 text-sm text-slate-500">
          Verifica email in corso...
        </p>
      }

      @if (form.controls.account.controls.email.hasError('emailTaken')) {
        <p class="mt-1 text-sm text-red-600">
          Questa email è già registrata.
        </p>
      }
    </div>

    <div>
      <label class="block text-sm font-medium text-slate-700">
        Password
      </label>

      <input
        class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
        type="password"
        formControlName="password">

      @if (form.controls.account.controls.password.hasError('weakPassword')) {
        <p class="mt-1 text-sm text-red-600">
          La password deve avere almeno 8 caratteri, una maiuscola,
          un numero e un carattere speciale.
        </p>
      }
    </div>

    <div>
      <label class="block text-sm font-medium text-slate-700">
        Conferma password
      </label>

      <input
        class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
        type="password"
        formControlName="confirmPassword">

      @if (form.controls.account.hasError('passwordsMismatch')) {
        <p class="mt-1 text-sm text-red-600">
          Le password non coincidono.
        </p>
      }
    </div>
  </section>

  <section formGroupName="profile" class="space-y-4">
    <h2 class="text-xl font-bold text-slate-900">Profilo</h2>

    <div>
      <label class="block text-sm font-medium text-slate-700">
        Ruolo
      </label>

      <select
        class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
        formControlName="role">
        <option value="frontend">Frontend Developer</option>
        <option value="backend">Backend Developer</option>
        <option value="fullstack">Full Stack Developer</option>
      </select>
    </div>

    <div>
      <label class="block text-sm font-medium text-slate-700">
        Bio
      </label>

      <textarea
        class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
        rows="4"
        formControlName="bio"></textarea>
    </div>
  </section>

  <section class="space-y-4">
    <div class="flex items-center justify-between">
      <h2 class="text-xl font-bold text-slate-900">
        Competenze
      </h2>

      <button
        type="button"
        class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
        (click)="addSkill()">
        Aggiungi competenza
      </button>
    </div>

    <div formArrayName="skills" class="space-y-3">
      @for (skill of skills.controls; track $index) {
        <div
          class="grid gap-3 rounded-xl border border-slate-200 p-4 md:grid-cols-[1fr_140px_auto]"
          [formGroupName]="$index">

          <input
            class="rounded-lg border border-slate-300 px-3 py-2"
            type="text"
            placeholder="Competenza"
            formControlName="name">

          <input
            class="rounded-lg border border-slate-300 px-3 py-2"
            type="number"
            placeholder="Anni"
            formControlName="years">

          <button
            type="button"
            class="rounded-lg border border-red-300 px-4 py-2 text-red-600"
            (click)="removeSkill($index)">
            Rimuovi
          </button>
        </div>
      }
    </div>
  </section>

  <button
    type="submit"
    class="w-full rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white disabled:opacity-50"
    [disabled]="form.pending">
    Salva profilo
  </button>

</form>

Gabimet e zakonshme në Format Reaktive

  • Vendosja e shumë logjikës në shabllon.
  • Mos përdornimarkAllAsTouched()pas dorëzimit.
  • Kryeni thirrje asinkrone në çdo shtypje tasti papërditësimiOn: 'turbullt'ose debounce.
  • Mos i ndani validuesit në skedarë të dedikuar.
  • Mos e drejtoni shtetinnë pritjetë vërtetimeve asinkrone.
  • Mos shtypja e saktë e formularëve në projekte të mëdha.

Praktikat më të mira

  • Përdorni Format Reaktive për forma komplekse dhe dinamike.
  • Përdorni format e drejtuara nga shabllonet vetëm për forma shumë të thjeshta.
  • Ekstraktoni vërtetuesit me porosi.
  • Shfaq mesazhet e gabimit vetëm më pasi prekurose dorëzoni.
  • Çaktivizo dërgimin ndërsa formulari ështënë pritje.
  • Normalizoni ngarkesën përpara se ta dërgoni në backend.

10. Provoni dyqanet dhe formularët

Një tutorial praktik nuk është i plotë pa testim. Ju nuk keni nevojë të provoni çdo detaj të kornizës, por është e rëndësishme të testoni logjikën tuaj të personalizuar: dyqane, vërtetues dhe transformim të të dhënave.

Testimi i një verifikuesi të fjalëkalimit

import { FormControl } from '@angular/forms';
import { passwordStrengthValidator } from './profile.validators';

describe('passwordStrengthValidator', () => {
  it('dovrebbe invalidare una password debole', () => {
    const control = new FormControl('abc');
    const validator = passwordStrengthValidator();

    const result = validator(control);

    expect(result).toEqual(
      jasmine.objectContaining({
        weakPassword: jasmine.any(Object)
      })
    );
  });

  it('dovrebbe accettare una password forte', () => {
    const control = new FormControl('Angular2026!');
    const validator = passwordStrengthValidator();

    const result = validator(control);

    expect(result).toBeNull();
  });
});

Testimi i dyqanit me sinjale

import { TodoStore } from './todo.store';

describe('TodoStore', () => {
  it('dovrebbe aggiungere una todo', () => {
    const store = new TodoStore();

    store.addTodo('Studiare Angular Signals');

    expect(store.todos().length).toBe(1);
    expect(store.todos()[0].title).toBe('Studiare Angular Signals');
  });

  it('dovrebbe filtrare le todo completate', () => {
    const store = new TodoStore();

    store.addTodo('Prima attività');
    const id = store.todos()[0].id;

    store.toggleTodo(id);
    store.filter.set('done');

    expect(store.visibleTodos().length).toBe(1);
    expect(store.visibleTodos()[0].done).toBeTrue();
  });
});

11. Performanca dhe mirëmbajtja

Në Angular, performanca dhe mirëmbajtja janë të lidhura. Një aplikacion i shpejtë por kaotik bëhet i vështirë për t'u zhvilluar. Një aplikacion i organizuar mirë, nga ana tjetër, tenton gjithashtu të jetë më i lehtë për t'u optimizuar.

Këshilla praktike

  • Ndani veçoritë në komponentë të vegjël.
  • Përdorni ngarkimin dembel për faqe dhe zona të rënda.
  • Shmangni logjikën komplekse brenda shablloneve.
  • SHBAudhënë cikle@për.
  • Shkatërroni automatikisht dëgjuesit, grafikët dhe abonimet e pamenaxhuara.
  • Mos vendosni gjithçka në dyqanin global.
  • Përdorni vlerat e llogaritura në vend të rillogaritjes së të dhënave në shabllon.
  • Shmangni vërtetimet asinkrone tepër agresive.

12. Lista e fundit kontrolluese për një projekt solid Angular

  • A ka projekti një strukturë të qartë?
  • A janë komponentët të vegjël dhe të fokusuar?
  • A janë bibliotekat e jashtme të përfshira në komponentë ose shërbime?
  • A është shkatërruar saktë Chart.js?
  • A përdoret vazhdimisht Tailwind?
  • A është shteti lokal kur është e mundur dhe global vetëm kur nevojitet?
  • A përdorin format komplekse Format Reaktive?
  • A janë testuar vërtetuesit me porosi?
  • A janë gabimet e formularit të kuptueshëm nga përdoruesi?
  • A kanë thirrjet asinkrone trajtimin e gabimeve?
  • A është kodi i lexueshëm edhe për një zhvillues të ri?

FAQ

A janë sinjalet apo NgRx më mirë?

Varet nga kompleksiteti. Sinjalet janë të shkëlqyera për status të thjeshtë ose mesatar. NgRx është më i përshtatshmi për aplikacione të mëdha me flukse komplekse, korrigjim të avancuar dhe ekipe të mëdha.

A mund të përdor Sinjalet dhe NgRx së bashku?

po. Mund të përdorni NgRx për gjendjen globale dhe Sinjalet për gjendjen lokale të UI, filtrat, panelet, tab dhe vlerat e prejardhura në komponentë.

A është Tailwind CSS i përshtatshëm për aplikacionet e ndërmarrjeve?

Po, nëse përdoret me konventa të qarta. Është e rëndësishme të krijohen komponentë të përbashkët dhe të mos kopjohen klasa kudo pa kriter.

A mjafton Chart.js për panele komplekse?

Për shumë tabela, po. Nëse keni nevojë për grafikë jashtëzakonisht të avancuar, ndërveprime të personalizuara ose sasi të mëdha të dhënash, ju mund të konsideroni biblioteka më të specializuara.

Kur të përdorni Format Reaktive?

Përdorni Format Reaktive kur keni vërtetime komplekse, forma dinamike, FormArrays, kontrolle të varura reciprokisht ose vërtetime asinkrone.

Si të trajtohen gabimet në formularë?

Shfaq gabimet vetëm pasi të jetë prekur fusha ose pas dorëzimit. Shmangni shfaqjen e shumë gabimeve ndërsa përdoruesi është ende duke shkruar.

A e ngadalësojnë aplikacionin verifikimet asinkrone?

Ata mund ta ngadalësojnë atë nëse bëhet çdo personazh. SHBApërditësimiOn: 'turbullt', debounc dhe caching kur është e nevojshme.

A është Akita ende një zgjedhje e vlefshme?

Mund të jetë në projekte që tashmë e përdorin atë ose në ekipe që preferojnë një qasje më të lehtë. Për projektet e reja, vlerësoni me kujdes Sinjalet dhe NgRx përpara se të prezantoni një bibliotekë të jashtme.

A është më mirë të përdoren bibliotekat e mbështjellësit për Chart.js?

varet. Një mbështjellës mund të thjeshtojë integrimin, por përdorimi i tij drejtpërdrejt ofron më shumë kontroll. Për raste të thjeshta, integrimi i drejtpërdrejtë shpesh është i mjaftueshëm.

Si të shmangni komponentët Angular që janë shumë të mëdhenj?

Ekstraktoni logjikën në shërbime, dyqane ose validues. Ndani UI-në në komponentë më të vegjël dhe përdorni shërbimet hyrëse/dalëse ose të përbashkëta kur është e nevojshme.

konkluzioni

Angular ofron mjete të fuqishme për ndërtimin e aplikacioneve moderne, por ndryshimi është se si ato kombinohen. Tailwind CSS thjeshton ndërfaqen, Chart.js ju lejon të ndërtoni panele efektive, animacionet përmirësojnë përvojën, Sinjalet e bëjnë reaktivitetin më të lehtë, NgRx strukturon gjendjen e ndërmarrjes dhe Format Reaktive trajtojnë forma komplekse fuqishëm.

Zgjedhja më e mirë nuk është të përdorni gjithmonë teknologjinë më të avancuar, por të zgjidhni mjetin më të përshtatshëm për problemin. Filloni thjesht, shtoni kompleksitet vetëm kur është e nevojshme dhe mbajeni kodin të lexueshëm.

💬 Shënime nga lexuesit

0 shënime

Shkruaj një shënim

Ndaj mendimin tënd, një sugjerim ose një kompliment

Shënimet e fundit

Ende asnjë shënim. Bëhu i pari që komenton!