Community Post

How to create a Drag and Drop file directive in angular2 with angular-cli [Part 1]

Luis Moncaris

Hi everyone, In this post we will continue what we started on the last post How to create a Drag and Drop file directive in angular2 with angular-cli [Part 1]. First we are going to do is emiting changes on our directive to our parent component. To do this we need a full understanding of even binding on angular.

Sending events to parent component

To emit a custom event on angular, we use EventEmiter object that angular bring to us :

@Directive({
  selector: '[appDnd]'
})
export class DndDirective {
  private filesChangeEmiter : EventEmitter<FileList> = new EventEmitter();
  // The rest of the code with @HostBinding and @HostListeners ...
}

Is really important to check the declaration line, first of all we need to import EventEmitter from @angular/core, then in declaration line we strong-type the property filesChangeEmitter as a EventEmitter type, also we need to explicit set the type of objects we are going to emit, in this case we will emit a FileList (Array of files - like) Object every time a user drops files on the dropzone, so as you may think, we need to change @HostListener for event drop.

@HostListener('drop', ['$event']) public onDrop(evt){
    evt.preventDefault();
    evt.stopPropagation();
    this.background = '#eee';
    let files = evt.dataTransfer.files;
    if(files.length > 0){
      this.filesChangeEmiter.emit(files);
    }
  }

By doing this, we will be able to check any time files changes on dropzone, can we?. Well, almost. To be able to listen to this event, we need to expose the emitter one level to the parent component. To acomplish this task we are going to need the @Output decorator, this will allow to "listen" to this event in the parent html component by using event binding syntax.

@Output() private filesChangeEmiter : EventEmitter<FileList> = new EventEmitter();

It's also really important to import @Output from @angular/core.

Listening to events in parent component

To listen to this new event emited by the DnD directive, we need to modify our dnd.component.html and add a event binding property on the directive modified tag div.dropzone :

<div class="dropzone" appDnd (filesChangeEmiter)="onFilesChange($event)">
  <div class="text-wrapper">
    <div class="centered">Drop your file here!</div>
  </div>
</div>

By doing this, we are going to bind a function from parent (dnd.component) to the event triggering of files changes on directive. the $event param that we are passing to this new function is the emited value from the directive.

export class DndComponent {
  constructor() { }
  onFilesChange(fileList : FileList){
    // do stuff here
  }
}

For showing porpouse, I am going to console log the file list once the files are dropped and check the result in the next screenshoot :

As we can see, we receive in the parent a FileList object that is an array like object with File objects inside, we can do whatever we need to do with them. So to illustrative purpouse I'm going to create a list that will be listing the files dropped on the div.dropzone.

To do this, we need to create a property on dnd.component to save the data sended by directive.

export class DndComponent {
  private fileList : any = [];
  constructor() { }
  onFilesChange(fileList : FileList){
    this.fileList = fileList;
  }
}

And in the HTML file dnd.component.html, we can use a ngFor directive to go over the list and show all the uploaded files in the zone.

<div class="dropzone-list">
  <ul>
    <li *ngFor="let file of fileList">
      {{ file.name }}
    </li>
  </ul>
</div>

Also a little of style in file dnd.component.css :

.dropzone-list > ul {
  width: 30%;
  list-style: none;
}
.dropzone-list > ul > li:before {
  content : '+';
  color: #6def9a;
}
.dropzone-list > ul > li {
  border: 1px solid black;
  padding-bottom: 5px;
  padding-left: 10px;
}

Also we can create a "security" option to allow only some type of files in the drag and drop directive, to do this first we need to pass as argument a list of extensions we will allow dropping and emiting from directive. To acomplish this tasks we will create a list on the directive named allowed_extensions.

export class DndDirective {
  private allowed_extensions : Array<string> = [];
  // previous code with @HostBinding, @HostListeners and @Output events ...
  }

Now we can iterate over the allowed_extensions property to allow or deny files from the FileList, but we are no exposing this variable to the partner component. To do this we will need to use @Input decorator, than the oposite of @Output used for expose events, @Input will help us to pass information from parent to child by using binding property syntax.

First we need to import @Input from @angular/core and also we need to decorate our property :

@Input() private vallowed_extensions : Array<string> = [];

Once this have been exposed, we can pass with property binding syntax in dnd.component.html the array of the valid file extensions that will be used on the div.dropzone.

<div class="dropzone" appDnd (filesChangeEmiter)="onFilesChange($event)" [allowed_extensions]="['png', 'jpg', 'bmp']" >
  <div class="text-wrapper">
    <div class="centered">Drop your file here!</div>
  </div>
</div>

As in the example, we will only allow some image extensions on the dropzone. Later on we need to check any time the user drop the files, the extension to know if the file is a valid file.

export class DndDirective {
  ...
  @Output() private filesChangeEmiter : EventEmitter<File[]> = new EventEmitter();
  ...
  @HostListener('drop', ['$event']) public onDrop(evt){
    evt.preventDefault();
    evt.stopPropagation();
    this.background = '#eee';
    let files = evt.dataTransfer.files;
    let valid_files : Array<File> = [];
    if(files.length > 0){
      forEach(files, (file: File) =>{
        let ext = file.name.split('.')[file.name.split('.').length - 1];
        if(this.allowed_extensions.lastIndexOf(ext) != -1){
          valid_files.push(file);
        }
      });
      this.filesChangeEmiter.emit(valid_files)
    }
  }

But first there are some changes to do, first of all we need to create a new array of files that we will populate if the extension is a valid one. Also we are not emiting any more a FileList object, but we are going to use a Array instead, this will require that the filesChangesEmiter changes its typing. And for last but not less important in dnd.component.ts file, we need to change the typing also of the function signature onFilesChange that is binding to emiter :

 onFilesChange(fileList : Array<File>){
    this.fileList = fileList;
  }

With those changes, application will be working as before but now we are going to check extension of files to allow them or not to be pass from div.dropzone.

We also to improve UX we may show the user the invalid files that have been droped on the zone with the same technique we used before, a event listener and event listener property binding in dnd.component.html.

export class DndDirective {
  @Output() private filesInvalidEmiter : EventEmitter<File[]> = new EventEmitter();
  // ...
  @HostListener('drop', ['$event']) public onDrop(evt){
    evt.preventDefault();
    evt.stopPropagation();
    this.background = '#eee';
    let files = evt.dataTransfer.files;
    let valid_files : Array<File> = [];
    let invalid_files : Array<File> = [];
    if(files.length > 0){
      forEach(files, (file: File) =>{
        let ext = file.name.split('.')[file.name.split('.').length - 1];
        if(this.allowed_extensions.lastIndexOf(ext) != -1){
          valid_files.push(file);
        }else{
          invalid_files.push(file);
        }
      });
      this.filesChangeEmiter.emit(valid_files);
      this.filesInvalidEmiter.emit(invalid_files);
    }
  }
}

And again once we have prepare the emitting event, then we should pass to parent through event property binding and show it on the parent component.

  <ul class="invalid">
    <li *ngFor="let file of invalidFiles">
      {{ file.name }}
    </li>
  </ul>

and styling component also :

.dropzone-list > ul.invalid > li:before {
  content : '-';
  color: #ef000f;
}

Other important things that we may do is create events for error handling events, like files with invalid extensions, allow more than (x) quantity of files in the dropzone, etc. All this stuff can be done with the @Output decorator and event emitters.

Conclusion

  • Now we open a connection point between DnD directive and it's parent with EventEmits and @Input @Output directives
  • We now can show information from directive and use it to do other operations on this info
  • And more important we have learned how to work with events, custom events and other stuff that will help us on our UX in angular 2 projects.

I Hope you have enyoied this series of 2 post about angular, have a good day. I'll continue writing more Angular 2 and other interesting topics.

Luis Moncaris

2 posts

Backend developer with more than 4 year experience in PHP, Python/Django and other web frameworks. Also working 2 years in frontend technologies to create rich UI and UX. Actually developer at Nokia Colombia as Full Stack developer.