Hogyan készítsünk egy drag-and-drop felületet az Angular CDK-val?

Képzeljen el egy felhasználói felületet, ahol az elemek mozgatása, rendezése vagy épp áthelyezése egy másik listába olyan természetesen történik, mintha a fizikai valóságban tenné. A „húzd és engedd”, vagy ahogy angolul nevezzük, a drag-and-drop funkcionalitás mára alapvető elvárássá vált a modern webes alkalmazásokban. Legyen szó feladatlistákról, fájlkezelőkről, táblázatokról vagy akár játékokról, ez az interaktív elem rendkívül sokat dob a felhasználói élményen (UX).

Bár a drag-and-drop megvalósítása elsőre bonyolultnak tűnhet, az Angular ökoszisztémája szerencsére kínál egy elegáns és hatékony megoldást: az Angular CDK-t (Component Dev Kit). Ez a cikk részletesen bemutatja, hogyan építhetünk fel egy robusztus és testreszabható húzd-és-engedd felületet az Angular CDK segítségével, lépésről lépésre, a kezdeti beállítástól a haladó funkciókig.

Mi az az Angular CDK?

Az Angular CDK nem más, mint egy olyan eszközkészlet, amely magas szintű, viselkedés alapú alapkomponenseket (primitives) biztosít az Angular komponensek építéséhez, anélkül, hogy előre meghatározott UI stílusokat erőltetne. Gondoljunk rá úgy, mint egy funkcionális alapra, amelyre ráépíthetjük a saját vizuális megjelenésünket. A CDK modulok széles skáláját kínálja, többek között:

  • Accessibility: Eszközök a jobb akadálymentességhez.
  • Overlay: Felugró ablakok, menük kezelése.
  • Scrolling: Gördítési viselkedés testreszabása.
  • Drag and Drop: Ez az, ami minket a legjobban érdekel. Lehetővé teszi az elemek húzását és eldobását a DOM-ban.

A CDK legnagyobb előnye, hogy elvonatkoztat a vizuális megjelenéstől, így teljes szabadságot ad a fejlesztőknek a stílusok és a dizájn kialakításában. Ezáltal a megvalósítás rendkívül rugalmas és könnyedén illeszthető bármilyen projekt egyedi igényeihez.

Előfeltételek és Telepítés

Mielőtt belevágunk a kódolásba, győződjünk meg róla, hogy rendelkezünk egy működő Angular projekttel. Ha még nincs, az Angular CLI segítségével könnyedén létrehozhat egyet:

ng new my-drag-app
cd my-drag-app

Ezután telepítenünk kell az Angular CDK-t. Ez magában foglalja az összes CDK modult, beleértve a drag-and-drop funkcionalitást is. A telepítéshez futtassuk az alábbi parancsot a projekt gyökérkönyvtárából:

ng add @angular/cdk

Ez a parancs hozzáadja a @angular/cdk csomagot a package.json fájlunkhoz, és automatikusan beállítja a szükséges konfigurációkat. A telepítés után készen állunk arra, hogy bevezessük a drag-and-drop funkciót az alkalmazásunkba.

Az Alapvető Húzd-és-Engedd Funkció: cdkDrag

A drag-and-drop alapja a DragDropModule. Ezt a modult importálnunk kell abba az Angular modulba (általában az AppModule-be vagy egy funkcionális modulba), ahol használni szeretnénk a drag-and-drop funkciót.

Nyissuk meg az src/app/app.module.ts fájlt, és adjuk hozzá a DragDropModule-t az imports tömbhöz:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { DragDropModule } from '@angular/cdk/drag-drop'; // <-- Ezt importáljuk

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    DragDropModule // <-- Ezt adjuk hozzá
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Most, hogy a modul importálva van, használhatjuk a cdkDrag direktívát bármely HTML elemen, amelyet húzhatóvá szeretnénk tenni. Tekintsünk meg egy egyszerű példát:

src/app/app.component.html:

<div class="container">
  <h2>Alapvető Húzd-és-Engedd Példa</h2>
  <div class="box" cdkDrag>
    Húzz engem!
  </div>
</div>

src/app/app.component.css:

.container {
  padding: 20px;
  text-align: center;
}

.box {
  width: 150px;
  height: 150px;
  background-color: #4CAF50;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 1.2em;
  cursor: grab;
  border-radius: 8px;
  margin: 20px auto;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

/* Az Angular CDK által hozzáadott osztályok */
.cdk-drag-preview {
  box-sizing: border-box;
  border-radius: 4px;
  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
              0 8px 10px 1px rgba(0, 0, 0, 0.14),
              0 3px 14px 2px rgba(0, 0, 0, 0.12);
}

.cdk-drag-placeholder {
  opacity: 0;
}

.cdk-drag-animating {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

Ha most elindítjuk az alkalmazásunkat (ng serve), láthatjuk, hogy a „Húzz engem!” feliratú doboz szabadon mozgatható az oldalon. A cdkDrag direktíva gondoskodik mindenről, ami a húzási művelethez szükséges: az egéresemények kezelésétől kezdve a pozíciófrissítésig. Fontos megjegyezni, hogy a CDK alapértelmezésben nem ad stílust az elemeknek; a fent látható CSS csak a doboz alap megjelenéséért felel, és néhány CDK által hozzáadott osztályhoz kapcsolódó vizuális visszajelzést ad.

Listák Rendezése: cdkDropList és moveItemInArray

A drag-and-drop gyakori felhasználási módja a listák elemeinek rendezése. Az Angular CDK ezt is rendkívül egyszerűvé teszi a cdkDropList direktívával és a cdkDropListDropped eseménnyel.

Definiáljunk egy listát az app.component.ts fájlban:

import { Component } from '@angular/core';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'my-drag-app';

  movies = [
    'Forrest Gump',
    'A remény rabjai',
    'Ponyvaregény',
    'A Gyűrűk Ura: A Gyűrű Szövetsége',
    'Mátrix',
    'Interstellar'
  ];

  drop(event: CdkDragDrop<string[]>) {
    moveItemInArray(this.movies, event.previousIndex, event.currentIndex);
  }
}

Ezután módosítsuk az app.component.html fájlt, hogy megjelenítse a filmlistát, és tegye azt rendezhetővé:

<div class="container">
  <h2>Filmek rendezése</h2>

  <div cdkDropList class="movie-list" (cdkDropListDropped)="drop($event)">
    <div class="movie-box" *ngFor="let movie of movies" cdkDrag>
      {{ movie }}
    </div>
  </div>
</div>

Ne felejtsük el frissíteni a CSS-t a listák megjelenéséhez:

/* ... (Előző CSS kód) ... */

.movie-list {
  width: 300px;
  max-width: 100%;
  border: solid 1px #ccc;
  min-height: 60px;
  display: flex;
  flex-direction: column;
  background: white;
  border-radius: 4px;
  overflow: hidden;
  margin: 20px auto;
}

.movie-box {
  padding: 20px 10px;
  border-bottom: solid 1px #ccc;
  color: rgba(0, 0, 0, 0.87);
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  cursor: grab;
  background: white;
  font-size: 14px;
}

.movie-box:last-child {
  border: none;
}

.movie-box.cdk-drag-placeholder {
  opacity: 0;
}

.cdk-drag-animating {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

.movie-box:active {
  cursor: grabbing;
}

.movie-list.cdk-drop-list-dragging .movie-box:not(.cdk-drag-placeholder) {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

Ebben a példában a cdkDropList direktíva jelöli ki a droppable területet (azaz azt a konténert, ahová az elemeket dobhatjuk). A (cdkDropListDropped)="drop($event)" eseményfigyelő hívja meg a drop metódust, amikor egy elem befejezi a húzási műveletet a listában. A drop metóduson belül a moveItemInArray segédfüggvényt használjuk, amely az @angular/cdk/drag-drop csomagból származik. Ez a függvény automatikusan átrendezi a tömb elemeit a megadott indexek alapján, és a CDK frissíti a DOM-ot ennek megfelelően. Ez a megoldás rendkívül hatékony és minimalista.

Elemek Áthelyezése Listák Között: cdkDropListConnectedTo és transferArrayItem

Gyakran van szükségünk arra, hogy ne csak rendezzük az elemeket egy listán belül, hanem áthelyezzük őket különböző listák közé. Az Angular CDK ezt is zökkenőmentesen kezeli a cdkDropListConnectedTo tulajdonság és a transferArrayItem segédfüggvény segítségével.

Képzeljünk el két listát: „Elintézendő” és „Kész”.

src/app/app.component.ts:

import { Component } from '@angular/core';
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop'; // <-- transferArrayItem is

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'my-drag-app';

  todo = [
    'Vásárolj élelmiszert',
    'Fizesd be a számlákat',
    'Küldj e-mailt Jánosnak',
    'Tervezd meg a hétvégét'
  ];

  done = [
    'Készítsd el a prezentációt',
    'Kódolj 2 órát'
  ];

  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      // Elem mozgatása ugyanazon a listán belül
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      // Elem áthelyezése listák között
      transferArrayItem(
        event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex
      );
    }
  }
}

src/app/app.component.html:

<div class="container">
  <h2>Feladatkezelő</h2>

  <div class="task-lists-wrapper">
    <div class="task-list">
      <h3>Elintézendő</h3>
      <div
        cdkDropList
        #todoList="cdkDropList"
        [cdkDropListData]="todo"
        [cdkDropListConnectedTo]="[doneList]"
        class="example-list"
        (cdkDropListDropped)="drop($event)"
      >
        <div class="example-box" *ngFor="let item of todo" cdkDrag>
          {{ item }}
        </div>
      </div>
    </div>

    <div class="task-list">
      <h3>Kész</h3>
      <div
        cdkDropList
        #doneList="cdkDropList"
        [cdkDropListData]="done"
        [cdkDropListConnectedTo]="[todoList]"
        class="example-list"
        (cdkDropListDropped)="drop($event)"
      >
        <div class="example-box" *ngFor="let item of done" cdkDrag>
          {{ item }}
        </div>
      </div>
    </div>
  </div>
</div>

src/app/app.component.css:

/* ... (Előző CSS kód) ... */

.task-lists-wrapper {
  display: flex;
  justify-content: center;
  margin-top: 20px;
  gap: 20px; /* Helyköz a listák között */
}

.task-list {
  width: 300px;
  border: solid 1px #ccc;
  min-height: 200px;
  border-radius: 4px;
  overflow: hidden;
  background: white;
  box-shadow: 0 3px 5px rgba(0, 0, 0, 0.1);
}

.task-list h3 {
  text-align: center;
  background-color: #f0f0f0;
  padding: 10px;
  margin: 0;
  border-bottom: solid 1px #eee;
}

.example-list {
  min-height: 60px;
  display: flex;
  flex-direction: column;
  padding: 10px;
}

.example-box {
  padding: 15px 10px;
  border: solid 1px #eee;
  margin-bottom: 8px;
  color: rgba(0, 0, 0, 0.87);
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  cursor: grab;
  background: #fff;
  font-size: 14px;
  border-radius: 4px;
  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1);
}

.example-box:last-child {
  margin-bottom: 0;
}

.cdk-drag-placeholder {
  opacity: 0;
}

.cdk-drag-animating {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

/* Kiemelés a listák közötti áthúzáskor */
.example-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) {
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}

.cdk-drop-list-receiving {
  background: #e0f2f7; /* Világoskék háttér, amikor a lista fogadó állapotban van */
}

.cdk-drop-list-dragging .example-box {
  opacity: 0; /* A húzott elem eredeti helyén eltűnik */
}
.cdk-drag-placeholder {
  background: #ccc;
  border: dotted 3px #999;
  min-height: 50px;
  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 1; /* Alapértelmezett placeholder láthatóvá tétele */
}

Ebben a kibővített példában a következő kulcsfontosságú elemeket figyelhetjük meg:

  • #todoList="cdkDropList" és #doneList="cdkDropList": Template reference változókat definiálunk az egyes cdkDropList direktívákhoz. Ezekre azért van szükség, hogy a listák egymásra tudjanak hivatkozni.
  • [cdkDropListData]="todo" és [cdkDropListData]="done": Ezzel a bindinggel adjuk meg a cdkDropList-nek, hogy mely adatforráshoz tartozik. Fontos, hogy ez ne egy statikus érték, hanem a komponensben definiált tömb legyen.
  • [cdkDropListConnectedTo]="[doneList]" és [cdkDropListConnectedTo]="[todoList]": Ez a tulajdonság határozza meg, hogy az adott cdkDropList mely más cdkDropList-ekkel van összekötve, azaz melyekből tud fogadni vagy melyekbe tud elemeket áthelyezni. Egy tömböt vár, amely a csatlakoztatni kívánt listák referenciáit tartalmazza.
  • drop(event: CdkDragDrop<string[]>): Ez a metódus most már ellenőrzi, hogy az elem ugyanazon a listán belül maradt-e (event.previousContainer === event.container) vagy áthelyezték-e egy másik listába.
    • Ha ugyanazon a listán belül maradt, a moveItemInArray-t hívjuk meg.
    • Ha másik listába került, a transferArrayItem-et használjuk. Ez a segédfüggvény áthelyezi az elemet az egyik tömbből a másikba, és frissíti a DOM-ot.

Ez a struktúra lehetővé teszi, hogy komplex drag-and-drop interakciókat építsünk ki, ahol az elemek szabadon mozoghatnak különböző, egymással összefüggő konténerek között.

Haladó Funkciók és Testreszabás

Az Angular CDK a fent bemutatott alapvető funkciókon túl számos lehetőséget kínál a drag-and-drop viselkedés testreszabására.

Egyéni Placeholder: cdkDragPlaceholder

Amikor egy elemet húzunk, a CDK alapértelmezésben egy „placeholder” elemet hagy maga után az eredeti helyén, ami egy üres, az eredeti elemmel megegyező méretű doboz. Ezt testreszabhatjuk a <ng-template cdkDragPlaceholder> segítségével.

<div class="example-box" *ngFor="let item of todo" cdkDrag>
  {{ item }}
  <ng-template cdkDragPlaceholder>
    <div class="custom-placeholder">Ide dobd!</div>
  </ng-template>
</div>
.custom-placeholder {
  background: #f0f8ff; /* Világoskék háttér */
  border: dotted 2px #007bff; /* Kék szaggatott szegély */
  padding: 15px 10px;
  margin-bottom: 8px;
  color: #007bff;
  text-align: center;
  font-weight: bold;
  border-radius: 4px;
}

Egyéni Előnézet: cdkDragPreview

Az elem húzásakor a CDK alapértelmezésben egy „preview” elemet hoz létre, amely az eredeti elem másolata. Ezt is testreszabhatjuk a <ng-template cdkDragPreview> segítségével.

<div class="example-box" *ngFor="let item of todo" cdkDrag>
  {{ item }}
  <ng-template cdkDragPreview>
    <div class="custom-preview">
      <span>Húzom: {{ item }}</span>
    </div>
  </ng-template>
</div>
.custom-preview {
  background: #007bff;
  color: white;
  padding: 10px 15px;
  border-radius: 4px;
  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
  opacity: 0.9;
  transform: rotate(3deg); /* Egy kis vizuális effekt */
}

Fogófelület Definiálása: cdkDragHandle

Ha csak az elem egy bizonyos részét szeretnénk húzhatóvá tenni, használhatjuk a cdkDragHandle direktívát. Ezt egy gyermekelemre helyezzük, és csak az ezen elemre kattintva lesz húzható a szülő elem.

<div class="example-box" *ngFor="let item of todo" cdkDrag>
  <div class="drag-handle" cdkDragHandle>☰</div>
  <span>{{ item }}</span>
</div>
.drag-handle {
  cursor: grab;
  margin-right: 10px;
  font-size: 1.5em;
  color: #555;
}

.drag-handle:active {
  cursor: grabbing;
}

.example-box {
  display: flex;
  align-items: center;
}

Húzás Letiltása: cdkDragDisabled és cdkDropListDisabled

Az elemek vagy a listák húzását/dobását programozottan is tilthatjuk a [cdkDragDisabled]="true" vagy [cdkDropListDisabled]="true" tulajdonságok segítségével. Ez hasznos lehet feltételes logikák esetén, például ha egy felhasználónak nincs jogosultsága az elemek mozgatására.

<div class="example-box" [cdkDragDisabled]="isDragDisabled" cdkDrag>
  {{ item }}
</div>

<div cdkDropList [cdkDropListDisabled]="isDropDisabled" class="example-list" (cdkDropListDropped)="drop($event)">
  ...
</div>

Húzási Korlátok: cdkDragBoundary

Megadhatjuk, hogy a húzható elem ne léphessen túl egy bizonyos konténeren. Ezt a cdkDragBoundary direktíva segítségével tehetjük meg, amely egy CSS szelektort vár.

<div class="boundary-container">
  <div class="box" cdkDrag [cdkDragBoundary]="'.boundary-container'">
    Korlátolt mozgás
  </div>
</div>

Gyakorlati Tippek és Bevált Gyakorlatok

  1. Stílusok Kezelése: Az Angular CDK szándékosan stílusmentes. Ez azt jelenti, hogy minden vizuális visszajelzést (mint például a cdk-drag-preview, cdk-drag-placeholder, cdk-drop-list-dragging, cdk-drop-list-receiving osztályok) nekünk kell megírnunk a CSS-ben. Használjuk ki ezeket az osztályokat a professzionális felhasználói élmény érdekében.
  2. Akadálymentesség (Accessibility): A CDK alapvetően támogatja az akadálymentességet (pl. billentyűzetről történő navigációt), de fontos, hogy mi magunk is odafigyeljünk rá. Használjunk releváns ARIA attribútumokat, és gondoskodjunk arról, hogy a billentyűzetes interakció is zökkenőmentes legyen.
  3. Teljesítmény: Nagy listák vagy komplex elemek esetén a drag-and-drop művelet befolyásolhatja a teljesítményt. Figyeljünk a Change Detection stratégiára (pl. OnPush), és optimalizáljuk az adatkezelést, ha szükséges. Kerüljük a feleslegesen sok újrarenderelést.
  4. Felhasználói Visszajelzés: Mindig biztosítsunk világos vizuális visszajelzést a felhasználónak. Mutassuk, mi az, ami húzható, hová dobható, és mi történik az elemmel a művelet során. A cursor: grab; és cursor: grabbing; CSS tulajdonságok, valamint a fent említett placeholder/preview testreszabás nagyszerűen szolgálják ezt a célt.
  5. Adatmodellezés: Győződjünk meg róla, hogy az adatmodellünk konzisztens marad a DOM változásaival. A moveItemInArray és transferArrayItem segédfüggvények gondoskodnak erről az egyszerű tömbök esetén, de komplexebb adatszerkezeteknél nekünk kell figyelni az adatok integritására.

Összefoglalás

Az Angular CDK DragDropModule egy rendkívül erőteljes és rugalmas eszköz a drag-and-drop funkcionalitás megvalósításához Angular alkalmazásokban. A könnyű telepítéstől az alapvető húzási és listarendezési képességeken át, egészen a listák közötti áthelyezésig és a fejlett testreszabási lehetőségekig, a CDK mindent megad, amire szükségünk lehet.

A stílusoktól való elvonatkoztatásnak köszönhetően teljes szabadságot élvezhetünk a design terén, miközben a CDK gondoskodik a mögöttes logikáról és az akadálymentességről. Reméljük, ez az útmutató segített megérteni, hogyan integrálhatja ezt az interaktív funkciót a saját projektjeibe, és hogyan emelheti a felhasználói élményt egy teljesen új szintre. Ne habozzon kísérletezni, és fedezze fel a CDK által kínált összes lehetőséget!

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük