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.
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.
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:
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;
}
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:
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.
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.
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
orbrew 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:
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;
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:
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
:
1
2
3
4
5
6
7
8
9
10
11
import { Component } from '@angular/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:
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
:
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:
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.
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:
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>
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:
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:
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:
1
2
3
4
5
6
7
8
9
10
11
13
14
15
16
17
18
19
import {Component} from '@angular/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:
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 '@angular/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:
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:
1
2
3
4
5
6
7
11
12
13
14
15
16
17
18
import {Component, OnInit} from '@angular/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.
1
2
3
4
21
22
23
24
25
26
27
66
67
68
69
70
71
import {Component, OnInit, EventEmitter, Output} from '@angular/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.
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:
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">
We will use Pipes to achieve this.
Let's create the DisplayMainImagePipe
inside 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 '@angular/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:
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
:
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:
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!
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.