Files and UploadFS

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this step we are going to add the ability to upload images into our app, and also sorting and naming them.

Angular-Meteor can use Meteor UploadFS which is a suite of Meteor packages that together provide a complete file management solution including uploading, downloading, storage, synchronization, manipulation, and copying.

It supports several storage adapters for saving files to the local filesystem, GridFS and additional storage adapters can be created.

The process is very similar for handling any other MongoDB Collection!

So let's add image upload to our app!

We will start by adding UploadFS to our project, by running the following command:

$ meteor add jalik:ufs

Now, we will decide the storage adapter we want to use. In this example, we will use the GridFS as storage adapters, so we will add the adapter by running this command:

$ meteor add jalik:ufs-gridfs

Note: you can find more information about Stores and Storage Adapters on the UploadFS's GitHub repository.

So now we have the UploadFS support and the storage adapter installed - we still need to create a UploadFS object to handle our files. Note that you will need to define the collection as shared resource because you will need to use the collection in both client and server side.

Creating the Mongo Collection and UploadFS Store

Let's start by creating both/collections/images.collection.ts file, and define a Mongo Collection object called "Images". Since we want to be able to make thumbnails we have to create another Collection called "Thumbs".

Also we will use the stadard Mongo Collection API that allows us to defined auth-rules.

21.2 Create Images and Thumbs collections both/collections/images.collection.ts
1
2
3
4
5
import { MongoObservable } from 'meteor-rxjs';
import { Meteor } from 'meteor/meteor';
 
export const Images = new MongoObservable.Collection('images');
export const Thumbs = new MongoObservable.Collection('thumbs');

Let's now create interfaces for both collections:

21.3 Define Image interface both/models/image.model.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export interface Image {
  _id?: string;
  complete: boolean;
  extension: string;
  name: string;
  progress: number;
  size: number;
  store: string;
  token: string;
  type: string;
  uploadedAt: Date;
  uploading: boolean;
  url: string;
  userId?: string;
}
21.4 Define Thumbs interface both/models/image.model.ts
12
13
14
15
16
17
18
19
20
  uploading: boolean;
  url: string;
  userId?: string;
}
 
export interface Thumb extends Image  {
  originalStore?: string;
  originalId?: string;
}

And use them on Images and Thumbs collections:

21.5 Add interfaces to Mongo Collections both/collections/images.collection.ts
1
2
3
4
5
6
import { MongoObservable } from 'meteor-rxjs';
import { Meteor } from 'meteor/meteor';
import { Thumb, Image } from "../models/image.model";
 
export const Images = new MongoObservable.Collection<Image>('images');
export const Thumbs = new MongoObservable.Collection<Thumb>('thumbs');

We have to create Stores for Images and Thumbs.

21.6 Create stores for Images and Thumbs both/collections/images.collection.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { MongoObservable } from 'meteor-rxjs';
import { Meteor } from 'meteor/meteor';
import { UploadFS } from 'meteor/jalik:ufs';
import { Thumb, Image } from "../models/image.model";
 
export const Images = new MongoObservable.Collection<Image>('images');
export const Thumbs = new MongoObservable.Collection<Thumb>('thumbs');
 
function loggedIn(userId) {
  return !!userId;
}
 
export const ThumbsStore = new UploadFS.store.GridFS({
  collection: Thumbs.collection,
  name: 'thumbs',
  permissions: new UploadFS.StorePermissions({
    insert: loggedIn,
    update: loggedIn,
    remove: loggedIn
  })
});
 
export const ImagesStore = new UploadFS.store.GridFS({
  collection: Images.collection,
  name: 'images',
  filter: new UploadFS.Filter({
    contentTypes: ['image/*']
  }),
  copyTo: [
    ThumbsStore
  ],
  permissions: new UploadFS.StorePermissions({
    insert: loggedIn,
    update: loggedIn,
    remove: loggedIn
  })
});
 

Let's explain a bit what happened.

  • We assigned Stores to their Collections, which is required.
  • We defined names of these Stores.
  • We added filter to ImagesStore so it can receive only images.
  • Every file will be copied to ThumbsStore.

There is a reason why we called one of the Collections the Thumbs!

Since we transfer every uploaded file to ThumbsStore, we can now easily add file manipulations.

Let's resize every file to 32x32:

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    insert: loggedIn,
    update: loggedIn,
    remove: loggedIn
  }),
  transformWrite(from, to, fileId, file) {
    // Resize to 32x32
    const gm = require('gm');
 
    gm(from, file.name)
      .resize(32, 32)
      .gravity('Center')
      .extent(32, 32)
      .quality(75)
      .stream()
      .pipe(to);
  }
});
 
export const ImagesStore = new UploadFS.store.GridFS({

We used gm module, let's install it:

$ meteor npm install gm --save

Note: To use this module, you need download and install GraphicsMagick or ImageMagick. In Mac OS X, you can use Homebrew and do: brew install graphicsmagick or brew install imagemagick.

Now because we used require, which is a NodeJS API, we need to add a TypeScript declaration, so let's install it:

$ meteor npm install @types/node --save

And let's import it in typings.d.ts file:

21.9 Import NodeJS @types typings.d.ts
1
2
3
4
5
6
7
/// <reference types="zone.js" />
/// <reference types="meteor-typings" />
/// <reference types="@types/underscore" />
/// <reference types="@types/node" />
 
declare module '*.html' {
  const template: string;

Image upload

Note that for file upload you can use basic HTML <input type="file"> or any other package - you only need the HTML5 File object to be provided.

For our application, we would like to add ability to drag-and-drop images, so we use Angular2 directive that handles file upload and gives us more abilities such as drag & drop, on the client side. In this example, We used angular2-file-drop, which is still in develop. In order to do this, let's add the package to our project:

$ meteor npm install angular2-file-drop --save

And let's add it's module to ours:

21.12 Include file drop module client/imports/app/app.module.ts
12
13
14
15
16
17
18
 
25
26
27
28
29
30
31
32
import { SHARED_DECLARATIONS } from './shared';
import { MaterialModule } from "@angular/material";
import { AUTH_DECLARATIONS } from "./auth/index";
import { FileDropModule } from "angular2-file-drop";
 
@NgModule({
  imports: [
...some lines skipped...
    AgmCoreModule.forRoot({
      apiKey: 'AIzaSyAWoBdZHCNh5R-hB5S5ZZ2oeoYyfdDgniA'
    }),
    MaterialModule.forRoot(),
    FileDropModule
  ],
  declarations: [
    AppComponent,

Now, let's create the PartiesUpload component. It will be responsible for uploading photos, starting with a stub of the view:

1
2
3
4
5
<div class="parties-update-container">
  <div>
    <div>Drop an image to here</div>
  </div>
</div>

And the Component:

21.14 Create a PartiesUpload component client/imports/app/parties/parties-upload.component.ts
1
2
3
4
5
6
7
8
9
10
11
import { Component } from [email protected]/core';
 
import template from './parties-upload.component.html';
 
@Component({
  selector: 'parties-upload',
  template
})
export class PartiesUploadComponent {
  constructor() {}
}

And let's add it to our declarations file:

21.15 Added PartiesUpload component to the index file client/imports/app/parties/index.ts
1
2
3
4
5
6
7
8
9
10
11
import { PartiesFormComponent } from './parties-form.component';
import { PartiesListComponent } from './parties-list.component';
import { PartyDetailsComponent } from './party-details.component';
import {PartiesUploadComponent} from "./parties-upload.component";
 
export const PARTIES_DECLARATIONS = [
  PartiesFormComponent,
  PartiesListComponent,
  PartyDetailsComponent,
  PartiesUploadComponent
];

We want to use it in PartiesForm:

21.16 Use PartiesUploadComponent inside the form client/imports/app/parties/parties-form.component.html
17
18
19
20
21
22
23
              <br/>
              <md-checkbox formControlName="public">Public party?</md-checkbox>
              <br/><br/>
              <parties-upload #upload></parties-upload>
              <button color="accent" md-raised-button type="submit">Add my party!</button>
            </div>
            <div class="form-extras">

Now, let's implement fileDrop directive:

1
2
3
4
5
6
7
8
<div class="parties-update-container">
  <div fileDrop
       [ngClass]="{'file-is-over': fileIsOver}"
       (fileOver)="fileOver($event)"
       (onFileDrop)="onFileDrop($event)">
    <div>Drop an image to here</div>
  </div>
</div>

As you can see we used fileOver event. It tells the component if file is over the drop zone.

We can now handle it inside the component:

7
8
9
10
11
12
13
14
15
16
17
  template
})
export class PartiesUploadComponent {
  fileIsOver: boolean = false;
 
  constructor() {}
 
  fileOver(fileIsOver: boolean): void {
    this.fileIsOver = fileIsOver;
  }
}

Second thing is to handle onFileDrop event:

21.19 Implement onFileDrop method client/imports/app/parties/parties-upload.component.ts
14
15
16
17
18
19
20
21
  fileOver(fileIsOver: boolean): void {
    this.fileIsOver = fileIsOver;
  }
 
  onFileDrop(file: File): void {
    console.log('Got file');
  }
}

Now our component is able to catch any dropped file, so let's create a function to upload that file into server.

21.20 Implement the upload method both/methods/images.methods.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { UploadFS } from 'meteor/jalik:ufs';
import { ImagesStore } from '../collections/images.collection';
 
export function upload(data: File): Promise<any> {
  return new Promise((resolve, reject) => {
    // pick from an object only: name, type and size
    const file = {
      name: data.name,
      type: data.type,
      size: data.size,
    };
 
    const upload = new UploadFS.Uploader({
      data,
      file,
      store: ImagesStore,
      onError: reject,
      onComplete: resolve
    });
 
    upload.start();
  });
}

Quick explanation. We need to know the name, the type and also the size of file we want to upload. We can get it from data object.

Now we can move on to use that function in PartiesUpload component:

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 
import template from './parties-upload.component.html';
 
import { upload } from '../../../../both/methods/images.methods';
 
@Component({
  selector: 'parties-upload',
  template
})
export class PartiesUploadComponent {
  fileIsOver: boolean = false;
  uploading: boolean = false;
 
  constructor() {}
 
...some lines skipped...
  }
 
  onFileDrop(file: File): void {
    this.uploading = true;
 
    upload(file)
      .then(() => {
        this.uploading = false;
      })
      .catch((error) => {
        this.uploading = false;
        console.log(`Something went wrong!`, error);
      });
  }
}

Now let's take a little break and solve those annoying missing modules errors. Since the uploading packages we used in the upload method are package that comes from Meteor Atmosphere and they not provide TypeScript declaration (.d.ts files), we need to create one for them.

Let's add it:

21.22 Declare meteor/jalik typings/jalik-ufs.d.ts
1
2
3
4
5
6
7
8
9
10
11
declare module "meteor/jalik:ufs" {
  interface Uploader {
    start: () => void;
  }
 
  interface UploadFS {
    Uploader: (options: any) => Uploader;
  }
 
  export var UploadFS;
}

Let's also add the file-uploading css class:

1
2
3
4
5
6
<div class="parties-update-container">
  <div fileDrop
       [ngClass]="{'file-is-over': fileIsOver, 'file-uploading': uploading}"
       (fileOver)="fileOver($event)"
       (onFileDrop)="onFileDrop($event)">
    <div>Drop an image to here</div>

Display Uploaded Images

Let's create a simple gallery to list the images in the new party form.

First thing to do is to create a Publication for thumbnails:

21.24 Implement publications of Images and Thumbs server/imports/publications/images.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Meteor } from 'meteor/meteor';
import { Thumbs, Images } from '../../../both/collections/images.collection';
 
Meteor.publish('thumbs', function(ids: string[]) {
  return Thumbs.collection.find({
    originalStore: 'images',
    originalId: {
      $in: ids
    }
  });
});
 
Meteor.publish('images', function() {
  return Images.collection.find({});
});

As you can see we also created a Publication for images. We will use it later.

We still need to add it on the server-side:

21.25 Import those publications in the server entry point server/main.ts
5
6
7
8
9
10
11
import './imports/publications/parties';
import './imports/publications/users';
import '../both/methods/parties.methods';
import './imports/publications/images';
 
Meteor.startup(() => {
  loadParties();

Now let's take care of UI. This will need to be reactive, so we will use again the MeteorObservable wrapper and RxJS.

Let's create a Subject that will be in charge of notification regarding files actions:

21.26 Use RxJS to keep track of files client/imports/app/parties/parties-upload.component.ts
1
2
3
4
5
6
7
8
9
10
11
 
13
14
15
16
17
18
19
import {Component} from [email protected]/core';
 
import template from './parties-upload.component.html';
 
import { upload } from '../../../../both/methods/images.methods';
 
import {Subject, Subscription} from "rxjs";
 
@Component({
  selector: 'parties-upload',
  template
...some lines skipped...
export class PartiesUploadComponent {
  fileIsOver: boolean = false;
  uploading: boolean = false;
  files: Subject<string[]> = new Subject<string[]>();
 
  constructor() {}
 

Let's now subscribe to thumbs publication with an array of those ids we created in the previous step:

21.27 Subscribe to the thumbs publication client/imports/app/parties/parties-upload.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import {Component, OnInit} from [email protected]/core';
 
import template from './parties-upload.component.html';
 
import { upload } from '../../../../both/methods/images.methods';
import {Subject, Subscription} from "rxjs";
import {MeteorObservable} from "meteor-rxjs";
 
@Component({
  selector: 'parties-upload',
  template
})
export class PartiesUploadComponent implements OnInit {
  fileIsOver: boolean = false;
  uploading: boolean = false;
  files: Subject<string[]> = new Subject<string[]>();
  thumbsSubscription: Subscription;
 
  constructor() {}
 
  ngOnInit() {
    this.files.subscribe((filesArray) => {
      MeteorObservable.autorun().subscribe(() => {
        if (this.thumbsSubscription) {
          this.thumbsSubscription.unsubscribe();
          this.thumbsSubscription = undefined;
        }
 
        this.thumbsSubscription = MeteorObservable.subscribe("thumbs", filesArray).subscribe();
      });
    });
  }
 
  fileOver(fileIsOver: boolean): void {
    this.fileIsOver = fileIsOver;
  }

Now we can look for thumbnails that come from ImagesStore:

3
4
5
6
7
8
9
10
11
12
 
17
18
19
20
21
22
23
 
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import template from './parties-upload.component.html';
 
import { upload } from '../../../../both/methods/images.methods';
import {Subject, Subscription, Observable} from "rxjs";
import {MeteorObservable} from "meteor-rxjs";
import {Thumb} from "../../../../both/models/image.model";
import {Thumbs} from "../../../../both/collections/images.collection";
 
@Component({
  selector: 'parties-upload',
...some lines skipped...
  uploading: boolean = false;
  files: Subject<string[]> = new Subject<string[]>();
  thumbsSubscription: Subscription;
  thumbs: Observable<Thumb[]>;
 
  constructor() {}
 
...some lines skipped...
          this.thumbsSubscription = undefined;
        }
 
        this.thumbsSubscription = MeteorObservable.subscribe("thumbs", filesArray).subscribe(() => {
          this.thumbs = Thumbs.find({
            originalStore: 'images',
            originalId: {
              $in: filesArray
            }
          }).zone();
        });
      });
    });
  }

We still don't see any thumbnails, so let's add a view for the thumbs:

21.29 Implement the thumbnails in the view client/imports/app/parties/parties-upload.component.html
5
6
7
8
9
10
11
12
13
14
       (onFileDrop)="onFileDrop($event)">
    <div>Drop an image to here</div>
  </div>
  <div *ngIf="thumbs" class="thumbs">
    <div *ngFor="let thumb of thumbs | async" class="thumb">
      <img [src]="thumb.url"/>
    </div>
    <div class="clear"></div>
  </div>
</div>

Since we are working on a view right now, let's add some style.

We need to create parties-upload.component.scss file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
.file-uploading {
  opacity: 0.3;
}
 
.file-is-over {
  opacity: 0.7;
}
 
.parties-update-container {
  width: 90%;
  margin: 15px;
 
  .thumbs {
    margin-top: 10px;
    margin-bottom: 10px;
 
    .clear {
      clear: both;
    }
 
    .thumb {
      float: left;
      width: 60px;
      height: 60px;
      display: flex;
      justify-content: center;
      align-items: center;
      overflow: hidden;
      margin-right: 5px;
 
      img {
        flex-shrink: 0;
        min-width: 100%;
        min-height: 100%
      }
    }
  }
}
 
[filedrop] {
  width: 100%;
  height: 60px;
  line-height: 60px;
  text-align: center;
  border: 3px dashed rgba(255, 255, 255, 0.7);
}

And let's import the SCSS file into our Component:

21.31 Added import for the styles file client/imports/app/parties/parties-upload.component.ts
1
2
3
4
5
6
7
 
11
12
13
14
15
16
17
18
import {Component, OnInit} from [email protected]/core';
 
import template from './parties-upload.component.html';
import style from './parties-upload.component.scss';
 
import { upload } from '../../../../both/methods/images.methods';
import {Subject, Subscription, Observable} from "rxjs";
...some lines skipped...
 
@Component({
  selector: 'parties-upload',
  template,
  styles: [ style ]
})
export class PartiesUploadComponent implements OnInit {
  fileIsOver: boolean = false;

Great! We can move on to the next step. Let's do something with the result of the upload function.

We will create the addFile method that updates the files property, and we will add the actual array the in charge of the notifications in files (which is a Subject and only in charge of the notifications, not the actual data):

17
18
19
20
21
22
23
 
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
export class PartiesUploadComponent implements OnInit {
  fileIsOver: boolean = false;
  uploading: boolean = false;
  filesArray: string[] = [];
  files: Subject<string[]> = new Subject<string[]>();
  thumbsSubscription: Subscription;
  thumbs: Observable<Thumb[]>;
...some lines skipped...
    this.uploading = true;
 
    upload(file)
      .then((result) => {
        this.uploading = false;
        this.addFile(result);
      })
      .catch((error) => {
        this.uploading = false;
        console.log(`Something went wrong!`, error);
      });
  }
 
  addFile(file) {
    this.filesArray.push(file._id);
    this.files.next(this.filesArray);
  }
}

We want a communication between PartiesUpload and PartiesForm. Let's use Output decorator and the EventEmitter to notify PartiesForm component about every new file.

21.33 Emit event with the new file client/imports/app/parties/parties-upload.component.ts
1
2
3
4
 
21
22
23
24
25
26
27
 
66
67
68
69
70
71
import {Component, OnInit, EventEmitter, Output} from [email protected]/core';
 
import template from './parties-upload.component.html';
import style from './parties-upload.component.scss';
...some lines skipped...
  files: Subject<string[]> = new Subject<string[]>();
  thumbsSubscription: Subscription;
  thumbs: Observable<Thumb[]>;
  @Output() onFile: EventEmitter<string> = new EventEmitter<string>();
 
  constructor() {}
 
...some lines skipped...
  addFile(file) {
    this.filesArray.push(file._id);
    this.files.next(this.filesArray);
    this.onFile.emit(file._id);
  }
}

On the receiving side of this connection we have the PartiesForm component.

Create a method that handles an event with the new file and put images inside the FormBuilder.

21.34 Add images to the PartiesForm component client/imports/app/parties/parties-form.component.ts
14
15
16
17
18
19
20
 
48
49
50
51
52
53
54
 
56
57
58
59
60
61
62
63
export class PartiesFormComponent implements OnInit {
  addForm: FormGroup;
  newPartyPosition: {lat:number, lng: number} = {lat: 37.4292, lng: -122.1381};
  images: string[] = [];
 
  constructor(
    private formBuilder: FormBuilder
...some lines skipped...
          lat: this.newPartyPosition.lat,
          lng: this.newPartyPosition.lng
        },
        images: this.images,
        public: this.addForm.value.public,
        owner: Meteor.userId()
      });
...some lines skipped...
      this.addForm.reset();
    }
  }
 
  onImage(imageId: string) {
    this.images.push(imageId);
  }
}

To keep Party interface up to date, we need to add images to it:

21.35 Add images property to the Party interface both/models/party.model.ts
8
9
10
11
12
13
14
  public: boolean;
  invited?: string[];
  rsvps?: RSVP[];
  images?: string[];
}
 
interface RSVP {

The last step will be to create an event binding for onFile.

17
18
19
20
21
22
23
              <br/>
              <md-checkbox formControlName="public">Public party?</md-checkbox>
              <br/><br/>
              <parties-upload #upload (onFile)="onImage($event)"></parties-upload>
              <button color="accent" md-raised-button type="submit">Add my party!</button>
            </div>
            <div class="form-extras">

Display the main image of each party on the list

We will use Pipes to achieve this.

Let's create the DisplayMainImagePipe inside client/imports/app/shared/display-main-image.pipe.ts:

21.37 Create DisplayMainImage pipe client/imports/app/shared/display-main-image.pipe.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Pipe, PipeTransform} from [email protected]/core';
import { Images } from '../../../../both/collections/images.collection';
import { Party } from '../../../../both/models/party.model';
 
@Pipe({
  name: 'displayMainImage'
})
export class DisplayMainImagePipe implements PipeTransform {
  transform(party: Party) {
    if (!party) {
      return;
    }
 
    let imageUrl: string;
    let imageId: string = (party.images || [])[0];
 
    const found = Images.findOne(imageId);
 
    if (found) {
      imageUrl = found.url;
    }
 
    return imageUrl;
  }
}

Since we have it done, let's add it to PartiesList:

21.38 Add DisplayMainImage pipe client/imports/app/shared/index.ts
1
2
3
4
5
6
7
8
9
import { DisplayNamePipe } from './display-name.pipe';
import {RsvpPipe} from "./rsvp.pipe";
import {DisplayMainImagePipe} from "./display-main-image.pipe";
 
export const SHARED_DECLARATIONS: any[] = [
  DisplayNamePipe,
  RsvpPipe,
  DisplayMainImagePipe
];

We also need to subscribe to images:

21.39 Subscribe to the images publication client/imports/app/parties/parties-list.component.ts
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
 
120
121
122
123
124
125
  autorunSub: Subscription;
  location: Subject<string> = new Subject<string>();
  user: Meteor.User;
  imagesSubs: Subscription;
 
  constructor(
    private paginationService: PaginationService
  ) {}
 
  ngOnInit() {
    this.imagesSubs = MeteorObservable.subscribe('images').subscribe();
 
    this.optionsSub = Observable.combineLatest(
      this.pageSize,
      this.curPage,
...some lines skipped...
    this.partiesSub.unsubscribe();
    this.optionsSub.unsubscribe();
    this.autorunSub.unsubscribe();
    this.imagesSubs.unsubscribe();
  }
}

We can now just implement it:

19
20
21
22
23
24
25
    <pagination-controls class="pagination" (pageChange)="onPageChanged($event)"></pagination-controls>
 
    <md-card *ngFor="let party of parties | async" class="party-card">
      <img *ngIf="party.images && party.images.length > 0" class="party-main-image" [src]="party | displayMainImage">
      <h2 class="party-name">
        <a [routerLink]="['/party', party._id]">{{party.name}}</a>
      </h2>

Add some css rules to keep the control of images:

24
25
26
27
28
29
30
31
32
33
34
      margin: 20px;
      position: relative;
 
      img.party-main-image {
        max-width: 100%;
        max-height: 100%;
      }
 
      .party-name > a {
        color: black;
        text-decoration: none;

We still need to add the reset functionality to the component, since we want to manage what happens after images were added:

21.42 Add the reset method to PartiesUpload component client/imports/app/parties/parties-upload.component.ts
68
69
70
71
72
73
74
75
76
    this.files.next(this.filesArray);
    this.onFile.emit(file._id);
  }
 
  reset() {
    this.filesArray = [];
    this.files.next(this.filesArray);
  }
}

By using #upload we get access to the PartiesUpload component's API. We can now use the `reset()`` method:

6
7
8
9
10
11
12
        <h2>Add it now! ></h2>
      </div>
      <div class="form-center">
        <form *ngIf="user" [formGroup]="addForm" (ngSubmit)="addParty(); upload.reset();">
          <div style="display: table-row">
            <div class="form-inputs">
              <md-input dividerColor="accent" formControlName="name" placeholder="Party name"></md-input>

And that's it!

Cloud Storage

By storing files in the cloud you can reduce your costs and get a lot of other benefits.

Since this chapter is all about uploading files and UploadFS doesn't have built-in support for cloud services we should mention another library for that.

We recommend you to use Slingshot. You can install it by running:

$ meteor add edgee:slingshot

It's very easy to use with AWS S3, Google Cloud and other cloud storage services.

From slignshot's repository:

meteor-slingshot uploads the files directly to the cloud service from the browser without ever exposing your secret access key or any other sensitive data to the client and without requiring public write access to cloud storage to the entire public.