Lorsque l'on souhaite afficher du contenu dynamique sous Drupal, il n'y a que 3 solutions :
- On ne met rien en cache sur la page : c'est désastreux en terme de performances, on oublie
- On utilise la fonctionnalité Lazy builder de Drupal.
- On charge les éléments via Ajax.
La solution la plus propre est l'utilisation d'une technique nommé "lazy_builder", voir le très bon article sur le blog hashbangcode qui traite du sujet. Son fonctionnement est le suivant : lorsque Drupal fait son rendu de page, il stream le rendu de la page au navigateur, et s'il trouve une balise placeholder générée par la fonctionalité lazy_builder, il fait appel à la méthode défini pour gérer le rendu de manière "dynamique". L'inconvénient de cette solution est qu'elle ne s'applique que pour des utilisateurs connectés. Les utilisateurs anonymes verront toujours le même contenu, le contenu "dynamique" ayant été lui-même mis en cache, et non son placeholder.
La seconde solution est d'utiliser les mécanismes Ajax en alliant les fonctionnalités "Drupal.ajax()" "AjaxResponse" de Drupal. L'avantage de cette seconde solution est que le contenu mis en cache pour les utilisateurs anonymes et authentifiés n'est que du markup simple qui va permettre de faire un appel asynchrone en Ajax. Il peut donc bien être mis en cache, et le résultat de la requête Ajax peut avoir un traitement de cache plus fin, voir inexistant.
Nous allons voir ici un exemple pour le mettre en place.
Voici l'arborescence de notre module que l'on nommera ajax_load_form_example :
Analyse de la solution
L'exemple ci-dessous fonctionne de la manière suivante :
Un bloc affiche une balise composée d'attributs data que notre script javascript va pouvoir utiliser pour faire un appel Drupal.ajax à une route qui génère notre formulaire.
Décortiquons les fichiers un par un :
Création du bloc
Ce bloc nous permet juste d'afficher notre formulaire où nous le souhaitons dans la page.
Nous choisissons l'identifiant du container qui permettra à Drupal.ajax() de savoir où charger notre formulaire, soit ajax-load-form-block-example.
Le container status_messages nous permet lors du traitement du formulaire d'afficher des messages directement dans le formulaire plutôt que dans le bloc de message par défaut de Drupal.
Le container container_for_form va nous permettre à la fois de configurer l'appel Drupal.ajax(), mais également de recevoir le formulaire généré.
Les attributs data permettent de configurer le chargement automatique par Ajax, qui lui est défini dans notre librairie ajax_autoload.
- data-ajax-autoload : contient l'url d'appel au formulaire, cette route doit retourner une AjaxResponse.
- data-ajax-autoload-selector : Défini le selecteur CSS où l'on souhaite afficher le résultat de notre traitement, et donc où l'on souhaite afficher le formulaire généré.
Fichier src/Plugin/Block/AjaxFormExampleBlock.php.
<?php
namespace Drupal\ajax_load_form_example\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Url;
/**
* Provides a block to show ajax building in action.
*
* @Block(
* id = "ajax_form_block_example",
* admin_label = @Translation("Ajax Form block example"),
* )
*/
class AjaxFormExampleBlock extends BlockBase {
public function build() {
$container_id = 'ajax-load-form-block-example';
$build = [
// This container will receive the form result messages.
'status_messages' => [
'#type' => 'container',
'#attributes' => [
'id' => 'form-status-messages',
],
],
// This container will call and receive the form.
'container_for_form' => [
'#type' => 'container',
'#attributes' => [
'id' => $container_id,
],
'load' => [
'#type' => 'container',
'#title' => t('Load form'),
'#attributes' => [
// Drupal ajax will automatically load this url.
'data-ajax-autoload' => Url::fromRoute('ajax_load_form_example.load_form')->toString(),
// Drupal ajax will use this selector to add result.
'data-ajax-autoload-selector' => "#$container_id",
],
'#attached' => [
'library' => [
// This library is needed to use ajax autoload.
'ajax_load_form_example/ajax_autoload',
],
],
],
],
];
return $build;
}
}
La librairie
Nous avons besoin d'un peu de javascript pour le chargement automatique en fonction des attributs data ajoutés précédemment dans notre bloc.
Cette librairie est suffisamment générique pour pouvoir être réutilisé par d'autres parties du site qui utiliserai la même logique fonctionnelle.
La définition de la librairie ajax_load_form_example.libraries.yml.
ajax_autoload:
version: 1.0
js:
js/ajaxAutoload.js: {}
dependencies:
- core/drupal
- core/drupal.ajax
- core/once
Le fichier javascript js/ajaxAutoload.js qui va appeler Drupal.ajax() en fonction de nos attributs data.
(function (Drupal, once) {
Drupal.behaviors.ajax_load_form_example_ajax_autoload = {
attach(context) {
const elements = once('ajax_autoload_loaded', '[data-ajax-autoload]', context);
// `elements` is always an Array.
elements.forEach(function (value, index) {
const url = new URL(window.location.origin + value.getAttribute('data-ajax-autoload'));
url.searchParams.set(
'ajax_autoload_selector',
value.getAttribute('data-ajax-autoload-selector')
);
Drupal.ajax({
url: url.href,
error: function (xhr) {
console.log('AJAX error when requesting ' + url.href + ': "' + xhr.status + ' - ' + xhr.statusText + '"');
}
}).execute();
});
}
};
}(Drupal, once));
Définissons notre route
Nous définissons simplement un controller ajax_load_form_example.load_form dans le fichier ajax_load_form_example.routing.yml
ajax_load_form_example.load_form:
path: '/ajax_load_form_example/loadForm'
defaults:
_controller: '\Drupal\ajax_load_form_example\Controller\LoadForm::index'
_title: 'Load the form'
requirements:
_permission: 'access content'
Le controller
Le controller est doit répondre via un AjaxResponse, c'est ce qu'attend notre appel Drupal.Ajax().
On Charge le formulaire souhaité, ici Drupal\ajax_load_form_example\Form\AjaxForm::class, que l'on va injecté dans le markup correspondant à la sélection CSS passé en paramètres et défini dans notre bloc : #ajax-load-form-block-example
Fichier src/Controller/LoadForm.php.
<?php
namespace Drupal\ajax_load_form_example\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Controller\ControllerBase;
class LoadForm extends ControllerBase {
public function index() {
// Response has to be an ajax object.
$response = new AjaxResponse();
// Build the specific form you want.
$form = $this->formBuilder()->getForm(\Drupal\ajax_load_form_example\Form\AjaxForm::class);
// Get the selector to know where to add the form.
// It has been defined in data-ajax-autoload-selector attribute.
$selector = \Drupal::request()->get('ajax_autoload_selector');
$response->addCommand(new HtmlCommand($selector, $form));
return $response;
}
}
Le formulaire
Voici notre formulaire d'exemple. Le plus important est de voir que l'action sur le bouton est géré en ajax par l'intermédiaire de l'attribut de tableau de rendu #ajax et callback. Ainsi, à la soumission du formulaire, la page ne sera pas rechargée mais la soumission sera faite en Ajax et appellera le callback défini, ici la méthode submitAjaxForm.
Il est important de s'assurer que notre formulaire dispose bien d'un #build_id, qui pourrait ne pas être présent suite au rechargement de notre formulaire.
L'élement du formulaire status_messages permet de recevoir les messages de retour de validation du formulaire. Autrement ils ne s'afficheraient pas à la soumission mais au prochain rechargement de la page.
La méthode submitAjaxForm étant appelé en Ajax, et comme nous ne souhaitons pas recharger toute la page, nous utiliseront bien entendu une AjaxResponse.
En dehors de la gestion des erreurs, la première réponse est de type MessageCommand, ce qui permet de valider que notre formulaire a été saisi et d'en informer l'utilisateur.
La seconde est de type HtmlCommand et ré-affiche le formulaire.
Le fichier src/Form/AjaxForm.php :
<?php
namespace Drupal\ajax_load_form_example\Form;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
class AjaxForm extends FormBase {
public function getFormId() {
return 'ajax_load_form_example_ajax_form';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form += [
'status_messages' => [
'#type' => 'status_messages',
'#include_fallback' => TRUE,
'#weight' => -1000,
],
'time' => [
'#title' => 'Time',
'#type' => 'textfield',
'#default_value' => time(),
],
];
// Be sure to have build_id to send it to ajax call which needs to refresh
// this form.
if (!isset($form['#build_id'])) {
$form['#build_id'] = 'form-' . Crypt::randomBytesBase64();
}
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Créer'),
'#ajax' => [
'callback' => '::submitAjaxForm',
],
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
public function submitForm(array &$form, FormStateInterface $form_state) {
}
public function submitAjaxForm(array &$form, FormStateInterface $form_state) {
$selector =\Drupal::request()->get('ajax_autoload_selector');
if ($form_state->hasAnyErrors()) {
$response = new AjaxResponse();
$response->addCommand(new HtmlCommand($selector, $form));
return $response;
}
$title = $this->t('Contenu créé, Time = %time.', ['%time' => $form_state->getValue('time')]);
$response = new AjaxResponse();
$response->addCommand(new MessageCommand($title, '#form-status-messages'));
$form['time']['#value'] = $form['time']['#default_value'] = time();
$response->addCommand(new HtmlCommand($selector, $form));
return $response;
}
}
Faites appel à nos experts et développeurs pour vos projets Drupal et PHP !
Nous intervenons sur vos sites internet en conseil, design, développement et hébergement.