قابلیت کشیدن و رها کردن (ngDragDrop) در انگولار — به زبان ساده

۴۰ بازدید
آخرین به‌روزرسانی: ۱۱ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
قابلیت کشیدن و رها کردن (ngDragDrop) در انگولار — به زبان ساده

انگولار 7 با برخی قابلیت‌های جالب و جدید انتشار یافته است. در این راهنما به توضیح قابلیت کشیدن و رها کردن یعنی امکان ngDragDrop در انگولار می‌پردازیم. به این منظور یک اپلیکیشن می‌سازیم که داده‌ها را از پایگاه داده واکشی و آن‌ها را به رابط کاربری وصل می‌کند؛ سپس عملیات کشیدن و رها کردن را به صورت چند جهتی اجرا می‌کند.

برای مشاهده کد منبع پروژه‌ای که در این راهنما بررسی می‌کنیم، می‌توانید به این آدرس (+) مراجعه کنید.

نصب Angular CLI

چنان که احتمالاً حدس می‌زنید ما در این راهنما از Angular CLI استفاده می‌کنیم. اگر هنوز Angular CLI را نصب نکرده‌اید، پیشنهاد می‌کنیم هم‌اینک آن را نصب کنید (+). این یک CLI عالی برای انگولار است و مطمئناً از آن لذت خواهید برد. با اجرای دستور زیر می‌توانید آن را نصب کنید:

npm install -g @angular/cli

زمانی که این پروژه راه‌اندازی شد، از دستورهای Angular CLI استفاده خواهیم کرد. مستندات Angular CLI (+) به شما کمک می‌کند که بدانید با این CLI چه کارهایی می‌توانید انجام دهید.

ایجاد یک پروژه جدید

اینک زمان آن رسیده است که پروژه جدیدی را ایجاد کنیم. به این منظور از دستور زیر استفاده می‌کنیم:

ng new ngDragDrop

اکنون قادر خواهید بود همه کارهای دشواری که CLI برای ما انجام داده است را ببینید. اینک که اپلیکیشن خود را ایجاد کردیم نوبت آن رسیده است که ببینیم آیا کار می‌کند یا نه.

ng serve --open (if you need to open the browser by the app)

ng serve (if you want to manually open the browser).

You can always use 'ng s' as well

دستور فوق اپلیکیشن ما را ایجاد و آن را در مرورگر اجرا می‌کند. در زمان توسعه از Angular material برای طراحی استفاده خواهیم کرد. آن را به همراه انیمیشن و CDK می‌توانید نصب کنید. از نسخه‌های 6 به بالای انگولار این کار را با استفاده از دستور زیر نیز می‌توان انجام داد:

ng add @angular/material

تولید و راه‌اندازی کامپوننت هدر

اکنون اپلیکیشنی داریم که می‌توانیم روی آن کار کنیم و نوبت آن رسیده است که یک کامپوننت هدر بسازیم.

ng g c header

دستور فوق همه فایل‌هایی که برای کار با اپلیکیشن نیاز داریم را ایجاد می‌کند و این کامپوننت را به app.module.ts نیز اضافه می‌کند.

ما صرفاً HTML کامپوننت هدر را ویرایش می‌کنیم و قرار نیست هیچ منطقی به آن اضافه کنیم. البته شما می‌توانید بسته به نیاز خود مواردی را به آن بیفزایید.

1<div style="text-align:center">
2  <h1>
3    Welcome to ngDragDropg at <a href="https://sibeeshpassion.com">Sibeesh Passion</a>
4  </h1>
5</div>

راه‌اندازی کامپوننت فوتر

با اجرای دستور زیر کامپوننت فوتر را ایجاد می‌کنیم:

ng g c footer

می‌توانید بسته به میل خود آن را ویرایش یا استایل‌دهی کنید:

1<p>
2  Copyright @SibeeshPassion 2018 - 2019 :)
3</p>

راه‌اندازی فایل app-routing.module.ts

ما قصد داریم تنها یک مسیر برای صفحه اصلی بسازیم:

1import { NgModule } from '@angular/core';
2import { Routes, RouterModule } from '@angular/router';
3import { HomeComponent } from './home/home.component';
4const routes: Routes = [
5  {
6    path: '',
7    redirectTo: '/home',
8    pathMatch: 'full'
9  },
10  {
11    path: 'home',
12    component: HomeComponent
13  }
14];
15@NgModule({
16  imports: [RouterModule.forRoot(routes)],
17  exports: [RouterModule]
18})
19export class AppRoutingModule { }

راه‌اندازی خروجی روتر در فایل app.component.html

اکنون یک مسیر داریم و زمان آن رسیده که یک خروجی تنظیم کنیم:

1<app-header></app-header>
2<router-outlet>
3</router-outlet>
4<app-footer></app-footer>

راه‌اندازی فایل app.module.ts

هر اپلیکیشن انگولار دست کم یک کلاس NgModule به نام AppModule دارد که در فایل app.module.ts جای می‌گیرد. برای کسب اطلاعات بیشتر در مورد معماری انگولار به وب‌سایت آن (+) مراجعه کنید.

1import { BrowserModule } from '@angular/platform-browser';
2import { NgModule } from '@angular/core';
3import { MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule } from '@angular/material';
4import { AppRoutingModule } from './app-routing.module';
5import { AppComponent } from './app.component';
6import { HeaderComponent } from './header/header.component';
7import { FooterComponent } from './footer/footer.component';
8import { HomeComponent } from './home/home.component';
9import { MovieComponent } from './movie/movie.component';
10import { MovieService } from './movie.service';
11import { HttpModule } from '@angular/http';
12import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
13import { DragDropModule } from '@angular/cdk/drag-drop';
14@NgModule({
15  declarations: [
16    AppComponent,
17    HeaderComponent,
18    FooterComponent,
19    HomeComponent,
20    MovieComponent
21  ],
22  exports: [
23    HttpModule,
24    BrowserModule,
25    AppRoutingModule,
26    DragDropModule,
27    MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule, BrowserAnimationsModule
28  ],
29  imports: [
30    HttpModule,
31    BrowserModule,
32    AppRoutingModule,
33    DragDropModule,
34    MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule, BrowserAnimationsModule
35  ],
36  providers: [MovieService],
37  bootstrap: [AppComponent]
38})
39export class AppModule { }

آیا در این کد ماژول DragDropModule را دیدید؟ برای استفاده از قابلیت کشیدن و رها کردن باید آن را ایمپورت کنید. این قابلیت در بخش angular/cdk/drag-drop@ جای می‌گیرد. آن چنان که احتمالاً متوجه شده‌اید، ما یک سرویس به نام MovieService در آرایه ارائه‌دهنده افزوده‌ایم. اینک آن را می‌سازیم.

ایجاد یک سرویس فیلم به نام MovieService

1import { Injectable } from '@angular/core';
2import { RequestMethod, RequestOptions, Request, Http } from '@angular/http';
3import { config } from './config';
4@Injectable({
5  providedIn: 'root'
6})
7export class MovieService {
8  constructor(private http: Http) {
9  }
10async get(url: string) {
11    return await this.request(url, RequestMethod.Get);
12  }
13async request(url: string, method: RequestMethod) {
14    const requestOptions = new RequestOptions({
15      method: method,
16      url: `${config.api.baseUrl}${url}${config.api.apiKey}`
17    });
18const request = new Request(requestOptions);
19    return await this.http.request(request).toPromise();
20  }
21}

همچنان که مشاهده می‌کنید هنوز کار چندانی روی کلاس سرویس انجام نداده‌ایم و مکانیسم خطا و دیگر مواردی را که لازم است پیاده نکرده‌ایم تا در حد امکان خلاصه باشد.

این سرویس فیلم‌ها را از یک پایگاه داده آنلاین به نام TMDB واکشی می‌کند. در این مقاله و در این ریپازیتوری ما از فایل خودمان استفاده می‌کنیم. به شما نیز پیشنهاد می‌کنیم که به جای موردی که اشاره شد، از فایل خودتان استفاده کنید. در ادامه فایل پیکربندی را تنظیم می‌کنیم.

راه‌اندازی فایل config.ts

فایل پیکربندی روشی برای چیدمان چیزهای مختلف به صورت راحت ارائه می‌کند و می‌بایست در همه پروژه‌هایی که روی آن‌ها کار می‌کنید یک چنین فایلی را پیاده‌سازی کنید:

1const config = {
2    api: {
3        baseUrl: 'https://api.themoviedb.org/3/movie/',
4        apiKey: '&api_key=c412c072676d278f83c9198a32613b0d',
5        topRated: 'top_rated?language=en-US&page=1'
6    }
7};
8export { config };

ایجاد کامپوننت فیلم

در این بخش یک کامپوننت فیلم می‌سازیم که اطلاعات فیلم در آن جای می‌گیرد. در واقع ما از این کامپوننت فیلم درون یک div به نام cdkDropList استفاده می‌کنیم. کامپوننت فیلم ما دارای یک HTML به صورت زیر است:

1<mat-card>
2  <img mat-card-image src="https://image.tmdb.org/t/p/w185_and_h278_bestv2/{{movie?.poster_path}}" >
3</mat-card>

ما آن را تا حد امکان ساده ساخته‌ایم. در آینده می‌توانیم چند مشخصه دیگر نیز به کامپوننت فیلم خود اضافه کنیم و آن‌ها را اینجا نمایش دهیم. فایل تایپ‌اسکریپت یک مشخصه به همراه دکوراتور Input@ خواهد داشت تا بتوانیم مقادیری را از کامپوننت home اضافه کنیم.

1import { Component, OnInit, Input } from '@angular/core';
2import { Movie } from '../models/movie';
3@Component({
4  selector: 'app-movie',
5  templateUrl: './movie.component.html',
6  styleUrls: ['./movie.component.scss']
7})
8export class MovieComponent implements OnInit {
9  @Input()
10  movie: Movie;
11  
12  constructor() { 
13  }
14ngOnInit() {
15  }
16}

همچنان که اشاره کردیم این مدل فعلاً تنها یک مشخصه دارد و در آینده موارد دیگری را اضافه خواهیم کرد.

راه‌اندازی کامپوننت Home

این کامپوننت بخش اصلی برنامه و جایی است که فیلم‌ها را رندر کرده و عملیات کشیدن و رها کردن را اجرا می‌کنیم. ما یک کانتینر والد به نام <;"div style="display: flex> داریم که div-های داخلی آن به صورت افقی چیدمان یافته‌اند. دو کانتینر درونی دیگر نیز داریم که یکی برای نمایش هم فیلم‌ها و دیگری برای نمایش فیلم‌هایی است که می‌خواهیم تماشا کنیم. ما می‌توانیم از کانتینر چپ فیلم‌ها را به کانتینر راست کشیده و رها کنیم و یا برعکس آن را انجام دهیم.

در این بخش HTML خود را طرحی می‌کنیم. در ادامه کل مجموعه فیلم را می‌بینید:

1<div cdkDropList #allmovies="cdkDropList" [cdkDropListData]="movies" [cdkDropListConnectedTo]="[towatch]" (cdkDropListDropped)="drop($event)">
2    <app-movie *ngFor="let movie of movies" [movie]="movie" cdkDrag></app-movie>
3</div>

همچنان که می‌بینید چند مشخصه جدید وجود دارند که به هر دو کانتینر app-movie انتساب یافته‌اند:

  • cdkDropList اساساً کانتینری برای آیتم‌های کشیدنی و رها کردنی محسوب می‌شود.
  • "allmovies=”cdkDropList# همان ID کانتینر مبدأ ما است.
  • "[cdkDropListConnectedTo]=”[towatch] شیوه اتصال دو کانتینر app-movie را نشان می‎دهد. به خاطر داشته باشید که towatch همان ID کانتینر cdkDropList دیگر است.
  • "cdkDropListData]=”movies] برای توصیف شیوه انتساب داده‌های منبع به لیست استفاده می‌شود.
  • "(cdkDropListDropped)=”drop($event) هر زمان که رویداد کشیدن و رها کردن رخ می‌دهد یک رویداد clalback را تعریف می‌کند.

درون کانتینر cdkDropList یک حلقه را روی مقادیر تعریف می‌کنیم و فیلم را به کامپوننت فیلم خود ارسال می‌کنیم که app-movie است. ما باید مشخصه cdkDrag را در آیتم کشیدنی که چیزی به جز فیلم نیست نیز اضافه کنیم.

در ادامه کانتینر دیگری می‌سازیم که شامل مجموعه‌ای از فیلم‌ها است و قرار است تماشا کنیم.

1<div cdkDropList #towatch="cdkDropList" [cdkDropListData]="moviesToWatch" [cdkDropListConnectedTo]="[allmovies]" (cdkDropListDropped)="drop($event)">
2    <app-movie *ngFor="let movie of moviesToWatch" [movie]="movie" cdkDrag></app-movie>
3</div>

ما تقریباً از همان مشخصه‌هایی استفاده می‌کنیم که در کانتینر نخست استفاده کردیم به جز ID-ها که cdkDropListData و cdkDropListConnectedTo هستند.

در نهایت فایل home.component.html را به صورت زیر آماده می‌کنیم:

1<div style="display: flex;">
2  <div class="container">
3    <div class="row">
4      <h2 style="text-align: center">Movies</h2>
5      <div  cdkDropList #allmovies="cdkDropList" [cdkDropListData]="movies" [cdkDropListConnectedTo]="[towatch]"
6        (cdkDropListDropped)="drop($event)">
7        <app-movie *ngFor="let movie of movies" [movie]="movie" cdkDrag></app-movie>
8      </div>
9    </div>
10  </div>
11  <div class="container">
12    <div class="row">
13      <h2 style="text-align: center">Movies to watch</h2>
14      <div cdkDropList #towatch="cdkDropList" [cdkDropListData]="moviesToWatch" [cdkDropListConnectedTo]="[allmovies]"
15        (cdkDropListDropped)="drop($event)">
16        <app-movie *ngFor="let movie of moviesToWatch" [movie]="movie" cdkDrag></app-movie>
17      </div>
18    </div>
19  </div>
20</div>

اکنون باید برخی داده‌ها را با فراخوانی سرویس به دست آوریم. فایل home.component.ts را باز کنید:

1import { Component } from '@angular/core';
2import { MovieService } from '../movie.service';
3import { Movie } from '../models/movie';
4import { config } from '../config';
5import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
6@Component({
7  selector: 'app-home',
8  templateUrl: './home.component.html',
9  styleUrls: ['./home.component.scss']
10})
11export class HomeComponent {
12  movies: Movie[];
13  moviesToWatch: Movie[] = [{
14    poster_path: '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg'
15  }];
16  constructor(private movieService: MovieService) {
17    this.getMovies();
18  }
19  private async getMovies() {
20    const movies = await this.movieService.get(config.api.topRated);
21    return this.formatDta(movies.json().results);
22  }
23  formatDta(_body: Movie[]): void {
24    this.movies = _body.filter(movie => movie.poster_path !== '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg');
25  }
26  drop(event: CdkDragDrop<string[]>) {
27    if (event.previousContainer === event.container) {
28      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
29    } else {
30      transferArrayItem(event.previousContainer.data,
31        event.container.data,
32        event.previousIndex,
33        event.currentIndex);
34    }
35  }
36}

در این فایل CdkDragDrop ،moveItemInArray و transferArrayItem را از ‘angular/cdk/drag-drop@’ ایمپورت می‌کنیم. بدین ترتیب لازم است کشیدن و رها کردن را اجرا کنیم. در سازنده، داده‌ها را واکشی می‌کنیم و آن را به متغیر movies انتساب می‌دهیم. این یک آرایه از فیلم است.

1private async getMovies() {
2    const movies = await this.movieService.get(config.api.topRated);
3    return this.formatDta(movies.json().results);
4}

ما مجموعه «movies to watch» را به صورت زیر تعیین می‌کنیم که شامل فیلم‌هایی است که قرار است تماشا کنیم.

1moviesToWatch: Movie[] = [{
2    poster_path: '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg'
3  }];

به خاطر داشته باشید که کشیدن و رها کردن با دو منبع، در صورتی که دست کم یک آیتم در آن‌ها نباشد کار نخواهند کرد. از آنجا که یک فیلم در آن قرار داده‌ایم، نمایش دادن آن در مجموعه‌های دیگر معنای چندانی ندارد.

1formatDta(_body: Movie[]): void {
2   this.movies = _body.filter(movie => movie.poster_path !== '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg');
3}

در ادامه رویداد رها کردن را مشاهده می‌کنید.

1drop(event: CdkDragDrop<string[]>) {
2   if (event.previousContainer === event.container) {
3     moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
4   } else {
5     transferArrayItem(event.previousContainer.data,
6       event.container.data,
7       event.previousIndex,
8       event.currentIndex);
9   }
10 }

کد کامل فایل home.component.ts به صورت زیر است:

1import { Component } from '@angular/core';
2import { MovieService } from '../movie.service';
3import { Movie } from '../models/movie';
4import { config } from '../config';
5import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
6@Component({
7  selector: 'app-home',
8  templateUrl: './home.component.html',
9  styleUrls: ['./home.component.scss']
10})
11export class HomeComponent {
12  movies: Movie[];
13  moviesToWatch: Movie[] = [{
14    poster_path: '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg'
15  }];
16  constructor(private movieService: MovieService) {
17    this.getMovies();
18  }
19  private async getMovies() {
20    const movies = await this.movieService.get(config.api.topRated);
21    return this.formatDta(movies.json().results);
22  }
23  formatDta(_body: Movie[]): void {
24    this.movies = _body.filter(movie => movie.poster_path !== '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg');
25  }
26  drop(event: CdkDragDrop<string[]>) {
27    if (event.previousContainer === event.container) {
28      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
29    } else {
30      transferArrayItem(event.previousContainer.data,
31        event.container.data,
32        event.previousIndex,
33        event.currentIndex);
34    }
35  }
36}

استایل‌بندی سفارشی

ما استایل‌های سفارشی را روی برخی کامپوننت‌ها اعمال می‌کنیم که می‌توانید در ادامه مشاهده کنید.

فایل home.component.scss

1.container{
2  border: 1px solid rgb(248, 144, 144);
3  margin: 5%;
4  overflow: auto;
5  width: 40%;
6  height: 500px;
7}
8app-movie{
9  cursor: move;
10  width: 50%;
11  display: inline-flex;
12}

فایل movie.component.scss

1mat-card{
2    width: 70%;
3    padding: 26px;
4    margin: 5px;
5    border: 1px solid;
6}

خروجی

پس از پیاده‌سازی همه مراحل فوق، یک اپلیکیشن داریم که از عملیات کشیدن و رها کردن انگولار 7 با داده‌های سرور واقعی استفاده می‌کند. در ادامه اپلیکیشن را اجرا کرده‌ایم و آن را در عمل می‌بینید:

ngDragDrop
مرحله آغازین ngDragDrop
ngDragDrop
پس از افزودن ngDragDrop
ngDragDrop
افزودن موارد بیشتر ngDragDrop

دموی این اپلیکیشن را می‌توانید در این صفحه (+) مشاهده کنید. شما می‌توانید با ریپازیتوری گیت‌هاب (+) این پروژه هم کار کنید تا بر این موضوع احاطه بیشتری پیدا کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز برای شما مفید خواهند بود:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *