diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..b42d0428
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+/typings
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 00000000..b42d0428
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,2 @@
+/node_modules
+/typings
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..befa0692
--- /dev/null
+++ b/README.md
@@ -0,0 +1,451 @@
+# ng2-uploader
+
+For demos please see [demos page](http://ng2-uploader.com).
+
+## Angular2 File Uploader
+
+### Installation
+
+```
+npm install ng2-uploader
+```
+
+#### Examples
+
+1. [Basic Example](https://github.com/jkuri/ng2-uploader#basic-example)
+2. [Multiple Files Example](https://github.com/jkuri/ng2-uploader#multiple-files-example)
+3. [Basic Progressbar Example](https://github.com/jkuri/ng2-uploader#progressbar-example)
+4. [Multiple Files Progressbars Example](https://github.com/jkuri/ng2-uploader#multiple-files-progressbars-example)
+
+#### Backend Examples
+
+1. [NodeJS using HapiJS](https://github.com/jkuri/ng2-uploader#backend-example-using-hapijs)
+2. [PHP (Plain)](https://github.com/jkuri/ng2-uploader#backend-example-using-plain-php)
+
+### Basic Example
+
+`component.ts`
+````typescript
+import {Component} from 'angular2/core';
+import {UPLOAD_DIRECTIVES} from 'ng2-uploader';
+
+@Component({
+ selector: 'demo-app',
+ templateUrl: 'app/demo.html',
+ directives: [UPLOAD_DIRECTIVES],
+})
+export class DemoApp {
+ uploadFile: any;
+ options: Object = {
+ url: 'http://localhost:10050/upload'
+ };
+
+ handleUpload(data): void {
+ if (data && data.response) {
+ data = JSON.parse(data.response);
+ this.uploadFile = data;
+ }
+ }
+}
+````
+
+`component.html`
+````html
+
+
+
+Response: {{ uploadFile | json }}
+
+````
+
+### Multiple files example
+
+`component.ts`
+````typescript
+import {Component} from 'angular2/core';
+import {UPLOAD_DIRECTIVES} from 'ng2-uploader';
+
+@Component({
+ selector: 'basic-multiple',
+ templateUrl: 'basic-multiple.html',
+ directives: [UPLOAD_DIRECTIVES],
+})
+export class BasicMultiple {
+ uploadedFiles: any[] = [];
+ options: Object = {
+ url: 'http://localhost:10050/upload'
+ };
+
+ handleUpload(data): void {
+ if (data && data.response) {
+ data = JSON.parse(data.response);
+ this.uploadedFiles.push(data);
+ }
+ }
+}
+````
+
+`component.html`
+````html
+
+
+
+
+Response: {{ uploadedFiles | json }}
+
+````
+
+### Progressbar example
+
+`component.ts`
+````typescript
+import {Component, NgZone} from 'angular2/core';
+import {UPLOAD_DIRECTIVES} from 'ng2-uploader';
+
+@Component({
+ selector: 'basic-progressbar',
+ templateUrl: 'app/components/basic-progressbar/basic-progressbar.html',
+ directives: [UPLOAD_DIRECTIVES],
+})
+export class BasicProgressbar {
+ uploadFile: any;
+ uploadProgress: number;
+ uploadResponse: Object;
+ zone: NgZone;
+ options: Object = {
+ url: 'http://localhost:10050/upload'
+ };
+
+ constructor() {
+ this.uploadProgress = 0;
+ this.uploadResponse = {};
+ this.zone = new NgZone({ enableLongStackTrace: false });
+ }
+
+ handleUpload(data): void {
+ this.uploadFile = data;
+ this.zone.run(() => {
+ this.uploadProgress = data.progress.percent;
+ });
+ let resp = data.response;
+ if (resp) {
+ resp = JSON.parse(resp);
+ this.uploadResponse = resp;
+ }
+ }
+}
+````
+
+`component.html`
+````html
+
+
+
+ Choose file
+
+
+
+
+
+Progress: {{ uploadProgress }}%
+
+
+
+
+
Uploading file ({{ uploadProgress }}%)
+
+
+
+
+Response: {{ uploadFile | json }}
+
+````
+
+### Multiple files progressbars example
+
+`component.ts`
+````typescript
+import {Component, NgZone} from 'angular2/core';
+import {UPLOAD_DIRECTIVES} from 'ng2-uploader';
+
+@Component({
+ selector: 'multiple-progressbar',
+ templateUrl: 'app/components/multiple-progressbar/multiple-progressbar.html',
+ directives: [UPLOAD_DIRECTIVES]
+})
+export class MultipleProgressbar {
+ uploadFiles: any[];
+ uploadProgresses: any[] = [];
+ zone: NgZone;
+ options: Object = {
+ url: 'http://localhost:10050/upload'
+ };
+
+ constructor() {
+ this.zone = new NgZone({ enableLongStackTrace: false });
+ }
+
+ handleUpload(data): void {
+ let id = data.id;
+ let index = this.findIndex(id);
+ if (index === -1) {
+ this.uploadProgresses.push({id: id, percent: 0});
+ }
+ if (this.uploadProgresses[index]) {
+ this.zone.run(() => {
+ this.uploadProgresses[index].percent = data.progress.percent;
+ });
+ }
+ }
+
+ findIndex(id: string): number {
+ return this.uploadProgresses.findIndex(x => x.id === id);
+ }
+
+}
+````
+
+`component.html`
+````html
+
+
+
+ Choose files
+
+
+
+
+
+
+
+
+
+
Uploading file ({{ progressObj.percent }}%)
+
+
+````
+
+
+### Token-authorized call example
+
+`component.ts`
+````typescript
+import {Component} from 'angular2/core';
+import {UPLOAD_DIRECTIVES} from 'ng2-uploader';
+
+@Component({
+ selector: 'demo-app',
+ templateUrl: 'app/demo.html',
+ directives: [UPLOAD_DIRECTIVES],
+})
+export class DemoApp {
+ uploadFile: any;
+ options: Object = {
+ url: 'http://localhost:10050/upload',
+ withCredentials: true,
+ authToken: localStorage.getItem('token'),
+ authTokenPrefix: "Bearer" // required only if different than "Bearer"
+
+ };
+
+ handleUpload(data): void {
+ if (data && data.response) {
+ data = JSON.parse(data.response);
+ this.uploadFile = data;
+ }
+ }
+}
+````
+
+`component.html`
+````html
+
+
+
+Response: {{ uploadFile | json }}
+
+````
+
+### Custom field name example
+
+You may want to sent file with specific form field name. For that you can use options.fieldName. If not provided then the field will be named "file".
+
+`component.ts`
+````typescript
+import {Component} from 'angular2/core';
+import {UPLOAD_DIRECTIVES} from 'ng2-uploader';
+
+@Component({
+ selector: 'demo-app',
+ templateUrl: 'app/demo.html',
+ directives: [UPLOAD_DIRECTIVES],
+})
+export class DemoApp {
+ uploadFile: any;
+ options: Object = {
+ url: 'http://localhost:10050/upload',
+ fieldName: 'logo'
+ };
+
+ handleUpload(data): void {
+ if (data && data.response) {
+ data = JSON.parse(data.response);
+ this.uploadFile = data;
+ }
+ }
+}
+````
+
+`component.html`
+````html
+
+
+
+Response: {{ uploadFile | json }}
+
+````
+
+
+### Backend Example Using HapiJS
+
+````javascript
+'use strict';
+
+const Hapi = require('hapi');
+const Inert = require('inert');
+const Md5 = require('md5');
+const Multiparty = require('multiparty');
+const fs = require('fs');
+const path = require('path');
+const server = new Hapi.Server();
+
+server.connection({ port: 10050, routes: { cors: true } });
+server.register(Inert, (err) => {});
+
+const upload = {
+ payload: {
+ maxBytes: 209715200,
+ output: 'stream',
+ parse: false
+ },
+ handler: (request, reply) => {
+ const form = new Multiparty.Form();
+ form.parse(request.payload, (err, fields, files) => {
+ if (err) {
+ return reply({status: false, msg: err});
+ }
+
+ let responseData = [];
+
+ files.file.forEach((file) => {
+ let fileData = fs.readFileSync(file.path);
+ const originalName = file.originalFilename;
+ const generatedName = Md5(new Date().toString() +
+ originalName) + path.extname(originalName);
+ const filePath = path.resolve(__dirname, 'uploads',
+ generatedName);
+
+ fs.writeFileSync(filePath, fileData);
+ const data = {
+ originalName: originalName,
+ generatedName: generatedName
+ };
+
+ responseData.push(data);
+ });
+
+ reply({status: true, data: responseData});
+ });
+ }
+};
+
+const uploads = {
+ handler: {
+ directory: {
+ path: path.resolve(__dirname, 'uploads')
+ }
+ }
+};
+
+server.route([
+ { method: 'POST', path: '/upload', config: upload },
+ { method: 'GET', path: '/uploads/{path*}', config: uploads }
+]);
+
+server.start(() => {
+ console.log('Upload server running at', server.info.uri);
+});
+````
+
+### Backend example using plain PHP
+
+````php
+ false));
+ exit;
+}
+
+$path = 'uploads/';
+
+if (isset($_FILES['file'])) {
+ $originalName = $_FILES['file']['name'];
+ $ext = '.'.pathinfo($originalName, PATHINFO_EXTENSION);
+ $generatedName = md5($_FILES['file']['tmp_name']).$ext;
+ $filePath = $path.$generatedName;
+
+ if (!is_writable($path)) {
+ echo json_encode(array(
+ 'status' => false,
+ 'msg' => 'Destination directory not writable.'
+ ));
+ exit;
+ }
+
+ if (move_uploaded_file($_FILES['file']['tmp_name'], $filePath)) {
+ echo json_encode(array(
+ 'status' => true,
+ 'originalName' => $originalName,
+ 'generatedName' => $generatedName
+ ));
+ }
+}
+else {
+ echo json_encode(
+ array('status' => false, 'msg' => 'No file uploaded.')
+ );
+ exit;
+}
+
+?>
+````
+
+### Demos
+
+For more information, examples and usage examples please see [demos](http://ng2-uploader.com)
+
+#### LICENCE
+
+MIT
\ No newline at end of file
diff --git a/ng2-uploader.ts b/ng2-uploader.ts
new file mode 100644
index 00000000..4189d07f
--- /dev/null
+++ b/ng2-uploader.ts
@@ -0,0 +1,14 @@
+import {Ng2Uploader} from './src/services/ng2-uploader';
+import {NgFileSelect} from './src/directives/ng-file-select';
+import {NgFileDrop} from './src/directives/ng-file-drop';
+
+export * from './src/services/ng2-uploader';
+export * from './src/directives/ng-file-select';
+export * from './src/directives/ng-file-drop';
+
+export default {
+ directives: [NgFileSelect, NgFileDrop],
+ providers: [Ng2Uploader]
+}
+
+export const UPLOAD_DIRECTIVES: [any] = [NgFileSelect, NgFileDrop];
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..be42cd8b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "ng2-uploader",
+ "description": "Angular2 File Uploader",
+ "version": "0.5.0",
+ "license": "MIT",
+ "main": "./ng2-uploader.ts",
+ "author": "Jan Kuri ",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/jkuri/ng2-uploader.git"
+ },
+ "keywords": [
+ "ng2",
+ "angular",
+ "angular2",
+ "file",
+ "upload",
+ "uploader"
+ ],
+ "dependencies": {
+ "angular2": "2.0.0-beta.7",
+ "clang-format": "^1.0.35",
+ "es6-promise": "^3.0.2",
+ "es6-shim": "^0.33.3",
+ "reflect-metadata": "0.1.2",
+ "rxjs": "5.0.0-beta.2",
+ "systemjs": "0.19.20"
+ },
+ "devDependencies": {
+ "angular-cli": "0.0.*",
+ "angular-cli-github-pages": "^0.2.0",
+ "ember-cli-inject-live-reload": "^1.3.0",
+ "glob": "^6.0.4",
+ "jasmine-core": "^2.3.4",
+ "jasmine-spec-reporter": "^2.4.0",
+ "karma": "^0.13.15",
+ "karma-chrome-launcher": "^0.2.1",
+ "karma-jasmine": "^0.3.6",
+ "protractor": "^3.0.0",
+ "tslint": "^3.3.0",
+ "typescript": "^1.8.2",
+ "typings": "^0.6.6",
+ "ts-node": "^0.5.5"
+ }
+}
diff --git a/src/directives/ng-file-drop.ts b/src/directives/ng-file-drop.ts
new file mode 100644
index 00000000..98c9fe4f
--- /dev/null
+++ b/src/directives/ng-file-drop.ts
@@ -0,0 +1,51 @@
+import {Directive, ElementRef, EventEmitter} from 'angular2/core';
+import {Ng2Uploader} from '../services/ng2-uploader';
+
+@Directive({
+ selector: '[ng-file-drop]',
+ inputs: ['options: ng-file-drop'],
+ outputs: ['onUpload'],
+ host: { '(change)': 'onFiles()' }
+})
+export class NgFileDrop {
+ uploader: Ng2Uploader;
+ options: any;
+ onUpload: EventEmitter = new EventEmitter();
+
+ constructor(public el: ElementRef) {
+ this.uploader = new Ng2Uploader();
+ setTimeout(() => {
+ this.uploader.setOptions(this.options);
+ });
+
+ this.uploader._emitter.subscribe((data) => {
+ this.onUpload.emit(data);
+ });
+
+ this.initEvents();
+ }
+
+ initEvents(): void {
+ this.el.nativeElement.addEventListener('drop', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ let dt = e.dataTransfer;
+ let files = dt.files;
+
+ if (files.length) {
+ this.uploader.addFilesToQueue(files);
+ }
+ }, false);
+
+ this.el.nativeElement.addEventListener('dragenter', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }, false);
+
+ this.el.nativeElement.addEventListener('dragover', (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }, false);
+ }
+}
diff --git a/src/directives/ng-file-select.ts b/src/directives/ng-file-select.ts
new file mode 100644
index 00000000..65d85fcd
--- /dev/null
+++ b/src/directives/ng-file-select.ts
@@ -0,0 +1,32 @@
+import {Directive, ElementRef, EventEmitter} from 'angular2/core';
+import {Ng2Uploader} from '../services/ng2-uploader';
+
+@Directive({
+ selector: '[ng-file-select]',
+ inputs: ['options: ng-file-select'],
+ outputs: ['onUpload'],
+ host: { '(change)': 'onFiles()' }
+})
+export class NgFileSelect {
+ uploader: Ng2Uploader;
+ options: any;
+ onUpload: EventEmitter = new EventEmitter();
+
+ constructor(public el: ElementRef) {
+ this.uploader = new Ng2Uploader();
+ setTimeout(() => {
+ this.uploader.setOptions(this.options);
+ });
+
+ this.uploader._emitter.subscribe((data) => {
+ this.onUpload.emit(data);
+ });
+ }
+
+ onFiles(): void {
+ let files = this.el.nativeElement.files;
+ if (files.length) {
+ this.uploader.addFilesToQueue(files);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/services/ng2-uploader.ts b/src/services/ng2-uploader.ts
new file mode 100644
index 00000000..9dc4162f
--- /dev/null
+++ b/src/services/ng2-uploader.ts
@@ -0,0 +1,215 @@
+import {Injectable, EventEmitter} from 'angular2/core';
+
+class UploadedFile {
+ id: string;
+ status: number;
+ statusText: string;
+ progress: Object;
+ originalName: string;
+ size: number;
+ response: string;
+ done: boolean;
+ error: boolean;
+ abort: boolean;
+
+ constructor(id: string, originalName: string, size: number) {
+ this.id = id;
+ this.originalName = originalName;
+ this.size = size;
+ this.progress = {
+ loaded: 0,
+ total: 0,
+ percent: 0
+ };
+ this.done = false;
+ this.error = false;
+ this.abort = false;
+ }
+
+ setProgres(progress: Object): void {
+ this.progress = progress;
+ }
+
+ setError(): void {
+ this.error = true;
+ this.done = true;
+ }
+
+ setAbort(): void {
+ this.abort = true;
+ this.done = true;
+ }
+
+ onFinished(status: number, statusText: string, response: string): void {
+ this.status = status;
+ this.statusText = statusText;
+ this.response = response;
+ this.done = true;
+ }
+}
+
+@Injectable()
+export class Ng2Uploader {
+ url: string;
+ cors: boolean = false;
+ withCredentials: boolean = false;
+ multiple: boolean = false;
+ maxUploads: number = 3;
+ allowedExtensions: string[] = [];
+ maxSize: boolean = false;
+ data: Object = {};
+ noParams: boolean = true;
+ autoUpload: boolean = true;
+ multipart: boolean = true;
+ method: string = 'POST';
+ debug: boolean = false;
+ customHeaders: Object = {};
+ encodeHeaders: boolean = true;
+ authTokenPrefix: string = "Bearer";
+ authToken: string = undefined;
+ fieldName: string = "file";
+
+ _queue: any[] = [];
+ _emitter: EventEmitter = new EventEmitter(true);
+
+ setOptions(options: any): void {
+ this.url = options && options.url || this.url;
+ this.cors = options && options.cors || this.cors;
+ this.withCredentials = options && options.withCredentials || this.withCredentials;
+ this.multiple = options && options.multiple || this.multiple;
+ this.maxUploads = options && options.maxUploads || this.maxUploads;
+ this.allowedExtensions = options && options.allowedExtensions || this.allowedExtensions;
+ this.maxSize = options && options.maxSize || this.maxSize;
+ this.data = options && options.data || this.data;
+ this.noParams = options && options.noParams || this.noParams;
+ this.autoUpload = options && options.autoUpload || this.autoUpload;
+ this.multipart = options && options.multipart || this.multipart;
+ this.method = options && options.method || this.method;
+ this.debug = options && options.debug || this.debug;
+ this.customHeaders = options && options.customHeaders || this.customHeaders;
+ this.encodeHeaders = options && options.encodeHeaders || this.encodeHeaders;
+ this.authTokenPrefix = options && options.authTokenPrefix || this.authTokenPrefix;
+ this.authToken = options && options.authToken || this.authToken;
+ this.fieldName = options && options.fieldName || this.fieldName;
+
+ if (!this.multiple) {
+ this.maxUploads = 1;
+ }
+ }
+
+ uploadFilesInQueue(): void {
+ let newFiles = this._queue.filter((f) => { return !f.uploading; });
+ newFiles.forEach((f) => {
+ this.uploadFile(f);
+ });
+ };
+
+ uploadFile(file: any): void {
+ let xhr = new XMLHttpRequest();
+ let form = new FormData();
+ form.append(this.fieldName, file, file.name);
+
+ let uploadingFile = new UploadedFile(
+ this.generateRandomIndex(),
+ file.name,
+ file.size
+ );
+
+ let queueIndex = this._queue.findIndex(x => x === file);
+
+ xhr.upload.onprogress = (e) => {
+ if (e.lengthComputable) {
+ let percent = Math.round(e.loaded / e.total * 100);
+ uploadingFile.setProgres({
+ total: e.total,
+ loaded: e.loaded,
+ percent: percent
+ });
+
+ this._emitter.emit(uploadingFile);
+ }
+ }
+
+ xhr.upload.onabort = (e) => {
+ uploadingFile.setAbort();
+ this._emitter.emit(uploadingFile);
+ }
+
+ xhr.upload.onerror = (e) => {
+ uploadingFile.setError();
+ this._emitter.emit(uploadingFile);
+ }
+
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === XMLHttpRequest.DONE) {
+ uploadingFile.onFinished(
+ xhr.status,
+ xhr.statusText,
+ xhr.response
+ );
+ this.removeFileFromQueue(queueIndex);
+ this._emitter.emit(uploadingFile);
+ }
+ }
+
+ xhr.open(this.method, this.url, true);
+ xhr.withCredentials = this.withCredentials;
+
+ if (this.customHeaders) {
+ Object.keys(this.customHeaders).forEach((key) => {
+ xhr.setRequestHeader(key, this.customHeaders[key]);
+ });
+ }
+
+ if (this.authToken) {
+ xhr.setRequestHeader("Authorization", `${this.authTokenPrefix} ${this.authToken}`);
+ }
+
+ xhr.send(form);
+ }
+
+ addFilesToQueue(files: FileList[]): void {
+ for (let file of files) {
+ if (this.isFile(file) && !this.inQueue(file)) {
+ this._queue.push(file);
+ }
+ }
+
+ if (this.autoUpload) {
+ this.uploadFilesInQueue();
+ }
+ }
+
+ removeFileFromQueue(i: number): void {
+ this._queue.splice(i, 1);
+ }
+
+ clearQueue(): void {
+ this._queue = [];
+ }
+
+ getQueueSize(): number {
+ return this._queue.length;
+ }
+
+ inQueue(file: any): boolean {
+ let fileInQueue = this._queue.filter((f) => { return f === file; });
+ return fileInQueue.length ? true : false;
+ }
+
+ isFile(file: any): boolean {
+ return file !== null && (file instanceof Blob || (file.name && file.size));
+ }
+
+ log(msg: any): void {
+ if (!this.debug) {
+ return;
+ }
+ console.log('[Ng2Uploader]:', msg);
+ }
+
+ generateRandomIndex(): string {
+ return Math.random().toString(36).substring(7);
+ }
+
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..221bc2f7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "declaration": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "mapRoot": "",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "noEmitOnError": true,
+ "noImplicitAny": false,
+ "rootDir": ".",
+ "sourceMap": true,
+ "sourceRoot": "/",
+ "target": "es5"
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}