ng-file-upload trong AngularJs

Lightweight Angular directive để upload các file. Bạn có thể xem Demo để hiểu rõ hơn. Các phiên bản đã có: version 3.0.x, version 3.1.x, version 3.2.x, version 4.x.x, version 5.x.x, version 6.x.x, version 6.2.x, version 7.0.x, version 7.2.x, version 8.0.x, version 9.0.x, version 10.0.x, version 11.0.x, version 12.0.x, version 12.1.x, version 12.2.x

Nội dung sẽ trình bày bao gồm:

  • Các tính năng
  • Cài đặt (bằng tay, Bower, NuGet, NPM)
  • Các sử dụng
  • Trên các trình duyệt cũ

Tính năng

  • Upload file, xóa file
  • Kéo thả file (chỉ với html5)
  • Paste ảnh từ clipboard và kéo thả từ các trình duyệt khác (chỉ html5)
  • Thay đổi kích thước ảnh, center crop và người dùng có thể điều khiển crop thông qua ngImgCrop. Nhìn ví dụ ở crop sample (chỉ html5)
  • Sửa orientation cho các file ảnh jpeg cùng với dữ liệu exif orientation
  • Tải lên gián đoạn: Dừng lại/tiếp tục tải lên (chỉ trên html5)
  • Hỗ trợ validate kiểu, kích thước, chiều dài, chiều cao và góc cạnh của ảnh, thời gian video, âm thanh. Sử dụng ng-required cùng với pluggable custom sync hoặc async validations.
  • Trình diễn hình ảnh thu nhỏ hoặc là xem trước các lựa chọn hình ảnh/âm thanh/video.
  • Hỗ trợ CORS và tải lên trực tiếp tập tin dữ liệu nhị phân sử dụng Upload.$http()
  • Có sẵn trên npm, bower, meteor, nuget.
  • Phụ thuộc vào Flash FileAPI shim để tải lên nhưng không hỗ trợ trên trình duyệt html5

Cài đặt

  • Cài đặt bằng tay: Tải về tại đây
  • Cài đặt bằng brower
    • bower install ng-file-upload-shim --save(không hỗ trợ html5)
    • bower install ng-file-upload --save
  • Cài đặt bằng nuget: PM> Install-Package angular-file-upload
  • Cài đặt bằng npm: npm install ng-file-upload
<script src="angular(.min).js"></script>
<script src="ng-file-upload-shim(.min).js"></script> <!-- Không hỗ trợ trình duyệt html5 -->
<script src="ng-file-upload(.min).js"></script>

Cách sử dụng

<script src="angular.min.js"></script>
<!-- shim là cần thiết để hỗ trợ non-HTML5 FormData browsers (IE8-9)-->
<script src="ng-file-upload-shim.min.js"></script>
<script src="ng-file-upload.min.js"></script>

Tải lên từ form submit hoặc button click
<form ng-app="fileUpload" ng-controller="MyCtrl" name="form">
  validation mỗi ảnh tải lên
  <div class="button" ngf-select ng-model="file" name="file" ngf-pattern="'image/*'"
    ngf-accept="'image/*'" ngf-max-size="20MB" ngf-min-height="100"
    ngf-resize="{width: 100, height: 100}">Select</div>
  Tải nhiều files
  <div class="button" ngf-select ng-model="files" ngf-multiple="true">Select</div>
  Drop files: <div ngf-drop ng-model="files" class="drop-box">Drop</div>
  <button type="submit" ng-click="submit()">submit</button>
</form>

Upload right away after file selection:
<div class="button" ngf-select="upload($file)">Upload on file select</div>
<div class="button" ngf-select="uploadFiles($files)" multiple="multiple">Upload on file select</div>
  Drop File:
<div ngf-drop="uploadFiles($files)" class="drop-box"
  ngf-drag-over-class="'dragover'" ngf-multiple="true"
  ngf-pattern="'image/*,application/pdf'">Kéo thả ảnh hoặc file PDF ở đây</div>
<div ngf-no-file-drop>Kéo thả file không được hỗ trợ trên trình duyệt</div>

Thu nhỏ ảnh: <img ngf-thumbnail="file || '/thumb.jpg'">
Xem trước Âm thanh: <audio controls ngf-src="file"></audio>
Xem trước Video: <video controls ngf-src="file"></video>

Javascript code:

//inject trong directives và services.
var app = angular.module('fileUpload', ['ngFileUpload']);

app.controller('MyCtrl', ['$scope', 'Upload', function ($scope, Upload) {
    // Tải file lên sau khi ấn nút submit
    $scope.submit = function() {
      if ($scope.form.file.$valid && $scope.file) {
        $scope.upload($scope.file);
      }
    };

    // Tải file lên hoặc kéo thả
    $scope.upload = function (file) {
        Upload.upload({
            url: 'upload/url',
            data: {file: file, 'username': $scope.username}
        }).then(function (resp) {
            console.log('Success ' + resp.config.data.file.name + 'uploaded. Response: ' + resp.data);
        }, function (resp) {
            console.log('Error status: ' + resp.status);
        }, function (evt) {
            var progressPercentage = parseInt(100.0 * evt.loaded / evt.total);
            console.log('progress: ' + progressPercentage + '% ' + evt.config.data.file.name);
        });
    };
    // Đối việc upload nhiều file:
    $scope.uploadFiles = function (files) {
      if (files && files.length) {
        for (var i = 0; i < files.length; i++) {
          Upload.upload({..., data: {file: files[i]}, ...})...;
        }
        // Hoặc là gửi tất cả cùng với trình duyệt HTML5:
        Upload.upload({..., data: {file: files}, ...})...;
      }
    }
}]);

Tài liệu tham khảo đầy đủ

Lựa chọn và kéo thả file

Ít nhất một trong các ngf-select hoặc ngf-drop là bắt buộc đối với plugin để liên kết đến các phần tử. Chỉ các thuộc tính ngf-select được đánh dấu * và chỉ các thuộc tính ngf-drop được đánh dấu +

<div|button|input type="file"|ngf-select|ngf-drop...
  ngf-select="" or "upload($files, ...)" // được gọi khi file được lựa chọn hoặc xóa bỏ.
  ngf-drop="" or "upload($files, ...)" // được gọi khi file được kéo thả
    // You can use ng-model or ngf-change instead of specifying function for ngf-drop and ngf-select
    // function parameters are the same as ngf-change
  ngf-change="upload($files, $file, $newFiles, $duplicateFiles, $invalidFiles, $event)"
    // called when files are selected, dropped, or cleared
  ng-model="myFiles" // binds the valid selected/dropped file or files to the scope model
    // could be an array or single file depending on ngf-multiple and ngf-keep values.
  ngf-model-options="{updateOn: 'change click drop dropUrl paste', allowInvalid: false, debounce: 0}"
    // updateOn could be used to disable resetting on click, or updating on paste, browser image drop, etc.
    // allowInvalid default is false could allow invalid files in the model
    // debouncing will postpone model update (miliseconds). See angular ng-model-options for more details.
  ngf-model-invalid="invalidFile(s)" // binds the invalid selected/dropped file or files to this model.
  ngf-before-model-change="beforeChange($files, ...)" // called after file select/drop and before
    // model change, validation and resize is processed
  ng-disabled="boolean" // disables this element
  ngf-select-disabled="boolean" // default false, disables file select on this element
  ngf-drop-disabled="boolean" // default false, disables file drop on this element
  ngf-multiple="boolean" // default false, allows selecting multiple files
  ngf-keep="true|false|'distinct'" // default false, keep the previous ng-model files and
    // append the new files. "'distinct'" removes duplicate files
    // $newFiles and $duplicateFiles are set in ngf-change/select/drop functions.
  ngf-fix-orientation="boolean" //default false, would rotate the jpeg image files that have
    // exif orientation data. See #745. Could be a boolean function like shouldFixOrientation($file)
    // to decide wethere to fix that file or not.

  *ngf-capture="'camera'" or "'other'" // allows mobile devices to capture using camera
  *ngf-accept="'image/*'" // standard HTML accept attr, browser specific select popup window

  +ngf-allow-dir="boolean" // default true, allow dropping files only for Chrome webkit browser
  +ngf-include-dir="boolean" //default false, include directories in the dropped file array.
    //You can detect if they are directory or not by checking the type === 'directory'.
  +ngf-drag-over-class="{pattern: 'image/*', accept:'acceptClass', reject:'rejectClass', delay:100}"
                    or "'myDragOverClass'" or "calcDragOverClass($event)"
    // default "dragover". drag over css class behaviour. could be a string, a function
    // returning class name or a json object.
    // accept/reject class only works in Chrome, validating only the file mime type.
    // if pattern is not specified ngf-pattern will be used. See following docs for more info.
  +ngf-drag="drag($isDragging, $class, $event)" // function called on drag over/leave events.
    // $isDragging: boolean true if is dragging over(dragover), false if drag has left (dragleave)
    // $class is the class that is being set for the element calculated by ngf-drag-over-class
  +ngf-drop-available="dropSupported" // set the value of scope model to true or false based on file
                                     // drag&drop support for this browser
  +ngf-stop-propagation="boolean" // default false, whether to propagate drag/drop events.
  +ngf-hide-on-drop-not-available="boolean" // default false, hides element if file drag&drop is not
  +ngf-enable-firefox-paste="boolean" // *experimental* default false, enable firefox image paste by making element contenteditable

  ngf-resize="{width: 100, height: 100, quality: .8, type: 'image/jpeg',
               ratio: '1:2', centerCrop: true, pattern='.jpg', restoreExif: false}"
               or resizeOptions() // a function returning a promise which resolves into the options.
    // resizes the image to the given width/height or ratio. Quality is optional between 0.1 and 1.0),
    // type is optional convert it to the given image type format.
    // centerCrop true will center crop the image if it does not fit within the given width/height or ratio.
    // centerCrop false (default) will not crop the image and will fit it within the given width/height or ratio
    // so the resulting image width (or height) could be less than given width (or height).
    // pattern is to resize only the files that their name or type matches the pattern similar to ngf-pattern.
    // restoreExif boolean default true, will restore exif info on the resized image.
  ngf-resize-if="$width > 1000 || $height > 1000" or "resizeCondition($file, $width, $height)"
    // apply ngf-resize only if this function returns true. To filter specific images to be resized.
  ngf-validate-after-resize="boolean" // default false, if true all validation will be run after
    // the images are being resized, so any validation error before resize will be ignored.

  //validations:
  ngf-max-files="10" // maximum number of files allowed to be selected or dropped, validate error name: maxFiles
  ngf-pattern="'.pdf,.jpg,video/*,!.jog'" // comma separated wildcard to filter file names and types allowed
              // you can exclude specific files by ! at the beginning.
              // validate error name: pattern
  ngf-min-size, ngf-max-size, ngf-max-total-size="100" in bytes or "'10KB'" or "'10MB'" or "'10GB'"
              // validate as form.file.$error.maxSize=true and file.$error='maxSize'
              // ngf-max-total-size is for multiple file select and validating the total size of all files.
  ngf-min-height, ngf-max-height, ngf-min-width, ngf-max-width="1000" in pixels only images
              // validate error names: minHeight, maxHeight, minWidth, maxWidth
  ngf-ratio="8:10,1.6" // list of comma separated valid aspect ratio of images in float or 2:3 format
              // validate error name: ratio
  ngf-min-ratio, ngf-max-ratio="8:10" // min or max allowed aspect ratio for the image.
  ngf-dimensions="$width > 1000 || $height > 1000" or "validateDimension($file, $width, $height)"
              // validate the image dimensions, validate error name: dimensions
  ngf-min-duration, ngf-max-duration="100.5" in seconds or "'10s'" or "'10m'" or "'10h'" only audio, video
              // validate error name: maxDuration
  ngf-duration="$duration > 1000" or "validateDuration($file, $duration)"
              // validate the media duration, validate error name: duration

  ngf-validate="{size: {min: 10, max: '20MB'}, width: {min: 100, max:10000}, height: {min: 100, max: 300}
                ratio: '2x1', duration: {min: '10s', max: '5m'}, pattern: '.jpg'}"
                shorthand form for above validations in one place.
  ngf-validate-fn="validate($file)" // custom validation function, return boolean or string containing the error.
              // validate error name: validateFn
  ngf-validate-async-fn="validate($file)" // custom validation function, return a promise that resolve to
              // boolean or string containing the error. validate error name: validateAsyncFn
  ngf-validate-force="boolean" // default false, if true file.$error will be set if the dimension or duration
              // values for validations cannot be calculated for example image load error or unsupported video by the browser.
              // by default it would assume the file is valid if the duration or dimension cannot be calculated by the browser.
  ngf-ignore-invalid="'pattern maxSize'" // ignore the files that fail the specified validations. They will
              // just be ignored and will not show up in ngf-model-invalid or make the form invalid.
              // space separated list of validate error names.
  ngf-run-all-validations="boolean" // default false. Runs all the specified validate directives. By default
              // once a validation fails for a file it would stop running other validations for that file.

>Upload/Drop</div>

<div|... ngf-no-file-drop>File Drag/drop is not supported</div>

// filter to convert the file to base64 data url.
<a href="file | ngfDataUrl">image</a>

Xem trước file

<img|audio|video|div
  *ngf-src="file" //To preview the selected file, sets src attribute to the file data url.
  *ngf-background="file" //sets background-image style to the file data url.
  ngf-resize="{width: 20, height: 20, quality: 0.9}" // only for image resizes the image before setting it
             // as src or background image. quality is optional.
  ngf-no-object-url="true or false" // see #887 to force base64 url generation instead of object url. Default false
>

<div|span|...
 *ngf-thumbnail="file" //Generates a thumbnail version of the image file
 ngf-size="{width: 20, height: 20, quality: 0.9}" the image will be resized to this size
        // if not specified will be resized to this element`s client width and height.
 ngf-as-background="boolean" //if true it will set the background image style instead of src attribute.
>

Upload service:

var upload = Upload.upload({
  *url: 'server/upload/url', // upload.php script, node.js route, or servlet url
  /*
  Specify the file and optional data to be sent to the server.
  Each field including nested objects will be sent as a form data multipart.
  Samples: {pic: file, username: username}
    {files: files, otherInfo: {id: id, person: person,...}} multiple files (html5)
    {profiles: {[{pic: file1, username: username1}, {pic: file2, username: username2}]} nested array multiple files (html5)
    {file: file, info: Upload.json({id: id, name: name, ...})} send fields as json string
    {file: file, info: Upload.jsonBlob({id: id, name: name, ...})} send fields as json blob, 'application/json' content_type
    {picFile: Upload.rename(file, 'profile.jpg'), title: title} send file with picFile key and profile.jpg file name*/
  *data: {key: file, otherInfo: uploadInfo},
  /*
  This is to accommodate server implementations expecting nested data object keys in .key or [key] format.
  Example: data: {rec: {name: 'N', pic: file}} sent as: rec[name] -> N, rec[pic] -> file
     data: {rec: {name: 'N', pic: file}}, objectKey: '.k' sent as: rec.name -> N, rec.pic -> file */
  objectKey: '[k]' or '.k' // default is '[k]'
  /*
  This is to accommodate server implementations expecting array data object keys in '[i]' or '[]' or
  ''(multiple entries with same key) format.
  Example: data: {rec: [file[0], file[1], ...]} sent as: rec[0] -> file[0], rec[1] -> file[1],...
    data: {rec: {rec: [f[0], f[1], ...], arrayKey: '[]'} sent as: rec[] -> f[0], rec[] -> f[1],...*/
  arrayKey: '[i]' or '[]' or '.i' or '' //default is '[i]'
  method: 'POST' or 'PUT'(html5), default POST,
  headers: {'Authorization': 'xxx'}, // only for html5
  withCredentials: boolean,
  /*
  See resumable upload guide below the code for more details (html5 only) */
  resumeSizeUrl: '/uploaded/size/url?file=' + file.name // uploaded file size so far on the server.
  resumeSizeResponseReader: function(data) {return data.size;} // reads the uploaded file size from resumeSizeUrl GET response
  resumeSize: function() {return promise;} // function that returns a prommise which will be
                                            // resolved to the upload file size on the server.
  resumeChunkSize: 10000 or '10KB' or '10MB' // upload in chunks of specified size
  disableProgress: boolean // default false, experimental as hotfix for potential library conflicts with other plugins
  ... and all other angular $http() options could be used here.
})

// returns a promise
upload.then(function(resp) {
  // file is uploaded successfully
  console.log('file ' + resp.config.data.file.name + 'is uploaded successfully. Response: ' + resp.data);
}, function(resp) {
  // handle error
}, function(evt) {
  // progress notify
  console.log('progress: ' + parseInt(100.0 * evt.loaded / evt.total) + '% file :'+ evt.config.data.file.name);
});
upload.catch(errorCallback);
upload.finally(callback, notifyCallback);

/* access or attach event listeners to the underlying XMLHttpRequest */
upload.xhr(function(xhr){
  xhr.upload.addEventListener(...)
});

/* cancel/abort the upload in progress. */
upload.abort();

/*
alternative way of uploading, send the file binary with the file's content-type.
Could be used to upload files to CouchDB, imgur, etc... html5 FileReader is needed.
This is equivalent to angular $http() but allow you to listen to the progress event for HTML5 browsers.*/
Upload.http({
  url: '/server/upload/url',
  headers : {
    'Content-Type': file.type
  },
  data: file
})

/* Set the default values for ngf-select and ngf-drop directives*/
Upload.setDefaults({ngfMinSize: 20000, ngfMaxSize:20000000, ...})

// These two defaults could be decreased if you experience out of memory issues
// or could be increased if your app needs to show many images on the page.
// Each image in ngf-src, ngf-background or ngf-thumbnail is stored and referenced as a blob url
// and will only be released if the max value of the followings is reached.
Upload.defaults.blobUrlsMaxMemory = 268435456 // default max total size of files stored in blob urls.
Upload.defaults.blobUrlsMaxQueueSize = 200 // default max number of blob urls stored by this application.

/* Convert a single file or array of files to a single or array of
base64 data url representation of the file(s).
Could be used to send file in base64 format inside json to the databases */
Upload.base64DataUrl(files).then(function(urls){...});

/* Convert the file to blob url object or base64 data url based on boolean disallowObjectUrl value */
Upload.dataUrl(file, boolean).then(function(url){...});

/* Get image file dimensions*/
Upload.imageDimensions(file).then(function(dimensions){console.log(dimensions.width, dimensions.height);});

/* Get audio/video duration*/
Upload.mediaDuration(file).then(function(durationInSeconds){...});

/* Resizes an image. Returns a promise */
// options: width, height, quality, type, ratio, centerCrop, resizeIf, restoreExif
//resizeIf(width, height) returns boolean. See ngf-resize directive for more details of options.
Upload.resize(file, options).then(function(resizedFile){...});

/* returns boolean showing if image resize is supported by this browser*/
Upload.isResizeSupported()
/* returns boolean showing if resumable upload is supported by this browser*/
Upload.isResumeSupported()

/* returns a file which will be uploaded with the newName instead of original file name */
Upload.rename(file, newName)
/* converts the object to a Blob object with application/json content type
for jsob byte streaming support #359 (html5 only)*/
Upload.jsonBlob(obj)
/* converts the value to json to send data as json string. Same as angular.toJson(obj) */
Upload.json(obj)
/* converts a dataUrl to Blob object.*/
var blob = upload.dataUrltoBlob(dataurl, name);
/* returns true if there is an upload in progress. Can be used to prompt user before closing browser tab */
Upload.isUploadInProgress() boolean
/* downloads and converts a given url to Blob object which could be added to files model */
Upload.urlToBlob(url).then(function(blob) {...});
/* returns boolean to check if the object is file and could be used as file in Upload.upload()/http() */
Upload.isFile(obj);
/* fixes the exif orientation of the jpeg image file*/
Upload.applyExifRotation(file).then(...)

Giá trị ng-model sẽ là một file duy nhất thay vì một mảng nếu tất cả các điều sau đây là đúng:

  • ngf-multiple không được set hoặc là có giá trị false.
  • Thuộc tính multiple không được set trên các phần tử
  • ngf-keep không được set hoặc là có giá trị false.

Trên các trình duyệt cũ

Đối với các trình duyệt không hỗ trợ HTML5 FormData (IE8, IE9, ...) thì module FileAPI sẽ được sử dụng. Lưu ý: bạn cần cài thêm Flash trên browser khi FileAPI sử dụng Flash tải file.

Hai file FileAPI.min.js, FileAPI.flash.swf sẽ được load bởi các module theo yêu cầu (không cần phải được include trong html) nếu trình duyệt không hỗ trợ HTML5 FormData. Bạn có thể thay thế hai file trên bởi file angular-file-upload-shim(.min).js. Trên server của bạn sẽ được load tự động theo cùng một cách hoặc bạn có thể định nghĩa đường dẫn đến các file này, nếu các đường dẫn là khác nhau bạn có thể sử dụng theo script:

<script>
    //optional need to be loaded before angular-file-upload-shim(.min).js
    FileAPI = {
        //only one of jsPath or jsUrl.
        jsPath: '/js/FileAPI.min.js/folder/',
        jsUrl: 'yourcdn.com/js/FileAPI.min.js',

        //only one of staticPath or flashUrl.
        staticPath: '/flash/FileAPI.flash.swf/folder/',
        flashUrl: 'yourcdn.com/js/FileAPI.flash.swf',

        //forceLoad: true, html5: false //to debug flash in HTML5 browsers
        //noContentTimeout: 10000 (see #528)
    }
</script>
<script src="angular-file-upload-shim.min.js"></script>...

Những vấn đề trên trình duyệt cũ:

  • Không custom header được bởi vì Flash limitation #111 #224 #129
  • Trình duyệt cũ không chấp nhận request PUT #261

Tài liệu tham khảo

Bài viết này mình có tham khảo từ tài liệu sau: https://github.com/danialfarid/ng-file-upload/blob/master/README.md#full-reference