mk: Clientseitige Validierung von Checkboxen

Hallo an alle,

ich habe sinngemäß das folgende Skript:

<!DOCTYPE html>
<html>
<head> 
	<meta charset = "UTF-8">
</head>
<body>
	<form method = 'POST' action = 'irgendwas.php' Name = 'form01' onsubmit = 'return formularPruefen()'>
		<input type = 'checkbox' name = 'cb[]' id = 'cb1'> Kurs1
		<input type = 'checkbox' name = 'cb[]' id = 'cb2'> Kurs2
		<input type = 'checkbox' name = 'cb[]' id = 'cb3'> Kurs3 ...
		<input type = 'submit' value = 'senden'>
	</form>
</body>
</html>


Im wahren Skript werden die Checkboxen dynamisch aus einer Datenbank heraus erzeugt, ich weiss also nicht, wie viele davon existieren. Jetzt soll der Nutzer exakt 2 Kurse auswählen (also nicht 0, 1 oder 3 Kurse). Die Überprüfung hierzu soll erst einmal clientseitig erfolgen (hier liegt mein Problem), erst später zusätzlich serverseitig mittels PHP (letzteres stellt kein Problem dar).

Dazu muss ich irgendwie die Checkboxen in ein Javascript-Array bekommen, dieses dann durchlaufen und für jedes Arrayelement die Eigenschaft "checked == true" prüfen. Jeweils hochzählen und schauen, ob exakt zwei Checkboxen ausgewählt wurden.

Soweit die Theorie.

Leider bin ich scheinbar zu blöde, dies hinzubekommen.

Ich habe sowohl mit document.getElementsByName("cb"), document.getElementsByName("cb[]"), var cboxArray = document.form01.cb, var cboxArray = document.form01.cb[] und diversen anderen Lösungsansätzen versucht, die Infos in ein Array zu bekommen. Bislang leider vergeblich.

PS: Die eckigen Klammern im Namen der Checkboxen benötige ich zur Übernahme nach PHP.

Kann mir irgendjemand sagen, was ich hier falsch mache und wie die Funktion formularPruefen() aussehen müsste.

Besten Dank schon einmal.

  1. @@mk

    <form method = 'POST' action = 'irgendwas.php' Name = 'form01' onsubmit = 'return formularPruefen()'>

    JavaScript-Code sollte nicht in HTML-Attributen stehen, also nicht on… verwenden, sondern fortgeschrittene Ereignisverarbeitung

    BTW, die Leerzeichen um die Gleichheitszeichen machen den HTML-Code schlecht lesbar. Besser ohne, und Element- und Attributbezeichner in Kleinbuchstaben:

    <form method="POST" action="irgendwas.php" name="form01">
    

    Statt "form01" wäre eine sprechende Bezeichnung sinnvoll.

      <input type = 'checkbox' name = 'cb[]' id = 'cb1'> Kurs1
    

    Deine Checkboxen haben keine Beschriftung. So haben sie eine (wobei der Wert des for-Attributs mit der ID des entsprechenden Eingabe-Elements übereinstimmen muss):

    <input type="checkbox" name="cb[]" id="cb1"> <label for="cb1">Kurs1</label>
    
      <input type = 'checkbox' name = 'cb[]' id = 'cb2'> Kurs2
    

    Was das soll, verschieden Checkboxen denselben Wert fürs name-Attribut zu verpassen, entzieht sich meiner Kenntnis. Wie willst du dann serverseitig auswerten, welche Checkboxen angehakt waren?

      <input type = 'submit' value = 'senden'>
    

    Ein Button sollte kein input-Element sein, sondern ein button (aus Gründen):

    <button type="submit">senden</button>
    

    Jetzt soll der Nutzer exakt 2 Kurse auswählen (also nicht 0, 1 oder 3 Kurse). […] Dazu muss ich irgendwie die Checkboxen in ein Javascript-Array bekommen, dieses dann durchlaufen und für jedes Arrayelement die Eigenschaft "checked == true" prüfen. Jeweils hochzählen und schauen, ob exakt zwei Checkboxen ausgewählt wurden.

    Nein. Du musst lediglich alle angehakten Checkboxen selektieren:

    • querySelectorAll(':checked') liefert eine NodeList
    • mit length schaust du, wieviele es sind.

    Das tust du in einer Funktion (ich nenne sie mal "validate"), die als Eventhandler für das submit-Event des Formulars registriert wird:

    document.forms.form01.addEventListener('submit', validate, false);
    

    Die Funktion unterdrückt als erstes mit preventDefault() das Standardverhalten, d.h. das Abschicken des Formulars.

    Dann erfolgt die Prüfung. Bei genau 2 angehakten Checkboxen wird das Formular abgeschickt, andernfalls wird eine Meldung ausgegeben. (Da fällt dir noch was besseres ein als per alert()!)

    Da das Event vom Formular kommt, kann das Formular in der Funktion per this angesprochen werden:

    function validate(e)
    {
    	e.preventDefault();
    
    	if (this.querySelectorAll(':checked').length === 2)
    	{
    		this.submit(this);
    	}
    	else
    	{
    		alert("Nimm 2");
    	}
    }
    

    LLAP

    --
    „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
    1. Hallo Gunnar,

      danke für die ausführlich Antwort.

      Einige Sachen sind sicher interessant für mich. Aber mein eigentliches Problem ist noch nicht gelöst.

      Was das soll, verschieden Checkboxen denselben Wert fürs name-Attribut zu verpassen, entzieht sich meiner Kenntnis. Wie willst du dann serverseitig auswerten, welche Checkboxen angehakt waren?

      Dies ist schon bewusst so gemacht. Damit wird deutlich / soll deutlich werden, dass alle Checkboxen zusammen gehören. In PHP kommen dann alle ausgewählten Antworten in einem Array $_POST['cb'] an, womit ich sie optimal auswerten kann.

      • querySelectorAll(':checked') liefert eine NodeList

      Das gegebene Skript ist nur ein prinzipieller Auszug. Im wahren Skript sind weitere Formularelemenete, auch weitere Checkboxen mit anderem Namen vorhanden.

      • mit length schaust du, wieviele es sind.

      Natürlich weiss ich (bzw. kann ich mir besorgen) auch schon php-seitig, wie viele Checkboxen mit welchen Namen (und welcher unterschiedlicher id) vorhanden sind. Trotzdem scheint es mir für die Auswertung einfacher, über den (gleichen) Namen zu gehen. Dann benötige ich diese Information gar nicht.

      Meine Frage ist also mehr prinzipieller Natur. Und gibt es für meinen Weg (Auswertung über den gleichen Namen) eine Lösung?

      1. @@mk

        • querySelectorAll(':checked') liefert eine NodeList

        Im wahren Skript sind weitere Formularelemenete, auch weitere Checkboxen mit anderem Namen vorhanden.
        […] gibt es für meinen Weg (Auswertung über den gleichen Namen) eine Lösung?

        Ja, natürlich. Wenn du nur alle Elemente mit name="cb[]" selektieren willt, dann musst den den Selektor eben entsprechend anpassen. Den Attributselektor kennst du doch?

        LLAP

        --
        „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
      2. Moin,

        Was das soll, verschieden Checkboxen denselben Wert fürs name-Attribut zu verpassen, entzieht sich meiner Kenntnis. Wie willst du dann serverseitig auswerten, welche Checkboxen angehakt waren?

        Dies ist schon bewusst so gemacht. Damit wird deutlich / soll deutlich werden, dass alle Checkboxen zusammen gehören. In PHP kommen dann alle ausgewählten Antworten in einem Array $_POST['cb'] an, womit ich sie optimal auswerten kann.

        stimmt nicht ganz - das gilt nur, wenn der Name mit [] endet. Nur dann macht PHP aus gleichnamigen Parametern ein Array, ansonsten kann PHP nur auf einen dieser gleichnamigen Einträge zugreifen. AFAIK auf den letzten.
        Also: Gleicher Name für eine Gruppe ist okay, aber dann bitte nicht "cb", sondern "cb[]".

        EDIT: Sorry, ich habe mich täuschen lassen. Du hast ja tatsächlich die '[]' dran. Aber dann sollten die Checkboxen unterschiedliche values haben, damit du sie PHP-seitig unterscheiden kannst.

        Meine Frage ist also mehr prinzipieller Natur. Und gibt es für meinen Weg (Auswertung über den gleichen Namen) eine Lösung?

        Dass man das name-Attribut als gemeinsames Selektionskriterium nutzen kann, hat Gunnar ja schon beiläufig erklärt.

        So long,
         Martin

    2. @@Gunnar Bittersmann

      Nein. Du musst lediglich alle angehakten Checkboxen selektieren:

      • querySelectorAll(':checked') liefert eine NodeList
      • mit length schaust du, wieviele es sind.

      Halt! Was Wichtiges fehlt noch: Nicht alle Browser unterstützen querySelectorAll() (alte IE < 8). Diese dürfen natürlich nicht daran gehindert werden, das Formular abzuschicken. (Dann findet keine clientseitige Validierung statt. Das ist OK; es gibt ja noch die serverseitige.)

      Man darf also preventDefault() usw. nur dann ausführen, wenn der Browser querySelectorAll() versteht. Also abfragen, und die Eventbehandlung auch (feature detection):

      function validate(e)
      {
      	if (this.querySelectorAll && this.submit && e && e.preventDefault)
      	{
      		e.preventDefault();
      
      		if (this.querySelectorAll(':checked').length === 2)
      		{
      			this.submit(this);
      		}
      		else
      		{
      			alert("Nimm 2");
      		}
      	}
      }
      

      Damit in alten Browsern, die addEventListener() nicht kennen, kein Fehler geworfen wird, auch das vorher abfragen (und auch die Existenz des Elements):

      if (document.forms.form01 && document.forms.form01.addEventListener)
      {
      	document.forms.form01.addEventListener('submit', validate, false);
      }
      

      Oder man geht den Cutting-the-mustard-Weg (change) und schließt alte Browser generell von der Ausführung des Scripts aus:

      if ('classList' in document.createElement('div') && 'querySelector' in document && 'addEventListener' in window && Array.prototype.forEach)
      {
      	if (document.forms.form01)
      	{
      		document.forms.form01.addEventListener('submit', validate, false);
      	}
      
      	function validate(e)
      	{
      		e.preventDefault();
      
      		if (this.querySelectorAll(':checked').length === 2)
      		{
      			this.submit(this);
      		}
      		else
      		{
      			alert("Nimm 2");
      		}
      	}
      }
      

      LLAP

      --
      „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
      1. Tach!

        Halt! Was Wichtiges fehlt noch: Nicht alle Browser unterstützen querySelectorAll() (alte IE < 8). [...]
        Man darf also [...] nur dann ausführen, [...]
        Damit in alten Browsern, [...]
        Oder man geht [...]

        Oder man spart sich all die Kopfschmerzen und nimmt jQuery.

        dedlfix.

        1. @@dedlfix

          Oder man spart sich all die Kopfschmerzen und nimmt jQuery.

          1. Meh!
          2. Welche Kopfschmerzen?
          3. Wozu??

          84 Kilobyte extra laden, nur um einmal if zu sparen? Nicht dein Ernst?

          LLAP

          --
          „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
          1. Tach!

            1. Meh!
            2. Welche Kopfschmerzen?
            3. Wozu??

            84 Kilobyte extra laden, nur um einmal if zu sparen? Nicht dein Ernst?

            Es sind gzipped nur 32k und die sind dank CDN schon im Cache. Außerdem wird das garantiert nicht die einzige Stelle bleiben, an der man das nutzbringend verwenden kann. Eingesparte Arbeitszeit ist durchaus kein unwichtiger Grund.

            dedlfix.

            1. @@dedlfix

              Es sind gzipped nur 32k und die sind dank CDN schon im Cache. Außerdem wird das garantiert nicht die einzige Stelle bleiben, an der man das nutzbringend verwenden kann.

              Hängt vom Projekt ab.

              Eingesparte Arbeitszeit ist durchaus kein unwichtiger Grund.

              1. Ja. Aber eingesparte Arbeitszeit sollte nicht zu Lasten von Performanz gehen.

              2. Verlorengehende Fähigkeiten sind durchaus auch kein unwichtiger Grund. Wenn man nur noch Entwickler hat, die ein bisschen jQuery können, aber von JavaScript überhaupt keine Ahnung haben …

              LLAP

              --
              „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
              1. Tach!

                1. Verlorengehende Fähigkeiten sind durchaus auch kein unwichtiger Grund. Wenn man nur noch Entwickler hat, die ein bisschen jQuery können, aber von JavaScript überhaupt keine Ahnung haben …

                Das ist deine Interpretation. Entweder man kann programmieren oder nicht. Ich finde nicht, dass jQuery daran etwas wesentliches positiv oder negativ ändern kann. Und für mich zählt, die Probleme der Browser zu kennen, nicht zu Javascript-Wissen, sondern zu den Dingen, auf die die Welt verzichten kann.

                dedlfix.

                1. @@dedlfix

                  Und für mich zählt, die Probleme der Browser zu kennen, nicht zu Javascript-Wissen, sondern zu den Dingen, auf die die Welt verzichten kann.

                  Ja. Mich interessiert hier auch überhaupt nicht, welcher Browser welche Probleme hat.

                  Das ist ja das Wesen von progressive enhancement, sich dafür nicht zu interessieren, sondern Funktionaltität hinzuzufügen (hier: clientseitige Validierung), sofern der Browser das kann. Und zwar ohne vorhandene Funktionaltität kaputtzumachen (hier: das Absenden des Formulars).

                  Und ja, progressive enhancement zählt zu Javascript-Wissen.

                  LLAP

                  --
                  „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
                  1. Tach!

                    Und ja, progressive enhancement zählt zu Javascript-Wissen.

                    Und jQuery steht dem Progressive Enhancement störend im Wege? Wenn Progressive Enhancement heißt, dass ich die Entwicklungsgeschichte von Javascript kennen muss, um vom Urschleim an solange zu erweitern, bis ich in der Jetztzeit angelangt bin, dann nehm ich lieber 32k jQuery und einen Einzeiler.

                    dedlfix.

              2. Es sind gzipped nur 32k und die sind dank CDN schon im Cache. Außerdem wird das garantiert nicht die einzige Stelle bleiben, an der man das nutzbringend verwenden kann.

                Hängt vom Projekt ab.

                In Projekt "a" anderen Code zu schreiben als in Projekt "b" ist ein Argument, es projektunabhängig einzusetzen. Außerdem will man den Code von Projekt "c" nicht umschreiben, weil es auf einmal wächst.

                Eingesparte Arbeitszeit ist durchaus kein unwichtiger Grund.

                1. Ja. Aber eingesparte Arbeitszeit sollte nicht zu Lasten von Performanz gehen.

                Stimmt wohl, trifft auf 32K aus einem CDN aber schlicht nicht zu. Im Gegenteil: der eingesparte Code wächst mit der Menge des Codes.

                1. Verlorengehende Fähigkeiten sind durchaus auch kein unwichtiger Grund. Wenn man nur noch Entwickler hat, die ein bisschen jQuery können, aber von JavaScript überhaupt keine Ahnung haben …

                Auch das stimmt grundsätzlich. Man sollte die Hintergründe schon kennen. Was man aber auf keinen Fall kennen will und muss, sind die heute immer noch nötigen Workarounds zur Umgeung von Browser Bugs. Außer man entwickelt so etwas wie jQuery, um andere von dieser unnötigen Last zu befreien.

    3. Das tust du in einer Funktion (ich nenne sie mal "validate"), die als Eventhandler für das submit-Event des Formulars registriert wird:

      Die Funktion unterdrückt als erstes mit preventDefault() das Standardverhalten, d.h. das Abschicken des Formulars.

      Dann erfolgt die Prüfung.

      Das ist die klassische Variante, bei diesem Ablauf gibt es allerdings einen Nachteil: Der Browser hat zu dem Zeitpunkt, an dem das submit-Ereignis eintritt, schon die Formular-Eingaben auf Gültigkeit überprüft und den Nutzer über eventuelle Fehler benachrichtigt. Also zum Beispiel über fehlende Pflichtfelder oder zu hohe oder zu niedrige Werte bei numerischen Eingabefeldern. Dem Nutzer wurde als vom Browser bereits einmal Gelegenheit gegeben, seine Eingaben zu korrigieren und die letzten Eingaben haben die Validierung des Browsers passiert. Nun kommen wir und führen nach dieser Phase nochmal eine eigene benutzerdefinierte Validierung durch. Das ist für die Nutzer eher unkomfortabel, insbesondere für jene, die auf Screenreader angewiesen sind. Angenehmer wäre es, wenn wir unsere benutzerdefinierte Validierung in der regulären Validierungs-Phase des Browsers unterbringen, dafür gibt es heute die Constraint-Validation-API. Das Vorgehen gestaltet sich außerdem einfacher als bei der klassischen Variante, weil wir nicht selber darauf achten müssen, ob das Formular nun abgesendet wird oder nicht, wir haben als kein Hantier mit .preventDefault() und .submit().

      Statt beim Eintreten des submit-Ereignises nehmen wir unsere Validierung direkt bei Änderung an den Checkboxen vor. Wir müssen als auf das change- oder input-Ereignis lauschen:

      var checkListe = document.querySelectorAll('[name=cb\[\]]');
      var checkArray = Array.prototype.slice.call(checkListe);
      checkArray.forEach(function(checkbox){
         checkbox.addEventListener('input', validate);
      });
      

      Die validate-Funktion sieht dann auch ein wenig anders aus:

      function validate (event) {
        var checkbox = event.originalTarget;
        var markierteCheckboxen = document.querySelectorAll(':checked');
        checkbox.setCustomValidity('');
        checkbox.classList.remove('too-view-error');
        checkbox.classList.remove('too-many-error');
        if (!checkbox.checked && markierteCheckboxen.length < 2) {
           checkbox.setCustomValidity('Sie müssen noch eine weitere Option auswählen');
           checkbox.classList.add('too-view-error');
        } else if (checkbox.checked && markierteCheckboxen.length > 2) {
           checkbox.setCustomValidity('Sie haben zu viele Optionen gewählt');
           checkbox.classList.add('too-many-error');
      }
      

      Man sollte unbedingt noch für eine textliche Beschreibung des Fehlers sorgen, mit den Pseudoklassen :valid und :invalid und den Klassen .too-view-error und .too-many-error sollte das einfach möglich sein.

      1. @@1unitedpower

        dafür gibt es heute die Constraint-Validation-API. Das Vorgehen gestaltet sich außerdem einfacher als bei der klassischen Variante, weil wir nicht selber darauf achten müssen, ob das Formular nun abgesendet wird oder nicht, wir haben als kein Hantier mit .preventDefault() und .submit().

        Hört sich gut an.

        Die validate-Funktion sieht dann auch ein wenig anders aus:

        Aber fehlt da noch was? Die Browser schicken das Formular auch bei 0, 1, 3 angehakten Checkboxen ab.

        var checkListe = document.querySelectorAll('[name=cb[]]');

        Wie auch bei Attributen in HTML würde ich in Attributselektoren den Wert in Anführungszeichen setzen. Das dürfte auch das Escapen der Klammern sparen somit und den Code lesbarer machen.

        LLAP

        --
        „Talente finden Lösungen, Genies entdecken Probleme.“ (Hans Krailsheimer)
        1. Aber fehlt da noch was? Die Browser schicken das Formular auch bei 0, 1, 3 angehakten Checkboxen ab.

          Ja, da fehlte in der Tat noch was. Ein, zwei Syntax-Fehler und ein paar Kinderkrankheiten mussten dem noch ausgetrieben werden. Nun funktioniert es: http://jsfiddle.net/hqdpodv1/

          BTW: Die Fehlermeldungen sehen ziemlich schick aus, wenn man den Browser selbst die Darstellung wählen lässt. Zumindest in Chrome und IE, FF habe ich gerade nicht zum Testen bereit.

          var checkListe = document.querySelectorAll('[name=cb[]]');

          Wie auch bei Attributen in HTML würde ich in Attributselektoren den Wert in Anführungszeichen setzen. Das dürfte auch das Escapen der Klammern sparen somit und den Code lesbarer machen.

          Merci, das hat sich beim Schreiben schon komisch angefühlt, jetzt weiß ich wieso.

          1. Ja, da fehlte in der Tat noch was. Ein, zwei Syntax-Fehler und ein paar Kinderkrankheiten mussten dem noch ausgetrieben werden. Nun funktioniert es: http://jsfiddle.net/hqdpodv1/

            Und noch eine Kinderkrankheit weniger, jetzt aber: http://jsfiddle.net/hqdpodv1/1/

  2. Tach!

    		<input type = 'checkbox' name = 'cb[]' id = 'cb1'> Kurs1
    		<input type = 'checkbox' name = 'cb[]' id = 'cb2'> Kurs2
    		<input type = 'checkbox' name = 'cb[]' id = 'cb3'> Kurs3 ...
    

    Ich habe sowohl mit document.getElementsByName("cb"), document.getElementsByName("cb[]"), var cboxArray = document.form01.cb, var cboxArray = document.form01.cb[] und diversen anderen Lösungsansätzen versucht, die Infos in ein Array zu bekommen. Bislang leider vergeblich.

    Die Namen lauten cb[], mit den Klammern. Da die Klammern in Javascript eine besondere syntaktische Bedeutung haben, ist document.form01.cb[] falsch, weil sie damit als Array-Operator verwendet werden. document.form01.cb ist ebenfalls falsch, weil da die Klammern fehlen. Ebenso diese Variante: document.getElementsByName("cb"). Mit der übriggebliebenen - document.getElementsByName("cb[]") - hättest du aber ein Ergebnis bekommen sollen.

    console.log(document.getElementsByName("cb[]"));
    

    Das zeigt mit eine Nodelist mit 3 Elementen in der Konsole.

    PS: Die eckigen Klammern im Namen der Checkboxen benötige ich zur Übernahme nach PHP.

    Da gibt es nur ein Problem. Sag mir mal, welche beiden Elemente ich angehakt habe!

    Array (
        [cb] => Array (
                [0] => on
                [1] => on
            )
    )
    

    dedlfix.