Introduction
As you all know, 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 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 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 was out last week, I wanted to try a few things with the same and that is the reason 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 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 AppModule
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 and it resides in the @angular/cdk/drag-drop
. As you might have already noticed, we have added one service called MovieService
in the provider's 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 in 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 div
s 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
containers.
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 fetch 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.
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 think is needed? Could you find this post useful? If yes, please like/share/clap for me.
History
- 30th October, 2018: Initial version