Site icon Sibeesh Passion

New Angular Drag and Drop Feature – ngDragDrop

[toc]

Introduction

As you all know that Angular 7 is out with some cool new features. I really appreciate that you wanted to experience the brand new Angular. Here in this post, I am going to explain a bit about one of the Angular 7 feature, which is Drag and Drops. At the end of this article, you will have an application which fetches the real data from the database and binds it to the UI and then performs multi-directional drag and drops. Enough talking, let’s jump into the setup. I hope you will find this post useful.

Source Code

The source code can be found here.

Background

As Angular 7 is out last week, I wanted to try a few things with the same and that is the cause for this article. If you are really new to Angular, and if you need to try some other things, visiting my articles on the same topic wouldn’t be a bad idea.

Creating ngDragDrop app

The first thing we are going to do is to create a dummy application.

Installing Angular CLI

Yes, as you guessed, we are using Angular CLI. If you haven’t installed Angular CLI, I recommend you to install the same. It is a great CLI tool for Angular, I am sure you will love that. You can do that by running the below command.

npm install -g @angular/cli

Once we set up this project we will be using the Angular CLI commands and you can see here for understanding the things you can do with the CLI.

Generating a new project

Now it is time to generate our new project. We can use the below command for the same.

ng new ngDragDrop

And you will be able to see all the hard work this CLI is doing for us. Now that we have created our application, let’s run our application and see if it is working or not.

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

The command will build your application and run it in the browser.

As we develop we will be using the Angular material for the design and we can install it now itself along with the animation and cdk. With the Angular 6+ versions you can also do this by following the below command.

ng add @angular/material

Generate and set up header component

Now we have an application to work with and let’s create a header component now.

ng g c header

The above command will generate all the files you need to work with and it will also add this component to the app.module.ts. I am going to edit only the HTML of the header component for myself and not going to add any logic. You can add anything you wish.

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

Set up footer component

Create the footer component by running the below command.

ng g c footer

And you can edit or style them as you wish.

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

Set up app-routing.module.ts

We are going to create a route only for home.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    redirectTo: '/home',
    pathMatch: 'full'
  },
  {
    path: 'home',
    component: HomeComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Set up router outlet in app.component.html

Now we have a route and it is time to set up the outlet.

<app-header></app-header>
<router-outlet>
</router-outlet>
<app-footer></app-footer>

Set up app.module.ts

Every Angular app will be having at least one NgModule class AppModuleresides in app.module.ts. You can always learn about the Angular architecture here.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule } from '@angular/material';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { FooterComponent } from './footer/footer.component';
import { HomeComponent } from './home/home.component';
import { MovieComponent } from './movie/movie.component';
import { MovieService } from './movie.service';
import { HttpModule } from '@angular/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { DragDropModule } from '@angular/cdk/drag-drop';

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    FooterComponent,
    HomeComponent,
    MovieComponent
  ],
  exports: [
    HttpModule,
    BrowserModule,
    AppRoutingModule,
    DragDropModule,
    MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule, BrowserAnimationsModule
  ],
  imports: [
    HttpModule,
    BrowserModule,
    AppRoutingModule,
    DragDropModule,
    MatButtonModule, MatCheckboxModule, MatMenuModule, MatCardModule, MatSelectModule, BrowserAnimationsModule
  ],
  providers: [MovieService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Do you see a DragDropModule there? You should import it to use the drag and drop feature and it resides in the @angular/cdk/drag-drop. As you might have already noticed, we have added one service called MovieService in the providers array. We will create one now.

Creating a movie service

import { Injectable } from '@angular/core';
import { RequestMethod, RequestOptions, Request, Http } from '@angular/http';
import { config } from './config';

@Injectable({
  providedIn: 'root'
})
export class MovieService {
  constructor(private http: Http) {
  }

  async get(url: string) {
    return await this.request(url, RequestMethod.Get);
  }

  async request(url: string, method: RequestMethod) {
    const requestOptions = new RequestOptions({
      method: method,
      url: `${config.api.baseUrl}${url}${config.api.apiKey}`
    });

    const request = new Request(requestOptions);
    return await this.http.request(request).toPromise();
  }
}

As you can see I haven’t done much with the service class and didn’t implement the error mechanism and other things as I wanted to make this as short as possible. This service will fetch the movies from an online database TMDB and here in this article and repository, I am using mine. I strongly recommend you to create your own instead of using the one mentioned here. Can we set up the config file now?

Set up config.ts

A configuration file is a way to arrange things in handy and you must implement in all the projects you are working with.

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

Creating a movie component

Let’s create a new component now to load the movie into it. Basically, we will be using this movie component inside the cdkDropList div. Our movie component will be having the HTML as below.

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

I just made it as simple as is. But if future we can add a few more properties for the movie component and show them here. The typescript file will be having one property with @Input decorator so that we can input the values to it from the home component.

import { Component, OnInit, Input } from '@angular/core';
import { Movie } from '../models/movie';

@Component({
  selector: 'app-movie',
  templateUrl: './movie.component.html',
  styleUrls: ['./movie.component.scss']
})
export class MovieComponent implements OnInit {
  @Input()
  movie: Movie;
  
  constructor() { 
  }

  ngOnInit() {
  }

}

Below is my model movie.

export class Movie {
    poster_path: string;
}

As I said, it has only one property now, will add a few later.

Set up home component

Now here is the main part, the place where we render our movies and perform drag and drop. I will be having a parent container as<div style="display: flex;"> so that the inner divs will be arranged horizontally. And I will be having two inner containers, one is to show all the movies and another one is to show the movies I am going to watch. I can just drag the movie from the left container to right and vice versa. Let’s design the HTML now. Below is all the movie collection.

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

As you can see that there are a few new properties I am assigning to both the app-movie and app-movie container.

  • cdkDropList is basically a container for the drag and drop items
  • #allmovies=”cdkDropList” is the id of our source container
  • [cdkDropListConnectedTo]=”[towatch]” is how we are connecting two app-movie containers, remember the “towatch” is the id of another cdkDropList container
  • [cdkDropListData]=”movies” is how we assign the source data to the list
  • (cdkDropListDropped)=”drop($event)” is the callback event whenever there is a drag and drop happening.
  • Inside the cdkDropList container, we are looping through the values and pass the movie to our own movie component which is app-movie
  • We should also add the property cdkDrag in our Draggable item, which is nothing but a movie.

Now let us create another container which will be the collection of movies which I am going to watch.

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

As you can see we are almost using the same properties as we did for the first container except for the id, cdkDropListData, cdkDropListConnectedTo.

Finally, our home.component.html will be as follows.

<div style="display: flex;">
  <div class="container">
    <div class="row">
      <h2 style="text-align: center">Movies</h2>
      <div  cdkDropList #allmovies="cdkDropList" [cdkDropListData]="movies" [cdkDropListConnectedTo]="[towatch]"
        (cdkDropListDropped)="drop($event)">
        <app-movie *ngFor="let movie of movies" [movie]="movie" cdkDrag></app-movie>
      </div>
    </div>
  </div>
  <div class="container">
    <div class="row">
      <h2 style="text-align: center">Movies to watch</h2>
      <div cdkDropList #towatch="cdkDropList" [cdkDropListData]="moviesToWatch" [cdkDropListConnectedTo]="[allmovies]"
        (cdkDropListDropped)="drop($event)">
        <app-movie *ngFor="let movie of moviesToWatch" [movie]="movie" cdkDrag></app-movie>
      </div>
    </div>
  </div>
</div>

Now we need to get some data by calling our service, let’s open our home.component.ts file.

import { Component } from '@angular/core';
import { MovieService } from '../movie.service';
import { Movie } from '../models/movie';
import { config } from '../config';
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})

export class HomeComponent {
  movies: Movie[];
  moviesToWatch: Movie[] = [{
    poster_path: '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg'
  }];
  constructor(private movieService: MovieService) {
    this.getMovies();
  }
  private async getMovies() {
    const movies = await this.movieService.get(config.api.topRated);
    return this.formatDta(movies.json().results);
  }
  formatDta(_body: Movie[]): void {
    this.movies = _body.filter(movie => movie.poster_path !== '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg');
  }
  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      transferArrayItem(event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex);
    }
  }
}

Here I am importing CdkDragDrop, moveItemInArray, transferArrayItem from ‘@angular/cdk/drag-drop’, this helps us to perform the drag and drop. In the constructor, we are fetching the data and assign to the variable movies which are an array of the movie.

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

I am setting the movies to watch collection as below, as I have already planned to watch that movie.

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

Remember the drag and drop with two sources will not work if it doesn’t have at least one item in it. Because I set a movie in it, it doesn’t make any sense to show that movie in the other collection right?

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

And below is our drop event.

drop(event: CdkDragDrop<string[]>) {
   if (event.previousContainer === event.container) {
     moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
   } else {
     transferArrayItem(event.previousContainer.data,
       event.container.data,
       event.previousIndex,
       event.currentIndex);
   }
 }

The complete code for the home.component.ts will look like below.

import { Component } from '@angular/core';
import { MovieService } from '../movie.service';
import { Movie } from '../models/movie';
import { config } from '../config';
import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})

export class HomeComponent {
  movies: Movie[];
  moviesToWatch: Movie[] = [{
    poster_path: '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg'
  }];
  constructor(private movieService: MovieService) {
    this.getMovies();
  }
  private async getMovies() {
    const movies = await this.movieService.get(config.api.topRated);
    return this.formatDta(movies.json().results);
  }
  formatDta(_body: Movie[]): void {
    this.movies = _body.filter(movie => movie.poster_path !== '/uC6TTUhPpQCmgldGyYveKRAu8JN.jpg');
  }
  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      transferArrayItem(event.previousContainer.data,
        event.container.data,
        event.previousIndex,
        event.currentIndex);
    }
  }
}

 

Custom styling

I have applied some custom styles to some components, those are below.

home.component.scss

.container{
  border: 1px solid rgb(248, 144, 144);
  margin: 5%;
  overflow: auto;
  width: 40%;
  height: 500px;
}
app-movie{
  cursor: move;
  width: 50%;
  display: inline-flex;
}

movie.component.scss

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

Output

Once you have implemented all the steps, you will be having an application which uses Angular 7 Drag and Drop with actual server data. Now let us run the application and see it in action.

ngDragDrop Initial

ngDragDrop After Adding

ngDragDrop adding more

 

Conclusion

In this post, we have learned how to,

  1. Create an angular 7 application
  2. Work with Angular CLI
  3. Generate a service in Angular
  4. How to fetch data from the server using HttpModule
  5. Generate components in Angular
  6. Use Material design
  7. Work with Angular 7 Drag and Drop feature with real server data

Please feel free to play with this GitHub repository. Please do share me your findings while you work on the same. I really appreciate that, thanks in advance.

Your turn. What do you think?

Thanks a lot for reading. I will come back with another post on the same topic very soon. Did I miss anything that you may think which is needed? Could you find this post as useful? If yes, please like/share/clap for me.

Kindest Regards
Sibeesh Venu

Exit mobile version