get_field( 'temoignages', $post_id ) — cette seule ligne résume tout le problème : comment faire en sorte qu’un widget Elementor personnalisé restitue fidèlement un champ répéteur ACF Pro, avec sa mise en page propre, sans que l’éditeur ajoute une extension supplémentaire ni que le développeur réécrive une usine à gaz en JavaScript ?
Le cas est fréquent : un client utilise Elementor pour composer ses pages, et ACF Pro pour structurer des contenus répétés (témoignages, membres d’équipe, étapes d’un processus). Sans widget dédié, l’éditeur se retrouve à copier-coller des blocs de texte HTML dans un widget statique, ce qui casse la maintenance dès qu’un champ change. Ce tutoriel construit un widget Elementor qui lit directement les données ACF Pro, sans passer par un plugin tiers de type « ACF Elementor addon ».
Déclarer le widget et ses contrôles
Un widget Elementor s’enregistre en étendant \Elementor\Widget_Base. Le point clé est de ne pas dupliquer la structure du répéteur ACF dans les contrôles Elementor : on se contente d’un contrôle qui choisit le nom du champ ACF à afficher, laissant la structure des données côté ACF.
class WPM_Widget_Temoignages extends \Elementor\Widget_Base {
public function get_name() {
return 'wpm-temoignages-acf';
}
protected function register_controls() {
$this->start_controls_section( 'section_source', array(
'label' => __( 'Source ACF', 'wpm' ),
) );
$this->add_control( 'nom_champ_repeteur', array(
'label' => __( 'Nom du champ répéteur', 'wpm' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'temoignages',
) );
$this->end_controls_section();
}
}
Boucler sur le répéteur dans render()
La méthode render() est l’endroit où on résout réellement les données ACF Pro, avec get_field() plutôt que the_field() — cette dernière échappe directement à l’écran, ce qui empêche toute mise en forme via la structure de contrôles Elementor.

protected function render() {
$settings = $this->get_settings_for_display();
$lignes = get_field( $settings['nom_champ_repeteur'], get_the_ID() );
if ( empty( $lignes ) || ! is_array( $lignes ) ) {
echo '<p>' . esc_html__( 'Aucun témoignage renseigné.', 'wpm' ) . '</p>';
return;
}
echo '<div class="wpm-temoignages">';
foreach ( $lignes as $ligne ) {
printf(
'<blockquote><p>%s</p><cite>%s</cite></blockquote>',
esc_html( $ligne['citation'] ?? '' ),
esc_html( $ligne['auteur'] ?? '' )
);
}
echo '</div>';
}
Le piège du cache de rendu Elementor
Elementor met en cache le CSS et, selon la configuration, le HTML généré pour certains widgets afin d’accélérer l’affichage. Ce cache ignore par nature les changements de contenu ACF, puisque ce contenu ne transite pas par les contrôles Elementor eux-mêmes. Il faut donc accrocher une invalidation explicite sur le hook de sauvegarde ACF :
add_action( 'acf/save_post', function ( $post_id ) {
if ( class_exists( '\Elementor\Plugin' ) ) {
\Elementor\Plugin::$instance->files_manager->clear_cache();
}
}, 20 );
Sans cette ligne, un éditeur qui modifie un témoignage dans ACF peut constater que la page continue d’afficher l’ancienne version pendant plusieurs minutes, ce qui génère à coup sûr un ticket de support « le site est bloqué ».
Gérer les sous-champs de type image ou lien
- Un sous-champ image renvoie un tableau complet avec
ID,url,sizes: privilégierwp_get_attachment_image()plutôt que reconstruire la balise à la main. - Un sous-champ lien retourne
url,titleettarget: toujours vérifier!empty( $lien['url'] )avant d’imprimer un<a href>. - Un sous-champ relation (article, terme) doit être résolu avec
get_post()ouget_term(), jamais affiché brut sous forme d’ID.
Sur ce type de widget, la règle qu’on applique systématiquement : aucune donnée ACF n’est imprimée sans passer par une fonction d’échappement adaptée à son type —
esc_html()pour du texte,esc_url()pour un lien, jamais de concaténation brute.
Pour aller plus loin
Ce widget reste volontairement simple : un seul niveau de répéteur, un seul contrôle de nom de champ. Il peut évoluer vers un contrôle de sélection dynamique des champs disponibles via acf_get_field_groups(), ou vers un rendu par squelette PHP séparé pour faciliter la surcharge depuis un thème enfant. Mais le principe de base reste le même : laisser ACF Pro porter la structure de données, et laisser Elementor porter uniquement la présentation, sans jamais mélanger les deux responsabilités dans une seule classe.