javascript - Validación de Knockout Validadores asíncronos: ¿es esto un error o estoy haciendo algo mal?
asynchronous knockout.js (2)
Así que la pregunta que hice realmente tenía que ver con cómo usar los validadores asíncronos en ko.validation. Hay 2 grandes conclusiones que he aprendido de mi experiencia:
No cree validadores de regla personalizados
async
anónimos o de uso único . En su lugar, crearlos como reglas personalizadas . De lo contrario, terminará con la combinación de ping / ping infinito que se describe en mi pregunta.Si usa validadores
async
, no confíe enisValid()
hasta que todos los validadoresasync
lassubscriptions
isValidating
cambien a falso.
Si tiene varios validadores asíncronos, puede usar un patrón como el siguiente:
var viewModel = {
var self = this;
self.prop1 = ko.observable().extend({validateProp1Async: self});
self.prop2 = ko.observable().extend({validateProp2Async: self});
self.propN = ko.observable();
self.isValidating = ko.computed(function() {
return self.prop1.isValidating() || self.prop2.isValidating();
});
self.saveData = function(arg1, arg2, argN) {
if (self.isValidating()) {
setTimeout(function() {
self.saveData(arg1, arg2, argN);
}, 50);
return false;
}
if (!self.isValid()) {
self.errors.showAllMessages();
return false;
}
// data is now trusted to be valid
$.post(''/something'', ''data'', function() { doWhatever() });
}
};
También puede ver esto para otra referencia con soluciones alternativas similares .
Aquí hay un ejemplo de una "regla personalizada" asíncrona:
var validateProp1Async = {
async: true,
message: ''you suck because your input was wrong fix it or else'',
validator: function(val, otherVal, callback) {
// val will be the value of the viewmodel''s prop1() observable
// otherVal will be the viewmodel itself, since that was passed in
// via the .extend call
// callback is what you need to tell ko.validation about the result
$.ajax({
url: ''/path/to/validation/endpoint/on/server'',
type: ''POST'', // or whatever http method the server endpoint needs
data: { prop1: val, otherProp: otherVal.propN() } // args to send server
})
.done(function(response, statusText, xhr) {
callback(true); // tell ko.validation that this value is valid
})
.fail(function(xhr, statusText, errorThrown) {
callback(false); // tell ko.validation that his value is NOT valid
// the above will use the default message. You can pass in a custom
// validation message like so:
// callback({ isValid: false, message: xhr.responseText });
});
}
};
Básicamente, se utiliza el argumento de callback
a la función de validator
para indicar a ko.validation si la validación tuvo éxito o no. Esa llamada es lo que hará que los observables de validación en la propiedad validada observable vuelvan a ser false
(es decir, se haya completado la validación asíncrona y ahora se sabe si la entrada fue válida o no).
Lo anterior funcionará si los puntos finales de validación del lado del servidor devuelven un estado HTTP 200 (OK) cuando la validación se realiza correctamente. Eso hará que se .done
función .done
, ya que es el equivalente al success
$.ajax
. Si su servidor devuelve un estado HTTP 400 (Solicitud incorrecta) cuando falla la validación, se activará la función .fail
. Si su servidor devuelve un mensaje de validación personalizado con el 400, puede obtenerlo de xhr.responseText
para anular de manera efectiva el valor predeterminado you suck because your input was wrong fix it or else
mensaje.
Realmente me gusta la forma en que la biblioteca de validación de eliminación directa de Eric Barnard se integra con los observables, permite la agrupación y ofrece conectividad del validador personalizado (incluidos los validadores sobre la marcha). Hay un par de lugares donde podría ser más flexible y amigable con el UX, pero en general está razonablemente bien documentado ... excepto, imo, cuando se trata de validadores asíncronos .
Luché con esto por unas horas hoy antes de hacer una búsqueda y aterrizar en esto . Creo que tengo los mismos problemas / preguntas que el autor original, pero estoy de acuerdo en que no estaba claro exactamente qué estaba pidiendo Duxa. Quiero llamar más la atención a la pregunta, así que también pregunto aquí.
function MyViewModel() {
var self = this;
self.nestedModel1.prop1 = ko.observable().extend({
required: { message: ''Model1 Prop1 is required.'' },
maxLength: {
params: 140,
message: ''{0} characters max please.''
}
});
self.nestedModel2.prop2 = ko.observable().extend({
required: { message: ''Model2 Prop2 is required'' },
validation: {
async: true,
validator: function(val, opts, callback) {
$.ajax({ // BREAKPOINT #1
url: ''/validate-remote'',
type: ''POST'',
data: { ...some data... }
})
.success(function(response) {
if (response == true) callback(true); // BREAKPOINT #2
else callback(false);
});
},
message: ''Sorry, server says no :(''
}
});
}
ko.validation.group(self.nestedModel1);
ko.validation.group(self.nestedModel2);
Un par de notas sobre el código anterior: hay 2 grupos de validación separados, uno para cada modelo anidado. El modelo anidado # 1 no tiene validadores asíncronos, y el modelo anidado # 2 tiene una sincronización (requerida) y una asíncrona. El async invoca una llamada del servidor para validar las entradas. Cuando el servidor responde, el argumento de callback
se utiliza para indicar a ko.validation
si la entrada del usuario es buena o mala. Si coloca puntos de interrupción en las líneas indicadas y activa la validación utilizando un valor inválido conocido, terminará con un bucle infinito donde la función de success
ajax hace que se vuelva a llamar a la función de validator
. ko.validation
fuente de ko.validation
para ver qué estaba pasando.
ko.validation.validateObservable = function(observable) {
// set up variables & check for conditions (omitted for brevity)
// loop over validators attached to the observable
for (; i < len; i++) {
if (rule[''async''] || ctx[''async'']) {
//run async validation
validateAsync();
} else {
//run normal sync validation
if (!validateSync(observable, rule, ctx)) {
return false; //break out of the loop
}
}
}
//finally if we got this far, make the observable valid again!
observable.error = null;
observable.__valid__(true);
return true;
}
Esta función se encuentra en una cadena de suscripción adjunta a la entrada del usuario observable, de modo que cuando su valor cambie, el nuevo valor será validado. El algoritmo recorre cada validador adjunto a la entrada y ejecuta funciones separadas dependiendo de si el validador es asíncrono o no. Si la validación de la sincronización falla, el bucle se rompe y la función validateObservable
completa se cierra. Si todos los validadores de sincronización pasan, las últimas 3 líneas se ejecutan, esencialmente indicando a ko.validation
que esta entrada es válida. La función __valid__
en la biblioteca se ve así:
//the true holder of whether the observable is valid or not
observable.__valid__ = ko.observable(true);
Hay dos cosas que quitar de esto: __valid__
es un observable, y se establece en true
después de que la función validateAsync
sale. Ahora echemos un vistazo a validateAsync
:
function validateAsync(observable, rule, ctx) {
observable.isValidating(true);
var callBack = function (valObj) {
var isValid = false,
msg = '''';
if (!observable.__valid__()) {
// omitted for brevity, __valid__ is true in this scneario
}
//we were handed back a complex object
if (valObj[''message'']) {
isValid = valObj.isValid;
msg = valObj.message;
} else {
isValid = valObj;
}
if (!isValid) {
//not valid, so format the error message...
observable.error = ko.validation.formatMessage(...);
observable.__valid__(isValid);
}
// tell it that we''re done
observable.isValidating(false);
};
//fire the validator and hand it the callback
rule.validator(observable(), ctx.params || true, callBack);
}
Es importante tener en cuenta que solo las primeras y últimas líneas de esta función se ejecutan antes de que ko.validation.validateObservable
establezca el __valid__
observable en verdadero y salga. La función callBack
es lo que se pasa como el tercer parámetro a la función de validator
asíncrona declarada en MyViewModel
. Sin embargo, antes de que esto suceda, se isValidating
los suscriptores de un observable isValidating
para notificar que la validación asíncrona ha comenzado. Cuando se completa la llamada del servidor, se invoca la devolución de llamada (en este caso, simplemente se pasa verdadero o falso).
Ahora bien, aquí está la razón por la cual los puntos de interrupción en MyViewModel
están causando un bucle infinito de ping pong cuando falla la validación del lado del servidor: En la función callBack
anterior, observe cómo el observable __valid__
se establece en falso cuando falla la validación. Esto es lo que sucede:
- La entrada de usuario no válida cambia el observable
nestedModel2.prop2
. - El
ko.validation.validateObservable
se notifica a través de la suscripción de este cambio. - Se invoca la función
validateAsync
. - Se invoca el validador asíncrono personalizado, que envía una llamada asíncrona
$.ajax
al servidor y sale. - El
ko.validation.validateObservable
establece el__valid__
observable entrue
y sale . - El servidor devuelve una respuesta no válida y se
callBack(false)
. - La función
callBack
establece__valid__
enfalse
. - El
ko.validation.validateObservable
se notifica del cambio al__valid__
observable (callBack
cambió detrue
afalse
) Esto esencialmente repite el paso 2 anterior. - Los pasos 3, 4 y 5 anteriores se repiten.
- Como el valor del observable no ha cambiado, el servidor devuelve otra respuesta no válida, lo que desencadena los pasos 6, 7, 8 y 9 anteriores.
- Tenemos un partido de ping pong.
Entonces, parece que el problema es que el controlador de suscripción ko.validation.validateObservable
está escuchando los cambios no solo en el valor de entrada del usuario, sino también en su __valid__
anidado observable. ¿Es esto un error, o estoy haciendo algo mal?
Una pregunta secundaria
Puede ver en las fuentes de ko.validation
arriba que un valor de entrada de usuario con un validador asíncrono se considera válido mientras el servidor lo está validando. Debido a esto, no se puede confiar en que se llame a nestedModel2.isValid()
para "la verdad". En su lugar, parece que tenemos que usar los ganchos isValidating
para crear suscripciones a los validadores asíncronos, y solo tomar estas decisiones después de que notifiquen un valor de false
. ¿Es esto por diseño? Comparado con el resto de la biblioteca, este parece ser el más intuitivo, ya que los validadores no asíncronos no tienen una isValidating
de isValidating
a la cual suscribirse, y pueden confiar en .isValid()
para decir la verdad. ¿Es esto también por diseño, o estoy haciendo algo mal aquí también?
Tuve el mismo problema, anidados observables con validación. Así que la única magia: in self.errors = ko.validation.group(self.submissionAnswers, { deep: true, live: true });
preste atención en param adicional adicional: objeto que contiene el campo en live: true