%PDF- %PDF-
Direktori : /home/vacivi36/ava/lib/form/amd/src/ |
Current File : /home/vacivi36/ava/lib/form/amd/src/dynamicform.js |
// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Display an embedded form, it is only loaded and reloaded inside its container * * * @module core_form/dynamicform * @copyright 2019 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms * * @example * import DynamicForm from 'core_form/dynamicform'; * * const dynamicForm = new DynamicForm(document.querySelector('#mycontainer', 'pluginname\\form\\formname'); * dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => { * e.preventDefault(); * window.console.log(e.detail); * dynamicForm.container.innerHTML = 'Thank you, your form is submitted!'; * }); * dynamicForm.load(); * */ import * as FormChangeChecker from 'core_form/changechecker'; import * as FormEvents from 'core_form/events'; import Ajax from 'core/ajax'; import Fragment from 'core/fragment'; import Notification from 'core/notification'; import Pending from 'core/pending'; import Templates from 'core/templates'; import {get_strings as getStrings} from 'core/str'; import {serialize} from './util'; /** * @class core_form/dynamicform */ export default class DynamicForm { /** * Various events that can be observed. * * @type {Object} */ events = { // Form was successfully submitted - the response is passed to the event listener. // Cancellable (in order to prevent default behavior to clear the container). FORM_SUBMITTED: 'core_form_dynamicform_formsubmitted', // Cancel button was pressed. // Cancellable (in order to prevent default behavior to clear the container). FORM_CANCELLED: 'core_form_dynamicform_formcancelled', // User attempted to submit the form but there was client-side validation error. CLIENT_VALIDATION_ERROR: 'core_form_dynamicform_clientvalidationerror', // User attempted to submit the form but server returned validation error. SERVER_VALIDATION_ERROR: 'core_form_dynamicform_validationerror', // Error occurred while performing request to the server. // Cancellable (by default calls Notification.exception). ERROR: 'core_form_dynamicform_error', // Right after user pressed no-submit button, // listen to this event if you want to add JS validation or processing for no-submit button. // Cancellable. NOSUBMIT_BUTTON_PRESSED: 'core_form_dynamicform_nosubmitbutton', // Right after user pressed submit button, // listen to this event if you want to add additional JS validation or confirmation dialog. // Cancellable. SUBMIT_BUTTON_PRESSED: 'core_form_dynamicform_submitbutton', // Right after user pressed cancel button, // listen to this event if you want to add confirmation dialog. // Cancellable. CANCEL_BUTTON_PRESSED: 'core_form_dynamicform_cancelbutton', }; /** * Constructor * * Creates an instance * * @param {Element} container - the parent element for the form * @param {string} formClass full name of the php class that extends \core_form\modal , must be in autoloaded location */ constructor(container, formClass) { this.formClass = formClass; this.container = container; // Ensure strings required for shortforms are always available. getStrings([ {key: 'collapseall', component: 'moodle'}, {key: 'expandall', component: 'moodle'} ]).catch(Notification.exception); // Register delegated events handlers in vanilla JS. this.container.addEventListener('click', e => { if (e.target.matches('form input[type=submit][data-cancel]')) { e.preventDefault(); const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED, e.target); if (!event.defaultPrevented) { this.processCancelButton(); } } else if (e.target.matches('form input[type=submit][data-no-submit="1"]')) { e.preventDefault(); const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target); if (!event.defaultPrevented) { this.processNoSubmitButton(e.target); } } }); this.container.addEventListener('submit', e => { if (e.target.matches('form')) { e.preventDefault(); const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED); if (!event.defaultPrevented) { this.submitFormAjax(); } } }); } /** * Loads the form via AJAX and shows it inside a given container * * @param {Object} args * @return {Promise} * @public */ load(args = null) { const formData = serialize(args || {}); const pendingPromise = new Pending('core_form/dynamicform:load'); return this.getBody(formData) .then((resp) => this.updateForm(resp)) .then(pendingPromise.resolve); } /** * Triggers a custom event * * @private * @param {String} eventName * @param {*} detail * @param {Boolean} cancelable * @return {CustomEvent<unknown>} */ trigger(eventName, detail = null, cancelable = true) { const e = new CustomEvent(eventName, {detail, cancelable}); this.container.dispatchEvent(e); return e; } /** * Add listener for an event * * @param {array} args * @example: * const dynamicForm = new DynamicForm(...); * dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => { * e.preventDefault(); * window.console.log(e.detail); * dynamicForm.container.innerHTML = 'Thank you, your form is submitted!'; * }); */ addEventListener(...args) { this.container.addEventListener(...args); } /** * Get form body * * @param {String} formDataString form data in format of a query string * @private * @return {Promise} */ getBody(formDataString) { return Ajax.call([{ methodname: 'core_form_dynamic_form', args: { formdata: formDataString, form: this.formClass, } }])[0] .then(response => { return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}; }); } /** * On form submit * * @param {*} response Response received from the form's "process" method */ onSubmitSuccess(response) { const event = this.trigger(this.events.FORM_SUBMITTED, response); if (event.defaultPrevented) { return; } // Default implementation is to remove the form. Event listener should either remove or reload the form // since its contents is no longer correct. For example, if an element was created as a result of // form submission, the "id" in the form would be still zero. Also the server-side validation // errors from the previous submission may still be present. this.container.innerHTML = ''; } /** * On exception during form processing * * @private * @param {Object} exception */ onSubmitError(exception) { const event = this.trigger(this.events.ERROR, exception); if (event.defaultPrevented) { return; } Notification.exception(exception); } /** * Click on a "submit" button that is marked in the form as registerNoSubmitButton() * * @method submitButtonPressed * @param {Element} button that was pressed * @fires event:formSubmittedByJavascript */ processNoSubmitButton(button) { const pendingPromise = new Pending('core_form/dynamicform:nosubmit'); const form = this.getFormNode(); const formData = new URLSearchParams([...(new FormData(form)).entries()]); formData.append(button.getAttribute('name'), button.getAttribute('value')); FormEvents.notifyFormSubmittedByJavascript(form, true); // Add the button name to the form data and submit it. this.disableButtons(); this.getBody(formData.toString()) .then(this.updateForm) .then(pendingPromise.resolve) .catch(exception => this.onSubmitError(exception)); } /** * Get the form node from the Dialogue. * * @returns {HTMLFormElement} */ getFormNode() { return this.container.querySelector('form'); } /** * Notifies listeners that form dirty state should be reset. * * @fires event:formSubmittedByJavascript */ notifyResetFormChanges() { FormEvents.notifyFormSubmittedByJavascript(this.getFormNode(), true); FormChangeChecker.resetFormDirtyState(this.getFormNode()); } /** * Click on a "cancel" button */ processCancelButton() { // Notify listeners that the form is about to be submitted (this will reset atto autosave). this.notifyResetFormChanges(); const event = this.trigger(this.events.FORM_CANCELLED); if (!event.defaultPrevented) { // By default removes the form from the DOM. this.container.innerHTML = ''; } } /** * Update form contents * * @param {object} param * @param {string} param.html * @param {string} param.js * @returns {Promise} */ updateForm({html, js}) { return Templates.replaceNodeContents(this.container, html, js); } /** * Validate form elements * @return {Boolean} Whether client-side validation has passed, false if there are errors * @fires event:formSubmittedByJavascript */ validateElements() { // Notify listeners that the form is about to be submitted (this will reset atto autosave). FormEvents.notifyFormSubmittedByJavascript(this.getFormNode()); // Now the change events have run, see if there are any "invalid" form fields. const invalid = [...this.container.querySelectorAll('[aria-invalid="true"], .error')]; // If we found invalid fields, focus on the first one and do not submit via ajax. if (invalid.length) { invalid[0].focus(); return false; } return true; } /** * Disable buttons during form submission */ disableButtons() { this.container.querySelectorAll('form input[type="submit"]') .forEach(el => el.setAttribute('disabled', true)); } /** * Enable buttons after form submission (on validation error) */ enableButtons() { this.container.querySelectorAll('form input[type="submit"]') .forEach(el => el.removeAttribute('disabled')); } /** * Submit the form via AJAX call to the core_form_dynamic_form WS */ async submitFormAjax() { // If we found invalid fields, focus on the first one and do not submit via ajax. if (!(await this.validateElements())) { this.trigger(this.events.CLIENT_VALIDATION_ERROR, null, false); return; } this.disableButtons(); // Convert all the form elements values to a serialised string. const form = this.container.querySelector('form'); const formData = new URLSearchParams([...(new FormData(form)).entries()]); // Now we can continue... Ajax.call([{ methodname: 'core_form_dynamic_form', args: { formdata: formData.toString(), form: this.formClass } }])[0] .then((response) => { if (!response.submitted) { // Form was not submitted, it could be either because validation failed or because no-submit button was pressed. this.updateForm({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)}); this.enableButtons(); this.trigger(this.events.SERVER_VALIDATION_ERROR, null, false); } else { // Form was submitted properly. const data = JSON.parse(response.data); this.enableButtons(); this.notifyResetFormChanges(); this.onSubmitSuccess(data); } return null; }) .catch(exception => this.onSubmitError(exception)); } }