ng-model para `<input type =" file "/>` (con la directiva DEMO)

281

Traté de usar ng-model en la etiqueta de entrada con el archivo de tipo:

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

Pero después de seleccionar un archivo, en el controlador, $ scope.vm.uploadme aún no está definido.

¿Cómo obtengo el archivo seleccionado en mi controlador?

Endy Tjahjono
fuente
3
Ver stackoverflow.com/a/17923521/135114 , especialmente el ejemplo citado en línea en jsfiddle.net/danielzen/utp7j
Daryn
Creo que siempre debe especificar la propiedad de nombre en el elemento html cuando usa ngModel.
Sam

Respuestas:

327

Creé una solución alternativa con la directiva:

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

Y la etiqueta de entrada se convierte en:

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

O si solo se necesita la definición del archivo:

.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
fuente
44
Uso uploadme como src en una etiqueta img, por lo que puedo ver que la directiva lo establece. Sin embargo, si intento obtenerlo del controlador usando $ scope.uploadme, está "indefinido". Sin embargo, puedo configurar uploadme desde el controlador. Por ejemplo, $ scope.uploadme = "*" hace que la imagen desaparezca.
Por Quested Aronsson
55
El problema es que la directiva crea un childScope y establece uploadme en ese ámbito. El alcance original (principal) también tiene un uploadme, que no se ve afectado por childScope. Puedo actualizar uploadme en el HTML desde cualquier ámbito. ¿Hay alguna manera de evitar crear un childScope?
Por Quested Aronsson
44
@AlexC, bueno, la pregunta era sobre ng-model no funciona, no sobre cargar archivos :) En ese momento no necesitaba cargar el archivo. Pero recientemente aprendí cómo subir archivos desde este tutorial de egghead.io .
Endy Tjahjono
10
no olvides $ scope. $ on ('$ destory', function () {element.unbind ("change");}
Nawlbergs
2
Tengo una pregunta ... ¿No es demasiado complicado en comparación con JavaScript y HTML? En serio, realmente necesitas entender AngularJS para llegar a esta solución ... y parece que podría hacer lo mismo con un evento javascript. ¿Por qué hacerlo a la manera angular y no a la manera simple de JS?
AFP_555
53

Yo uso esta directiva:

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 invocarlo así:

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

La propiedad (editItem.editItem._attachments_uri.image) se completará con el contenido del archivo que seleccione como data-uri (!).

Tenga en cuenta que este script no cargará nada. Solo llenará su modelo con el contenido de su archivo codificado y un data-uri (base64).

Vea una demostración funcional aquí: http://plnkr.co/CMiHKv2BEidM9SShm9Vv

Elmer
fuente
44
Parece prometedor, ¿puede explicar la lógica detrás del código y comentar sobre la compatibilidad del navegador (IE y el navegador que no es FileAPI en su mayoría)?
Oleg Belousov
Además, según tengo entendido, si configuro el encabezado de tipo de contenido de la solicitud AJAX como indefinido e intento enviar dicho campo al servidor, angular lo cargará, suponiendo que el navegador sea compatible con fileAPI, estoy ¿Corrijo?
Oleg Belousov
@OlegTikhonov no estás en lo correcto! Este script no enviará nada. Leerá el archivo que seleccionó como una cadena Base64 y actualizará su modelo con esa cadena.
Elmer
@Elmer Sí, entiendo, lo que quiero decir es que al enviar un formulario que contiene un campo de archivo (una ruta relativa al archivo en la máquina del usuario en un objeto FileAPI), puede hacer que el archivo angular se cargue mediante una solicitud XHR configurando el encabezado de tipo de contenido como indefinido
Oleg Belousov
1
¿Cuál es el propósito de sobrescribir ngModella $renderfunción?
sp00m
39

Cómo habilitar <input type="file">para trabajar conng-model

Demostración de trabajo de la directiva que funciona con ng-model

La ng-modeldirectiva principal no funciona <input type="file">fuera de la caja.

Esta directiva permite a medida ng-modely tiene la ventaja adicional de permitir las ng-change, ng-requiredy ng-formlas directivas de trabajo 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>

georgeawg
fuente
Puede usar la condición para verificar si no hay archivos seleccionados, ng-model be undefined **** if (files.length> 0) {ngModel. $ SetViewValue (files); } else {ngModel. $ setViewValue (indefinido); }
Farshad Kazemi
¿Cómo obtener datos del archivo? ¿Y cuáles son los otros atributos que podemos usar como {{file.name}}
Adarsh ​​Singh
26

Esta es una adición a la solución de @ endy-tjahjono.

Terminé no pudiendo obtener el valor de uploadme desde el alcance. Aunque uploadme en el HTML fue visiblemente actualizado por la directiva, todavía no podía acceder a su valor por $ scope.uploadme. Sin embargo, pude establecer su valor desde el alcance. Misterioso, ¿verdad ..?

Al final resultó que, la directiva creó un ámbito secundario, y el ámbito secundario tenía su propio uploadme .

La solución fue utilizar un objeto en lugar de un primitivo para mantener el valor de uploadme .

En el controlador tengo:

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

y en el HTML:

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

No hay cambios en la directiva.

Ahora, todo funciona como se esperaba. Puedo obtener el valor de uploadme.src de mi controlador usando $ scope.uploadme.

Por Quested Aronsson
fuente
1
Sí, eso es exactamente.
Por Quested Aronsson
1
Confirmo la misma experiencia; Muy buena depuración y explicación. No estoy seguro de por qué la directiva está creando su propio alcance.
Adam Casey
Alternativamente, una declaración en línea:$scope.uploadme = { src: '' }
maxathousy
9

Hola chicos, creo una directiva y me registré en Bower.

Esta biblioteca lo ayudará a modelar el archivo de entrada, no solo devolver los datos del archivo, sino también el archivo 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 esperanza te ayudará

yozawiratama
fuente
¿Cómo usarlo usando $ scope? Intenté usar esto, pero no me definí al depurar.
Gujarat Santana
1
Buen trabajo yozawiratama! Funciona bien Y @GujaratSantana, si <input type = "file" ng-file-model = "myDocument" /> simplemente use $ scope.myDocument.name o, en general, $ scope.myDocument. <Any property> donde la propiedad sea una de las ["lastModified", "lastModifiedDate", "name", "size", "type", "data"]
Jay Dharmendra Solanki
no se puede instalar a través de
bower
¿Cómo usuario para la carga de múltiples archivos?
aldrien.h
El reader.readAsDataURLmétodo es obsoleto. El código moderno usa URL.create ObjectURL () .
georgeawg
4

Esta es una versión ligeramente modificada que le permite especificar el nombre del atributo en el ámbito, tal como lo haría con ng-model, uso:

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

Directiva:

.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
fuente
3

Para la entrada de múltiples archivos usando lodash o guión bajo:

.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
fuente
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
fuente
Felicitaciones por devolver el objeto de archivo . Las otras directivas que lo convierten en un DataURL dificultan a los controladores que desean cargar el archivo.
georgeawg
2

Tenía que hacer lo mismo en múltiples entradas, así que actualicé el método @Endy Tjahjono. Devuelve una matriz que contiene todos los archivos leídos.

  .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
fuente
1

Tuve que modificar la directiva de Endy para poder obtener Last Modified, lastModifiedDate, nombre, tamaño, tipo y datos, así como poder obtener una matriz de archivos. Para aquellos de ustedes que necesitaban estas características adicionales, aquí están.

ACTUALIZACIÓN: Encontré un error en el que si selecciona los archivos y luego vuelve a seleccionar, pero cancela, los archivos nunca se deseleccionan como parece. Así que actualicé mi código para arreglar eso.

 .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
fuente
1

Si desea algo un poco más elegante / integrado, puede usar un decorador para ampliar la inputdirectiva con soporte type=file. La advertencia principal a tener en cuenta es que este método no funcionará en IE9 ya que IE9 no implementó la API de archivos . El uso de JavaScript para cargar datos binarios independientemente del tipo a través de XHR simplemente no es posible de forma nativa en IE9 o versiones anteriores (el uso ActiveXObjectpara acceder al sistema de archivos local no cuenta, ya que el uso de ActiveX solo requiere problemas de seguridad).

Este método exacto también requiere AngularJS 1.4.xo posterior, pero es posible que pueda adaptar esto para usarlo en $provide.decoratorlugar de angular.Module.decorator: Escribí esta esencia para demostrar cómo hacerlo mientras se ajusta a la guía de estilo 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;
        };
    }
})();

¿Por qué no se hizo esto en primer lugar? El soporte de AngularJS está destinado a llegar tan atrás como IE9. Si no está de acuerdo con esta decisión y cree que deberían haberlo puesto de todos modos, salte al carro a Angular 2+ porque un mejor soporte moderno es, literalmente, por qué existe Angular 2.

El problema es (como se mencionó anteriormente) que sin el soporte de la API de archivos, hacer esto correctamente no es factible para el núcleo dado que nuestra línea de base es IE9 y el relleno múltiple de este material está fuera de cuestión para el núcleo.

Además, tratar de manejar esta entrada de una manera que no sea compatible con navegadores cruzados solo hace que sea más difícil para las soluciones de terceros, que ahora tienen que luchar / deshabilitar / solucionar la solución central.

...

Voy a cerrar esto justo cuando cerramos # 1236. Angular 2 se está creando para admitir navegadores modernos y con ese soporte de archivos estará fácilmente disponible.

p0lar_bear
fuente
-2

Prueba esto, esto me está funcionando en JS angular

    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
fuente