ساخت اپلیکیشن انگولار با امکان Drag and Drop — از صفر تا صد

۱۰۹ بازدید
آخرین به‌روزرسانی: ۱۱ شهریور ۱۴۰۲
زمان مطالعه: ۹ دقیقه
ساخت اپلیکیشن انگولار با امکان Drag and Drop — از صفر تا صد

Drag and Drop یا «کشیدن و رها کردن» یکی از قابلیت‌های بسیاری از وب‌اپلیکیشن‌های تعاملی است. این قابلیت یک روش شهودی در اختیار کاربران قرار می‌دهد تا داده‌هایشان را دستکاری کنند. افزودن قابلیت Drag and Drop به اپلیکیشن‌های انگولار کار آسانی است. در این مقاله به توضیح روش ساخت اپلیکیشن انگولار با امکان Drag and Drop یا همانطور که ذکر شد «کشیدن و رها کردن» می‌پردازیم.

در ادامه یک اپلیکیشن ToDo می‌سازیم که دو ستون دارد که یکی ستون to-do و دیگری ستون done است. بدین ترتیب می‌توانیم بین دو ستون عملیات کشیدن و رها کردن را اجرا کنیم. برای ساخت این اپلیکیشن از کتابخانه Angular Material (+) کمک می‌گیریم تا ظاهر اپلیکیشن را زیبا کنیم و قابلیت Drag and Drop را به روش آسانی عرضه نماییم. همچنین یک منوی ناوبری در نوار فوقانی اپلیکیشن وجود خواهد داشت.

شروع

برای آغاز ساخت اپلیکیشن، طبق مراحل زیر پیش می‌رویم.

ابتدا با اجرای دستور زیر Angular CLI را نصب می‌کنیم:

npm i @angular/cli

در زمانی که از شما سؤال می‌شود، گزینه‌های routing و استفاده از SCSS را نیز انتخاب کنید. سپس با اجرای دستور زیر یک پروژه جدید انگولار می‌سازیم:

ng new todo-app

پس از آن کتابخانه‌هایی که لازم داریم را با دستور زیر اضافه می‌کنیم:

npm i@angular/cdk @angular/material @ngrx/store

بدین ترتیب انگولار متریال و NGRX store به اپلیکیشنمان اضافه می‌شود. ما از Flux (+) به طور گسترده‌ای در اپلیکیشن خود استفاده خواهیم کرد. سپس کامپوننت‌ها و سرویس‌ها را با اجرای دستور زیر به اپلیکیشن اضافه می‌کنیم:

ng g component addTodoDialog
ng g component homePage

ng g component toolBar
ng g service todo

با اجرای دستور زیر، کد آماده‌ای را برای NGRX store اضافه می‌کنیم:

ng add @ngrx/store

اکنون می‌توانیم منطق اپلیکیشن خود را بسازیم. کد زیر را به فایل add-todo-dialog.component.ts اضافه می‌کنیم:

1import { Component, OnInit } from '@angular/core';
2import { NgForm } from '@angular/forms';
3import { MatDialogRef } from '@angular/material/dialog';
4import { Store } from '@ngrx/store';
5import { TodoService } from '../todo.service';
6import { SET_TODOS } from '../reducers/todo-reducer';
7@Component({
8  selector: 'app-add-todo-dialog',
9  templateUrl: './add-todo-dialog.component.html',
10  styleUrls: ['./add-todo-dialog.component.scss']
11})
12export class AddTodoDialogComponent implements OnInit {
13  todoData: any = <any>{
14    done: false
15  };
16  constructor(
17    public dialogRef: MatDialogRef<AddTodoDialogComponent>,
18    private todoService: TodoService,
19    private store: Store<any>
20  ) { }
21  ngOnInit() {
22  }
23  save(todoForm: NgForm) {
24    if (todoForm.invalid) {
25      return;
26    }
27    this.todoService.addTodo(this.todoData)
28      .subscribe(res => {
29        this.getTodos();
30        this.dialogRef.close();
31      })
32  }
33  getTodos() {
34    this.todoService.getTodos()
35      .subscribe(res => {
36        this.store.dispatch({ type: SET_TODOS, payload: res });
37      })
38  }
39}

این کد به کادر محاوره‌ای مربوط است که به ما امکان اضافه کردن آیتم‌های To-Do را به لیست می‌دهد. سپس می‌توانیم آخرین آیتم‌ها را گرفته و آن‌ها را در Store قرار دهیم. کدهای زیر را به فایل add-todo-dialog.component.html اضافه کنید:

1<h2>Add Todo</h2>
2<form #todoForm='ngForm' (ngSubmit)='save(todoForm)'>
3    <mat-form-field>
4        <input matInput placeholder="Description" required #description='ngModel' name='description'
5            [(ngModel)]='todoData.description'>
6        <mat-error *ngIf="description.invalid && (description.dirty || description.touched)">
7            <div *ngIf="content.errors.required">
8                Description is required.
9            </div>
10        </mat-error>
11    </mat-form-field>
12    <br>
13    <button mat-raised-button type='submit'>Add</button>
14</form>

این کد فرم افزودن آیتم‌های To-Do است. این فرم تنها یک فیلد دارد که توضیح آیتم است، چون می‌خواهیم آن را ساده حفظ کنیم. کدهای زیر را به فایل add-todo-dialog.component.scss اضافه کنید تا عرض فیلد فرم را تغییر دهیم:

1form {
2  mat-form-field {
3    width: 100%;
4    margin: 0 auto;
5  }
6}

طراحی صفحه اصلی اپلیکیشن

سپس صفحه اصلی وب اپلیکیشن را ایجاد می‌کنیم. این همان جایی است که دو لیست ما در آن جای می‌گیرند. کاربر می‌تواند بین دو لیست عملیات drag and drop را اجرا کند و حالت وظایف را تغییر دهد. به این منظور کدهای زیر را به فایل home-page.component.ts اضافه کنید:

1import { Component, OnInit } from '@angular/core';
2import { MatDialog } from '@angular/material/dialog';
3import { AddTodoDialogComponent } from '../add-todo-dialog/add-todo-dialog.component';
4import { TodoService } from '../todo.service';
5import { Store, select } from '@ngrx/store';
6import { SET_TODOS } from '../reducers/todo-reducer';
7import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
8@Component({
9  selector: 'app-home-page',
10  templateUrl: './home-page.component.html',
11  styleUrls: ['./home-page.component.scss']
12})
13export class HomePageComponent implements OnInit {
14  allTasks: any[] = [];
15  todo: any[] = [];
16  done: any[] = [];
17  constructor(
18    public dialog: MatDialog,
19    private todoService: TodoService,
20    private store: Store<any>
21  ) {
22    store.pipe(select('todos'))
23      .subscribe(allTasks => {
24        this.allTasks = allTasks || [];
25        this.todo = this.allTasks.filter(t => !t.done);
26        this.done = this.allTasks.filter(t => t.done);
27      })
28  }
29  ngOnInit() {
30    this.getTodos();
31  }
32  openAddTodoDialog() {
33    const dialogRef = this.dialog.open(AddTodoDialogComponent, {
34      width: '70vw',
35      data: {}
36    })
37  dialogRef.afterClosed().subscribe(result => {
38      console.log('The dialog was closed');
39    });
40  }
41  getTodos() {
42    this.todoService.getTodos()
43      .subscribe(res => {
44        this.store.dispatch({ type: SET_TODOS, payload: res });
45      })
46  }
47  drop(event: CdkDragDrop<any[]>) {
48    if (event.previousContainer === event.container) {
49      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
50    } else {
51      transferArrayItem(event.previousContainer.data,
52        event.container.data,
53        event.previousIndex,
54        event.currentIndex);
55    }
56    let data = event.container.data[0];
57    data.done = !data.done;
58    this.todoService.editTodo(data)
59      .subscribe(res => {
60})
61  }
62  removeTodo(index: number, tasks: any[]) {
63    const todoId = tasks[index].id;
64    this.todoService.removeTodo(todoId)
65      .subscribe(res => {
66        this.getTodos();
67      })
68  }
69}

در کد فوق تابع‌هایی برای مدیریت رها کردن آیتم‌های To-Do داریم و به کاربر امکان می‌دهیم که کادر محاوره‌ای add to-do را که قبلاً با تابع openAddTodoDialog ساخته‌ایم، باز کند. کاربر همچنین می‌تواند در این صفحه To-Do-ها را با استفاده از تابع removeTodo حذف کند. تابع drop به مدیریت رها کردن آیتم‌ها بین لیست‌ها می‌پردازد و همچنین حالت آیتم To-Do را تغییر می‌دهد.

ترتیب این کدها اهمیت دارد. بلوک if...else باید پیش از فراخوانی تابع editTodo بیاید، زیرا در غیر این صورت آیتم در آرایه event.container.data موجود نخواهد بود. removeTodo هم یک index و هم لیست tasks را می‌گیرد، زیرا برای هر دو آرایه todo و done مورد استفاده قرار می‌گیرد. کدهای زیر را به فایل home-page.component.html اضافه کنید:

1<div class="center">
2    <h1>Todos</h1>
3    <button mat-raised-button (click)='openAddTodoDialog()'>Add Todo</button>
4</div>
5<div class="content">
6    <div class="todo-container">
7        <h2>To Do</h2>
8<div cdkDropList #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList]"
9            class="todo-list" (cdkDropListDropped)="drop($event)">
10            <div class="todo-box" *ngFor="let item of todo; let i = index" cdkDrag>
11                {{item.description}}
12                <a class="delete-button" (click)='removeTodo(i, todo)'>
13                    <i class="material-icons">
14                        close
15                    </i>
16                </a>
17            </div>
18        </div>
19    </div>
20<div class="done-container">
21        <h2>Done</h2>
22<div cdkDropList #doneList="cdkDropList" [cdkDropListData]="done" [cdkDropListConnectedTo]="[todoList]"
23            class="todo-list" (cdkDropListDropped)="drop($event)">
24            <div class="todo-box" *ngFor="let item of done; let i = index" cdkDrag>
25                {{item.description}}
26                <a class="delete-button" (click)='removeTodo(i, done)'>
27                    <i class="material-icons">
28                        close
29                    </i>
30                </a>
31            </div>
32        </div>
33    </div>
34</div>

این قالب دو لیست به کاربر ارائه می‌کند که با استفاده از آن می‌تواند بین دو لیست آیتم‌ها را drag and drop کند و حالت را تغییر دهد. همچنین یک دکمه X وجود دارد که امکان حذف آیتم را به کاربران می‌دهد. کدهای زیر را به فایل home-page.component.scss اضافه می‌کنیم:

1$gray: gray;
2.content {
3  display: flex;
4  align-items: flex-start;
5  margin-left: 2vw;
6  div {
7    width: 45vw;
8  }
9}
10.todo-container {
11  width: 400px;
12  max-width: 100%;
13  margin: 0 25px 25px 0;
14  display: inline-block;
15  vertical-align: top;
16}
17.todo-list {
18  border: solid 1px $gray;
19  min-height: 70px;
20  background: white;
21  border-radius: 4px;
22  overflow: hidden;
23  display: block;
24}
25.todo-box {
26  padding: 20px 10px;
27  border-bottom: solid 1px $gray;
28  color: rgba(0, 0, 0, 3);
29  display: flex;
30  flex-direction: row;
31  align-items: center;
32  justify-content: space-between;
33  box-sizing: border-box;
34  cursor: move;
35  background: white;
36  font-size: 14px;
37  height: 70px;
38}
39.cdk-drag-preview {
40  box-sizing: border-box;
41  border-radius: 4px;
42  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 2), 0 8px 10px 1px rgba(0, 0, 0, 1), 0 3px 14px 2px rgba(0, 0, 0, 6);
43}
44.cdk-drag-placeholder {
45  opacity: 0;
46}
47.cdk-drag-animating {
48  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
49}
50.todo-box:last-child {
51  border: none;
52}
53.todo-list.cdk-drop-list-dragging .todo-box:not(.cdk-drag-placeholder) {
54  transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
55}
56.delete-button {
57  cursor: pointer;
58}

افزودن Reducer-ها

کدهای فوق به استایل‌بندی لیست‌ها و کادرها می‌پردازند به طوری که دارای حاشیه و سایه باشند. در ادامه reducer-ها را به store خود اضافه می‌کنیم. به این منظور با اجرای دستور زیر فایلی به نام menu-reducer.ts می‌سازیم:

ng g class menuReducer

کدهای زیر را به این فایل اضافه می‌کنیم:

1export const SET_MENU_STATE = 'SET_MENU_STATE';
2export function MenuReducer(state: boolean, action) {
3    switch (action.type) {
4        case SET_MENU_STATE:
5            return action.payload;
6        default:
7            return state;
8    }
9}

به طور مشابه، با اجرای دستور زیر فایلی به نام todo-reducer.ts می‌سازیم:

ng g class todoReducer

کدهای زیر را به این فایل اضافه می‌کنیم:

1const SET_TODOS = 'SET_TODOS';
2function todoReducer(state, action) {
3    switch (action.type) {
4        case SET_TODOS:
5            state = action.payload;
6            return state;
7        default:
8            return state
9    }
10}
11export { todoReducer, SET_TODOS };

کدهای زیر را نیز به فایل reducers/index.ts اضافه می‌کنیم:

1import { MenuReducer } from './menu-reducer';
2import { todoReducer } from './todo-reducer';
3export const reducers = {
4  menuState: MenuReducer,
5  todos: todoReducer
6};

به این ترتیب reducer-ها می‌توانند در هنگام ایمپورت شدن در app.module.ts به StoreModule ارسال شوند. این سه فایل در مجموع یک Store تشکیل می‌دهند که می‌توانیم برای ذخیره همه وظایف To-Do مورد استفاده قرار دهیم. سپس کدهای زیر را به فایل tool-bar.component.ts اضافه می‌کنیم:

1import { Component, OnInit } from '@angular/core';
2import { Store, select } from '@ngrx/store';
3import { SET_MENU_STATE } from '../reducers/menu-reducer';
4@Component({
5  selector: 'app-tool-bar',
6  templateUrl: './tool-bar.component.html',
7  styleUrls: ['./tool-bar.component.scss']
8})
9export class ToolBarComponent implements OnInit {
10  menuOpen: boolean;
11  constructor(
12    private store: Store<any>
13  ) {
14    store.pipe(select('menuState'))
15      .subscribe(menuOpen => {
16        this.menuOpen = menuOpen;
17      })
18  }
19  ngOnInit() {
20  }
21toggleMenu() {
22    this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen });
23  }
24}

کد فوق به کاربران امکان می‌دهد که منوی سمت چپ را باز و بسته کنند. سپس کدهای زیر را به فایل tool-bar.component.html اضافه می‌کنیم:

1<mat-toolbar>
2    <a (click)='toggleMenu()' class="menu-button">
3        <i class="material-icons">
4            menu
5        </i>
6    </a>
7    Todo App
8</mat-toolbar>

کد فوق نوار ابزار فوقانی و منو را اضافه می‌کند. کدهای زیر را در فایل tool-bar.component.scss قرار دهید:

1.menu-button {
2  margin-top: 6px;
3  margin-right: 10px;
4  cursor: pointer;
5}
6.mat-toolbar {
7  background: #009688;
8  color: white;
9}

این کدها مقداری فاصله‌بندی به دکمه منو و متن عنوان اضافه می‌کنند. در ادامه کد موجود در فایل app-routing.module.ts را با کدهای زیر عوض می‌کنیم:

1import { NgModule } from '@angular/core';
2import { Routes, RouterModule } from '@angular/router';
3import { HomePageComponent } from './home-page/home-page.component';
4const routes: Routes = [
5  { path: '', component: HomePageComponent },
6];
7@NgModule({
8  imports: [RouterModule.forRoot(routes)],
9  exports: [RouterModule]
10})
11export class AppRoutingModule { }

بدین ترتیب کاربران می‌توانند «صفحه اصلی» (Home Page) وب اپلیکیشن را ببینند. در ادامه کدهای زیر را در فایل app.component.ts قرار می‌دهیم:

1import { Component, HostListener } from '@angular/core';
2import { SET_MENU_STATE } from './reducers/menu-reducer';
3import { Store, select } from '@ngrx/store';
4@Component({
5  selector: 'app-root',
6  templateUrl: './app.component.html',
7  styleUrls: ['./app.component.scss']
8})
9export class AppComponent {
10  menuOpen: boolean;
11  constructor(
12    private store: Store<any>,
13  ) {
14    store.pipe(select('menuState'))
15      .subscribe(menuOpen => {
16        this.menuOpen = menuOpen;
17      })
18  }
19  @HostListener('document:click', ['$event'])
20  public onClick(event) {
21    const isOutside = !event.target.className.includes("menu-button") &&
22      !event.target.className.includes("material-icons") &&
23      !event.target.className.includes("mat-drawer-inner-container")
24    if (isOutside) {
25      this.menuOpen = false;
26      this.store.dispatch({ type: SET_MENU_STATE, payload: this.menuOpen });
27    }
28  }
29}

کد فوق برای این است که وقتی کاربر در خارج از دکمه منو و منو کلیک می‌کند، آن را باز و بسته کنیم. کدهای زیر را نیز به فایل app.component.html اضافه کنید:

1<mat-sidenav-container class="example-container">
2  <mat-sidenav mode="side" [opened]='menuOpen'>
3      <ul>
4          <li>
5              <b>
6                  New York Times
7              </b>
8          </li>
9          <li>
10              <a routerLink='/'>Home</a>
11          </li>
12      </ul>
13</mat-sidenav>
14  <mat-sidenav-content>
15      <app-tool-bar></app-tool-bar>
16      <div id='content'>
17          <router-outlet></router-outlet>
18      </div>
19  </mat-sidenav-content>
20</mat-sidenav-container>

ناوبری اپلیکیشن

کدهای فوق اقدام به افزودن منو، ناوبری سمت چپ و عنصر router-outlet می‌کنند تا افراد بتوانند مسیرهای تعریف شده را ببیند. کدهای زیر را به فایل app.component.scss اضافه کنید:

1#content {
2  padding: 20px;
3  min-height: 100vh;
4}
5ul {
6  list-style-type: none;
7  margin: 0;
8  li {
9    padding: 20px 5px;
10  }
11}

کد زیر Padding بیشتری به صفحه‌ها اضافه کرده و استایل لیست آیتم‌ها را در منوی سمت چپ تغییر می‌دهد. کد زیر را نیز به فایل environment.ts اضافه کنید:

1export const environment = {
2  production: false,
3  apiUrl: 'http://localhost:3000'
4};

این کد URL مربوط به API را اضافه می‌کند. کدهای زیر را به فایل styles.scss اضافه کنید:

1/* You can add global styles to this file, and also import other style files */
2@import "~@angular/material/prebuilt-themes/indigo-pink.css";
3body {
4  font-family: "Roboto", sans-serif;
5  margin: 0;
6}
7form {
8  mat-form-field {
9    width: 95vw;
10    margin: 0 auto;
11  }
12}
13.center {
14  text-align: center;
15}

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

1import { BrowserModule } from '@angular/platform-browser';
2import { NgModule } from '@angular/core';
3import { FormsModule } from '@angular/forms';
4import { AppRoutingModule } from './app-routing.module';
5import { AppComponent } from './app.component';
6import { HomePageComponent } from './home-page/home-page.component';
7import { StoreModule } from '@ngrx/store';
8import { reducers } from './reducers';
9import { MatSidenavModule } from '@angular/material/sidenav';
10import { MatToolbarModule } from '@angular/material/toolbar';
11import { MatInputModule } from '@angular/material/input';
12import { MatFormFieldModule } from '@angular/material/form-field';
13import { ToolBarComponent } from './tool-bar/tool-bar.component';
14import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
15import { MatButtonModule } from '@angular/material/button';
16import { HttpClientModule } from '@angular/common/http';
17import { MatSelectModule } from '@angular/material/select';
18import { MatCardModule } from '@angular/material/card';
19import { MatListModule } from '@angular/material/list';
20import { MatMenuModule } from '@angular/material/menu';
21import { MatIconModule } from '@angular/material/icon';
22import { MatGridListModule } from '@angular/material/grid-list';
23import { AddTodoDialogComponent } from './add-todo-dialog/add-todo-dialog.component';
24import { DragDropModule } from '@angular/cdk/drag-drop';
25@NgModule({
26  declarations: [
27    AppComponent,
28    HomePageComponent,
29    ToolBarComponent,
30    AddTodoDialogComponent
31  ],
32  imports: [
33    BrowserModule,
34    AppRoutingModule,
35    StoreModule.forRoot(reducers),
36    FormsModule,
37    MatSidenavModule,
38    MatToolbarModule,
39    MatInputModule,
40    MatFormFieldModule,
41    BrowserAnimationsModule,
42    MatButtonModule,
43    MatMomentDateModule,
44    HttpClientModule,
45    MatSelectModule,
46    MatCardModule,
47    MatListModule,
48    MatMenuModule,
49    MatIconModule,
50    MatGridListModule,
51    DragDropModule
52  ],
53  providers: [
54],
55  bootstrap: [AppComponent],
56  entryComponents: [
57    AddTodoDialogComponent
58  ]
59})
60export class AppModule { }
61text-align: center;
62}

کدهای زیر را به فایل todo.service.ts اضافه می‌کنیم:

1import { Injectable } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3import { environment } from 'src/environments/environment';
4@Injectable({
5  providedIn: 'root'
6})
7export class TodoService {
8  constructor(
9    private http: HttpClient
10  ) { }
11  getTodos() {
12    return this.http.get(`${environment.apiUrl}/todos`);
13  }
14  addTodo(data) {
15    return this.http.post(`${environment.apiUrl}/todos`, data);
16  }
17  editTodo(data) {
18    return this.http.put(`${environment.apiUrl}/todos/${data.id}`, data);
19  }
20  removeTodo(id) {
21    return this.http.delete(`${environment.apiUrl}/todos/${id}`);
22  }
23}

این تابع‌ها از طریق ایجاد درخواست‌های JSON API که با استفاده از پکیج JSON Server (+) در Node.js اضافه شده است، به ما امکان اجرای عملیات CRUD را روی آیتم‌های TO-DO فراهم می‌سازند. داده‌ها در یک فایل JSON ذخیره می‌شوند و از این رو لازم نیست برای اضافه کردن آن‌ها یک بک‌اند بسازیم تا داده‌های ساده را ذخیره کنیم. سرور را با دستور زیر نصب می‌کنیم:

npm i -g json-server

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

json-server --watch db.json

کد زیر را در فایل db.json قرار می‌دهیم:

1{
2  "todos": []
3}

اینک می‌توانیم از این نقاط انتهایی برای ذخیره کردن داده‌ها در db.json کمک بگیریم. در نهایت نتیجه زیر به دست می‌آید:

اپلیکیشن انگولار با امکان Drag and Drop

اپلیکیشن انگولار با امکان Drag and Drop

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

==

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

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