Bug 1476345 - Disable the address form save button when the form is invalid. r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Wed, 15 Aug 2018 12:07:55 -0700
changeset 431802 e2ee996553e5edeeb93272ea543fe78051b31f37
parent 431801 ad4b3942b9af6445273427bf47e9033a07f6214f
child 431803 05b0499d4be328eb3330613f6c47752d70c09814
push id34451
push userebalazs@mozilla.com
push dateThu, 16 Aug 2018 09:25:15 +0000
treeherdermozilla-central@161817e6d127 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1476345
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1476345 - Disable the address form save button when the form is invalid. r=jaws Tests for this are in the next commit (e.g. test_address_form.html) because this can't be tested properly due to existing tests not filling fields in ways that fire input/change events. MozReview-Commit-ID: 62CckFP6Ou3
browser/components/payments/res/containers/address-form.js
browser/components/payments/res/containers/basic-card-form.js
browser/components/payments/test/browser/head.js
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -83,16 +83,22 @@ export default class AddressForm extends
       this.formHandler = new EditAddress({
         form,
       }, record, {
         DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
         getFormFormat: PaymentDialogUtils.getFormFormat,
         supportedCountries: PaymentDialogUtils.supportedCountries,
       });
 
+      // The EditAddress constructor adds `input` event listeners on the same element,
+      // which update field validity. By adding our event listeners after this constructor,
+      // validity will be updated before our handlers get the event
+      this.form.addEventListener("input", this);
+      this.form.addEventListener("invalid", this);
+
       this.body.appendChild(this.persistCheckbox);
       this.body.appendChild(this.genericErrorText);
 
       this.footer.appendChild(this.cancelButton);
       this.footer.appendChild(this.backButton);
       this.footer.appendChild(this.saveButton);
       // Only call the connected super callback(s) once our markup is fully
       // connected, including the shared form fetched asynchronously.
@@ -192,24 +198,34 @@ export default class AddressForm extends
       // Subtract 10px for the padding-top and padding-bottom.
       data.span.style.top = (data.top - 10) + "px";
       if (isRTL) {
         data.span.style.right = data.right + "px";
       } else {
         data.span.style.left = data.left + "px";
       }
     }
+
+    this.updateSaveButtonState();
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
+      case "input": {
+        this.onInput(event);
+        break;
+      }
+      case "invalid": {
+        this.onInvalid(event);
+        break;
+      }
     }
   }
 
   onClick(evt) {
     switch (evt.target) {
       case this.cancelButton: {
         paymentRequest.cancel();
         break;
@@ -226,42 +242,57 @@ export default class AddressForm extends
           state[previousId] = Object.assign({}, currentState[previousId], {
             preserveFieldValues: true,
           });
         }
         this.requestStore.setState(state);
         break;
       }
       case this.saveButton: {
-        this.saveRecord();
+        if (this.form.checkValidity()) {
+          this.saveRecord();
+        }
         break;
       }
       default: {
         throw new Error("Unexpected click target");
       }
     }
   }
 
+  onInput(event) {
+    this.updateSaveButtonState();
+  }
+
+  onInvalid(event) {
+    this.saveButton.disabled = true;
+  }
+
   updateRequiredState() {
-    for (let formElement of this.form.elements) {
-      let container = formElement.closest(`#${formElement.id}-container`);
-      if (formElement.localName == "button" || !container) {
+    for (let field of this.form.elements) {
+      let container = field.closest(`#${field.id}-container`);
+      if (field.localName == "button" || !container) {
         continue;
       }
       let span = container.querySelector("span");
       span.setAttribute("fieldRequiredSymbol", this.dataset.fieldRequiredSymbol);
-      let required = formElement.required && !formElement.disabled;
+      let required = field.required && !field.disabled;
       if (required) {
         container.setAttribute("required", "true");
       } else {
         container.removeAttribute("required");
       }
     }
   }
 
+  updateSaveButtonState() {
+    this.saveButton.disabled = !this.form.checkValidity();
+    log.debug("updateSaveButtonState", this.saveButton.disabled);
+  }
+
   async saveRecord() {
     let record = this.formHandler.buildFormObject();
     let currentState = this.requestStore.getState();
     let {
       page,
       tempAddresses,
       savedBasicCards,
       "address-page": addressPage,
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -81,17 +81,17 @@ export default class BasicCardForm exten
       let addresses = [];
       this.formHandler = new EditCreditCard({
         form,
       }, record, addresses, {
         isCCNumber: PaymentDialogUtils.isCCNumber,
         getAddressLabel: PaymentDialogUtils.getAddressLabel,
       });
 
-      // The EditCreditCard constructor adds input event listeners on the same element,
+      // The EditCreditCard constructor adds `input` event listeners on the same element,
       // which update field validity. By adding our event listeners after this constructor,
       // validity will be updated before our handlers get the event
       form.addEventListener("input", this);
       form.addEventListener("invalid", this);
 
       let fragment = document.createDocumentFragment();
       fragment.append(this.addressAddLink);
       fragment.append(" ");
--- a/browser/components/payments/test/browser/head.js
+++ b/browser/components/payments/test/browser/head.js
@@ -430,17 +430,17 @@ async function submitAddressForm(frame, 
 
     let oldAddresses = await PaymentTestUtils.DialogContentUtils.getCurrentState(content);
 
     // submit the form to return to summary page
     content.document.querySelector("address-form button:last-of-type").click();
 
     let currState = await PaymentTestUtils.DialogContentUtils.waitForState(content, (state) => {
       return state.page.id == "payment-summary";
-    }, "Switched back to payment-summary");
+    }, "submitAddressForm: Switched back to payment-summary");
 
     let savedCount = Object.keys(currState.savedAddresses).length;
     let tempCount = Object.keys(currState.tempAddresses).length;
     let oldSavedCount = Object.keys(oldAddresses.savedAddresses).length;
     let oldTempCount = Object.keys(oldAddresses.tempAddresses).length;
 
     if (options.isEditing) {
       is(tempCount, oldTempCount, "tempAddresses count didn't change");