Quando trabalhamos com aplicações AngularJS é comum e até trivial criarmos formulários e seus respectivos bindings para um objeto Javascript do $scope
de um determinado controller. Ao submeter o formulário esse objeto será então enviado para o backend para o processamento da ação do formulário em questão, mas e o que acontece quando nem todos os campos do formulário são obrigatórios?
Funcionamento padrão
Por padrão, o tal do campo não-obrigatório só será incluído no binding quando o usuário interagir de alguma forma com ele. Para ilustrar considere o Controller abaixo.
myApp.controller('ContactController', ['$scope', '$http',
function($scope, $http) {
$scope.contact = {};
$scope.submit = function() {
console.log($scope.contact);
$http.post('/contact').success(function(response) {
console.log('Sent!');
});
};
}
]);
Temos então um atributo chamado contact
que será o objeto com os dados do contato do formulário apresentado a seguir.
<form name="form" ng-submit="submit()">
<label for="name">Name:</label>
<input type="text" name="name" id="name" ng-model="contact.name" ng-required="true" />
<label for="obs">Obs:</label>
<textarea name="obs" id="obs" ng-model="contact.obs"></textarea>
<input type="submit" value="Enviar" />
</form>
O que acontece quando o nome é preenchido e o botão “Enviar” é pressionado, sem nem sequer interagir com o campo “Observação”? Se verificar o console, o nosso contact
estará assim:
Object {name:"Nome"}
Por que que o campo “Observação” não está presente? Porque por padrão o AngularJS exige que os campos tenham alguma interação do usuário para efetuar o binding. Experimente preencher algo no campo “Observação” e confira o que aparecerá no console:
Object {name:"Nome", obs: "Algum conteúdo"}
Agora experimente preencher algo e logo depois apagar o que preencheu e confira o resultado:
Object {name:"Nome", obs: ""}
Os estados do Formulário
O AngularJS define alguns estados quando trabalhamos com formulários e campos, você pode conferir esses estados ao inspecionar o formulário via Chrome DevTools e checar as classes presentes:
$pristine
: O formulário/campo ainda não teve nenhuma interação do usuário$dirty
: O formulário/campo já teve interação com o usuário$valid
: O formulário/campo está preenchido e válido$invalid
: O formulário/campo está preenchido indevidamente
Por que é importante saber sobre esses estados? Para entender como o AngularJS trata toda essa interação do usuário com o formulário e para entender a possível solução que encontrei para esse problema!
Resolvendo
Então já que o backend exige que o campo obs
esteja sempre presente, mesmo quando vazio, precisaremos forçar que campos $pristine
sejam incluídos no binding do nosso objeto. Considere que o código abaixo esteja incluído na função $scope.submit
:
angular.forEach($scope.form, function(value, key) {
if (value.hasOwnProperty('$modelValue')) {
if (!value.$viewValue) {
value.$setViewValue("");
}
}
});
Ao testar novamente o formulário, preenchendo somente o campo e enviando o formulário, o resultado no console será:
Object {name:"Nome", obs: ""}
O que esse código fez? Primeiro é definido um forEach
para todos os campos do formulário.
angular.forEach($scope.form, function(value, key) {
Depois é verificado se o elemento atual tem binding com algum model.
if (value.hasOwnProperty('$modelValue')) {
Se o campo tem binding e não está preenchido ou não existe interação com ele, define que seu valor será ""
e reflita isso no model.
value.$setViewValue("");
Pronto, agora campos que estão $pristine
serão marcados como $dirty
e o binding sempre incluirá todos os campos de seu model ! Só que esse código possui um pequeno problema: o binding vai ser executado até com o formulário inválido. Isso pode ser resolvido facilmente:
if (form.$valid) {
// código para forçar o binding
}
Note que, com esta solução seria necessário que todos os controllers tenham o trecho de código apresentado, então seria interessante criar uma directive com esse código para que posteriormente você só defina isso como um atributo de seu formulário, algo como apresentado abaixo.
<form name="form" ng-submit="submit()" force-bind>
A directive pode ficar algo como o seguinte código.
myApp.directive('forceBind', function() {
return {
require: '^form',
priority: -1,
link: function (scope, element, attrs, form) {
element.bind('submit', function() {
if (form.$valid) {
angular.forEach(form, function(value, key) {
if (value.hasOwnProperty('$modelValue')) {
if (!value.$viewValue) {
value.$setViewValue("");
}
}
});
}
});
}
};
});
Foge do escopo do artigo explicar mais detalhadamente directives, mas o que está sendo feito é:
- Executar a directive com prioridade -1, ou seja, antes do
ngSubmit
, que tem prioridade 0 - Quando o evento
submit
for executado, força obind
caso o formulário seja válido.
Referências
- Angular.js programmatically setting a form field to dirty
- ngSubmit
- Shake that Login Form with AngularJS
Veja o código funcionando em: http://jsfiddle.net/pgfLaomd/3/.
Até a próxima.