jQuery Validate - requer que pelo menos um campo em um grupo seja preenchido

98

Estou usando o excelente jQuery Validate Plugin para validar alguns formulários. Em um formulário, preciso garantir que o usuário preencha pelo menos um de um grupo de campos. Acho que tenho uma solução muito boa e gostaria de compartilhá-la. Por favor, sugira quaisquer melhorias que você possa imaginar.

Não encontrando uma maneira integrada de fazer isso, pesquisei e encontrei o método de validação personalizado de Rebecca Murphey , que foi muito útil.

Eu melhorei isso de três maneiras:

  1. Para permitir que você passe um seletor para o grupo de campos
  2. Para permitir que você especifique quantos desse grupo devem ser preenchidos para a validação passar
  3. Para mostrar todas as entradas no grupo como aprovadas na validação assim que uma delas passar na validação. (Veja a mensagem para Nick Craver no final.)

Portanto, você pode dizer "pelo menos X entradas que correspondem ao seletor Y devem ser preenchidas."

O resultado final, com marcação como esta:

<input class="productinfo" name="partnumber">
<input class="productinfo" name="description">

... é um grupo de regras como este:

// Both these inputs input will validate if 
// at least 1 input with class 'productinfo' is filled
partnumber: {
   require_from_group: [1,".productinfo"]
  }
description: {
   require_from_group: [1,".productinfo"]
}

O item nº 3 assume que você está adicionando uma classe de .checkedàs suas mensagens de erro após a validação bem-sucedida. Você pode fazer isso da seguinte maneira, conforme demonstrado aqui .

success: function(label) {  
        label.html(" ").addClass("checked"); 
}

Como na demonstração vinculada acima, eu uso CSS para dar a cada span.errorum uma imagem X como plano de fundo, a menos que ele tenha a classe .checked, caso em que recebe uma imagem de marca de seleção.

Este é meu código até agora:

jQuery.validator.addMethod("require_from_group", function(value, element, options) {
    var numberRequired = options[0];
    var selector = options[1];
    //Look for our selector within the parent form
    var validOrNot = $(selector, element.form).filter(function() {
         // Each field is kept if it has a value
         return $(this).val();
         // Set to true if there are enough, else to false
      }).length >= numberRequired;

    // The elegent part - this element needs to check the others that match the
    // selector, but we don't want to set off a feedback loop where each element
    // has to check each other element. It would be like:
    // Element 1: "I might be valid if you're valid. Are you?"
    // Element 2: "Let's see. I might be valid if YOU'RE valid. Are you?"
    // Element 1: "Let's see. I might be valid if YOU'RE valid. Are you?"
    // ...etc, until we get a "too much recursion" error.
    //
    // So instead we
    //  1) Flag all matching elements as 'currently being validated'
    //  using jQuery's .data()
    //  2) Re-run validation on each of them. Since the others are now
    //     flagged as being in the process, they will skip this section,
    //     and therefore won't turn around and validate everything else
    //  3) Once that's done, we remove the 'currently being validated' flag
    //     from all the elements
    if(!$(element).data('being_validated')) {
    var fields = $(selector, element.form);
    fields.data('being_validated', true);
    // .valid() means "validate using all applicable rules" (which 
    // includes this one)
    fields.valid();
    fields.data('being_validated', false);
    }
    return validOrNot;
    // {0} below is the 0th item in the options field
    }, jQuery.format("Please fill out at least {0} of these fields."));

Hooray!

Gritar

Agora, para essa mensagem - originalmente, meu código apenas ocultou cegamente as mensagens de erro nos outros campos correspondentes em vez de revalidá-los, o que significava que se houvesse outro problema (como 'apenas números são permitidos e você inseriu letras') , ficou oculto até que o usuário tentou enviar. Isso porque eu não sabia como evitar o ciclo de feedback mencionado nos comentários acima. Eu sabia que devia haver um caminho, então fiz uma pergunta e Nick Craver me esclareceu. Obrigado, Nick!

Questão Resolvida

Originalmente, esse era um tipo de pergunta "deixe-me compartilhar isso e ver se alguém pode sugerir melhorias". Embora eu ainda receba feedback, acho que está bem completo neste ponto. (Pode ser mais curto, mas quero que seja fácil de ler e não necessariamente conciso.) Então, divirta-se!

Atualização - agora parte da validação jQuery

Isso foi adicionado oficialmente à validação do jQuery em 03/04/2012.

Nathan Long
fonte
Além disso, consulte a regra intimamente relacionada - "Pule esses campos ou preencha pelo menos X deles" - stackoverflow.com/questions/1888976/…
Nathan Long,
Por que uma entrada arbitrária seria responsável por verificar se outras entradas foram preenchidas? Isso não faz sentido. Talvez você possa incluir um pouco de marcação com os elementos envolvidos?
montrealist
@dalbaeb - Esclareço um pouco o exemplo. Não é que uma entrada arbitrária seja responsável por verificar outras; é que cada entrada em um grupo é responsável por verificar todas as outras.
Nathan Long
Foi o que pensei, muito obrigado!
montrealist
3
Obrigado, isso funciona para mim, mas os outros campos obrigatórios no formulário agora não respondem mais, a menos que ganhem e percam o foco após a verificação. (Alguém adicionou isso como uma resposta à sua outra pergunta, mas teve que ser sinalizado porque não é uma resposta).
mydoghasworms,

Respostas:

21

Essa é uma excelente solução, Nathan. Muito obrigado.

Esta é uma maneira de fazer o código acima funcionar, caso alguém tenha problemas para integrá-lo, como eu:

Código dentro do arquivo additional-methods.js :

jQuery.validator.addMethod("require_from_group", function(value, element, options) {
...// Nathan's code without any changes
}, jQuery.format("Please fill out at least {0} of these fields."));

// "filone" is the class we will use for the input elements at this example
jQuery.validator.addClassRules("fillone", {
    require_from_group: [1,".fillone"]
});

Código dentro do arquivo html :

<input id="field1" class="fillone" type="text" value="" name="field1" />
<input id="field2" class="fillone" type="text" value="" name="field2" />
<input id="field3" class="fillone" type="text" value="" name="field3" />
<input id="field4" class="fillone" type="text" value="" name="field4" />

Não se esqueça de incluir o arquivo additional-methods.js!


fonte
Que bom que foi útil para você e obrigado por fornecer informações. No entanto, em vez de usar o método addClassRules, prefiro usar uma matriz de regras em cada formulário individual. Se você acessar esta página ( jquery.bassistance.de/validate/demo/milk ) e clicar em "mostrar script usado nesta página", verá um exemplo. Vou um passo adiante: declaro uma matriz chamada "regras" e, separadamente, as uso com var validator = $ ('# formtovalidate'). Validate (regras);
Nathan Long
Outro pensamento: a classe 'fillone' que você mostra aqui pode ser problemática. E se, no mesmo formulário, você precisar exigir pelo menos um número de peça E pelo menos um nome de contato? Sua regra permitirá 0 nomes de contato, desde que haja pelo menos um número de peça. Acho que é melhor definir regras como require_from_group: [1,".partnumber"]e ...[1,".contactname"]garantir que você está validando as coisas certas.
Nathan Long
6

Ótima solução. No entanto, tive o problema de outras regras obrigatórias não funcionarem. Executar .valid () no formulário corrigiu esse problema para mim.

if(!$(element).data('being_validated')) {
  var fields = $(selector, element.form);
  fields.data('being_validated', true); 
  $(element.form).valid();
  fields.data('being_validated', false);
}
Sean
fonte
1
Obrigado Sean, eu também estava tendo esse problema. Porém, há um problema com essa solução, quando o usuário acessa o formulário pela primeira vez - assim que ele preenche o primeiro campo requerer do grupo, todos os outros campos do formulário serão validados e, portanto, marcados como defeituosos. A maneira como resolvi isso foi adicionar um manipulador form.submit () antes de criar uma instância do validador, na qual defini um sinalizador validator.formSubmit = true. No método require-from-group, eu verifico esse sinalizador; se está lá eu faço $(element.form).valid();, caso contrário, eu faço fields.valid();.
Christof
Alguém pode explicar o que realmente está acontecendo aqui? Eu tenho uma regra bastante semelhante, que funcionou, mas na qual não abordamos o problema de revalidação (outros campos no grupo ainda marcados como inválidos). Mas agora estou percebendo que o formulário é enviado mesmo se for inválido. Se os campos agrupados forem válidos, ele não entra no submithandler e se inválido, ele insere invalidHandler, mas envia de qualquer maneira! Eu diria que este é um bug bastante sério no plugin de validação? O retorno de uma regra válida aplica-se apenas a essa regra (nem mesmo a todo o campo), então por que um formulário inválido está sendo enviado?
Adam,
Eu investiguei mais e são campos antes do grupo que não validam proeprly. Eu fiz isso como uma pergunta separada (com uma solução alternativa parcial que eu descobri): stackoverflow.com/questions/12690898/…
Adam,
4

Obrigado, sean. Isso corrigiu o problema que eu tinha com o código ignorando outras regras.

Também fiz algumas alterações para que a mensagem 'Preencha pelo menos 1 campo ...' seja exibida em um div separado em vez de todos os campos.

colocar no formulário validar o script

showErrors: function(errorMap, errorList){
            $("#form_error").html("Please fill out at least 1 field before submitting.");
            this.defaultShowErrors();
        },

adicione isso em algum lugar da página

<div class="error" id="form_error"></div>

adicionar ao método require_from_group função addMethod

 if(validOrNot){
    $("#form_error").hide();
}else{
    $("#form_error").show();
}
......
}, jQuery.format(" &nbsp;(!)"));
Walter Kelly
fonte
4

Eu enviei um patch que não apresenta os problemas da versão atual (por meio do qual a opção "obrigatório" para de funcionar corretamente em outros campos, uma discussão sobre os problemas com a versão atual está no github .

Exemplo em http://jsfiddle.net/f887W/10/

jQuery.validator.addMethod("require_from_group", function (value, element, options) {
var validator = this;
var minRequired = options[0];
var selector = options[1];
var validOrNot = jQuery(selector, element.form).filter(function () {
    return validator.elementValue(this);
}).length >= minRequired;

// remove all events in namespace require_from_group
jQuery(selector, element.form).off('.require_from_group');

//add the required events to trigger revalidation if setting is enabled in the validator
if (this.settings.onkeyup) {
    jQuery(selector, element.form).on({
        'keyup.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

if (this.settings.onfocusin) {
    jQuery(selector, element.form).on({
        'focusin.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

if (this.settings.click) {
    jQuery(selector, element.form).on({
        'click.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

if (this.settings.onfocusout) {
    jQuery(selector, element.form).on({
        'focusout.require_from_group': function (e) {
            jQuery(selector, element.form).valid();
        }
    });
}

return validOrNot;
}, jQuery.format("Please fill at least {0} of these fields."));
docflabby
fonte
3

Iniciar um nome de variável com $ é necessário em PHP, mas muito estranho (IMHO) em Javascript. Além disso, acredito que você se refere a ele como "$ módulo" duas vezes e "módulo" uma vez, certo? Parece que esse código não deveria funcionar.

Além disso, não tenho certeza se é a sintaxe normal do plugin jQuery, mas posso adicionar comentários acima de sua chamada addMethod, explicando o que você realiza. Mesmo com sua descrição de texto acima, é difícil seguir o código, porque não estou familiarizado com a que fieldset, :illed, value, element ou selector se referem. Talvez a maior parte disso seja óbvio para alguém familiarizado com o plug-in Validate, portanto, use seu julgamento sobre qual é a quantidade certa de explicação.

Talvez você possa separar alguns vars para autodocumentar o código; gostar,

var atLeastOneFilled = module.find(...).length > 0;
if (atLeastOneFilled) {
  var stillMarkedWithErrors = module.find(...).next(...).not(...);
  stillMarkedWithErrors.text("").addClass(...)

(presumindo que eu entendi o significado desses pedaços de seu código! :))

Não tenho certeza do que "módulo" significa, na verdade - há um nome mais específico que você poderia dar a essa variável?

Bom código, no geral!

Michael Gundlach
fonte
Obrigado pelas sugestões - esclareci os nomes das variáveis ​​e dividi o código para ficar um pouco mais legível.
Nathan Long de
2

Como o formulário em que estou trabalhando tem várias regiões clonadas com entradas agrupadas como essas, passei um argumento extra para o construtor require_from_group, alterando exatamente uma linha de sua função addMethod:

var commonParent = $(element).parents(options[2]);

e desta forma um seletor, ID ou nome de elemento pode ser passado uma vez:

jQuery.validator.addClassRules("reqgrp", {require_from_group: [1, ".reqgrp", 'fieldset']});

e o validador restringirá a validação aos elementos com aquela classe apenas dentro de cada fieldset, em vez de tentar contar todos os elementos classificados .reqgrp no formulário.

Andrew Roazen
fonte
2

Aqui está minha chance de responder a Rocket Hazmat, tentando resolver o problema de outros campos definidos que também precisam ser validados, mas marcando todos os campos como válidos no preenchimento bem-sucedido de um.

jQuery.validator.addMethod("require_from_group", function(value, element, options){
    var numberRequired = options[0],
    selector = options[1],
    $fields = $(selector, element.form),
    validOrNot = $fields.filter(function() {
        return $(this).val();
    }).length >= numberRequired,
    validator = this;
    if(!$(element).data('being_validated')) {
        $fields.data('being_validated', true).each(function(){
            validator.valid(this);
        }).data('being_validated', false);
    }
    if (validOrNot) {
    $(selector).each(function() {
            $(this).removeClass('error');
            $('label.error[for='+$(this).attr('id')+']').remove();
        });
    }
    return validOrNot;
}, jQuery.format("Please fill out at least {0} of these fields."));

O único problema restante com isso agora é o caso extremo em que o campo está vazio, depois preenchido e, em seguida, vazio novamente ... nesse caso, o erro será aplicado ao único campo, não ao grupo. Mas isso parece tão improvável de acontecer com qualquer frequência e ainda funciona tecnicamente nesse caso.

quadrado
fonte
Não há sentido nessa resposta, já que este método / regra foi integrado ao plug-in em abril de 2012.
Sparky
Eu tenho o mesmo problema que Rocket Hazmat tem com o método que agora vem com o validador. Ele valida aquele grupo de campos, mas nenhum outro campo usando outros métodos é validado. Esta resposta é uma tentativa de resolver esse problema. Se você tiver uma solução melhor, por favor me avise.
quadrado de
Até que o desenvolvedor conserte o problema permanentemente, em vez de aumentar qualquer confusão, eu simplesmente recomendo qualquer solução temporária que esteja sendo endossada aqui: github.com/jzaefferer/jquery-validation/issues/412
Sparky
1

Eu estava tendo problemas com outras regras que não estavam sendo verificadas em conjunto com isso, então mudei:

fields.valid();

Para isso:

var validator = this;
fields.each(function(){
   validator.valid(this);
});

Também fiz algumas melhorias (pessoais) e esta é a versão que estou usando:

jQuery.validator.addMethod("require_from_group", function(value, element, options){
    var numberRequired = options[0],
    selector = options[1],
    $fields = $(selector, element.form),
    validOrNot = $fields.filter(function() {
        return $(this).val();
    }).length >= numberRequired,
    validator = this;
    if(!$(element).data('being_validated')) {
        $fields.data('being_validated', true).each(function(){
            validator.valid(this);
        }).data('being_validated', false);
    }
    return validOrNot;
}, jQuery.format("Please fill out at least {0} of these fields."));
Foguete Hazmat
fonte
Funciona fazendo com que os outros campos sejam validados novamente, mas agora, quando todos os campos de um grupo são marcados como inválidos e você preenche um deles, apenas aquele é validado. Pelo menos para mim?
Christof,
@Chris - veja minha nova resposta que se baseia nesta e aborda isso.
quadrado doce
0

Obrigado, Nathan. Você me economizou muito tempo.

No entanto, devo notar que esta regra não está pronta para jQuery.noConflict (). Então, deve-se substituir todo $ por jQuery para trabalhar, digamos,var $j = jQuery.noConflict()

E eu tenho uma pergunta: como eu faria isso se comportar como uma regra embutida? Por exemplo, se eu inserir o e-mail, a mensagem "Insira um e-mail válido" desaparece automaticamente, mas se eu preencher um dos campos do grupo, a mensagem de erro permanece.

Rinat
fonte
Você está certo - eu não considerei a situação de não conflito. Posso atualizar isso no futuro, mas você pode facilmente localizar e substituir, se desejar. Para sua segunda pergunta, não vejo o mesmo problema. Com um teste rápido, se um campo de um grupo for obrigatório, assim que digito algo, todo o grupo passa nessa regra. Se for necessário mais de um, assim que o último necessário for preenchido e perder o foco, todo o grupo será aprovado. É isso o que você vê?
Nathan Long
hmm, por algum motivo, a marcação está errada e não tive sucesso em consertá-la
Rinat
Rinat - você pode simplificar e reduzir o problema? Tente usar meu código em um formulário mais simples que não precise de alterações sem conflito. Faça a forma mais simples possível para testar e faça com que funcione primeiro.
Nathan Long