heinetz: Formular per Ajax versenden

Hallo Forum,

ich habe in der Vergangenheit immer mal wieder Formulare per Ajax versendet, die Response ausgewertet und per JS darauf reagiert ... aber mein Development-Stack war ein LAMP-System und ich habe dazu jQuery und u.U. jQ-Plugins verwendet. Nun heisst meine Entwicklungsumgebung NODE.js und ich will kein jQuery verwenden, sondern ES6 und u.U. NODE-Module. Ich fühle mich in der Umgebung noch nicht wirklich sicher und bin bis hierher gekommen:

import BaseModule from '../../../../javascripts/helpers/baseModule';

export default class Commentsform extends BaseModule {
  constructor(element) {
    super(element, element.getAttribute('data-js-module'));

    this.getElements([
      { id: 'messageSubmit', selector: 'messageSubmit' }
    ]);

    this.bindEvents();
  }

  bindEvents() {
    this.elements.messageSubmit.addEventListener('click', this.submitForm.bind(this));
  }

  submitForm(event) {
    console.log('submitForm()');
    event.preventDefault();
    const request = url => {
      const ajax = new XMLHttpRequest();
      ajax.open('GET', url, true);
      ajax.send();
      ajax.onload = () => {
        console.log(ajax);
      };
    };
    request('/html/index.html');
  }
}

Was damit funktioniert:

Der Click auf den Submit-Button löst einen Ajax-Request aus und das Ajax-Object wird in der Console ausgegeben. Darin enthalten die Response …

Was fehlt:

  • Ich möchte das Formular per POST versenden.
  • Ich erwarte JSON als Antwort vom Server.
  • Das Formular wird zur Zeit mit der HTML5-Funktionalität des Browsers (required-Attribut etc.) validiert. Sobald ich das Verhalten des buttons per preventDefault abschalte, funktioniert die Validierung nicht. Lässt sich das vernünftig lösen?

Grundsätzlich...

... stelle ich mir die Frage, ob ich auf dem richtigen Weg bin. Sicher lässt sich das komplett "zu Fuss" machen. Z.B. werde ich das Formular ja serialisieren müssen.

Kann mir jemand dabei helfen, das sinnvoll zu Ende zu entwickeln?

danke und

gruss, heinetz

  1. Kann mir jemand dabei helfen, das sinnvoll zu Ende zu entwickeln?

    Ich würde nicht mit XMLHttpRequest, sondern mit fetch arbeiten und statt das Formular manuell zu serialisieren, würde ich FormData benutzen. Ohne das ganze objektorientierte Gedöns drumherum, würde das etwa so aussehen:

    function submit (form) {
        const {action, method} = form;
        const body = new FormData(form);
        return fetch(action, {method, body});
    }
    
    1. Danke

      Ich hatte auch schon von fetch gelesen, aber war davon abgekommen, wegen dem fehlenden IE11-Support ... hab aber nun einen Polyfill gefunden und werd's mal probieren.

      gruss, heinetz

      1. @@heinetz

        Ich hatte auch schon von fetch gelesen, aber war davon abgekommen, wegen dem fehlenden IE11-Support

        Gähn. Der Anteil der Nutzer ohne JavaScript dürfte inzwischen höher sein als der Anteil der IE-Nutzer.

        Progressive enhancement: Die Anwendung sollte auch ohne JavaScript funktionieren (Formular normal absenden, serverseitig verarbeiten und Ergebnisseite ausliefern), Verbesserung der UX bei Ausführung des JavaScripts.

        Wenn das Formular ohne JavaScript funktioniert, hast du auch gleich den Fallback für ältere Browser. Ob es sich lohnt, für IE noch weiteren Aufwand zu betreiben, obwohl die Seite ja funktioniert, musst du wissen.

        LLAP 🖖

        --
        „Wer durch Wissen und Erfahrung der Klügere ist, der sollte nicht nachgeben. Und nicht aufgeben.“ —Kurt Weidemann
    2. ... ich hab's mal ausprobiert:

        submitForm(event) {
          event.preventDefault();
          const { action, method } = this.elements.commentForm;
          const body = new FormData(this.elements.commentForm);
          console.log(fetch(action, { method, body }));
          //return fetch(action, {method, body});
        }
      

      Ich versuche, den Code zu interpretieren:

      • Die Konstante(n) { action, method } werden mit den Attribut-Werten des Formulars gefüllt.
      • In der Konstante body werden mit den Attribut-Werten des Formulars gefüllt.

      Die Console gibt zwei Zeilen aus:

      Promise {<pending>}
      POST http://localhost:8000/html/templates/[object%20HTMLInputElement] 404 (Not Found)
      

      Ich glaube, ich habe den (ersten) Fehler gefunden:

      Der POST-Request wird ja an eine etwas 'eigenartige' URL gesendet. Nachdem in der Konstante 'method' erwartungsgemäss der Wert aus dem Attribut 'method' stand, wunderte ich mich, warum das nicht analog mit dem Attribut 'action' funktionierte ... bis ich herausgefunden habe, dass das Formular ein Input-Feld vom type="hidden" mit dem Namen 'action' enthält.

      gruss, heinetz

      1. hi,

        POST http://localhost:8000/html/templates/[object%20HTMLInputElement] 404 (Not Found)

        404 muss ja nicht sein: Sende den POST an die Seite selbst. Hierzu lass das action-Attribut ganz einfach weg.

        bis ich herausgefunden habe, dass das Formular ein Input-Feld vom type="hidden" mit dem Namen 'action' enthält.

        Da liegt höchstens eine gedankliche Kollision vor. Technisch sind die Namen der inputfelder nicht relevant.

        MfG

      2. Der POST-Request wird ja an eine etwas 'eigenartige' URL gesendet. Nachdem in der Konstante 'method' erwartungsgemäss der Wert aus dem Attribut 'method' stand, wunderte ich mich, warum das nicht analog mit dem Attribut 'action' funktionierte ... bis ich herausgefunden habe, dass das Formular ein Input-Feld vom type="hidden" mit dem Namen 'action' enthält.

        WTF‽ Dachte erst das wäre ein Browser-Bug, denn die Spec ist bei action unmissverständlich was drin zu stehen hat. Anscheinend ignorieren Browser hier aber die Spec, aus Gründen der Abwärtskompatibilität.

        Die sauberste Lösung, die ich auf die Schnelle finden konnte, benutzt eine Kopie des Formulars ohne die Kind-Elemente:

        const {action, method} = form.cloneNode(false);
        
        1. Nicht hübsch, aber sauber ;)

          danke für den Tipp und

          gruss, heinetz

        2. Hello,

          const {action, method} = form;

          Die Notation war mir nicht bekannt. Auf diese Weise stehen mir also hinterher zwei Konstanten zur Verfügung, deren Werte den Attribut-Werte von form entsprechen.

          Das lässt sich doch sicher auch so formulieren, dass die Werte hinterher in einem Object:

          options = {
           action : 'value',
           method : 'value'
          }
          

          ... stehen, oder?

          gruss, heinetz

          1. const {action, method} = form;
            

            Die Notation war mir nicht bekannt. Auf diese Weise stehen mir also hinterher zwei Konstanten zur Verfügung, deren Werte den Attribut-Werte von form entsprechen.

            Richtig, das ist äquivalent zu:

            const action = form.action;
            const method = form.method;
            

            Das lässt sich doch sicher auch so formulieren, dass die Werte hinterher in einem Object:

            options = {
             action : 'value',
             method : 'value'
            }
            

            ... stehen, oder?

            Ja, zum Beispiel mit

            const options = {action, method};
            

            oder

            const options = {
                action: form.action,
                method: form.method
            }
            
            1. Ja, zum Beispiel mit

              const options = {
                  action: form.action,
                  method: form.method
              }
              

              Das ist klar.

              const options = {action, method};
              

              Damit stehen in options aber nicht die Argumente aus dem form.

              gruss, heinetz

              1. const options = {action, method};
                

                Damit stehen in options aber nicht die Argumente aus dem form.

                Das war missverständlich von mir, die Zeile sollte nicht für sich allein stehen, sondern nach der Konstanten-Deklaration für action und method.

    3. Meine Frage hat nur indirekt mit der fetch-API zu tun. Aber ich bleib dennoch im Thread. Ich habe das nun wie folgt umgesetzt und das funktioniert soweit auch. Aber unabhängig davon, dass ich das nicht sehr elegant finde, krieg ich Mecker von eslint wegen der unnamed function in der Methode handleResponse. Kann mir jemand verraten, wie ich mich "besser ausdrücken" kann?

      import BaseModule from '../../../../javascripts/helpers/baseModule';
      
      export default class Commentsform extends BaseModule {
        constructor(element) {
          super(element, element.getAttribute('data-js-module'));
      
          this.getElements([
            { id: 'messageSubmit', selector: 'messageSubmit' }
          ]);
      
          this.bindEvents();
        }
      
        bindEvents() {
          this.elements.messageSubmit.addEventListener('click', this.submitForm.bind(this));
        }
      
        submitForm(event) {
          event.preventDefault();
          const { action, method } = this.elements.commentForm.cloneNode(false);
          const body = new FormData(this.elements.commentForm);
          const headers = {
            'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
          };
          fetch(action, { method, headers, body }).then(this.handleResponse.bind(this));
        }
      
        handleResponse(response) {
          if (response.status === 200) {
            response.json().then(
              function(json) {
                this.response = json;
                this.showMessage();
              }.bind(this)
            );
          } else {
            this.response = {
              state: 'error',
              message: 'networkerror'
            };
            this.showMessage();
          }
        }
      
        showMessage() {
          console.log('showMessage()');
          console.log(this.response);
        }
      }
      

      danke und

      gruss, heinetz

      1. Meine Frage hat nur indirekt mit der fetch-API zu tun. Aber ich bleib dennoch im Thread. Ich habe das nun wie folgt umgesetzt und das funktioniert soweit auch. Aber unabhängig davon, dass ich das nicht sehr elegant finde, krieg ich Mecker von eslint wegen der unnamed function in der Methode handleResponse. Kann mir jemand verraten, wie ich mich "besser ausdrücken" kann?

        Die quick-and-dirty-Lösung wären Fat-Arrow-Functions zu verwenden wo this nicht dynamisch sein soll. Statt function(){}.bind(this) also () => {} zu benutzen.

        Also hieraus:

        response.json().then(
            function(json) {
                this.response = json;
                this.showMessage();
            }.bind(this)
        );
        

        könntest du das hier machen:

        response.json().then(json => {
            this.response = json;
            this.showMessage();
        });
        

        Schöner wäre es, den Kontrollfluss mit Promises zu entwirren und allen Handler-Funktionen aussagekräftige Namen zu geben. Folgenden Code hab ich mangels Zeit nicht getestet, aber er sollte die Funktionsweise verdeutlichen, Fehler kann ich gerade nicht ganz ausschließen:

        function submitAndHandleResponse(form) {
            return submit(form)
                .then(handleHttpResponse)
                .then(handleHttpSuccess)
                .then(handleJsonSuccess);
        }
        function submit(form) {
            const { action, method } = form.cloneNode(false);
            const body = new FormData(form);
            return fetch(action, { method, body });
        }
        function handleHttpResponse(response) {
            return response.ok
                ? Promise.resolve(response);
                : Promise.reject(response);
        }
        function handleHttpSuccess(response) {
            return response.json();
        }
        function handleJsonSuccess(json) {
            console.dir(json);
            return Promise.resolve(json);
        }
        

        In der Praxis wird das noch etwas komplexer, weil du zu jedem Promise auch noch einen Error-Handler haben möchtest.

        1. Vielen Dank! Ich musst mir das einige Male durchlesen, die Funktionsweise wird deutlich aber es erfordert doch einiges umdenken. Ich hab die Vorlage mal zu Ende und eingebaut:

            submitAndHandleResponse(event) {
              event.preventDefault();
              return this.submitForm()
                .then(this.handleHttpResponse)
                .then(this.handleHttpSuccess, this.handleHttpError)
                .then(this.handleJsonSuccess)
                .then(this.showMessage.bind(this));
            }
          
            submitForm() {
              const { action, method } = this.elements.commentForm.cloneNode(false);
              const body = new FormData(this.elements.commentForm);
              const headers = {
                'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
              };
              return fetch(action, { method, headers, body });
            }
          
            handleHttpResponse(response) {
              return response.ok ? Promise.resolve(response) : Promise.reject(response);
            }
          
            handleHttpSuccess(response) {
              return response.json();
            }
          
            handleHttpError(response) {
              return {
                state: 'error'
              };
            }
          
            handleJsonSuccess(json) {
              return Promise.resolve(json);
            }
          
            showMessage(json) {
              if (json.message) {
                this.elements.messageBox.innerHTML = json.message;
              }
              this.elements.self.dataset.state = json.state;
            }
          }
          

          So tut's genau das selbe, wie die Quick&Dirty-Lösung. Dennoch erschliesste sich mir der Mehrwert nicht.

          Gruss, heinetz

    • Das Formular wird zur Zeit mit der HTML5-Funktionalität des Browsers (required-Attribut etc.) validiert. Sobald ich das Verhalten des buttons per preventDefault abschalte, funktioniert die Validierung nicht. Lässt sich das vernünftig lösen?

    Ja, mit form.reportValidity()

  2. hi,

    ich habe in der Vergangenheit immer mal wieder Formulare per Ajax versendet, die Response ausgewertet und per JS darauf reagiert ... aber mein Development-Stack war ein LAMP-System und ich habe dazu jQuery und u.U. jQ-Plugins verwendet. Nun heisst meine Entwicklungsumgebung NODE.js und ich will kein jQuery verwenden, sondern ES6 und u.U. NODE-Module.

    Dann schau doch mal, ob da ein Serializer dabei ist.

    Ich möchte das Formular per POST versenden.

    ajax.open('POST',url);

    und

    ajax.send(serializedData);

    Wobei Du das Letztere auch selbst zusammenstellen kannst, das sind ja nur name=value Paare der Eingabefelder. Wobei beim Aneinanderhängen ein encodeURIComponent(value) erfolgen muss.

    MfG