Skip to content

Tudo para WordPress, desenvolvimento web — e não só

🛠️ Widgets personalizados para o Elementor: ciclo completo do plugin aos controlos

🛠️ Widgets personalizados para o Elementor: ciclo completo do plugin aos controlos

A atualização do tema partiu o bloco personalizado que mostrava as promoções na página inicial. Parece-lhe familiar? O código vive no functions.php, os estilos estão espalhados pelo style.css e o handler de JS está pregado ao footer.php. Mude de tema ou faça uma atualização grande do Elementor e terá de reconstruir tudo isto do zero.

O problema não é «o código ser mau». O problema é que ele vive no sítio errado. As modificações personalizadas enfiadas no tema ficam permanentemente ligadas a ele: transferir para outro site é impossível, depurar conflitos é doloroso e o carregamento condicional de assets é inexistente.

A abordagem correta é empacotar o widget personalizado como um plugin separado. É exatamente isso que a documentação oficial do Elementor recomenda para qualquer personalização não trivial. Isolamento do código, independência do tema, versionamento adequado e carregamento de JS apenas nas páginas com o widget. Neste guia, o ciclo completo: da estrutura do plugin a um widget funcional com edição inline, na API atual do Elementor (3.x/4.x).

Editores de código num ecrã de computador

💡 Resumo rápido:

  • Registar um plugin separado para o widget, isolar o código do tema de uma vez por todas.
  • Construir a classe principal com verificações de compatibilidade: Elementor ativo, versão não inferior a 3.5, PHP a partir da 7.4.
  • Criar a classe do widget: estender Widget_Base, definir register_controls() e o método de renderização.
  • Ligar a edição de texto inline através de add_inline_editing_attributes() e de um template Backbone.
  • Adicionar campos personalizados: seletor de imagem da biblioteca de media, dropdowns, grupos de botões, tipografia.
  • Engatar o handler de JavaScript em elementor/frontend/init, o ponto de entrada para qualquer lógica do lado do cliente.

Estrutura do plugin e classe principal

Vamos criar o plugin Elementor Awesomesauce. Estrutura mínima de ficheiros:

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

Ficheiro principal do plugin, cabeçalho padrão do WordPress mais uma classe loader com verificações de compatibilidade. Esta é exatamente a estrutura descrita no guia do primeiro addon do Elementor. O código abaixo funciona no Elementor 3.5+ e no Elementor 4.x: métodos sem o prefixo _ obsoleto, constantes para versões modernas do PHP.

Adicione este código ao elementor-awesomesauce.php. O ficheiro deve estar em /wp-content/plugins/elementor-awesomesauce/. Antes de ativar, faça um backup completo do site.

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();

O que é importante aqui. A constante MINIMUM_ELEMENTOR_VERSION, 3.5.0. A partir desta versão, entraram em vigor novas convenções para a nomenclatura de hooks e métodos sem o prefixo _. Para o Elementor 4.x (versão atual em junho de 2026) este mesmo código funciona sem alterações, a API de widgets no 4.x não foi quebrada. PHP, mínimo 7.4, mas na prática o 8.x já é o padrão de facto. A classe é declarada final: este é o ponto de entrada, não há necessidade de herdar dela.

Classe do plugin: singleton e registo do widget

Ficheiro plugin.php, um singleton que carrega os assets JS e regista o widget através do hook elementor/widgets/register. Esta é a principal mudança em relação aos guias antigos: o hook elementor/widgets/widgets_registered foi declarado obsoleto desde a versão 3.5.0, o método register_widget_type() foi substituído por 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 aceita um parâmetro $widgets_manager. No novo hook, o gestor é passado diretamente, não é obtido através de ElementorPlugin::instance()->widgets_manager. Mais limpo e sem o import extra.

Classe do widget: herança, controlos e renderização

Ficheiro widgets/awesomesauce.php, o coração do plugin. Estender Widget_Base, definir nome, título, ícone e categoria. O método register_controls() adiciona três campos de texto: linha única, multilinha e WYSIWYG. O método render() produz o markup no frontend e content_template() define o template Backbone para a pré-visualização ao vivo no 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}

O que mudou relativamente aos guias desatualizados. Os métodos register_controls() e content_template() são escritos sem o prefixo _, esta é uma alteração desde o Elementor 3.1. Para a saída em render(), utilize print_render_attribute_string() e print_unescaped_setting() em vez de echo direto, uma abordagem moderna recomendada desde o Elementor 3.x e que funciona na 4.x. O ícone foi substituído de fa fa-pencil (Font Awesome 4, removido do núcleo) para eicon-pencil do conjunto nativo do Elementor.

Campos personalizados: media, seletores, tipografia

Os campos de texto não limitam as possibilidades. Vamos abordar quatro tipos de controlo que cobrem a maioria dos cenários reais.

Campo de media

Controls_Manager::MEDIA adiciona a seleção de imagem padrão da biblioteca de media do WordPress. O método Utils::get_placeholder_image_src() funciona nas versões atuais e fornece um placeholder cinzento se nenhuma imagem for selecionada.

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 suspensa

Controls_Manager::SELECT, escolha a partir de valores predefinidos. Abaixo está um exemplo para a tag HTML de cabeçalho:

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);

Grupo de botões

Controls_Manager::CHOOSE exibe uma linha de ícones para seleção visual. Ícones, apenas do conjunto eicon-*, não 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 através de Group Control

O group control de tipografia fornece o conjunto completo: tipo de letra, tamanho, espaçamento entre letras, peso, tudo com pontos de quebra responsivos. Mais detalhes na documentação do Group_Control_Typography. Ponto-chave: Scheme_Typography foi descontinuado desde o Elementor 3.x, não o utilize. Em vez disso, ligue a estilos globais ou omita completamente a chave 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);

Porquê um plugin e não o functions.php

Um widget personalizado no tema funciona. Mas apenas até mudar de tema. Num plugin separado obtém três vantagens importantes:

  • Carregamento condicional. O Elementor chama widget_scripts apenas quando o widget é realmente exibido na página, e não em todo o site. Para projetos com dezenas de widgets, isto representa uma poupança significativa em pedidos HTTP.
  • Isolamento. A lógica PHP, o CSS e o JS não se misturam com o tema. Encontrar e corrigir um bug demora minutos, não horas a escavar 2000 linhas no functions.php.
  • Portabilidade. Ative o plugin noutro site e o widget funciona. Sem copiar e colar e editar caminhos manualmente.
Programador numa secretária com dois monitores

Vídeo: demonstração ao vivo de um plugin vazio a um widget funcional

A teoria é boa, mas ver o código em ação é mais rápido. Neste guia de 30 minutos, o autor percorre todo o processo: de uma pasta vazia a um widget com controlos e renderização.

⁉️🤔 Perguntas frequentes

Porque é que o widget não aparece no painel do Elementor?

Primeiro, verifique o hook de registo. Desde o Elementor 3.5, utiliza-se elementor/widgets/register (e não elementor/widgets/widgets_registered, que está obsoleto). Segundo: o método register_controls() deve estar sem o prefixo _. Terceiro: get_categories() deve devolver um array com uma categoria existente, 'general' funciona sempre. Quarto: limpe a cache do WordPress após ativar o plugin.

Qual é a diferença entre print_render_attribute_string() e o echo direto no render()?

O print_render_attribute_string() aplica automaticamente filtros de atributos, incluindo edição inline e atributos de dados do Elementor. Um echo direto não fornecerá estes dados, a edição inline simplesmente não será ativada. Para imprimir valores de configuração, utilize print_unescaped_setting(), que trata corretamente do escape dentro dos controlos.

É possível funcionar sem um ficheiro JS separado?

Sim, se o widget se limitar a renderizar markup PHP sem interatividade. Mas assim que surgirem sliders, animações, carregamento AJAX ou qualquer dinâmica do lado do cliente, o JS é obrigatório. Mesmo um handler mínimo (como no exemplo acima) fornece um ponto de entrada para lógica futura, sem exigir a reescrita do registo mais tarde.

O namespace é obrigatório no plugin?

Formalmente, não. Mas sem um namespace, arrisca-se a ter conflitos de nomes de classes com outro plugin ou tema; nomes como Plugin ou Widget estão longe de ser únicos. Para produção, é obrigatório. O prefixo ElementorAwesomesauceWidgets garante praticamente a unicidade.

Como atualizar um widget antigo escrito segundo guias desatualizados?

O plano de migração está descrito no guia oficial de depreciações do Elementor: (1) renomeie os métodos com o prefixo _, _register_controls()register_controls(), _content_template()content_template(); (2) substitua o hook por elementor/widgets/register e o método de registo por $manager->register(); (3) substitua os ícones fa fa-* por eicon-*, remova Scheme_Typography. Após as edições, aumente a MINIMUM_ELEMENTOR_VERSION para a atual e teste o widget a três níveis: painel do editor, pré-visualização ao vivo e frontend.

O que fazer com widgets antigos: plano de migração para a API atual

Se já tem widgets personalizados escritos segundo guias de 2019-2021, não entre em pânico. O Elementor mantém compatibilidade retroativa com manipuladores de depreciação até 8 versões principais. Mas é melhor atualizar o código agora, antes que os avisos de depreciação se transformem em erros fatais.

Lista de verificação da migração:

  • **Métodos sem **_: em todo o lado onde vir _register_controls e _content_template, remova o prefixo.
  • Hook de registo: elementor/widgets/widgets_registeredelementor/widgets/register.
  • Método de registo: $manager->register_widget_type()$manager->register().
  • Ícones: fa fa-*eicon-* (conjunto nativo do Elementor) ou dashicons-* (conjunto do WordPress).
  • Tipografia: Scheme_Typography → ou estilos globais através de 'global' => [...], ou valores diretos sem a chave scheme.

Após as alterações, teste o widget a três níveis: o painel de controlos abre no editor, a pré-visualização ao vivo funciona (template Backbone), o frontend renderiza sem erros. E lembre-se do teste em seco antes de implementar em produção: o Elementor ignora silenciosamente controlos quebrados sem fazer crash da página inteira, pelo que a verificação visual é obrigatória.

Código antigo no functions.php ou plugin de raiz: o que escolher

Se está a começar um novo projeto, apenas um plugin. Já não há argumentos "a favor" do functions.php: mesmo para um micro-widget de 20 linhas, a estrutura de plugin compensa com a primeira atualização do tema.

Se já tem código funcional no tema, extraia-o para um plugin na próxima refatoração. O processo é simples: crie uma pasta e o ficheiro principal do plugin de acordo com a estrutura acima, mova a classe do widget para widgets/, configure o registo através de elementor/widgets/register e teste em staging. Na prática, isto demora 15 a 20 minutos para um widget típico.

Comece com a classe base deste guia, copie o ficheiro principal e o plugin.php como esqueleto, substitua o nome do widget pelo seu. E quando surgirem dúvidas, consulte a secção de Widgets em developers.elementor.com: aborda a renderização de media, o trabalho com campos repetidores e a otimização da saída.