ng-model para `<input type =" file "/>` (com a diretiva DEMO)

281

Eu tentei usar o ng-model na tag de entrada com o tipo file:

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

Mas depois de selecionar um arquivo, no controlador, $ scope.vm.uploadme ainda está indefinido.

Como obtenho o arquivo selecionado no meu controlador?

Endy Tjahjono
fonte
3
Veja stackoverflow.com/a/17923521/135114 , especialmente o exemplo citado online em jsfiddle.net/danielzen/utp7j
Daryn
Acredito que você sempre precise especificar a propriedade name no elemento html ao usar o ngModel.
Sam

Respostas:

327

Eu criei uma solução alternativa com diretiva:

.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 a tag de entrada se torna:

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

Ou, se apenas a definição do arquivo for necessária:

.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
fonte
4
Eu uso uploadme como src em uma tag img, para que eu possa ver que está sendo definido pela diretiva. No entanto, se eu tentar pegá-lo do controlador usando $ scope.uploadme, ele será "indefinido". Eu posso definir o uploadme do controlador, no entanto. Por exemplo, $ scope.uploadme = "*" faz a imagem desaparecer.
Por Aronsson consultado
5
O problema é que a diretiva cria um childScope e define uploadme nesse escopo. O escopo original (pai) também possui um uploadme, que não é afetado pelo childScope. Eu posso atualizar o uploadme no HTML de qualquer um dos escopos. Existe uma maneira de evitar a criação de um childScope?
Por Quested Aronsson
4
@ AlexC bem, a pergunta era sobre ng-model não está funcionando, não sobre o upload de arquivos :) Naquela época, eu não precisava fazer o upload do arquivo. Mas recentemente eu aprendi como fazer upload de arquivos deste tutorial egghead.io .
Endy Tjahjono
10
não se esqueça de $ âmbito $ on ( '$ destory', function () {element.unbind ( "mudança");}.
Nawlbergs
2
Eu tenho uma pergunta .... Não é muito complicado se comparado a javascript e html simples? Sério, você realmente precisa entender o AngularJS para alcançar essa solução ... e parece que eu poderia fazer o mesmo com um evento javascript. Por que fazer da maneira Angular e não da JS simples?
AFP_555 21/03/19
53

Eu uso esta diretiva:

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 invoque-o assim:

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

A propriedade (editItem.editItem._attachments_uri.image) será preenchida com o conteúdo do arquivo que você selecionar como uri de dados (!).

Observe que este script não carregará nada. Ele somente preencherá seu modelo com o conteúdo do seu arquivo codificado e um data-uri (base64).

Confira uma demonstração de trabalho aqui: http://plnkr.co/CMiHKv2BEidM9SShm9Vv

Elmer
fonte
4
Parece promissor, você pode explicar a lógica por trás do código e comentar sobre a compatibilidade do navegador (principalmente o IE e o navegador não fileAPI)?
Oleg Belousov
Além disso, da melhor forma possível, se eu definir o cabeçalho do tipo de conteúdo da solicitação AJAX como indefinido e tentar enviar esse campo para o servidor, o angular fará o upload, assumindo que o navegador suporta fileAPI, Eu corrijo?
11553 Oleg Belousov
@OlegTikhonov você não está correto! Este script não enviará nada. Ele lerá o arquivo que você selecionou como uma string Base64 e atualizará seu modelo com essa string.
Elmer #
@Elmer Sim, eu entendo, o que quero dizer é que, enviando um formulário que contém um campo de arquivo (um caminho relativo para o arquivo na máquina do usuário em um objeto FileAPI), você pode fazer o upload angular do arquivo por uma solicitação XHR definindo o cabeçalho tipo de conteúdo para indefinido
Oleg Belousov
1
Qual é o propósito da substituição ngModelda $renderfunção?
Sp00m
39

Como habilitar <input type="file">para trabalhar comng-model

Demonstração de trabalho da diretiva que funciona com ng-model

A ng-modeldiretiva principal não funciona <input type="file">imediatamente.

Esta directiva personalizado permite ng-modele tem a vantagem adicional de permitir que os ng-change, ng-requirede ng-formdirectivas para trabalhar com <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
fonte
Você pode usar a condição para verificar se não há arquivos selecionados, o modelo ng não pode ser definido **** if (files.length> 0) {ngModel. $ SetViewValue (files); } else {ngModel. $ setViewValue (indefinido); }
Farshad Kazemi
Como obter dados do arquivo? E quais são os outros atributos que podemos usar como {{file.name}}
Adarsh Singh
26

Este é um adendo à solução do @ endy-tjahjono.

Acabei não conseguindo obter o valor de uploadme do escopo. Mesmo que uploadme no HTML tenha sido visivelmente atualizado pela diretiva, eu ainda não consegui acessar seu valor com $ scope.uploadme. Eu era capaz de definir seu valor do escopo, no entanto. Misterioso, certo ..?

Como se viu, um escopo filho foi criado pela diretiva e o escopo filho teve seu próprio uploadme .

A solução foi usar um objeto em vez de um primitivo para armazenar o valor de uploadme .

No controlador eu tenho:

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

e no HTML:

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

Não há alterações na diretiva.

Agora, tudo funciona como o esperado. Posso pegar o valor de uploadme.src do meu controlador usando $ scope.uploadme.

Por Aronsson requerido
fonte
1
Sim, é exatamente isso.
Por Quested Aronsson
1
Eu confirmo a mesma experiência; depuração e explicação muito agradáveis. Não sei por que a diretiva está criando seu próprio escopo.
Adam Casey
Como alternativa, uma declaração embutida:$scope.uploadme = { src: '' }
maxathousand 5/17/17
9

Oi pessoal, eu crio uma diretiva e registrei no caramanchão.

essa lib o ajudará a modelar o arquivo de entrada, não apenas retornará dados do arquivo, mas também o dataurl do arquivo ou a 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/

A esperança o ajudará

yozawiratama
fonte
como usá-lo usando $ scope? Eu tentei usar isso, mas fiquei indefinido ao depurar.
Gujarat Santana
1
Bom trabalho yozawiratama! Isso funciona bem. E @GujaratSantana, se <input type = "file" ng-file-model = "myDocument" />, use apenas $ scope.myDocument.name ou, em geral, $ scope.myDocument.name ou, em geral, $ scope.myDocument. <Any property> onde a propriedade é uma das propriedades [ "lastModified", "LastModifiedDate", "nome", "tamanho", "tipo", "dados"]
Jay Dharmendra Solanki
não pode ser instalado via caramanchão
Pimgd 9/09/16
como usuário para upload de vários arquivos?
Aldrien.h
O reader.readAsDataURLmétodo está obsoleto. O código moderno usa URL.create ObjectURL () .
Georgeawg
4

Esta é uma versão ligeiramente modificada que permite especificar o nome do atributo no escopo, assim como você faria com o ng-model, use:

    <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
fonte
3

Para entrada de vários arquivos usando lodash ou sublinhado:

.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
fonte
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
fonte
Parabéns por retornar o objeto de arquivo . As outras diretivas que o convertem em DataURL tornam difícil para os controladores que desejam fazer upload do arquivo.
georgeawg 12/05/19
2

Eu tive que fazer o mesmo em várias entradas, então atualizei o método @Endy Tjahjono. Retorna uma matriz contendo todos os arquivos lidos.

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

Eu tive que modificar a diretiva de Endy para obter Last Modified, lastModifiedDate, nome, tamanho, tipo e dados, além de poder obter uma matriz de arquivos. Para aqueles de vocês que precisavam desses recursos extras, aqui está.

ATUALIZAÇÃO: Encontrei um bug no qual, se você selecionar o (s) arquivo (s) e depois selecionar novamente, mas cancelar, os arquivos nunca serão desmarcados como parece. Então atualizei meu código para corrigir isso.

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

Se você quiser algo um pouco mais elegante / integrado, use um decorador para estender a inputdiretiva com suporte type=file. A principal ressalva a ser lembrada é que esse método não funcionará no IE9, pois o IE9 não implementou a API de arquivos . O uso de JavaScript para fazer upload de dados binários, independentemente do tipo via XHR, simplesmente não é possível nativamente no IE9 ou anterior (uso deActiveXObject para acessar o sistema de arquivos local não conta, pois o uso do ActiveX está apenas solicitando problemas de segurança).

Esse método exato também requer o AngularJS 1.4.x ou posterior, mas você pode adaptá-lo para usá-lo em $provide.decoratorvez de angular.Module.decorator- escrevi essa essência para demonstrar como fazê-lo, em conformidade com o guia de estilo do John Papa no AngularJS :

(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 que isso não foi feito em primeiro lugar? O suporte ao AngularJS deve atingir apenas o IE9. Se você não concorda com essa decisão e acha que eles deveriam simplesmente colocar isso de qualquer maneira, vá para o Angular 2+, pois melhor suporte moderno é literalmente o motivo pelo qual o Angular 2 existe.

O problema é (como mencionado anteriormente) que, sem o suporte da API do arquivo, fazer isso corretamente é inviável para o núcleo, dado que nossa linha de base é o IE9 e o preenchimento de polifill está fora de questão.

Além disso, tentar lidar com essa entrada de uma maneira que não seja compatível com vários navegadores apenas dificulta as soluções de terceiros, que agora precisam combater / desativar / solucionar a solução principal.

...

Eu vou fechar isso assim que fechamos o # 1236. O Angular 2 está sendo construído para oferecer suporte a navegadores modernos e com esse suporte a arquivos estará facilmente disponível.

p0lar_bear
fonte
-2

Tente isso, isso está funcionando para mim em 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
fonte