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 +
+ + +
+ +
+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 +
+ + +
+ +
+ +
+
+
+
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" + ] +}