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 imports/api/images/collection.js
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
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
export const Images = new Mongo.Collection('images');
export const Thumbs = new Mongo.Collection('thumbs');
function loggedIn(userId) {
return !!userId;
}
Thumbs.allow({
insert: loggedIn,
update: loggedIn,
remove: loggedIn
});
Images.allow({
insert: loggedIn,
update: loggedIn,
remove: loggedIn
});
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
import { UploadFS } from 'meteor/jalik:ufs';
import { Images, Thumbs } from './collection';
export const ThumbsStore = new UploadFS.store.GridFS({
collection: Thumbs,
name: 'thumbs'
});
export const ImagesStore = new UploadFS.store.GridFS({
collection: Images,
name: 'images',
filter: new UploadFS.Filter({
contentTypes: ['image/*']
}),
copyTo: [
ThumbsStore
]
});
Let's explain a bit what happened.
Now we can create index.js
file to export Collections and Stores:
1
2
export * from './collection';
export * from './store';
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:
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const ThumbsStore = new UploadFS.store.GridFS({
collection: Thumbs,
name: 'thumbs',
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
Be aware that this module requires at least one of these libraries: GraphicsMagick, ImageMagick.
Now, let's create the PartyUpload
component. It will be responsible for cropping and uploading photos.
1
2
3
4
5
6
7
8
9
<div layout="column">
<div>
<div>Click here to select image</div>
<div>
<strong>OR</strong>
</div>
<div>You can also drop image to here</div>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import { Meteor } from 'meteor/meteor';
import template from './partyUpload.html';
class PartyUpload {}
const name = 'partyUpload';
// create a module
export default angular.module(name, [
angularMeteor
]).component(name, {
template,
controllerAs: name,
controller: PartyUpload
});
We want to use it in PartyAdd
:
5
6
7
8
9
10
11
32
33
34
35
36
37
38
39
import template from './partyAdd.html';
import { Parties } from '../../../api/parties';
import { name as PartyUpload } from '../partyUpload/partyUpload';
class PartyAdd {
constructor() {
...some lines skipped...
// create a module
export default angular.module(name, [
angularMeteor,
PartyUpload
]).component(name, {
template,
bindings: {
19
20
21
22
23
<div flex>
<md-button ng-click="partyAdd.submit()" class="md-raised">Add Party!</md-button>
</div>
<party-upload files="partyAdd.party.images"></party-upload>
</div>
As you can see we used files
directive. We will create it later. For now, let's only say that this is a two-way data binding.
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 AngularJS directive that handles file upload and gives us more abilities such as drag & drop, file validation on the client side. In this example, We will use ng-file-upload
, which have many features for file upload. In order to do this, let's add the package to our project:
$ meteor npm install ng-file-upload --save
Now, lets add a dependency in the PartyUpload
:
1
2
3
4
5
6
12
13
14
15
16
17
18
19
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngFileUpload from 'ng-file-upload';
import { Meteor } from 'meteor/meteor';
...some lines skipped...
// create a module
export default angular.module(name, [
angularMeteor,
ngFileUpload
]).component(name, {
template,
controllerAs: name,
Now, let's add the usage of ng-file-upload
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div layout="column">
<div ngf-drop
ngf-select
ngf-change="partyUpload.addImages($files)"
ngf-drag-over-class="{accept:'dragover', reject:'dragover-err', delay:100}"
class="drop-box"
ngf-multiple="false"
ngf-allow-dir="false"
ngf-accept="'image/*'"
ngf-drop-available="true"
ng-hide="partyUpload.cropImgSrc">
<div>Click here to select image</div>
<div>
<strong>OR</strong>
</div>
<div>You can also drop image to here</div>
</div>
</div>
Now let's make it better looking:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
party-upload {
.drop-box {
background: #F8F8F8;
border: 5px dashed #DDD;
text-align: center;
padding: 25px;
margin-left: 10px;
margin-bottom: 20px;
}
.drop-box.dragover {
border: 5px dashed blue;
}
.drop-box.dragover-err {
border: 5px dashed red;
}
}
then import new .less file in the PartyAdd
1
@import "../partyUpload/partyUpload.less";
and in the PartyAddButton
:
3
4
5
6
right: 15px;
bottom: 15px;
}
@import "../partyAdd/partyAdd.less";
And that's it! now we can upload images by using drag and drop! Just note that the Application UI still don't show the new images we upload... we will add this later. Now let's add some more cool features, And make the uploaded image visible!
One of the most common actions we want to make with pictures is to edit them before saving. We will add to our example ability to crop images before uploading them to the server, using ngImgCrop package. So lets start by adding the package to our project:
$ meteor npm install ng-img-crop --save
And add a dependency in our module:
1
2
3
4
5
6
7
8
9
39
40
41
42
43
44
45
46
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngFileUpload from 'ng-file-upload';
import 'ng-img-crop/compile/minified/ng-img-crop';
import 'ng-img-crop/compile/minified/ng-img-crop.css';
import { Meteor } from 'meteor/meteor';
...some lines skipped...
// create a module
export default angular.module(name, [
angularMeteor,
ngFileUpload,
'ngImgCrop'
]).component(name, {
template,
controllerAs: name,
We want to perform the crop on the client, before uploading it, so let's get the uploaded image, and instead of saving it to the server - we will get the Data Url of it, and use it in the ngImgCrop
:
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
import template from './partyUpload.html';
class PartyUpload {
constructor($scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
}
addImages(files) {
if (files.length) {
this.currentFile = files[0];
const reader = new FileReader;
reader.onload = this.$bindToContext((e) => {
this.cropImgSrc = e.target.result;
this.myCroppedImage = '';
});
reader.readAsDataURL(files[0]);
} else {
this.cropImgSrc = undefined;
}
}
}
const name = 'partyUpload';
We took the file object and used HTML5 FileReader
API to read the file from the user on the client side, without uploading it to the server.
Then we saved the DataURL of the image into a variable cropImgSrc
.
Next, we will need to use this DataURI with the ngImgCrop
directive as follow:
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
</div>
<div>You can also drop image to here</div>
</div>
<div ng-show="partyUpload.cropImgSrc" layout="column" class="ng-crop">
<div>
<h3>Edit & crop</h3>
<md-button ng-click="partyUpload.save()" class="md-primary">
Save Image
</md-button>
<md-button ng-click="partyUpload.reset()">
Cancel
</md-button>
</div>
<div class="ng-crop-container">
<img-crop image="partyUpload.cropImgSrc" result-image="partyUpload.myCroppedImage" area-type="square"></img-crop>
</div>
</div>
</div>
Moreover we add
ng-hide
to the upload control, in order to hide it, after the user picks an image to crop.
And add some CSS to make it look better:
13
14
15
16
17
18
19
20
21
22
23
24
25
.drop-box.dragover-err {
border: 5px dashed red;
}
.ng-crop {
h3 {
margin-top: 0;
}
}
.ng-crop-container {
width: 400px;
height: 225px;
}
}
Now we need to handle uploading files.
Since we're using DataURL
and we need UploadFS expects ArrayBuffer
let's add few helper methods.
Add dataURLToBlob
to converts DataURL to Blob
object:
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
/**
* Converts DataURL to Blob object
*
* https://github.com/ebidel/filer.js/blob/master/src/filer.js#L137
*
* @param {String} dataURL
* @return {Blob}
*/
export function dataURLToBlob(dataURL) {
const BASE64_MARKER = ';base64,';
if (dataURL.indexOf(BASE64_MARKER) === -1) {
const parts = dataURL.split(',');
const contentType = parts[0].split(':')[1];
const raw = decodeURIComponent(parts[1]);
return new Blob([raw], {type: contentType});
}
const parts = dataURL.split(BASE64_MARKER);
const contentType = parts[0].split(':')[1];
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], {type: contentType});
}
We have now Blob object so let's convert it to expected ArrayBuffer
by creating a function called blobToArrayBuffer
:
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
return new Blob([uInt8Array], {type: contentType});
}
/**
* Converts Blob object to ArrayBuffer
*
* @param {Blob} blob Source file
* @param {Function} callback Success callback with converted object as a first argument
* @param {Function} errorCallback Error callback with error as a first argument
*/
export function blobToArrayBuffer(blob, callback, errorCallback) {
const reader = new FileReader();
reader.onload = (e) => {
callback(e.target.result);
};
reader.onerror = (e) => {
if (errorCallback) {
errorCallback(e);
}
};
reader.readAsArrayBuffer(blob);
}
Now we can take care of uploading by creating a upload
function:
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
import { UploadFS } from 'meteor/jalik:ufs';
import { ImagesStore } from './store';
import { dataURLToBlob, blobToArrayBuffer } from './helpers';
/**
* Uploads a new file
*
* @param {String} dataUrl [description]
* @param {String} name [description]
* @param {Function} resolve [description]
* @param {Function} reject [description]
*/
export function upload(dataUrl, name, resolve, reject) {
// convert to Blob
const blob = dataURLToBlob(dataUrl);
blob.name = name;
// pick from an object only: name, type and size
const file = _.pick(blob, 'name', 'type', 'size');
// convert to ArrayBuffer
blobToArrayBuffer(blob, (data) => {
const upload = new UploadFS.Uploader({
data,
file,
store: ImagesStore,
onError: reject,
onComplete: resolve
});
upload.start();
}, reject);
}
dataUrl
is the file we want to uploadname
is a name of the fileresolve
is a callback function that will be invoked on successreject
is a callback function that will be invoked on errorname
, type
and size
from Blob object to send these information to UploadFS.Uploader
What's left is just to export this method:
1
2
3
export * from './collection';
export * from './store';
export * from './methods';
We previously defined two methods: save()
and reset()
. Let's implement them using recently created upload
function:
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { Meteor } from 'meteor/meteor';
import template from './partyUpload.html';
import { upload } from '../../../api/images';
class PartyUpload {
constructor($scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.uploaded = [];
}
addImages(files) {
...some lines skipped...
this.cropImgSrc = undefined;
}
}
save() {
upload(this.myCroppedImage, this.currentFile.name, this.$bindToContext((file) => {
this.uploaded.push(file);
this.reset();
}), (e) => {
console.log('Oops, something went wrong', e);
});
}
reset() {
this.cropImgSrc = undefined;
this.myCroppedImage = '';
}
}
const name = 'partyUpload';
Let's explain the code.
myCroppedImage
variable so we used it to as the file to be uploaded.currentFile.name
to get name of the file.$bindToContext
on the first callback (resolve
) to keep it inside digest cycle.console.log
to notify about an error.Last thing to do is to import Images and Thumb on the server-side:
1
2
3
4
import '../imports/startup/fixtures';
import '../imports/api/parties';
import '../imports/api/users';
import '../imports/api/images';
Let's create a simple gallery to list the images in the new party form:
29
30
31
32
33
34
35
36
37
<img-crop image="partyUpload.cropImgSrc" result-image="partyUpload.myCroppedImage" area-type="square"></img-crop>
</div>
</div>
<div layout="row" class="images-container-title">
<div class="party-image-container" ng-class="{'main-image': $index === 0}" ng-repeat="thumb in partyUpload.thumbs">
<img ng-src="{{ thumb.url }}"/>
</div>
</div>
</div>
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
width: 400px;
height: 225px;
}
.images-container-title {
padding-bottom: 30px;
}
.party-image-container {
position: relative;
margin-right: 10px;
max-height: 200px;
max-width: 200px;
img {
max-height: 100%;
max-width: 100%;
}
}
.main-image {
border: 2px solid #ddd;
}
}
We have to somehow get thumbnails to show them in this gallery.
Since we keep them in Collections, we have to create proper publications:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Meteor } from 'meteor/meteor';
import { Thumbs, Images } from './collection';
if (Meteor.isServer) {
Meteor.publish('thumbs', function(ids) {
return Thumbs.find({
originalStore: 'images',
originalId: {
$in: ids
}
});
});
Meteor.publish('images', function() {
return Images.find({});
});
}
1
2
3
4
import './publish';
export * from './collection';
export * from './store';
export * from './methods';
Now we can implement thumbnails:
8
9
10
11
12
13
14
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
54
55
56
57
58
59
60
61
62
63
64
65
import { Meteor } from 'meteor/meteor';
import template from './partyUpload.html';
import { Thumbs, upload } from '../../../api/images';
class PartyUpload {
constructor($scope, $reactive) {
...some lines skipped...
$reactive(this).attach($scope);
this.uploaded = [];
this.subscribe('thumbs', () => [
this.getReactively('files', true) || []
]);
this.helpers({
thumbs() {
return Thumbs.find({
originalStore: 'images',
originalId: {
$in: this.getReactively('files', true) || []
}
});
}
});
}
addImages(files) {
...some lines skipped...
save() {
upload(this.myCroppedImage, this.currentFile.name, this.$bindToContext((file) => {
this.uploaded.push(file);
if (!this.files || !this.files.length) {
this.files = [];
}
this.files.push(file._id);
this.reset();
}), (e) => {
console.log('Oops, something went wrong', e);
There are few things that need to be explained:
thumbs
publication with one argument.thumbs
helper that fetches all thumbnails of our saved files.files
variable. It contains identifiers of uploaded files.We can also add an ability to sort and update the images order. To get the ability to sort the images, let's use angular-sortable-view.
$ meteor npm install angular-sortable-view --save
Let's add it as a dependency:
1
2
3
4
5
6
7
79
80
81
82
83
84
85
86
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import ngFileUpload from 'ng-file-upload';
import 'angular-sortable-view';
import 'ng-img-crop/compile/minified/ng-img-crop';
import 'ng-img-crop/compile/minified/ng-img-crop.css';
...some lines skipped...
export default angular.module(name, [
angularMeteor,
ngFileUpload,
'ngImgCrop',
'angular-sortable-view'
]).component(name, {
template,
controllerAs: name,
Implement directives of angular-sortable-view:
29
30
31
32
33
34
35
36
37
<img-crop image="partyUpload.cropImgSrc" result-image="partyUpload.myCroppedImage" area-type="square"></img-crop>
</div>
</div>
<div layout="row" class="images-container-title" sv-root sv-part="partyUpload.thumbs">
<div sv-element class="party-image-container" ng-class="{'main-image': $index === 0}" ng-repeat="thumb in partyUpload.thumbs">
<img draggable="false" ng-src="{{ thumb.url }}"/>
</div>
</div>
</div>
You can now easily change their order.
Remember! First file is the main file which will be shown on parties list. We will implement it later.
Since we have sorting, croping and uploading. We can now add binding we prepared previously:
83
84
85
86
87
88
89
90
91
'angular-sortable-view'
]).component(name, {
template,
bindings: {
files: '=?'
},
controllerAs: name,
controller: PartyUpload
});
PartyImage
.
1
<img ng-src="{{partyImage.mainImage.url}}"/>
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
import angular from 'angular';
import angularMeteor from 'angular-meteor';
import template from './partyImage.html';
import { Images } from '../../../api/images';
class PartyImage {
constructor($scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
mainImage() {
const images = this.getReactively('images', true);
if (images) {
return Images.findOne({
_id: images[0]
});
}
}
});
}
}
const name = 'partyImage';
// create a module
export default angular.module(name, [
angularMeteor
]).component(name, {
template,
bindings: {
images: '<'
},
controllerAs: name,
controller: PartyImage
});
As you can see we defined images
which is a one-way binding that tells to the PartyImage component which images have been uploaded to the party.
Objects of
Images
andThumbs
collections contains.url
property with absolute url to the file.
Now let's implement it to the PartiesList
by adding dependency and what is more important, by subscribing images
. Subscription is needed to fetch defined images.
25
26
27
28
29
30
31
32
33
34
35
</span>
<span class="md-subhead">{{party.description}}</span>
</md-card-title-text>
<md-card-title-media ng-if="party.images">
<div class="md-media-lg card-media">
<party-image images="party.images"></party-image>
</div>
</md-card-title-media>
</md-card-title>
<md-card-content>
<party-rsvps-list rsvps="party.rsvps"></party-rsvps-list>
14
15
16
17
18
19
20
37
38
39
40
41
42
43
83
84
85
86
87
88
89
90
import { name as PartyCreator } from '../partyCreator/partyCreator';
import { name as PartyRsvp } from '../partyRsvp/partyRsvp';
import { name as PartyRsvpsList } from '../partyRsvpsList/partyRsvpsList';
import { name as PartyImage } from '../partyImage/partyImage';
class PartiesList {
constructor($scope, $reactive) {
...some lines skipped...
]);
this.subscribe('users');
this.subscribe('images');
this.helpers({
parties() {
...some lines skipped...
PartyRemove,
PartyCreator,
PartyRsvp,
PartyRsvpsList,
PartyImage
]).component(name, {
template,
controllerAs: name,
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.