Validar campos después de que el usuario haya dejado un campo

92

Con AngularJS, puedo usar ng-pristineo ng-dirtypara detectar si el usuario ha ingresado al campo. Sin embargo, quiero hacer la validación del lado del cliente solo después de que el usuario haya abandonado el área de campo. Esto se debe a que cuando un usuario ingresa a un campo como correo electrónico o teléfono, siempre recibirá un error hasta que haya terminado de escribir su correo electrónico completo, y esta no es una experiencia de usuario óptima.

Ejemplo


ACTUALIZAR:

Angular ahora se envía con un evento de desenfoque personalizado: https://docs.angularjs.org/api/ng/directive/ngBlur

mcranston18
fuente
3
Supongo que esto es algo que angularjs debería haber admitido ...
csg
Tal vez. La validación de formulario incorporada funciona bastante bien, por lo que no sé si me gustaría que estuviera incorporada. Después de todo, la mayoría de los casos de uso quiero que Angular valide inmediatamente. es decir, en un campo numérico, quiero que el formulario se invalide inmediatamente si el usuario comienza a escribir letras.
mcranston18
11
la validación en el desenfoque es común desde hace 15 años ...
Olivvv

Respuestas:

75

Angular 1.3 ahora tiene ng-model-options, y puede configurar la opción, { 'updateOn': 'blur'}por ejemplo, e incluso puede eliminar el rebote, cuando el uso es escribir demasiado rápido o si desea guardar algunas operaciones DOM costosas (como escribir un modelo a varios lugares DOM y no desea que ocurra un ciclo de $ digest con cada tecla presionada)

https://docs.angularjs.org/guide/forms#custom-triggers y https://docs.angularjs.org/guide/forms#non-immediate-debounced-model-updates

De forma predeterminada, cualquier cambio en el contenido activará una actualización del modelo y la validación del formulario. Puede anular este comportamiento utilizando la directiva ngModelOptions para enlazar solo a una lista específica de eventos. Es decir, ng-model-options = "{updateOn: 'blur'}" se actualizará y validará solo después de que el control pierda el foco. Puede configurar varios eventos usando una lista delimitada por espacios. Es decir, ng-model-options = "{updateOn: 'mousedown blur'}"

Y rebotar

Puede retrasar la actualización / validación del modelo utilizando la tecla antirrebote con la directiva ngModelOptions. Este retraso también se aplicará a analizadores, validadores y marcas de modelo como $ dirty o $ pristine.

Es decir, ng-model-options = "{debounce: 500}" esperará medio segundo desde el último cambio de contenido antes de activar la actualización del modelo y la validación del formulario.

pocesar
fuente
2
En la mayoría de los casos, esto no resuelve el problema, porque probablemente aún desee recibir ng-changeeventos antes de que el usuario abandone el campo. Por ejemplo, en mi caso, solicito nuevos datos de mi API tan pronto como el campo de entrada sea válido, pero no quiero mostrar un mensaje de error tan pronto como sea inválido; solo quiero hacer esto cuando es inválido y ocurrió el evento de desenfoque.
Tom
@Tom Estoy probando 1.3.0 rc-3y no se dispara changehasta que blur, verifique esto: plnkr.co/edit/RivVU3r7xfHO1hUybqMu?p=preview
Gill Bates
Esta debería ser la mejor respuesta aceptada. ¿Por qué agregar una directiva, cuando puede agregar una sola propiedad?
Dan Hulton
92

A partir de la versión 1.3.0, esto se puede hacer fácilmente $touched, lo cual es cierto después de que el usuario haya abandonado el campo.

<p ng-show="form.field.$touched && form.field.$invalid">Error</p>

https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

usuario1506145
fuente
1
¿No seguiría ejecutando la validación después de cada activación? OP declaró que quería hacer la validación solo después de que el usuario haya abandonado el campo.
comecme
@comecme sí lo haría, el ng-model-options predeterminado es lo updateOn: 'default'que significa, para entradas, tecla arriba y para selecciones, al cambiar
pocesar
5
Sin embargo, el gran defecto aquí es que no tendrá la misma experiencia de validación si regresa al campo. Estás de vuelta en el punto de partida, donde se valida con cada pulsación de tecla, ya que ya está configurado para tocar. Me sorprende que esto haya recibido tantos votos a favor.
dudewad
3
@dudewad eso es correcto, pero creo que es el comportamiento deseado en cuanto a UX: si regresa a un campo no válido, el mensaje de error debe persistir hasta que el valor sea válido, no hasta que haya escrito la primera letra.
danbars
@dudewad ¿No ayudaría al usuario a validar en cada prensa después de saber que un campo no era válido después de la primera vez que lo completó? Una vez que sepan que el campo no es válido, es probable que quieran saber rápidamente cuándo el campo es válido y, ¿quizás validar en cada pulsación de tecla aceleraría ese proceso?
Alan Dunning
32

Resolví esto ampliando lo que sugirió @jlmcdonald. Creé una directiva que se aplicaría automáticamente a todos los elementos de entrada y selección:

var blurFocusDirective = function () {
    return {
        restrict: 'E',
        require: '?ngModel',
        link: function (scope, elm, attr, ctrl) {
            if (!ctrl) {
                return;
            }

            elm.on('focus', function () {
                elm.addClass('has-focus');

                scope.$apply(function () {
                    ctrl.hasFocus = true;
                });
            });

            elm.on('blur', function () {
                elm.removeClass('has-focus');
                elm.addClass('has-visited');

                scope.$apply(function () {
                    ctrl.hasFocus = false;
                    ctrl.hasVisited = true;
                });
            });

            elm.closest('form').on('submit', function () {
                elm.addClass('has-visited');

                scope.$apply(function () {
                    ctrl.hasFocus = false;
                    ctrl.hasVisited = true;
                });
            });

        }
    };
};

app.directive('input', blurFocusDirective);
app.directive('select', blurFocusDirective);

Esto agregará has-focusy has-visitedclases a varios elementos a medida que el usuario se enfoca / visita los elementos. Luego puede agregar estas clases a sus reglas CSS para mostrar errores de validación:

input.has-visited.ng-invalid:not(.has-focus) {
    background-color: #ffeeee;   
}

Esto funciona bien en el sentido de que los elementos aún obtienen $ propiedades no válidas, etc., pero el CSS se puede usar para brindar al usuario una mejor experiencia.

lambinator
fuente
2
¿Por qué se requiere '? NgModel' en lugar de simplemente 'ngModel'? Entiendo que es necesario que ngModel esté allí, pero ¿por qué el signo de interrogación?
Noremac
2
Gracias por la idea. Debería señalar que requiere jQuery (creo que no pude ver un .closest () en jql).
iandotkelly
1
No todos los elementos <input> son compatibles con ngModel (por ejemplo, tipo de entrada = enviar). Los ? requiere ngModel solo en elementos que lo admitan.
Chmanie
5
Para ser honesto, no debe desviarse de la forma angular de establecer estados de campo de entrada como formName.inputName.$dirty. Sugeriría editar la directiva para escribir un booleano similar formName.inputName.$focused, en lugar de usar nombres de clase para desenfoque y booleanos para validación.
Tom
17

Me las arreglé para hacer esto con un poco de CSS bastante simple. Esto requiere que los mensajes de error sean hermanos de la entrada con la que se relacionan y que tengan una clase de error.

:focus ~ .error {
    display:none;
}

Después de cumplir con esos dos requisitos, esto ocultará cualquier mensaje de error relacionado con un campo de entrada enfocado, algo que creo que angularjs debería estar haciendo de todos modos. Parece un descuido.

Nicolás
fuente
2
Ahh. Esta es una forma inteligente de hacerlo. Prefiero esto a escribir javascript para hacerlo.
cubierta
5
La desventaja de este enfoque es que cuando alguien está corrigiendo un error, el error ya no es visible, lo que no es ideal. Lo ideal sería que el error no se muestre hasta que esté borroso, pero no desaparezca hasta que se corrija.
Chris Nicola
4

Esto parece estar implementado como estándar en las versiones más nuevas de angular.

Las clases ng-untouched y ng-touch se establecen respectivamente antes y después de que el usuario se haya centrado en un elemento validado.

CSS

input.ng-touched.ng-invalid {
   border-color: red;
}
Arg0n
fuente
4

Con respecto a la solución de @ lambinator ... recibí el siguiente error en angular.js 1.2.4:

Error: [$ rootScope: inprog] $ digest ya está en curso

No estoy seguro de si hice algo mal o si se trata de un cambio en Angular, pero la eliminación de las scope.$applydeclaraciones resolvió el problema y las clases / estados aún se actualizan.

Si también está viendo este error, pruebe lo siguiente:

var blurFocusDirective = function () {
  return {
    restrict: 'E',
    require: '?ngModel',
    link: function (scope, elm, attr, ctrl) {
      if (!ctrl) {
        return;
      }
      elm.on('focus', function () {
        elm.addClass('has-focus');
        ctrl.$hasFocus = true;
      });

      elm.on('blur', function () {
        elm.removeClass('has-focus');
        elm.addClass('has-visited');
        ctrl.$hasFocus = false;
        ctrl.$hasVisited = true;
      });

      elm.closest('form').on('submit', function () {
        elm.addClass('has-visited');

        scope.$apply(function () {
          ctrl.hasFocus = false;
          ctrl.hasVisited = true;
        });
      });
    }
  };
};
app.directive('input', blurFocusDirective);
app.directive('select', blurFocusDirective);
melcher
fuente
2

Podría funcionar para usted escribir una directiva personalizada que envuelva el método javascript blur () (y ejecute una función de validación cuando se active); hay un problema de Angular que tiene un ejemplo (así como una directiva genérica que puede vincularse a otros eventos que no son compatibles de forma nativa con Angular):

https://github.com/angular/angular.js/issues/1277

Si no desea seguir esa ruta, su otra opción sería configurar $ watch en el campo, activando nuevamente la validación cuando el campo se completa.

jlmcdonald
fuente
2

Para profundizar más en las respuestas dadas, puede simplificar el etiquetado de entrada utilizando pseudoclases CSS3 y solo marcando los campos visitados con una clase para retrasar la visualización de los errores de validación hasta que el usuario pierda el enfoque en el campo:

(El ejemplo requiere jQuery)

JavaScript

module = angular.module('app.directives', []);
module.directive('lateValidateForm', function () {
    return {
        restrict: 'AC',
        link: function (scope, element, attrs) {
            $inputs = element.find('input, select, textarea');

            $inputs.on('blur', function () {
                $(this).addClass('has-visited');
            });

            element.on('submit', function () {
                $inputs.addClass('has-visited');
            });
        }
    };
});

CSS

input.has-visited:not(:focus):required:invalid,
textarea.has-visited:not(:focus):required:invalid,
select.has-visited:not(:focus):required:invalid {
  color: #b94a48;
  border-color: #ee5f5b;
}

HTML

<form late-validate-form name="userForm">
  <input type="email" name="email" required />
</form>
Patrick Glandien
fuente
2

basado en la respuesta de @nicolas .. CSS puro debería ser el truco, solo mostrará el mensaje de error en el desenfoque

<input type="email" id="input-email" required
               placeholder="Email address" class="form-control" name="email"
               ng-model="userData.email">
        <p ng-show="form.email.$error.email" class="bg-danger">This is not a valid email.</p>

CSS

.ng-invalid:focus ~ .bg-danger {
     display:none;
}
keithics
fuente
Esto es bueno, pero el mensaje de error desaparecerá cuando el usuario vuelva a hacer clic en el campo, lo que no es ideal.
Bennett McElwee
2

Aquí hay un ejemplo que usa ng-messages (disponible en angular 1.3) y una directiva personalizada.

El mensaje de validación se muestra en desenfoque la primera vez que el usuario abandona el campo de entrada, pero cuando corrige el valor, el mensaje de validación se elimina inmediatamente (ya no en desenfoque).

JavaScript

myApp.directive("validateOnBlur", [function() {
    var ddo = {
        restrict: "A",
        require: "ngModel",
        scope: {},
        link: function(scope, element, attrs, modelCtrl) {
            element.on('blur', function () {
                modelCtrl.$showValidationMessage = modelCtrl.$dirty;
                scope.$apply();
            });
        }
    };
    return ddo;
}]);

HTML

<form name="person">
    <input type="text" ng-model="item.firstName" name="firstName" 
        ng-minlength="3" ng-maxlength="20" validate-on-blur required />
    <div ng-show="person.firstName.$showValidationMessage" ng-messages="person.firstName.$error">
        <span ng-message="required">name is required</span>
        <span ng-message="minlength">name is too short</span>
        <span ng-message="maxlength">name is too long</span>
    </div>
</form>

PD. No olvide descargar e incluir ngMessages en su módulo:

var myApp = angular.module('myApp', ['ngMessages']);
jurgemar
fuente
1

ng-model-options en AngularJS 1.3 (beta al momento de escribir este artículo) está documentado para admitir {updateOn: 'blur'}. Para versiones anteriores, algo como lo siguiente funcionó para mí:

myApp.directive('myForm', function() {
  return {
    require: 'form',
    link: function(scope, element, attrs, formController) {
      scope.validate = function(name) {
        formController[name].isInvalid
            = formController[name].$invalid;
      };
    }
  };
});

Con una plantilla como esta:

<form name="myForm" novalidate="novalidate" data-my-form="">
<input type="email" name="eMail" required="required" ng-blur="validate('eMail')" />
<span ng-show="myForm.eMail.isInvalid">Please enter a valid e-mail address.</span>
<button type="submit">Submit Form</button>
</form>
Terence Bandoian
fuente
1

Usar estado del campo $ tocado El campo se ha tocado para esto como se muestra en el siguiente ejemplo.

<div ng-show="formName.firstName.$touched && formName.firstName.$error.required">
    You must enter a value
</div>
Kushal Sharma
fuente
0

Puede configurar dinámicamente la clase css has-error (asumiendo que está usando bootstrap) usando ng-class y una propiedad en el alcance del controlador asociado:

plunkr: http://plnkr.co/edit/HYDlaTNThZE02VqXrUCH?p=info

HTML:

<div ng-class="{'has-error': badEmailAddress}">
    <input type="email" class="form-control" id="email" name="email"
        ng-model="email" 
        ng-blur="emailBlurred(email.$valid)">
</div>

Controlador:

$scope.badEmailAddress = false;

$scope.emailBlurred = function (isValid) {
    $scope.badEmailAddress = !isValid;
};
Miguel
fuente
0

Si usa bootstrap 3 y lesscss, puede habilitar la validación de desenfoque con el siguiente fragmento menos:

:focus ~ .form-control-feedback.glyphicon-ok {
  display:none;
}

:focus ~ .form-control-feedback.glyphicon-remove {
  display:none;
}

.has-feedback > :focus {
  & {
    .form-control-focus();
  }
}
Stefan
fuente
0

outUsé una directiva. Aquí está el código:

app.directive('onBlurVal', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attrs, controller) {

            element.on('focus', function () {
                element.next().removeClass('has-visited');
                element.next().addClass('has-focus');
            });

            element.on('blur', function () {

                element.next().removeClass('has-focus');
                element.next().addClass('has-visited');
            });
        }
    }
})

Todo mi control de entrada tiene un elemento de intervalo como elemento siguiente, que es donde se muestra mi mensaje de validación y, por lo tanto, la directiva como atributo se agrega a cada control de entrada.

También tengo (opcional) .has-focus y la clase css visitada en mi archivo css al que se hace referencia en la directiva.

NOTA: recuerde agregar 'on-blur-val' exactamente de esta manera a su control de entrada sin los apóstrofos

user3676608
fuente
0

Al usar ng-focus , puede lograr su objetivo. debe proporcionar ng-focus en su campo de entrada. Y mientras escribe sus derivadas ng-show, también debe escribir una lógica que no sea igual. Como el siguiente código:

<input type="text" class="form-control" name="inputPhone" ng-model="demo.phoneNumber" required ng-focus> <div ng-show="demoForm.inputPhone.$dirty && demoForm.inputPhone.$invalid && !demoForm.inputPhone.$focused"></div>

Rayo
fuente
0

Podemos usar las funciones onfocus y onblur. Sería mejor y más sencillo.

<body ng-app="formExample">
  <div ng-controller="ExampleController">
  <form novalidate class="css-form">
    Name: <input type="text" ng-model="user.name" ng-focus="onFocusName='focusOn'" ng-blur="onFocusName=''" ng-class="onFocusName" required /><br />
    E-mail: <input type="email" ng-model="user.email" ng-focus="onFocusEmail='focusOn'" ng-blur="onFocusEmail=''" ng-class="onFocusEmail" required /><br />
  </form>
</div>

<style type="text/css">
 .css-form input.ng-invalid.ng-touched {
    border: 1px solid #FF0000;
    background:#FF0000;
   }
 .css-form input.focusOn.ng-invalid {
    border: 1px solid #000000;
    background:#FFFFFF;
 }
</style>

Intenta aquí:

http://plnkr.co/edit/NKCmyru3knQiShFZ96tp?p=preview

Naveen Kumar
fuente