[AngularJS] preenchimento automático de HTML5

O preenchimento automático é um padrão de UX bastante comum; Há muitos anos que o usamos através de navegadores e aplicativos móveis, mas devido à falta de suporte a navegadores, na maioria das vezes temos que usar bibliotecas Javascript para realizar a tarefa.

Formulário de preenchimento automático usado no Airbnb
Cenário

Nos casos em que as APIs do Google podem fornecer as informações, usar suas bibliotecas é uma boa opção. As bibliotecas realizam chamadas XHR seguras por meio de JSONP e carregam dinamicamente um conjunto de entradas que é então manipulado por CSS customizado para posicionar as entradas em um local que pareça um autocompletar. A maioria das bibliotecas javascript segue a mesma abordagem.

Código gerado pela biblioteca de preenchimento automático do Google
Cenário

Apresentando Datalist

A <datalist>tag permite que os navegadores modernos exibam um autocomplete nativo. Embora o suporte não seja o melhor que existe, existem alguns polyfills que podem fazer com que funcione.

Autocomplete nativo no Chrome
Cenário

Como usar o Datalist?

Para usar <datalist>, você precisa de uma <input>tag que permita especificar um listatributo. O valor desse atributo é o iddo <datalist>elemento. Um datalist contém um conjunto de <option>tags (da mesma forma que a <select>tag) que serão exibidos ao usuário conforme ele digita. O estilo da lista suspensa depende do navegador.

Uso de caso real: recurso de pesquisa

Implementaremos um recurso de pesquisa para destinos de voos em diferentes países usando <datalist>e AngularJS. Isso requer três elementos-chave para que funcione:

  1. A capacidade de carregar um conjunto de dados de entradas para pesquisar
  2. Recarregue as entradas buscadas dependendo da entrada do usuário
  3. Reaja às ações do usuário para realizar o # 1

Buscando o conjunto de entradas

Isso pode ser feito facilmente com um serviço e promessas AngularJS. Aqui está um exemplo de código:

.service('Geolocator', function($q, $http){
var API_URL = 'http://jjperezaguinaga.webscript.io/waymate?term=';
this.searchFlight = function(term) {
var deferred = $q.defer();
$http
.get(API_URL+term).then(function(flights){
var _flights = {};
var flights = flights.data;
for(var i = 0, len = flights.length; i < len; i++) {
_flights
[flights[i].name] = flights[i].country;
}
deferred
.resolve(_flights);
}, function() {
deferred
.reject(arguments);
});
return deferred.promise;
}
})

Neste caso, temos um endpoint de API que receberá um termpara pesquisar. Pode ser um personagem ou uma cidade inteira. Isso retornará uma série de destinos de voo possíveis que serão resolvidos por meio de uma promessa na forma de um mapa.

Com um controlador, podemos recuperar esse serviço e vinculá-lo ao nosso escopo de visualização:

.controller('myController', function($scope, Geolocator) {
$scope
.selectedCountry = null;
$scope
.countries = {};
$scope
.searchFlight = function(term) {
Geolocator.searchFlight(term).then(function(countries){
$scope
.countries = countries;
});
}
})

Em nossa opinião, usaremos a diretiva. Tentei usar para as tags, mas parece que o AngularJS não renderiza corretamente. A melhor maneira era usar a opção de fallback (uma tag dentro do datalist) com o estilo para renderizar as opções corretamente. Observe que é um mapa, portanto, o . Explicarei a diretiva na próxima seção.ng-optionsng-repeat<option><select>display: none;countries(k,v)keyboard-poster

<input type="text" keyboard-poster post-function="searchFlight" name="first_name" placeholder="Zurich, Switzerland" list="_countries" style='margin-bottom: 100px'>
<datalist id="_countries">
<select style="display: none;" id="_select" name="_select" ng-model='selectedCountry' ng-options='k as v for (k,v) in countries'></select>
</datalist>

Recarregue as entradas buscadas dependendo da entrada do usuário

Para recarregar as ações do usuário, usaremos uma diretiva AngularJS. Isso chamará uma função com escopo (de nosso controlador) sob o atributo. Como nossa função é , podemos recarregar a fonte sempre que quisermos. Essa parte da diretiva se parece com isto:post-functionsearchFlightdatalist

var model = $parse(attrs.postFunction);
var poster = model(scope);
poster
(angular.element(element).val());

Onde elementestá a <input>tag que trata a diretiva. Neste caso, o que fazemos é executar a função poster (analisada do atributo na diretiva, no nosso caso do nosso controlador) com o valor (quaisquer que sejam as entradas do usuário).keyboard-postersearchFlight<input>

Reaja às ações do usuário

Aqui está a parte mais problemática de todas, seguida pela pergunta “A que ação reagimos?”. Depois de tentar muitas coisas (clicar, focalizar, pressionar a tecla), recorri ao nosso adorável inputevento. Isso encerrará nossa diretiva e, em seguida, acionará a ação.

.directive('keyboardPoster', function($parse, $timeout){
var DELAY_TIME_BEFORE_POSTING = 0;
return function(scope, elem, attrs) {

var element = angular.element(elem)[0];
var currentTimeout = null;

element
.oninput = function() {
var model = $parse(attrs.postFunction);
var poster = model(scope);

if(currentTimeout) {
$timeout
.cancel(currentTimeout)
}
currentTimeout
= $timeout(function(){
poster
(angular.element(element).val());
}, DELAY_TIME_BEFORE_POSTING)
}
}
})

A diretiva dispara oninpute cria um timeout; isso permite que o usuário escreva um pouco antes de carregar os dados de origem. Isso pode ser feito para evitar uma carga pesada em seu banco de dados; nesse caso, você aumentaria o DELAY_TIME_BEFORE_POSTINGe usaria algum tipo de feedback (talvez um botão giratório dentro da <input>tag) para informar ao usuário que o carregamento está sendo feito.

Conclusão

Cenário

Embora esteja em sua infância, acredito que os aplicativos modernos podem contar <datalist>com uma experiência um tanto nativa para um campo de entrada de preenchimento automático. Algumas desvantagens incluem estilizar a lista de entradas exibida, bem como ter controle total sobre quando exibir a lista. Tive alguns problemas ao exibir a lista, especialmente quando minha fonte (no meu caso, Webscript.io e Waymate ) demorava algum tempo para entregar as novas entradas. Algumas vezes precisava de até dois caracteres para exibir a lista, mas principalmente porque ainda não estava carregado; isso pode confundir o usuário, fazendo-o pensar que não há entradas para sua entrada. Uma boa abordagem para resolver isso é usar o girador de carregamento mencionado anteriormente.

Sinta-se à vontade para navegar por todo o código em Codepen.io