ng-model pour `<input type =" file "/>` (avec directive DEMO)

281

J'ai essayé d'utiliser ng-model sur une balise d'entrée avec un fichier de type:

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

Mais après avoir sélectionné un fichier, dans le contrôleur, $ scope.vm.uploadme n'est toujours pas défini.

Comment obtenir le fichier sélectionné dans mon contrôleur?

Endy Tjahjono
la source
3
Voir stackoverflow.com/a/17923521/135114 , en particulier l'exemple cité en ligne sur jsfiddle.net/danielzen/utp7j
Daryn
Je crois que vous devez toujours spécifier la propriété name sur l'élément html lorsque vous utilisez ngModel.
Sam

Réponses:

327

J'ai créé une solution de contournement avec directive:

.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]);
            });
        }
    }
}]);

Et la balise d'entrée devient:

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

Ou si seulement la définition du fichier est nécessaire:

.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;
                });
            });
        }
    }
}]);
Endy Tjahjono
la source
4
J'utilise uploadme comme src dans une balise img, donc je peux voir que cela est défini par la directive. Cependant, si j'essaye de l'attraper du contrôleur en utilisant $ scope.uploadme, il est "non défini". Je peux cependant définir uploadme depuis le contrôleur. Par exemple, $ scope.uploadme = "*" fait disparaître l'image.
Per Quested Aronsson
5
Le problème est que la directive crée un childScope et définit uploadme dans cette étendue. La portée d'origine (parent) a également un uploadme, qui n'est pas affecté par le childScope. Je peux mettre à jour uploadme dans le HTML à partir de n'importe quelle portée. Existe-t-il un moyen d'éviter de créer un ChildScope?
Per Quested Aronsson
4
@AlexC et la question était sur le fait que le modèle ng ne fonctionnait pas, pas sur le téléchargement de fichiers :) A cette époque, je n'avais pas besoin de télécharger le fichier. Mais récemment, j'ai appris à télécharger un fichier à partir de ce tutoriel egghead.io .
Endy Tjahjono
10
n'oubliez pas de $ scope. $ on ('$ destory', function () {element.unbind ("change");}
Nawlbergs
2
J'ai une question .... N'est-ce pas trop compliqué par rapport au javascript et au html? Sérieusement, vous devez vraiment comprendre AngularJS pour arriver à cette solution ... et il semble que je pourrais faire de même avec un événement javascript. Pourquoi le faire de manière angulaire et non pas de manière simple JS?
AFP_555
53

J'utilise cette directive:

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
});

et l'invoquer comme ceci:

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

La propriété (editItem.editItem._attachments_uri.image) sera remplie avec le contenu du fichier que vous sélectionnez comme uri de données (!).

Veuillez noter que ce script ne téléchargera rien. Il ne remplira votre modèle qu'avec le contenu de votre fichier encodé et un uri de données (base64).

Découvrez une démonstration de travail ici: http://plnkr.co/CMiHKv2BEidM9SShm9Vv

Elmer
la source
4
Regardez prometteur, pouvez-vous expliquer la logique derrière le code et commenter la compatibilité du navigateur (IE et navigateur non-fileAPI principalement)?
Oleg Belousov
De plus, au mieux de ma compréhension, si je vais définir l'en-tête de type de contenu de la demande AJAX sur non défini et que j'essaierai d'envoyer un tel champ au serveur, angular le téléchargera, en supposant que le navigateur prend en charge le fichierAPI, am Je corrige?
Oleg Belousov
@OlegTikhonov vous n'avez pas raison! Ce script n'enverra rien. Il lira le fichier que vous avez sélectionné en tant que chaîne Base64 et mettra à jour votre modèle avec cette chaîne.
Elmer
@Elmer Oui, je comprends, ce que je veux dire, c'est qu'en envoyant un formulaire qui contient un champ de fichier (un chemin relatif vers le fichier dans la machine de l'utilisateur dans un objet FileAPI), vous pouvez faire le téléchargement angulaire du fichier par une demande XHR en définissant l'en-tête du type de contenu sur undefined
Oleg Belousov
1
Quel est le but de l'écrasement de ngModella $renderfonction?
sp00m
39

Comment permettre <input type="file">de travailler avecng-model

Démonstration de travail d'une directive qui fonctionne avec ng-model

La ng-modeldirective de base ne fonctionne pas avec <input type="file">out of the box.

Cette directive personnalisée permet ng-modelet a l'avantage de permettre aux ng-change, ng-requiredet des ng-formdirectives pour travailler avec <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>

georgeawg
la source
Vous pouvez utiliser condition pour vérifier s'il n'y a pas de fichiers sélectionnés, ng-model be undefined **** if (files.length> 0) {ngModel. $ SetViewValue (files); } else {ngModel. $ setViewValue (non défini); }
Farshad Kazemi
Comment obtenir les données du fichier? Et quels sont les autres attributs que nous pouvons utiliser comme {{file.name}}
Adarsh ​​Singh
26

Ceci est un addendum à la solution de @ endy-tjahjono.

J'ai fini par ne pas pouvoir obtenir la valeur de uploadme de la portée. Même si uploadme dans le HTML était visiblement mis à jour par la directive, je ne pouvais toujours pas accéder à sa valeur par $ scope.uploadme. J'ai cependant pu définir sa valeur à partir de la portée. Mystérieux, non ..?

Il s'est avéré qu'une portée enfant a été créée par la directive et que la portée enfant avait son propre uploadme .

La solution consistait à utiliser un objet plutôt qu'une primitive pour conserver la valeur de uploadme .

Dans le contrôleur, j'ai:

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

et dans le HTML:

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

Il n'y a aucun changement à la directive.

Maintenant, tout fonctionne comme prévu. Je peux récupérer la valeur de uploadme.src sur mon contrôleur en utilisant $ scope.uploadme.

Par Aronsson interrogé
la source
1
Oui, c'est exactement ça.
Per Quested Aronsson
1
Je confirme la même expérience; très bon débogage et explication. Je ne sais pas pourquoi la directive crée son propre champ d'application.
Adam Casey
Alternativement, une déclaration en ligne:$scope.uploadme = { src: '' }
maxathous et
9

Salut les gars, je crée une directive et je m'inscris sur bower.

cette bibliothèque vous aidera à modéliser le fichier d'entrée, non seulement à renvoyer les données du fichier mais aussi le fichier dataurl ou la 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/

L'espoir vous aidera

yozawiratama
la source
comment l'utiliser en utilisant $ scope? J'ai essayé d'utiliser ceci mais je n'ai pas été défini lors du débogage.
Gujarat Santana
1
Beau travail yozawiratama! Ça marche bien. Et @GujaratSantana, si <input type = "file" ng-file-model = "myDocument" /> alors utilisez simplement $ scope.myDocument.name ou en général $ scope.myDocument. <Any property> où property étant l'un des ["lastModified", "lastModifiedDate", "nom", "taille", "type", "données"]
Jay Dharmendra Solanki
ne peut pas être installé via bower
Pimgd
comment utiliser pour le téléchargement de fichiers multiples?
aldrien.h
La reader.readAsDataURLméthode est obsolète. Le code moderne utilise URL.create ObjectURL () .
georgeawg
4

Il s'agit d'une version légèrement modifiée qui vous permet de spécifier le nom de l'attribut dans la portée, tout comme vous le feriez avec ng-model, utilisation:

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

Directif:

.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'

    };
});
asiop
la source
3

Pour la saisie de plusieurs fichiers en utilisant lodash ou underscore:

.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);
                });
            });
        }
    }
}]);
Uelb
la source
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);
coder000001
la source
Félicitations pour avoir renvoyé l' objet fichier . Les autres directives qui le convertissent en DataURL compliquent la tâche des contrôleurs qui souhaitent télécharger le fichier.
georgeawg
2

J'ai dû faire de même sur plusieurs entrées, j'ai donc mis à jour la méthode @Endy Tjahjono. Il renvoie un tableau contenant tous les fichiers lus.

  .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] );
          }
        });

      }
    }
  });
Hugo
la source
1

J'ai dû modifier la directive d'Endy afin de pouvoir obtenir Last Modified, lastModifiedDate, nom, taille, type et données ainsi que pouvoir obtenir un tableau de fichiers. Pour ceux d'entre vous qui avaient besoin de ces fonctionnalités supplémentaires, c'est parti.

MISE À JOUR: J'ai trouvé un bogue où si vous sélectionnez le (s) fichier (s) puis allez à nouveau sélectionner mais annulez à la place, les fichiers ne sont jamais désélectionnés comme il apparaît. J'ai donc mis à jour mon code pour y remédier.

 .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] );
                    }
                });

            }
        }
    });
Parley Hammon
la source
1

Si vous voulez quelque chose d'un peu plus élégant / intégré, vous pouvez utiliser un décorateur pour étendre la inputdirective avec le support de type=file. La principale mise en garde à garder à l'esprit est que cette méthode ne fonctionnera pas dans IE9 car IE9 n'a pas implémenté l'API de fichier . L'utilisation de JavaScript pour télécharger des données binaires quel que soit leur type via XHR n'est tout simplement pas possible en natif dans IE9 ou antérieur (l'utilisation de ActiveXObjectpour accéder au système de fichiers local ne compte pas car l'utilisation d'ActiveX demande simplement des problèmes de sécurité).

Cette méthode exacte nécessite également AngularJS 1.4.x ou version ultérieure, mais vous pourrez peut-être l'adapter à l'utilisation $provide.decoratorplutôt que angular.Module.decorator- j'ai écrit cet essentiel pour montrer comment le faire tout en se conformant au guide de style AngularJS de 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;
        };
    }
})();

Pourquoi cela n'a-t-il pas été fait en premier lieu? La prise en charge d'AngularJS est destinée à n'atteindre que IE9. Si vous n'êtes pas d'accord avec cette décision et que vous pensez qu'ils auraient dû le mettre de toute façon, sautez le wagon vers Angular 2+ car un meilleur support moderne est littéralement la raison pour laquelle Angular 2 existe.

Le problème est (comme cela a été mentionné précédemment) que sans le support de l'API de fichier, faire cela correctement est impossible pour le noyau étant donné que notre base de référence est IE9 et le polyfilling de ce genre de choses est hors de question pour le noyau.

De plus, essayer de gérer cette entrée d'une manière qui n'est pas compatible avec tous les navigateurs ne fait que compliquer la tâche des solutions tierces, qui doivent désormais combattre / désactiver / contourner la solution principale.

...

Je vais fermer cela juste au moment où nous fermons # 1236. Angular 2 est en cours de construction pour prendre en charge les navigateurs modernes et avec ce fichier, la prise en charge sera facilement disponible.

p0lar_bear
la source
-2

Essayez ceci, cela fonctionne pour moi dans JS angulaire

    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);
RRR
la source