Uploaded image for project: 'Moodle'
  1. Moodle
  2. MDL-80608

Modal forms: Not possible to handle errors during form definition

XMLWordPrintable

    • MOODLE_402_STABLE
    • MOODLE_404_STABLE
    • MDL-80608-main
    • Hide

      I have a situation where in some cases, the parameters to the form (to simplify, let's say the form is 'delete_thingy') are invalid. This most often occurs if somebody else has already deleted the thingy, between when the user loaded the page listing thingies and when they clicked to delete it.

      In its definition, the form ends up calling something like $thingy = thingy::get($thingyid) where $thingyid is a form parameter. This throws an exception because the thingy no longer exists.

      I would like to show a nicer error in this case.

      Here is the workaround I am using, in the form:

      protected function definition() {
          // ...
          try {
              // ...all the existing definition, some of which (at start) throws exception...
          } catch (\coding_exception $e) {
              // If there is an error finding data required, we show a 'loadingerror' element, which
              // is used by JavaScript to detect the error and automatically cancel the form.
              $mform->addElement('static', 'loadingerror', '', '<div hidden>' . s($e->getMessage()) . '</div>');
          }
      }
      

      And in the JavaScript just after showing the modal form:

      // This event happens when the form has actually loaded. It is a jQuery event and doesn't get
      // sent natively.
      jQuery(document.body).one(ModalEvents.bodyRendered, async() => {
          // Check if the form contains an error. We can't handle exceptions if they occur during
          // form loading, so the form adds a special item instead of throwing an exception, and
          // we can then show it as a modal.
          if (modalForm.getFormNode().querySelector('div[id^=fitem_id_loadingerror]')) {
              modalForm.modal.destroy();
              const modal = await ModalFactory.create({
                  type: ModalFactory.types.ALERT,
                  title: getString(...),
                  body: getString(...),
                  removeOnClose: true,
              });
              modal.show();
              modal.getRoot().on(ModalEvents.hidden, () => {
                  location.reload();
              });
          }
      });
      

      You might be wondering why the event listens on document.body, this is obviously rubbish but without doing that you actually need another event listener ModalForm.events.LOADED so that the modal exists (I'm not sure you can even get at the modal from there anyway, but mabe you can) but I couldn't be bothered with nested events.

      This gives the same user experience you'd get if the ERROR event works (popup form starts to appear, then goes away and you get an error modal instead) but the code is a bit ick.

      Show
      I have a situation where in some cases, the parameters to the form (to simplify, let's say the form is 'delete_thingy') are invalid. This most often occurs if somebody else has already deleted the thingy, between when the user loaded the page listing thingies and when they clicked to delete it. In its definition, the form ends up calling something like $thingy = thingy::get($thingyid) where $thingyid is a form parameter. This throws an exception because the thingy no longer exists. I would like to show a nicer error in this case. Here is the workaround I am using, in the form: protected function definition() { // ... try { // ...all the existing definition, some of which (at start) throws exception... } catch (\coding_exception $e) { // If there is an error finding data required, we show a 'loadingerror' element, which // is used by JavaScript to detect the error and automatically cancel the form. $mform->addElement('static', 'loadingerror', '', '<div hidden>' . s($e->getMessage()) . '</div>'); } } And in the JavaScript just after showing the modal form: // This event happens when the form has actually loaded. It is a jQuery event and doesn't get // sent natively. jQuery(document.body).one(ModalEvents.bodyRendered, async() => { // Check if the form contains an error. We can't handle exceptions if they occur during // form loading, so the form adds a special item instead of throwing an exception, and // we can then show it as a modal. if (modalForm.getFormNode().querySelector('div[id^=fitem_id_loadingerror]')) { modalForm.modal.destroy(); const modal = await ModalFactory.create({ type: ModalFactory.types.ALERT, title: getString(...), body: getString(...), removeOnClose: true, }); modal.show(); modal.getRoot().on(ModalEvents.hidden, () => { location.reload(); }); } }); You might be wondering why the event listens on document.body, this is obviously rubbish but without doing that you actually need another event listener ModalForm.events.LOADED so that the modal exists (I'm not sure you can even get at the modal from there anyway, but mabe you can) but I couldn't be bothered with nested events. This gives the same user experience you'd get if the ERROR event works (popup form starts to appear, then goes away and you get an error modal instead) but the code is a bit ick.
    • Hide
      1. The attached mdl80608.zip is a local plugin. Expand the zip and drop the mdl80608 directory inside 'local' in your Moodle installation, then visit admin page to install.
      2. Visit [Your moodle site url]/local/mdl80608/ to test. There should be 3 buttons.
      3. Click the 'submission exception' button.
      4. Click on 'Save changes'.
      5. Close the error popup that appears.
        • Confirm that 'Cancel' and 'Save changes' buttons are enabled (these would be disabled pre-patch).
      6. Close all popups.
      7. Click the 'definition exception' button.
        • Confirm you do not see Moodle's default exception popup, and instead see a popup with the contents 'This would usualbly be a nice error message...Exception during definition'.
      Show
      The attached mdl80608.zip is a local plugin. Expand the zip and drop the mdl80608 directory inside 'local' in your Moodle installation, then visit admin page to install. Visit [Your moodle site url] /local/mdl80608/ to test. There should be 3 buttons. Click the 'submission exception' button. Click on 'Save changes'. Close the error popup that appears. Confirm that 'Cancel' and 'Save changes' buttons are enabled (these would be disabled pre-patch). Close all popups. Click the 'definition exception' button. Confirm you do not see Moodle's default exception popup, and instead see a popup with the contents 'This would usualbly be a nice error message...Exception during definition'.

      When using modal forms, a number of events are available, including ERROR. The ModalForms.ERROR event, according to its documentation, is:

      'Error occurred while performing request to the server.'

      What it actually does is detect any error during the AJAX call to core_form_dynamic_form when submitting the form. Whoever catches the event can either process and cancel it, or leave it alone in which case Notification.Exception will be called to display the exception.

      This is great but there is no way to do anything similar during the AJAX call made to core_form_dynamic_form when originally loading the form - in show() via getBody and setBodyContent, or in processNoSubmitButton(). Both these cases just call Notification.Exception with no event.

      Based on the comment on the ERROR event, I think it should be called in both those other cases as well. This would be a slight change in behaviour, so arguably it might be more backwards-compatible to create a different event (DEFINITION_ERROR perhaps). However, if people agree that is not a big concern, then perhaps we can just fix the simple bug - that the in-code documentation currently claims that the ERRROR event is fired for errors while carrying out server requests, but in fact it only sometimes is.

      While fixing this I am also going to fix a minor secondary problem, which is that if an exception occurs during submission (that's the one you could already trap), it doesn't re-enable the buttons, so after closing the [default or overridden] exception popup, you can't e.g. change the form contents and resubmit, because all buttons are greyed out.

            quen Sam Marshall
            quen Sam Marshall
            David Woloszyn David Woloszyn
            Huong Nguyen Huong Nguyen
            Kim Jared Lucas Kim Jared Lucas
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated:
              Resolved:

                Estimated:
                Original Estimate - 0 minutes
                0m
                Remaining:
                Remaining Estimate - 0 minutes
                0m
                Logged:
                Time Spent - 1 hour, 52 minutes
                1h 52m

                  Error rendering 'clockify-timesheets-time-tracking-reports:timer-sidebar'. Please contact your Jira administrators.