Introduction
As you all know Angular 7 is out with some cool new features. I really appreciate that you want to experience the brand new Angular. Here, in this post, I am going to explain a bit about one of the Angular 7 features, which is "Drag and Drop". 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 drop. Enough talking, let’s jump into the setup. I hope you will find this post useful. You can always read this article in my blog here.
Source Code
The source code can be found here.
Background
As Angular 7 was 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. You can visit here for understanding the things you can do with Angular 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 it 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 the app, we will be using Angular Material for the designing part and we can install it now itself along with the animation and cdk. With 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. 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, AppModule, that resides 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 which 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();
- }
- }
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, I am using my own repository. 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 handily 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 in the future, we can add a few more properties to 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, 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, and cdkDropListConnectedTo.
Finally, our home.component.html will be as given below.
- <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: 'https://cdn.sibeeshpassion.com/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 !== 'https://cdn.sibeeshpassion.com/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 assigning to the variable "movies" which is 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: 'https://cdn.sibeeshpassion.com/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 !== 'https://cdn.sibeeshpassion.com/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: 'https://cdn.sibeeshpassion.com/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 !== 'https://cdn.sibeeshpassion.com/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: 1 px solid# f89090;
- margin: 5 % ;
- overflow: auto;
- width: 40 % ;
- height: 500 px;
- }
- app - movie {
- cursor: move;
- width: 50 % ;
- display: inline - flex;
- }
movie.component.scss
- mat - card {
- width: 70 % ;
- padding: 26 px;
- margin: 5 px;
- border: 1 px 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 After Adding
ngDragDrop adding more
Conclusion
In this post, we have learned how to,
- Create an Angular 7 application
- Work with Angular CLI
- Generate a service in Angular
- How to fetch data from the server using HttpModule
- Generate components in Angular
- Use Material design
- Work with Angular 7 Drag and Drop feature with real server data
Please feel free to play with this GitHub repository. Please do share 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 is needed? Did you find this post useful? If yes, please like/share/clap for me.