ng-model per `<input type =“ file ”/>` (con direttiva DEMO)


281

Ho provato a usare ng-model sul tag di input con type file:

<input type="file" ng-model="vm.uploadme" />

Ma dopo aver selezionato un file, nel controller, $ scope.vm.uploadme è ancora indefinito.

Come posso ottenere il file selezionato nel mio controller?


3
Vedi stackoverflow.com/a/17923521/135114 , in particolare l'esempio citato online su jsfiddle.net/danielzen/utp7j
Daryn

Credo che sia sempre necessario specificare la proprietà name sull'elemento html quando si utilizza ngModel.
Sam

Risposte:


327

Ho creato una soluzione alternativa con direttiva:

.directive("fileread", [function () {
    return {
        scope: {
            fileread: "="
        },
        link: function (scope, element, attributes) {
            element.bind("change", function (changeEvent) {
                var reader = new FileReader();
                reader.onload = function (loadEvent) {
                    scope.$apply(function () {
                        scope.fileread = loadEvent.target.result;
                    });
                }
                reader.readAsDataURL(changeEvent.target.files[0]);
            });
        }
    }
}]);

E il tag di input diventa:

<input type="file" fileread="vm.uploadme" />

O se è necessaria solo la definizione del file:

.directive("fileread", [function () {
    return {
        scope: {
            fileread: "="
        },
        link: function (scope, element, attributes) {
            element.bind("change", function (changeEvent) {
                scope.$apply(function () {
                    scope.fileread = changeEvent.target.files[0];
                    // or all selected files:
                    // scope.fileread = changeEvent.target.files;
                });
            });
        }
    }
}]);

4
Uso uploadme come src in un tag img, quindi vedo che viene impostato dalla direttiva. Tuttavia, se provo a catturarlo dal controller usando $ scope.uploadme, è "indefinito". Tuttavia, posso impostare uploadme dal controller. Ad esempio, $ scope.uploadme = "*" fa scomparire l'immagine.
Per Quested Aronsson,

5
Il problema è che la direttiva crea un childScope e imposta uploadme in tale ambito. L'ambito (genitore) originale ha anche un uploadme, che non è interessato da childScope. Posso aggiornare uploadme in HTML da entrambi gli ambiti. Esiste un modo per evitare di creare un childScope?
Per Quested Aronsson,

4
@AlexC bene la domanda era che ng-model non funzionava, non sul caricamento di file :) A quel tempo non avevo bisogno di caricare il file. Ma recentemente ho imparato a caricare file da questo tutorial di egghead.io .
Endy Tjahjono,

10
non dimenticare di $ scope. $ on ('$ destory', function () {element.unbind ("change");}
Nawlbergs,

2
Ho una domanda ... Non è così complicato rispetto al semplice javascript e html? Seriamente, devi davvero capire AngularJS per raggiungere questa soluzione ... e sembra che potrei fare lo stesso con un evento javascript. Perché farlo in modo angolare e non in modo semplice JS?
AFP_555

53

Uso questa direttiva:

angular.module('appFilereader', []).directive('appFilereader', function($q) {
    var slice = Array.prototype.slice;

    return {
        restrict: 'A',
        require: '?ngModel',
        link: function(scope, element, attrs, ngModel) {
                if (!ngModel) return;

                ngModel.$render = function() {};

                element.bind('change', function(e) {
                    var element = e.target;

                    $q.all(slice.call(element.files, 0).map(readFile))
                        .then(function(values) {
                            if (element.multiple) ngModel.$setViewValue(values);
                            else ngModel.$setViewValue(values.length ? values[0] : null);
                        });

                    function readFile(file) {
                        var deferred = $q.defer();

                        var reader = new FileReader();
                        reader.onload = function(e) {
                            deferred.resolve(e.target.result);
                        };
                        reader.onerror = function(e) {
                            deferred.reject(e);
                        };
                        reader.readAsDataURL(file);

                        return deferred.promise;
                    }

                }); //change

            } //link
    }; //return
});

e invocalo in questo modo:

<input type="file" ng-model="editItem._attachments_uri.image" accept="image/*" app-filereader />

La proprietà (editItem.editItem._attachments_uri.image) verrà popolata con il contenuto del file selezionato come data-uri (!).

Si noti che questo script non caricherà nulla. Popolerà il tuo modello solo con il contenuto del tuo file codificato e un uri di dati (base64).

Guarda una demo funzionante qui: http://plnkr.co/CMiHKv2BEidM9SShm9Vv


4
Sembri promettente, puoi spiegare la logica alla base del codice e commentare la compatibilità del browser (IE e browser non fileAPI principalmente)?
Oleg Belousov,

Inoltre, per quanto ne so, se imposterò l'intestazione del tipo di contenuto della richiesta AJAX su indefinita e proverò a inviare un tale campo al server, angular lo caricherà, supponendo che il browser supporti fileAPI, am Io correggo?
Oleg Belousov,

@OlegTikhonov non hai ragione! Questo script non invierà nulla. Leggerà il file selezionato come stringa Base64 e aggiornerà il modello con quella stringa.
Elmer,

@Elmer Sì, ho capito, intendo che inviando un modulo che contiene un campo file (un percorso relativo al file nella macchina dell'utente in un oggetto FileAPI), è possibile effettuare il caricamento angolare del file mediante una richiesta XHR impostando l'intestazione del tipo di contenuto su indefinito
Oleg Belousov

1
Qual è lo scopo della overwritting ngModel's $renderfunzione?
sp00m,

39

Come abilitare <input type="file">a lavorare conng-model

Demo funzionante della direttiva che funziona con ng-model

La ng-modeldirettiva principale non funziona con <input type="file">out of the box.

Questa direttiva personalizzato consente ng-modele ha il vantaggio di consentire i ng-change, ng-requirede ng-formle direttive per il lavoro con <input type="file">.

angular.module("app",[]);

angular.module("app").directive("selectNgFiles", function() {
  return {
    require: "ngModel",
    link: function postLink(scope,elem,attrs,ngModel) {
      elem.on("change", function(e) {
        var files = elem[0].files;
        ngModel.$setViewValue(files);
      })
    }
  }
});
<script src="//unpkg.com/angular/angular.js"></script>
  <body ng-app="app">
    <h1>AngularJS Input `type=file` Demo</h1>
    
    <input type="file" select-ng-files ng-model="fileArray" multiple>

    <code><table ng-show="fileArray.length">
    <tr><td>Name</td><td>Date</td><td>Size</td><td>Type</td><tr>
    <tr ng-repeat="file in fileArray">
      <td>{{file.name}}</td>
      <td>{{file.lastModified | date  : 'MMMdd,yyyy'}}</td>
      <td>{{file.size}}</td>
      <td>{{file.type}}</td>
    </tr>
    </table></code>
    
  </body>


Puoi usare condition per verificare se non ci sono file selezionati, il modello ng non è definito **** if (files.length> 0) {ngModel. $ SetViewValue (files); } else {ngModel. $ setViewValue (non definito); }
Farshad Kazemi,

Come ottenere i dati del file? E quali altri attributi possiamo usare come {{file.name}}
Adarsh ​​Singh,


26

Questo è un addendum alla soluzione di @ endy-tjahjono.

Ho finito per non essere in grado di ottenere il valore di uploadme dall'ambito. Anche se uploadme in HTML è stato visibilmente aggiornato dalla direttiva, non ho ancora potuto accedere al suo valore di $ scope.uploadme. Sono stato in grado di impostare il suo valore dall'ambito, però. Misterioso, vero ..?

Come si è scoperto, la direttiva ha creato un ambito figlio e l'ambito figlio ha il suo uploadme .

La soluzione era usare un oggetto piuttosto che una primitiva per contenere il valore di uploadme .

Nel controller ho:

$scope.uploadme = {};
$scope.uploadme.src = "";

e in HTML:

 <input type="file" fileread="uploadme.src"/>
 <input type="text" ng-model="uploadme.src"/>

Non ci sono modifiche alla direttiva.

Ora funziona tutto come previsto. Posso ottenere il valore di uploadme.src dal mio controller usando $ scope.uploadme.


1
Sì, è esattamente così.
Per Quested Aronsson,

1
Confermo la stessa esperienza; debug e spiegazione molto belli. Non sono sicuro del motivo per cui la direttiva sta creando il proprio ambito di applicazione.
Adam Casey,

In alternativa, una dichiarazione in linea:$scope.uploadme = { src: '' }
maxathousand

9

Salve ragazzi creo una direttiva e mi registro sul pergolato.

questa lib ti aiuterà a modellare il file di input, non solo restituire i dati del file ma anche file dataurl o base 64.

{
    "lastModified": 1438583972000,
    "lastModifiedDate": "2015-08-03T06:39:32.000Z",
    "name": "gitignore_global.txt",
    "size": 236,
    "type": "text/plain",
    "data": "data:text/plain;base64,DQojaWdub3JlIHRodW1ibmFpbHMgY3JlYXRlZCBieSB3aW5kb3dz…xoDQoqLmJhaw0KKi5jYWNoZQ0KKi5pbGsNCioubG9nDQoqLmRsbA0KKi5saWINCiouc2JyDQo="
}

https://github.com/mistralworks/ng-file-model/

La speranza ti aiuterà


come usarlo usando $ scope? Ho provato a usarlo ma non sono stato definito durante il debug.
Gujarat Santana,

1
Bel lavoro yozawiratama! Funziona bene E @GujaratSantana, se <input type = "file" ng-file-model = "myDocument" /> usa semplicemente $ scope.myDocument.name o in generale $ scope.myDocument. <Qualsiasi proprietà> dove la proprietà è una delle proprietà ["lastModified", "lastModifiedDate", "name", "size", "type", "data"]
Jay Dharmendra Solanki

non può essere installato via bower
Pimgd

come utente per il caricamento di più file?
aldrien.h,

Il reader.readAsDataURLmetodo è obsoleto Il codice moderno utilizza URL.create ObjectURL () .
georgeawg,

4

Questa è una versione leggermente modificata che ti consente di specificare il nome dell'attributo nell'ambito, proprio come faresti con ng-model, utilizzo:

    <myUpload key="file"></myUpload>

Direttiva:

.directive('myUpload', function() {
    return {
        link: function postLink(scope, element, attrs) {
            element.find("input").bind("change", function(changeEvent) {                        
                var reader = new FileReader();
                reader.onload = function(loadEvent) {
                    scope.$apply(function() {
                        scope[attrs.key] = loadEvent.target.result;                                
                    });
                }
                if (typeof(changeEvent.target.files[0]) === 'object') {
                    reader.readAsDataURL(changeEvent.target.files[0]);
                };
            });

        },
        controller: 'FileUploadCtrl',
        template:
                '<span class="btn btn-success fileinput-button">' +
                '<i class="glyphicon glyphicon-plus"></i>' +
                '<span>Replace Image</span>' +
                '<input type="file" accept="image/*" name="files[]" multiple="">' +
                '</span>',
        restrict: 'E'

    };
});

3

Per input di più file usando lodash o trattino basso:

.directive("fileread", [function () {
    return {
        scope: {
            fileread: "="
        },
        link: function (scope, element, attributes) {
            element.bind("change", function (changeEvent) {
                return _.map(changeEvent.target.files, function(file){
                  scope.fileread = [];
                  var reader = new FileReader();
                  reader.onload = function (loadEvent) {
                      scope.$apply(function () {
                          scope.fileread.push(loadEvent.target.result);
                      });
                  }
                  reader.readAsDataURL(file);
                });
            });
        }
    }
}]);

3

function filesModelDirective(){
  return {
    controller: function($parse, $element, $attrs, $scope){
      var exp = $parse($attrs.filesModel);
      $element.on('change', function(){
        exp.assign($scope, this.files[0]);
        $scope.$apply();
      });
    }
  };
}
app.directive('filesModel', filesModelDirective);

Complimenti per la restituzione dell'oggetto file . Le altre direttive che lo convertono in un DataURL rendono difficile per i controller che desiderano caricare il file.
georgeawg,

2

Ho dovuto fare lo stesso con più input, quindi ho aggiornato il metodo @Endy Tjahjono. Restituisce un array contenente tutti i file letti.

  .directive("fileread", function () {
    return {
      scope: {
        fileread: "="
      },
      link: function (scope, element, attributes) {
        element.bind("change", function (changeEvent) {
          var readers = [] ,
              files = changeEvent.target.files ,
              datas = [] ;
          for ( var i = 0 ; i < files.length ; i++ ) {
            readers[ i ] = new FileReader();
            readers[ i ].onload = function (loadEvent) {
              datas.push( loadEvent.target.result );
              if ( datas.length === files.length ){
                scope.$apply(function () {
                  scope.fileread = datas;
                });
              }
            }
            readers[ i ].readAsDataURL( files[i] );
          }
        });

      }
    }
  });

1

Ho dovuto modificare la direttiva di Endy in modo da poter ottenere Last Modified, lastModifiedDate, nome, dimensione, tipo e dati oltre a poter ottenere un array di file. Per quelli di voi che avevano bisogno di queste funzionalità extra, ecco qui.

AGGIORNAMENTO: ho trovato un bug in cui se si selezionano i file e poi si seleziona nuovamente ma si annulla, i file non vengono mai deselezionati come appare. Quindi ho aggiornato il mio codice per risolverlo.

 .directive("fileread", function () {
        return {
            scope: {
                fileread: "="
            },
            link: function (scope, element, attributes) {
                element.bind("change", function (changeEvent) {
                    var readers = [] ,
                        files = changeEvent.target.files ,
                        datas = [] ;
                    if(!files.length){
                        scope.$apply(function () {
                            scope.fileread = [];
                        });
                        return;
                    }
                    for ( var i = 0 ; i < files.length ; i++ ) {
                        readers[ i ] = new FileReader();
                        readers[ i ].index = i;
                        readers[ i ].onload = function (loadEvent) {
                            var index = loadEvent.target.index;
                            datas.push({
                                lastModified: files[index].lastModified,
                                lastModifiedDate: files[index].lastModifiedDate,
                                name: files[index].name,
                                size: files[index].size,
                                type: files[index].type,
                                data: loadEvent.target.result
                            });
                            if ( datas.length === files.length ){
                                scope.$apply(function () {
                                    scope.fileread = datas;
                                });
                            }
                        };
                        readers[ i ].readAsDataURL( files[i] );
                    }
                });

            }
        }
    });

1

Se vuoi qualcosa di un po 'più elegante / integrato, puoi usare un decoratore per estendere la inputdirettiva con il supporto di type=file. L'avvertenza principale da tenere a mente è che questo metodo non funzionerà in IE9 poiché IE9 non ha implementato l'API File . L'uso di JavaScript per caricare dati binari indipendentemente dal tipo tramite XHR non è semplicemente possibile in modo nativo in IE9 o precedenti (l'uso di ActiveXObjectper accedere al filesystem locale non conta poiché l'utilizzo di ActiveX richiede solo problemi di sicurezza).

Questo metodo esatto richiede anche AngularJS 1.4.xo versioni successive, ma potresti essere in grado di adattarlo per usarlo $provide.decoratorpiuttosto che angular.Module.decorator- ho scritto questo riassunto per dimostrare come farlo mentre è conforme alla guida di stile AngularJS di John Papa :

(function() {
    'use strict';

    /**
    * @ngdoc input
    * @name input[file]
    *
    * @description
    * Adds very basic support for ngModel to `input[type=file]` fields.
    *
    * Requires AngularJS 1.4.x or later. Does not support Internet Explorer 9 - the browser's
    * implementation of `HTMLInputElement` must have a `files` property for file inputs.
    *
    * @param {string} ngModel
    *  Assignable AngularJS expression to data-bind to. The data-bound object will be an instance
    *  of {@link https://developer.mozilla.org/en-US/docs/Web/API/FileList `FileList`}.
    * @param {string=} name Property name of the form under which the control is published.
    * @param {string=} ngChange
    *  AngularJS expression to be executed when input changes due to user interaction with the
    *  input element.
    */
    angular
        .module('yourModuleNameHere')
        .decorator('inputDirective', myInputFileDecorator);

    myInputFileDecorator.$inject = ['$delegate', '$browser', '$sniffer', '$filter', '$parse'];
    function myInputFileDecorator($delegate, $browser, $sniffer, $filter, $parse) {
        var inputDirective = $delegate[0],
            preLink = inputDirective.link.pre;

        inputDirective.link.pre = function (scope, element, attr, ctrl) {
            if (ctrl[0]) {
                if (angular.lowercase(attr.type) === 'file') {
                    fileInputType(
                        scope, element, attr, ctrl[0], $sniffer, $browser, $filter, $parse);
                } else {
                    preLink.apply(this, arguments);
                }
            }
        };

        return $delegate;
    }

    function fileInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
        element.on('change', function (ev) {
            if (angular.isDefined(element[0].files)) {
                ctrl.$setViewValue(element[0].files, ev && ev.type);
            }
        })

        ctrl.$isEmpty = function (value) {
            return !value || value.length === 0;
        };
    }
})();

Perché non è stato fatto in primo luogo? Il supporto di AngularJS è destinato a raggiungere solo IE9. Se non sei d'accordo con questa decisione e pensi che avrebbero dovuto inserirla comunque, salta il carro su Angular 2+ perché un migliore supporto moderno è letteralmente il motivo per cui Angular 2 esiste.

Il problema è (come accennato in precedenza) che senza il supporto del file API fare questo in modo corretto è impossibile per il core dato che la nostra linea di base è IE9 e polifilare questa roba è fuori discussione per core.

Inoltre, provare a gestire questo input in un modo che non è compatibile con più browser rende solo più difficile per le soluzioni di terze parti, che ora devono combattere / disabilitare / aggirare la soluzione principale.

...

Lo chiuderò proprio mentre chiudiamo il n. 1236. Angular 2 è stato creato per supportare i browser moderni e con quel supporto file sarà facilmente disponibile.


-2

Prova questo, questo funziona per me in JS angolare

    let fileToUpload = `${documentLocation}/${documentType}.pdf`;
    let absoluteFilePath = path.resolve(__dirname, fileToUpload);
    console.log(`Uploading document ${absoluteFilePath}`);
    element.all(by.css("input[type='file']")).sendKeys(absoluteFilePath);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.