Skip to content

Tutto per WordPress, lo sviluppo web — e non solo

🛠️ Widget Elementor personalizzati: ciclo completo dal plugin ai controlli

🛠️ Widget Elementor personalizzati: ciclo completo dal plugin ai controlli

L’aggiornamento del tema ha rotto il blocco personalizzato che mostrava le promozioni in homepage. Ti suona familiare? Il codice sta in functions.php, gli stili sono spalmati su style.css e il gestore JS è inchiodato a footer.php. Cambi tema o fai un aggiornamento importante di Elementor e devi ricostruire tutto da zero.

Il problema non è che «il codice è fatto male». Il problema è che risiede nel posto sbagliato. Le modifiche personalizzate infilate nel tema gli sono legate in modo permanente: trasferirle su un altro sito è impossibile, fare debug dei conflitti è doloroso e il caricamento condizionale degli asset è inesistente.

L’approccio corretto è impacchettare il widget personalizzato come plugin separato. È esattamente ciò che raccomanda la documentazione ufficiale di Elementor per qualsiasi personalizzazione non banale. Isolamento del codice, indipendenza dal tema, versionamento corretto e caricamento JS solo sulle pagine dove il widget è presente. In questa guida, il ciclo completo: dalla struttura del plugin a un widget funzionante con editing inline, sulle API attuali di Elementor (3.x/4.x).

Editor di codice su uno schermo di computer

💡 Panoramica rapida:

  • Registra un plugin separato per il widget, isola il codice dal tema una volta per tutte.
  • Costruisci la classe principale con controlli di compatibilità: Elementor attivo, versione non inferiore alla 3.5, PHP dalla 7.4.
  • Crea la classe del widget: estendi Widget_Base, definisci register_controls() e il metodo di render.
  • Collega l’editing inline del testo tramite add_inline_editing_attributes() e un template Backbone.
  • Aggiungi campi personalizzati: selettore immagini dalla libreria media, dropdown, gruppi di pulsanti, tipografia.
  • Aggancia il gestore JavaScript a elementor/frontend/init, il punto di ingresso per qualsiasi logica lato client.

Struttura del plugin e classe principale

Creeremo il plugin Elementor Awesomesauce. Struttura minima dei file:

1elementor-awesomesauce/
2├── elementor-awesomesauce.php ← entry point
3├── plugin.php ← plugin singleton class
4├── widgets/
5│ └── awesomesauce.php ← widget class
6└── assets/
7 └── js/
8 └── awesomesauce.js ← frontend JS handler

File principale del plugin, header standard di WordPress più una classe loader con controlli di compatibilità. Questa è esattamente la struttura descritta nella guida al primo addon Elementor. Il codice qui sotto funziona con Elementor 3.5+ ed Elementor 4.x: metodi senza il prefisso deprecato _, costanti per versioni PHP moderne.

Aggiungi questo codice a elementor-awesomesauce.php. Il file deve trovarsi in /wp-content/plugins/elementor-awesomesauce/. Prima di attivare, fai un backup completo del sito.

1<?php
2/**
3 * Plugin Name: Elementor Awesomesauce
4 * Description: Custom Elementor widget with live editing of text, images and styles.
5 * Plugin URI: https://techblog.sdstudio.top/
6 * Version: 1.0.0
7 * Author: TechBlog SD Studio
8 * Text Domain: elementor-awesomesauce
9 */
10
11if ( ! defined( 'ABSPATH' ) ) exit;
12
13final class Elementor_Awesomesauce {
14
15 const VERSION = '1.0.0';
16 const MINIMUM_ELEMENTOR_VERSION = '3.5.0';
17 const MINIMUM_PHP_VERSION = '7.4';
18
19 public function __construct() {
20 add_action( 'init', array( $this, 'i18n' ) );
21 add_action( 'plugins_loaded', array( $this, 'init' ) );
22 }
23
24 public function i18n() {
25 load_plugin_textdomain( 'elementor-awesomesauce' );
26 }
27
28 public function init() {
29 if ( ! did_action( 'elementor/loaded' ) ) {
30 add_action( 'admin_notices', array( $this, 'admin_notice_missing_main_plugin' ) );
31 return;
32 }
33
34 if ( ! version_compare( ELEMENTOR_VERSION, self::MINIMUM_ELEMENTOR_VERSION, '>=' ) ) {
35 add_action( 'admin_notices', array( $this, 'admin_notice_minimum_elementor_version' ) );
36 return;
37 }
38
39 if ( version_compare( PHP_VERSION, self::MINIMUM_PHP_VERSION, '<' ) ) {
40 add_action( 'admin_notices', array( $this, 'admin_notice_minimum_php_version' ) );
41 return;
42 }
43
44 require_once( 'plugin.php' );
45 }
46
47 public function admin_notice_missing_main_plugin() {
48 if ( isset( $_GET['activate'] ) ) {
49 unset( $_GET['activate'] );
50 }
51 $message = sprintf(
52 esc_html__( '"%1$s" requires "%2$s" to be installed and activated.', 'elementor-awesomesauce' ),
53 '<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>',
54 '<strong>' . esc_html__( 'Elementor', 'elementor-awesomesauce' ) . '</strong>'
55 );
56 printf( '<p>%1$s</p>', $message );
57 }
58
59 public function admin_notice_minimum_elementor_version() {
60 if ( isset( $_GET['activate'] ) ) {
61 unset( $_GET['activate'] );
62 }
63 $message = sprintf(
64 esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementor-awesomesauce' ),
65 '<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>',
66 '<strong>' . esc_html__( 'Elementor', 'elementor-awesomesauce' ) . '</strong>',
67 self::MINIMUM_ELEMENTOR_VERSION
68 );
69 printf( '<p>%1$s</p>', $message );
70 }
71
72 public function admin_notice_minimum_php_version() {
73 if ( isset( $_GET['activate'] ) ) {
74 unset( $_GET['activate'] );
75 }
76 $message = sprintf(
77 esc_html__( '"%1$s" requires "%2$s" version %3$s or greater.', 'elementor-awesomesauce' ),
78 '<strong>' . esc_html__( 'Elementor Awesomesauce', 'elementor-awesomesauce' ) . '</strong>',
79 '<strong>' . esc_html__( 'PHP', 'elementor-awesomesauce' ) . '</strong>',
80 self::MINIMUM_PHP_VERSION
81 );
82 printf( '<p>%1$s</p>', $message );
83 }
84}
85
86new Elementor_Awesomesauce();

Cosa è importante qui. La costante MINIMUM_ELEMENTOR_VERSION, 3.5.0. A partire da questa versione sono entrate in vigore nuove convenzioni per la denominazione di hook e metodi senza il prefisso _. Per Elementor 4.x (versione corrente a giugno 2026) questo stesso codice funziona senza modifiche, l’API dei widget nella 4.x non è stata rotta. PHP, minimo 7.4, ma in pratica la 8.x è già lo standard di fatto. La classe è dichiarata final: è il punto di ingresso, non serve ereditare da essa.

Classe del plugin: singleton e registrazione del widget

File plugin.php, un singleton che carica gli asset JS e registra il widget tramite l’hook elementor/widgets/register. Questa è la differenza principale rispetto alle vecchie guide: l’hook elementor/widgets/widgets_registered è stato dichiarato deprecato dalla versione 3.5.0, il metodo register_widget_type() è stato sostituito con register().

1<?php
2namespace ElementorAwesomesauce;
3
4use Elementor\Plugin as ElementorPlugin;
5
6class Plugin {
7
8 private static $_instance = null;
9
10 public static function instance() {
11 if ( is_null( self::$_instance ) ) {
12 self::$_instance = new self();
13 }
14 return self::$_instance;
15 }
16
17 public function widget_scripts() {
18 wp_register_script(
19 'elementor-awesomesauce',
20 plugins_url( '/assets/js/awesomesauce.js', __FILE__ ),
21 [ 'jquery' ],
22 false,
23 true
24 );
25 }
26
27 private function include_widgets_files() {
28 require_once( __DIR__ . '/widgets/awesomesauce.php' );
29 }
30
31 public function register_widgets( $widgets_manager ) {
32 $this->include_widgets_files();
33 $widgets_manager->register( new \ElementorAwesomesauceWidgets\Awesomesauce() );
34 }
35
36 public function __construct() {
37 add_action( 'elementor/frontend/after_register_scripts', [ $this, 'widget_scripts' ] );
38 add_action( 'elementor/widgets/register', [ $this, 'register_widgets' ] );
39 }
40}
41
42Plugin::instance();

Nota: register_widgets accetta un parametro $widgets_manager. Nel nuovo hook, il manager viene passato direttamente, non recuperato tramite ElementorPlugin::instance()->widgets_manager. Più pulito e senza l’import extra.

Classe del widget: ereditarietà, controlli e render

File widgets/awesomesauce.php, il cuore del plugin. Estendi Widget_Base, definisci nome, titolo, icona e categoria. Il metodo register_controls() aggiunge tre campi di testo: riga singola, multilinea e WYSIWYG. Il metodo render() produce il markup nel frontend, e content_template() definisce il template Backbone per l’anteprima live nell’editor.

1<?php
2namespace ElementorAwesomesauceWidgets;
3
4use Elementor\Widget_Base;
5use Elementor\Controls_Manager;
6
7if ( ! defined( 'ABSPATH' ) ) exit;
8
9class Awesomesauce extends Widget_Base {
10
11 public function get_name() {
12 return 'awesomesauce';
13 }
14
15 public function get_title() {
16 return __( 'Awesomesauce', 'elementor-awesomesauce' );
17 }
18
19 public function get_icon() {
20 return 'eicon-pencil';
21 }
22
23 public function get_categories() {
24 return [ 'general' ];
25 }
26
27 public function get_keywords() {
28 return [ 'awesomesauce', 'custom', 'demo' ];
29 }
30
31 protected function register_controls() {
32 $this->start_controls_section(
33 'section_content',
34 [
35 'label' => __( 'Content', 'elementor-awesomesauce' ),
36 ]
37 );
38
39 $this->add_control(
40 'title',
41 [
42 'label' => __( 'Title', 'elementor-awesomesauce' ),
43 'type' => Controls_Manager::TEXT,
44 'default' => __( 'Title', 'elementor-awesomesauce' ),
45 ]
46 );
47
48 $this->add_control(
49 'description',
50 [
51 'label' => __( 'Description', 'elementor-awesomesauce' ),
52 'type' => Controls_Manager::TEXTAREA,
53 'default' => __( 'Description', 'elementor-awesomesauce' ),
54 ]
55 );
56
57 $this->add_control(
58 'content',
59 [
60 'label' => __( 'Content', 'elementor-awesomesauce' ),
61 'type' => Controls_Manager::WYSIWYG,
62 'default' => __( 'Content', 'elementor-awesomesauce' ),
63 ]
64 );
65
66 $this->end_controls_section();
67 }
68
69 protected function render() {
70 $settings = $this->get_settings_for_display();
71
72 $this->add_inline_editing_attributes( 'title', 'none' );
73 $this->add_inline_editing_attributes( 'description', 'basic' );
74 $this->add_inline_editing_attributes( 'content', 'advanced' );
75 ?>
76 <div class="elementor-awesomesauce">
77 <h2 <?php $this->print_render_attribute_string( 'title' ); ?>>
78 <?php $this->print_unescaped_setting( 'title' ); ?>
79 </h2>
80 <div <?php $this->print_render_attribute_string( 'description' ); ?>>
81 <?php $this->print_unescaped_setting( 'description' ); ?>
82 </div>
83 <div <?php $this->print_render_attribute_string( 'content' ); ?>>
84 <?php $this->print_unescaped_setting( 'content' ); ?>
85 </div>
86 </div>
87 <?php
88 }
89
90 protected function content_template() {
91 ?>
92 <#
93 view.addInlineEditingAttributes( 'title', 'none' );
94 view.addInlineEditingAttributes( 'description', 'basic' );
95 view.addInlineEditingAttributes( 'content', 'advanced' );
96 #>
97 <div class="elementor-awesomesauce">
98 <h2 {{{ view.getRenderAttributeString( 'title' ) }}}>{{{ settings.title }}}</h2>
99 <div {{{ view.getRenderAttributeString( 'description' ) }}}>{{{ settings.description }}}</div>
100 <div {{{ view.getRenderAttributeString( 'content' ) }}}>{{{ settings.content }}}</div>
101 </div>
102 <?php
103 }
104}

Cosa è cambiato rispetto alle guide obsolete. I metodi register_controls() e content_template() si scrivono senza il prefisso _, una modifica introdotta con Elementor 3.1. Per l'output in render(), usa print_render_attribute_string() e print_unescaped_setting() invece di un echo diretto, un approccio moderno raccomandato da Elementor 3.x e funzionante nella 4.x. L'icona è stata sostituita da fa fa-pencil (Font Awesome 4, rimosso dal core) a eicon-pencil del set nativo di Elementor.

Campi personalizzati: media, select, tipografia

I campi di testo non esauriscono le possibilità. Vediamo quattro tipi di controllo che coprono la maggior parte degli scenari reali.

Campo media

Controls_Manager::MEDIA aggiunge la selezione standard delle immagini dalla libreria media di WordPress. Il metodo Utils::get_placeholder_image_src() funziona nelle versioni correnti e fornisce un placeholder grigio se non viene selezionata alcuna immagine.

1$this->add_control(
2 'mask_image',
3 [
4 'label' => __( 'Mask Image', 'elementor-awesomesauce' ),
5 'type' => Controls_Manager::MEDIA,
6 'default' => [
7 'url' => \Elementor\Utils::get_placeholder_image_src(),
8 ],
9 ]
10);

Lista a discesa

Controls_Manager::SELECT, scelta tra valori predefiniti. Di seguito un esempio per il tag HTML dell'intestazione:

1$this->add_control(
2 'title_tag',
3 [
4 'label' => __( 'Title HTML Tag', 'elementor-awesomesauce' ),
5 'type' => Controls_Manager::SELECT,
6 'default' => 'h2',
7 'options' => [
8 'h1' => 'H1',
9 'h2' => 'H2',
10 'h3' => 'H3',
11 'h4' => 'H4',
12 ],
13 ]
14);

Gruppo di pulsanti

Controls_Manager::CHOOSE mostra una fila di icone per una selezione visiva. Solo icone del set eicon-*, non fa fa-*:

1$this->add_control(
2 'text_align',
3 [
4 'label' => __( 'Alignment', 'elementor-awesomesauce' ),
5 'type' => Controls_Manager::CHOOSE,
6 'options' => [
7 'left' => [
8 'title' => __( 'Left', 'elementor-awesomesauce' ),
9 'icon' => 'eicon-text-align-left',
10 ],
11 'center' => [
12 'title' => __( 'Center', 'elementor-awesomesauce' ),
13 'icon' => 'eicon-text-align-center',
14 ],
15 'right' => [
16 'title' => __( 'Right', 'elementor-awesomesauce' ),
17 'icon' => 'eicon-text-align-right',
18 ],
19 ],
20 'default' => 'center',
21 'toggle' => true,
22 ]
23);

Tipografia tramite Group Control

Il group control per la tipografia fornisce il set completo: font, dimensione, spaziatura delle lettere, peso, il tutto con breakpoint responsive. Maggiori dettagli nella documentazione di Group_Control_Typography. Punto chiave: Scheme_Typography è deprecato da Elementor 3.x, non usarlo. Al suo posto, collega il controllo agli stili globali oppure ometti del tutto la chiave scheme.

1use Elementor\Group_Control_Typography;
2
3$this->add_group_control(
4 Group_Control_Typography::get_type(),
5 [
6 'name' => 'content_typography',
7 'label' => __( 'Typography', 'elementor-awesomesauce' ),
8 'selector' => '{{WRAPPER}} .elementor-awesomesauce',
9 'fields_options' => [
10 'letter_spacing' => [
11 'range' => [
12 'min' => 0,
13 'max' => 100,
14 ],
15 ],
16 ],
17 ]
18);

Perché un plugin e non functions.php

Un widget personalizzato nel tema funziona. Ma solo finché non cambi tema. In un plugin separato ottieni tre vantaggi importanti:

  • Caricamento condizionale. Elementor chiama widget_scripts solo quando il widget è effettivamente visualizzato nella pagina, non sull'intero sito. Per progetti con decine di widget, questo comporta un risparmio notevole di richieste HTTP.
  • Isolamento. La logica PHP, i CSS e il JS non si mescolano con il tema. Trovare e correggere un bug richiede minuti, non ore passate a scavare tra 2000 righe di functions.php.
  • Portabilità. Attivi il plugin su un altro sito e il widget funziona. Niente copia-incolla e modifiche manuali dei percorsi.
Sviluppatore alla scrivania con due monitor

Video: dimostrazione live da plugin vuoto a widget funzionante

La teoria è utile, ma vedere il codice in azione è più rapido. In questa guida di 30 minuti, l'autore percorre l'intero processo: da una cartella vuota a un widget con controlli e render.

⁉️🤔 Domande frequenti

Perché il widget non compare nel pannello di Elementor?

Per prima cosa, verifica l'hook di registrazione. Da Elementor 3.5 si usa elementor/widgets/register (non elementor/widgets/widgets_registered, che è deprecato). Secondo: il metodo register_controls() deve essere senza il prefisso _. Terzo: get_categories() deve restituire un array con una categoria esistente, 'general' funziona sempre. Quarto: svuota la cache di WordPress dopo aver attivato il plugin.

Qual è la differenza tra print_render_attribute_string() e l'echo diretto in render()?

print_render_attribute_string() applica automaticamente i filtri sugli attributi, inclusi l'editing inline e gli attributi dati di Elementor. Un echo diretto non fornirà questi dati, l'editing inline semplicemente non si attiverà. Per emettere i valori delle impostazioni, usa print_unescaped_setting(), che gestisce correttamente l'escape all'interno dei controlli.

Si può fare a meno di un file JS separato?

Sì, se il widget si limita a generare markup PHP senza interattività. Ma non appena compaiono slider, animazioni, caricamenti AJAX o qualsiasi dinamica lato client, il JS è obbligatorio. Anche un handler minimo (come nell'esempio sopra) fornisce un punto di ingresso per logiche future, senza dover riscrivere la registrazione in seguito.

Un namespace è obbligatorio nel plugin?

Formalmente no. Ma senza namespace rischi conflitti di nomi di classe con un altro plugin o tema, nomi come Plugin o Widget sono tutt'altro che unici. Per la produzione, è obbligatorio. Il prefisso ElementorAwesomesauceWidgets garantisce praticamente l'unicità.

Come aggiornare un vecchio widget scritto secondo guide obsolete?

Il piano di migrazione è descritto nella guida ufficiale alle deprecazioni di Elementor: (1) rinomina i metodi con il prefisso _, _register_controls()register_controls(), _content_template()content_template(); (2) sostituisci l'hook con elementor/widgets/register, e il metodo di registrazione con $manager->register(); (3) sostituisci le icone fa fa-* con eicon-*, rimuovi Scheme_Typography. Dopo le modifiche, aggiorna MINIMUM_ELEMENTOR_VERSION alla versione corrente e testa il widget su tre livelli: pannello editor, anteprima live, frontend.

Cosa fare con i vecchi widget: piano di migrazione all'API attuale

Se hai già widget personalizzati scritti secondo guide del 2019-2021, niente panico. Elementor mantiene la retrocompatibilità con gestori di deprecazione fino a 8 versioni major. Ma è meglio aggiornare il codice ora, prima che gli avvisi di deprecazione diventino errori fatali.

Checklist di migrazione:

  • Metodi senza _: ovunque vedi _register_controls e _content_template, rimuovi il prefisso.
  • Hook di registrazione: elementor/widgets/widgets_registeredelementor/widgets/register.
  • Metodo di registrazione: $manager->register_widget_type()$manager->register().
  • Icone: fa fa-*eicon-* (set nativo Elementor) o dashicons-* (set WordPress).
  • Tipografia: Scheme_Typography → o stili globali tramite 'global' => [...], oppure valori diretti senza la chiave scheme.

Dopo le modifiche, testa il widget su tre livelli: il pannello dei controlli si apre nell'editor, l'anteprima live funziona (template Backbone), il frontend viene renderizzato senza errori. E ricorda il dry-run prima del deploy in produzione: Elementor salta silenziosamente i controlli danneggiati senza mandare in crash l'intera pagina, quindi la verifica visiva è obbligatoria.

Vecchio codice in functions.php o plugin da zero: cosa scegliere

Se inizi un nuovo progetto, solo plugin. Non ci sono più argomenti "a favore" di functions.php: anche per un micro-widget di 20 righe, la struttura a plugin si ripaga al primo aggiornamento del tema.

Se hai già codice funzionante nel tema, estrailo in un plugin al prossimo refactoring. Il processo è semplice: crea una cartella e il file principale del plugin secondo la struttura sopra, sposta la classe del widget in widgets/, imposta la registrazione tramite elementor/widgets/register e testa in staging. In pratica, per un widget tipico ci vogliono 15-20 minuti.

Parti dalla classe base di questa guida, copia il file principale e plugin.php come scheletro, sostituisci il nome del widget con il tuo. E quando sorgono domande, consulta la sezione Widgets su developers.elementor.com: copre il rendering dei media, il lavoro con i campi repeater e l'ottimizzazione dell'output.