Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
// This file is part of Moodle - http://moodle.org/
2
//
3
// Moodle is free software: you can redistribute it and/or modify
4
// it under the terms of the GNU General Public License as published by
5
// the Free Software Foundation, either version 3 of the License, or
6
// (at your option) any later version.
7
//
8
// Moodle is distributed in the hope that it will be useful,
9
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
// GNU General Public License for more details.
12
//
13
// You should have received a copy of the GNU General Public License
14
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
15
 
16
/**
17
 * Display an embedded form, it is only loaded and reloaded inside its container
18
 *
19
 *
20
 * @module     core_form/dynamicform
21
 * @copyright  2019 Marina Glancy
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 * See also https://docs.moodle.org/dev/Modal_and_AJAX_forms
24
 *
25
 * @example
26
 *    import DynamicForm from 'core_form/dynamicform';
27
 *
28
 *    const dynamicForm = new DynamicForm(document.querySelector('#mycontainer', 'pluginname\\form\\formname');
29
 *    dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => {
30
 *        e.preventDefault();
31
 *        window.console.log(e.detail);
32
 *        dynamicForm.container.innerHTML = 'Thank you, your form is submitted!';
33
 *    });
34
 *    dynamicForm.load();
35
 *
36
 */
37
 
38
import * as FormChangeChecker from 'core_form/changechecker';
39
import * as FormEvents from 'core_form/events';
40
import Ajax from 'core/ajax';
41
import Fragment from 'core/fragment';
42
import Notification from 'core/notification';
43
import Pending from 'core/pending';
44
import Templates from 'core/templates';
45
import {getStrings} from 'core/str';
46
import {serialize} from './util';
47
 
48
/**
49
 * @class core_form/dynamicform
50
 */
51
export default class DynamicForm {
52
 
53
    /**
54
     * Various events that can be observed.
55
     *
56
     * @type {Object}
57
     */
58
    events = {
59
        // Form was successfully submitted - the response is passed to the event listener.
60
        // Cancellable (in order to prevent default behavior to clear the container).
61
        FORM_SUBMITTED: 'core_form_dynamicform_formsubmitted',
62
        // Cancel button was pressed.
63
        // Cancellable (in order to prevent default behavior to clear the container).
64
        FORM_CANCELLED: 'core_form_dynamicform_formcancelled',
65
        // User attempted to submit the form but there was client-side validation error.
66
        CLIENT_VALIDATION_ERROR: 'core_form_dynamicform_clientvalidationerror',
67
        // User attempted to submit the form but server returned validation error.
68
        SERVER_VALIDATION_ERROR: 'core_form_dynamicform_validationerror',
69
        // Error occurred while performing request to the server.
70
        // Cancellable (by default calls Notification.exception).
71
        ERROR: 'core_form_dynamicform_error',
72
        // Right after user pressed no-submit button,
73
        // listen to this event if you want to add JS validation or processing for no-submit button.
74
        // Cancellable.
75
        NOSUBMIT_BUTTON_PRESSED: 'core_form_dynamicform_nosubmitbutton',
76
        // Right after user pressed submit button,
77
        // listen to this event if you want to add additional JS validation or confirmation dialog.
78
        // Cancellable.
79
        SUBMIT_BUTTON_PRESSED: 'core_form_dynamicform_submitbutton',
80
        // Right after user pressed cancel button,
81
        // listen to this event if you want to add confirmation dialog.
82
        // Cancellable.
83
        CANCEL_BUTTON_PRESSED: 'core_form_dynamicform_cancelbutton',
84
    };
85
 
86
    /**
87
     * Constructor
88
     *
89
     * Creates an instance
90
     *
91
     * @param {Element} container - the parent element for the form
92
     * @param {string} formClass full name of the php class that extends \core_form\modal , must be in autoloaded location
93
     */
94
    constructor(container, formClass) {
95
        this.formClass = formClass;
96
        this.container = container;
97
 
98
        // Ensure strings required for shortforms are always available.
99
        getStrings([
100
            {key: 'collapseall', component: 'moodle'},
101
            {key: 'expandall', component: 'moodle'}
102
        ]).catch(Notification.exception);
103
 
104
        // Register delegated events handlers in vanilla JS.
105
        this.container.addEventListener('click', e => {
106
            if (e.target.matches('form input[type=submit][data-cancel]')) {
107
                e.preventDefault();
108
                const event = this.trigger(this.events.CANCEL_BUTTON_PRESSED, e.target);
109
                if (!event.defaultPrevented) {
110
                    this.processCancelButton();
111
                }
112
            } else if (e.target.matches('form input[type=submit][data-no-submit="1"]')) {
113
                e.preventDefault();
114
                const event = this.trigger(this.events.NOSUBMIT_BUTTON_PRESSED, e.target);
115
                if (!event.defaultPrevented) {
116
                    this.processNoSubmitButton(e.target);
117
                }
118
            }
119
        });
120
 
121
        this.container.addEventListener('submit', e => {
122
            if (e.target.matches('form')) {
123
                e.preventDefault();
124
                const event = this.trigger(this.events.SUBMIT_BUTTON_PRESSED);
125
                if (!event.defaultPrevented) {
126
                    this.submitFormAjax();
127
                }
128
            }
129
        });
130
    }
131
 
132
    /**
133
     * Loads the form via AJAX and shows it inside a given container
134
     *
135
     * @param {Object} args
136
     * @return {Promise}
137
     * @public
138
     */
139
    load(args = null) {
140
        const formData = serialize(args || {});
141
        const pendingPromise = new Pending('core_form/dynamicform:load');
142
        return this.getBody(formData)
143
        .then((resp) => this.updateForm(resp))
144
        .then(pendingPromise.resolve);
145
    }
146
 
147
    /**
148
     * Triggers a custom event
149
     *
150
     * @private
151
     * @param {String} eventName
152
     * @param {*} detail
153
     * @param {Boolean} cancelable
154
     * @return {CustomEvent<unknown>}
155
     */
156
    trigger(eventName, detail = null, cancelable = true) {
157
        const e = new CustomEvent(eventName, {detail, cancelable});
158
        this.container.dispatchEvent(e);
159
        return e;
160
    }
161
 
162
    /**
163
     * Add listener for an event
164
     *
165
     * @param {array} args
166
     * @example:
167
     *    const dynamicForm = new DynamicForm(...);
168
     *    dynamicForm.addEventListener(dynamicForm.events.FORM_SUBMITTED, e => {
169
     *        e.preventDefault();
170
     *        window.console.log(e.detail);
171
     *        dynamicForm.container.innerHTML = 'Thank you, your form is submitted!';
172
     *    });
173
     */
174
    addEventListener(...args) {
175
        this.container.addEventListener(...args);
176
    }
177
 
178
    /**
179
     * Get form body
180
     *
181
     * @param {String} formDataString form data in format of a query string
182
     * @private
183
     * @return {Promise}
184
     */
185
    getBody(formDataString) {
186
        return Ajax.call([{
187
            methodname: 'core_form_dynamic_form',
188
            args: {
189
                formdata: formDataString,
190
                form: this.formClass,
191
            }
192
        }])[0]
193
        .then(response => {
194
            return {html: response.html, js: Fragment.processCollectedJavascript(response.javascript)};
195
        });
196
    }
197
 
198
    /**
199
     * On form submit
200
     *
201
     * @param {*} response Response received from the form's "process" method
202
     */
203
    onSubmitSuccess(response) {
204
        const event = this.trigger(this.events.FORM_SUBMITTED, response);
205
        if (event.defaultPrevented) {
206
            return;
207
        }
208
 
209
        // Default implementation is to remove the form. Event listener should either remove or reload the form
210
        // since its contents is no longer correct. For example, if an element was created as a result of
211
        // form submission, the "id" in the form would be still zero. Also the server-side validation
212
        // errors from the previous submission may still be present.
213
        this.container.innerHTML = '';
214
    }
215
 
216
    /**
217
     * On exception during form processing
218
     *
219
     * @private
220
     * @param {Object} exception
221
     */
222
    onSubmitError(exception) {
223
        const event = this.trigger(this.events.ERROR, exception);
224
        if (event.defaultPrevented) {
225
            return;
226
        }
227
 
228
        Notification.exception(exception);
229
    }
230
 
231
    /**
232
     * Click on a "submit" button that is marked in the form as registerNoSubmitButton()
233
     *
234
     * @method submitButtonPressed
235
     * @param {Element} button that was pressed
236
     * @fires event:formSubmittedByJavascript
237
     */
238
    processNoSubmitButton(button) {
239
        const pendingPromise = new Pending('core_form/dynamicform:nosubmit');
240
        const form = this.getFormNode();
241
        const formData = new URLSearchParams([...(new FormData(form)).entries()]);
242
        formData.append(button.getAttribute('name'), button.getAttribute('value'));
243
 
244
        FormEvents.notifyFormSubmittedByJavascript(form, true);
245
 
246
        // Add the button name to the form data and submit it.
247
        this.disableButtons();
248
 
249
        this.getBody(formData.toString())
250
        .then(resp => this.updateForm(resp))
251
        .then(pendingPromise.resolve)
252
        .catch(exception => this.onSubmitError(exception));
253
    }
254
 
255
    /**
256
     * Get the form node from the Dialogue.
257
     *
258
     * @returns {HTMLFormElement}
259
     */
260
    getFormNode() {
261
        return this.container.querySelector('form');
262
    }
263
 
264
    /**
265
     * Notifies listeners that form dirty state should be reset.
266
     *
267
     * @fires event:formSubmittedByJavascript
268
     */
269
    notifyResetFormChanges() {
270
        FormEvents.notifyFormSubmittedByJavascript(this.getFormNode(), true);
271
        FormChangeChecker.resetFormDirtyState(this.getFormNode());
272
    }
273
 
274
    /**
275
     * Click on a "cancel" button
276
     */
277
    processCancelButton() {
278
        // Notify listeners that the form is about to be submitted (this will reset atto autosave).
279
        this.notifyResetFormChanges();
280
 
281
        const event = this.trigger(this.events.FORM_CANCELLED);
282
        if (!event.defaultPrevented) {
283
            // By default removes the form from the DOM.
284
            this.container.innerHTML = '';
285
        }
286
    }
287
 
288
    /**
289
     * Update form contents
290
     *
291
     * @param {object} param
292
     * @param {string} param.html
293
     * @param {string} param.js
294
     * @returns {Promise}
295
     */
296
    updateForm({html, js}) {
297
        return Templates.replaceNodeContents(this.container, html, js);
298
    }
299
 
300
    /**
301
     * Validate form elements
302
     * @return {Boolean} Whether client-side validation has passed, false if there are errors
303
     * @fires event:formSubmittedByJavascript
304
     */
305
    validateElements() {
306
        // Notify listeners that the form is about to be submitted (this will reset atto autosave).
307
        FormEvents.notifyFormSubmittedByJavascript(this.getFormNode());
308
 
309
        // Now the change events have run, see if there are any "invalid" form fields.
310
        const invalid = [...this.container.querySelectorAll('[aria-invalid="true"], .error')];
311
 
312
        // If we found invalid fields, focus on the first one and do not submit via ajax.
313
        if (invalid.length) {
314
            invalid[0].focus();
315
            return false;
316
        }
317
 
318
        return true;
319
    }
320
 
321
    /**
322
     * Disable buttons during form submission
323
     */
324
    disableButtons() {
325
        this.container.querySelectorAll('form input[type="submit"]')
326
            .forEach(el => el.setAttribute('disabled', true));
327
    }
328
 
329
    /**
330
     * Enable buttons after form submission (on validation error)
331
     */
332
    enableButtons() {
333
        this.container.querySelectorAll('form input[type="submit"]')
334
            .forEach(el => el.removeAttribute('disabled'));
335
    }
336
 
337
    /**
338
     * Submit the form via AJAX call to the core_form_dynamic_form WS
339
     */
340
    async submitFormAjax() {
341
        // If we found invalid fields, focus on the first one and do not submit via ajax.
342
        if (!(await this.validateElements())) {
343
            this.trigger(this.events.CLIENT_VALIDATION_ERROR, null, false);
344
            return;
345
        }
346
        this.disableButtons();
347
 
348
        // Convert all the form elements values to a serialised string.
349
        const form = this.container.querySelector('form');
350
        const formData = new URLSearchParams([...(new FormData(form)).entries()]);
351
 
352
        // Now we can continue...
353
        Ajax.call([{
354
            methodname: 'core_form_dynamic_form',
355
            args: {
356
                formdata: formData.toString(),
357
                form: this.formClass
358
            }
359
        }])[0]
360
        .then((response) => {
361
            if (!response.submitted) {
362
                // Form was not submitted, it could be either because validation failed or because no-submit button was pressed.
363
                this.updateForm({html: response.html, js: Fragment.processCollectedJavascript(response.javascript)});
364
                this.enableButtons();
365
                this.trigger(this.events.SERVER_VALIDATION_ERROR, null, false);
366
            } else {
367
                // Form was submitted properly.
368
                const data = JSON.parse(response.data);
369
                this.enableButtons();
370
                this.notifyResetFormChanges();
371
                this.onSubmitSuccess(data);
372
            }
373
            return null;
374
        })
375
        .catch(exception => this.onSubmitError(exception));
376
    }
377
}