Skip to content

Commit

Permalink
Forms: render sections
Browse files Browse the repository at this point in the history
  • Loading branch information
AdrienClairembault committed Jan 12, 2024
1 parent c90c316 commit a20c088
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 25 deletions.
223 changes: 223 additions & 0 deletions js/form_renderer_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/**
* ---------------------------------------------------------------------
*
* GLPI - Gestionnaire Libre de Parc Informatique
*
* http://glpi-project.org
*
* @copyright 2015-2023 Teclib' and contributors.
* @copyright 2003-2014 by the INDEPNET Development Team.
* @licence https://www.gnu.org/licenses/gpl-3.0.html
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* ---------------------------------------------------------------------
*/

/**
* Client code to handle users actions on the form_renderer template
*/
class GlpiFormRendererController
{
/**
* Target form (jquery selector)
* @type {string}
*/
#target;

/**
* Active section index
* @type {number}
*/
#section_index;

/**
* Total number of sections
* @type {number}
*/
#number_of_sections;

/**
* Create a new GlpiFormRendererController instance for the given target.
* The target must be a valid form.
*
* @param {string} target
*/
constructor(target) {
// Target must be a valid form
this.#target = target;
if ((this.#target).prop("tagName") != "FORM") {
console.error("Target must be a valid form");
}

// Init section data
this.#section_index = 0;
this.#number_of_sections = $(this.#target)
.find("[data-glpi-form-renderer-section]")
.length;

// Init event handlers
this.#initEventHandlers();
}

/**
* Init event handlers for each possible actions, identified by the data
* attribute "data-glpi-form-renderer-action".
*
* The available actions are:
* - "submit": submit the form
* - "next-section": go to the next section
* - "previous-section": go to the previous section
*
* () => fn() is used for most actions to keep the context of the
* GlpiFormRendererController instance
*/
#initEventHandlers() {
const action_attribute = "data-glpi-form-renderer-action";

// Submit form action
$(this.#target)
.find(`[${action_attribute}=submit]`)
.on("click", () => this.#submitForm());

// Next section form action
$(this.#target)
.find(`[${action_attribute}=next-section]`)
.on("click", () => this.#goToNextSection());

// Previous section form action
$(this.#target)
.find(`[${action_attribute}=previous-section]`)
.on("click", () => this.#goToPreviousSection());
}

/**
* Submit the target form using an AJAX request.
*
* The event "glpi-form-renderer-submit-success" is triggered on success,
* with the response as argument.
*
* The event "glpi-form-renderer-submit-failed" is triggered on failure,
* with the response as argument.
*/
async #submitForm() {
// Form will be sumitted using an AJAX request instead
try {
// Submit form using AJAX
const response = await $.post({
url: $(this.#target).prop("action"),
data: $(this.#target).serialize(),
});

// Success event
$(document).trigger('glpi-form-renderer-submit-success', response);
} catch (e) {
// Failure event
$(document).trigger('glpi-form-renderer-submit-failed', response);
}
}

/**
* Go to the next section of the form.
*/
#goToNextSection() {
// Hide current section
$(this.#target)
.find(`[data-glpi-form-renderer-section=${this.#section_index}]`)
.addClass("d-none");

// Show next section
this.#section_index++;
$(this.#target)
.find(`[data-glpi-form-renderer-section=${this.#section_index}]`)
.removeClass("d-none");

// Update actions visibility
this.#updateActionsVisiblity();
}

/**
* Go to the previous section of the form.
*/
#goToPreviousSection() {
// Hide current section
$(this.#target)
.find(`[data-glpi-form-renderer-section=${this.#section_index}]`)
.addClass("d-none");

// Show preview section
this.#section_index--;
$(this.#target)
.find(`[data-glpi-form-renderer-section=${this.#section_index}]`)
.removeClass("d-none");

// Update actions visibility
this.#updateActionsVisiblity();
}

/**
* Update the visibility of the actions buttons depending on the active
* section of the form.
*/
#updateActionsVisiblity() {
if (this.#section_index == 0) {
// First section, show next button
$(this.#target)
.find("[data-glpi-form-renderer-action=submit]")
.addClass("d-none");

$(this.#target)
.find("[data-glpi-form-renderer-action=next-section]")
.removeClass("d-none");

$(this.#target)
.find("[data-glpi-form-renderer-action=previous-section]")
.addClass("d-none");

} else if (this.#section_index == (this.#number_of_sections - 1)) { // Minus 1 because section_index is 0-based
// Last section, show submit and previous button
$(this.#target)
.find("[data-glpi-form-renderer-action=submit]")
.removeClass("d-none");

$(this.#target)
.find("[data-glpi-form-renderer-action=next-section]")
.addClass("d-none");

$(this.#target)
.find("[data-glpi-form-renderer-action=previous-section]")
.removeClass("d-none");

} else {
// Any middle section, show next and previous button
$(this.#target)
.find("[data-glpi-form-renderer-action=submit]")
.addClass("d-none");

$(this.#target)
.find("[data-glpi-form-renderer-action=next-section]")
.removeClass("d-none");

$(this.#target)
.find("[data-glpi-form-renderer-action=previous-section]")
.removeClass("d-none");
}
}
}
4 changes: 4 additions & 0 deletions src/Form/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
use Glpi\DBAL\QuerySubQuery;
use Glpi\Form\QuestionType\QuestionTypeShortAnswerText;
use Glpi\Form\QuestionType\QuestionTypesLoader;
use Html;
use Log;

/**
Expand Down Expand Up @@ -94,6 +95,9 @@ public function showForm($id, array $options = [])
}
$this->initForm($id, $options);

// We will be previewing forms from this page
echo Html::script("js/form_renderer_controller.js");

// Render twig template
$twig = TemplateRenderer::getInstance();
$twig->display('pages/admin/form/form_editor.html.twig', [
Expand Down
8 changes: 8 additions & 0 deletions src/Form/Renderer/FormRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ final class FormRenderer
*/
public function render(Form $form): string
{
// Note: the "form_renderer_controller" must not be loaded here as this code
// may be called multiple times using AJAX requests, thus trying to load the
// javascript "GlpiFormRendererController" class multiple times and causing an
// error.
// Each pages that call this method through AJAX must instead include the
// JS controller themselves.

// Load template
$twig = TemplateRenderer::getInstance();
return $twig->render('pages/admin/form/render_form.html.twig', [
'form' => $form,
Expand Down
2 changes: 1 addition & 1 deletion templates/pages/admin/form/form_editor.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@
});
// Handle preview successful submit
$(document).on('forms-form-submit-success', function(e, data) {
$(document).on('glpi-form-renderer-submit-success', function(e, data) {
// Close modal
$("#preview_form_modal").modal('hide');
Expand Down
78 changes: 54 additions & 24 deletions templates/pages/admin/form/render_form.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,43 @@
# ---------------------------------------------------------------------
#}

{# TODO: Maybe it will also be possible to render form outside of modal #}
{# In this case, the modal specific structure and classes references could be conditioned with a parameter #}
{# Is this a single or multi sections forms ? #}
{% set is_single_section_form = form.getSections()|length == 1 %}

<form id="forms_form_answers" method="POST" action="{{ path("ajax/form/answer.php") }}">
<div class="modal-header">
<h5 class="modal-title" id="display_form_modal_label">
{{ form.fields.name }}
</h5>
<div class="mt-3">
<h5 class="modal-title mb-3" id="display_form_modal_label">
{{ form.fields.name }}
</h5>

<div class="text-muted">
{{ form.fields.header|safe_html }}
</div>
</div>

<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ __("Close") }}"></button>
</div>

<div class="modal-body">
<div class="text-muted mb-4">
{{ form.fields.header|safe_html }}
</div>

{% for section in form.getSections() %}


{% for section in form.getSections() %}
{# Is this the first section of a form ? #}
{% set is_first_section = loop.index0 == 0 %}

{# Show only the first section of a multi section forms #}
<div
data-glpi-form-renderer-section="{{ loop.index0 }}"
class="{{ not is_single_section_form and not is_first_section ? "d-none": "" }}"
>
<h2 class="mb-3">{{ section.fields.name }}</h2>

{% for question in section.getQuestions() %}
{# Compute question type #}
{% set question_type = question.getQuestionType() %}

{# Skip unknown types (may be a disabled plugin) #}
{% if question_type is not null %}
<div class="mb-3">
Expand All @@ -64,28 +82,40 @@
</div>
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% endfor %}

<input type="hidden" name="forms_id" value="{{ form.fields.id }}">
<input type="hidden" name="forms_id" value="{{ form.fields.id }}">
</div>

<div class="modal-footer">
<button type="submit" class="btn btn-primary">{{ __("Send form") }}</button>
<button
type="button"
data-glpi-form-renderer-action="previous-section"
class="btn btn-primary d-none"
>
{{ __("Back") }}
</button>

<button
type="button"
data-glpi-form-renderer-action="next-section"
class="btn btn-primary {{ is_single_section_form ? "d-none": "" }}"
>
{{ __("Continue") }}
</button>

<button
type="button"
data-glpi-form-renderer-action="submit"
class="btn btn-primary {{ not is_single_section_form ? "d-none": "" }}"
>
{{ __("Send form") }}
</button>
</div>
</form>

<script>
$('#forms_form_answers').on('submit', async function(e) {
e.preventDefault();
try {
const response = await $.post({
url: $(this).prop("action"),
data: $(this).serialize(),
});
$(document).trigger('forms-form-submit-success', response);
} catch (e) {
$(document).trigger('forms-form-submit-failed', response);
}
});
new GlpiFormRendererController($('#forms_form_answers'));
</script>

0 comments on commit a20c088

Please sign in to comment.