Skip to content

Wszystko o WordPressie, tworzeniu stron — i nie tylko

🛠️ Niestandardowe widgety Elementor: pełny cykl od wtyczki do kontrolek

🛠️ Niestandardowe widgety Elementor: pełny cykl od wtyczki do kontrolek

Zaktualizowali Państwo motyw i niestandardowy blok, który wyświetlał promocje na stronie głównej, rozjechał się wizualnie. Znajoma sytuacja? Kod leży w functions.php, style są rozsmarowane po style.css, a obsługa JS przybita gwoździami do footer.php. Zmiana motywu lub duża aktualizacja Elementora i wszystko to trzeba składać od nowa.

Problem nie w tym, że „kod jest zły". Problem w tym, że znajduje się nie tam, gdzie powinien. Niestandardowe modyfikacje upychane w motywie są z nim trwale związane: przeniesienie na inną stronę jest niemożliwe, debugowanie konfliktów bolesne, a warunkowe ładowanie zasobów nie istnieje.

Właściwą ścieżką jest wydzielenie niestandardowego widżetu do osobnej wtyczki. Dokładnie tak oficjalna dokumentacja Elementora zaleca postępować z wszelkimi nietrywialnymi modyfikacjami. Izolacja kodu, niezależność od motywu, normalne wersjonowanie i ładowanie JS tylko na stronach z widżetem. W tym poradniku: pełny cykl, od struktury wtyczki po działający widżet z edycją inline, na aktualnym API Elementora (3.x/4.x).

Edytory kodu na ekranie komputera

💡 Szybki przegląd:

  • Rejestrujemy osobną wtyczkę dla widżetu, izolujemy kod od motywu raz na zawsze.
  • Budujemy główną klasę z kontrolą zgodności: Elementor aktywny, wersja nie niższa niż 3.5, PHP od 7.4.
  • Tworzymy klasę widżetu: dziedziczymy Widget_Base, definiujemy register_controls() i metodę renderowania.
  • Podłączamy edycję tekstu inline przez add_inline_editing_attributes() i szablon Backbone.
  • Dodajemy niestandardowe pola: wybór obrazu z biblioteki mediów, listy rozwijane, grupę przycisków, typografię.
  • Podpinamy obsługę JavaScript do haka elementor/frontend/init, punktu wejścia dla dowolnej logiki po stronie klienta.

Struktura wtyczki i główna klasa

Stwórzmy wtyczkę Elementor Awesomesauce. Minimalna struktura plików:

1elementor-awesomesauce/
2├── elementor-awesomesauce.php ← точка входа
3├── plugin.php ← класс-синглтон плагина
4├── widgets/
5│ └── awesomesauce.php ← класс виджета
6└── assets/
7 └── js/
8 └── awesomesauce.js ← JS-обработчик фронтенда

Główny plik wtyczki, standardowy nagłówek WordPress plus klasa ładująca z kontrolą zgodności. Właśnie taką strukturę opisuje poradnik Elementora dotyczący pierwszego addonu. Kod poniżej działa w Elementorze 3.5+ oraz Elementorze 4.x: metody bez przestarzałego prefiksu _, stałe dostosowane do współczesnych wersji PHP.

Proszę dodać ten kod do elementor-awesomesauce.php. Plik musi znajdować się w /wp-content/plugins/elementor-awesomesauce/. Przed aktywacją proszę wykonać pełną kopię zapasową strony.

1<?php
2/**
3 * Plugin Name: Elementor Awesomesauce
4 * Description: Кастомный виджет Elementor с живым редактированием текста, картинок и стилей.
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();

Co jest tu ważne. Stała MINIMUM_ELEMENTOR_VERSION, 3.5.0. Właśnie od tej wersji weszły w życie nowe konwencje nazewnictwa haków i metod bez prefiksu _. Dla Elementora 4.x (aktualna wersja na czerwiec 2026) ten sam kod działa bez zmian, API widżetów w 4.x nie zostało naruszone. PHP co najmniej 7.4, ale w praktyce 8.x jest już de facto standardem. Klasa zadeklarowana jako final: to punkt wejścia, nie ma potrzeby po niej dziedziczyć.

Klasa Plugin: singleton i rejestracja widżetu

Plik plugin.php, singleton, który podłącza zasoby JS i rejestruje widżet przez hak elementor/widgets/register. To główna zmiana w porównaniu ze starymi poradnikami: hak elementor/widgets/widgets_registered został uznany za przestarzały od wersji 3.5.0; metodę register_widget_type() zastąpiono metodą 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();

Proszę zwrócić uwagę: register_widgets przyjmuje parametr $widgets_manager. W nowym haku menedżer jest przekazywany bezpośrednio, a nie pobierany przez ElementorPlugin::instance()->widgets_manager. Czyściej i bez zbędnego importu.

Klasa widżetu: dziedziczenie, kontrolki i renderowanie

Plik widgets/awesomesauce.php, serce wtyczki. Dziedziczymy Widget_Base, definiujemy nazwę, tytuł, ikonę i kategorię. Metoda register_controls() dodaje trzy pola tekstowe: jednowierszowe, wielowierszowe i WYSIWYG. Metoda render() wyświetla znaczniki na frontendzie, a content_template() definiuje szablon Backbone dla podglądu na żywo w edytorze.

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' => __( 'Заголовок', 'elementor-awesomesauce' ),
45 ]
46 );
47
48 $this->add_control(
49 'description',
50 [
51 'label' => __( 'Description', 'elementor-awesomesauce' ),
52 'type' => Controls_Manager::TEXTAREA,
53 'default' => __( 'Описание', 'elementor-awesomesauce' ),
54 ]
55 );
56
57 $this->add_control(
58 'content',
59 [
60 'label' => __( 'Content', 'elementor-awesomesauce' ),
61 'type' => Controls_Manager::WYSIWYG,
62 'default' => __( 'Контент', '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}

Co zmieniło się w porównaniu z przestarzałymi poradnikami. Metody register_controls() i content_template() zapisuje się bez prefiksu _, to zmiana z Elementor 3.1. Do wyprowadzania w render() używa się print_render_attribute_string() i print_unescaped_setting() zamiast bezpośredniego echo, nowoczesne podejście rekomendowane od Elementor 3.x, które działa w 4.x. Ikonę zmieniono z fa fa-pencil (Font Awesome 4, usunięty z rdzenia) na eicon-pencil z natywnego zestawu Elementor.

Pola niestandardowe: media, selektory, typografia

Możliwości nie ograniczają się do pól tekstowych. Omówimy cztery typy kontrolek, które pokrywają większość rzeczywistych scenariuszy.

Pole mediów

Controls_Manager::MEDIA dodaje standardowy wybór obrazu z biblioteki mediów WordPress. Metoda Utils::get_placeholder_image_src() działa w aktualnych wersjach i zwraca szary placeholder, jeśli obraz nie został wybrany.

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 rozwijana

Controls_Manager::SELECT, wybór spośród wstępnie zdefiniowanych wartości. Poniżej przykład dla znacznika HTML nagłówka:

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

Grupa przycisków

Controls_Manager::CHOOSE pokazuje rząd ikon do wyboru wizualnego. Ikony, wyłącznie z zestawu eicon-*, nie 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);

Typografia przez Group Control

Grupowa kontrolka typografii daje pełen zestaw: czcionkę, rozmiar, odstępy między literami, grubość, a wszystko z adaptacyjnymi punktami granicznymi. Więcej szczegółów w dokumentacji Group_Control_Typography. Kluczowa kwestia: Scheme_Typography jest przestarzały od Elementor 3.x, proszę go nie używać. Zamiast tego należy albo wiązać się ze stylami globalnymi, albo całkowicie pomijać klucz 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);

Dlaczego wtyczka, a nie functions.php

Niestandardowy widget w motywie działa. Ale tylko do momentu zmiany motywu. W osobnej wtyczce otrzymują Państwo trzy istotne korzyści:

  • Ładowanie warunkowe. Elementor wywołuje widget_scripts tylko wtedy, gdy widget jest faktycznie wyświetlany na stronie, a nie na całej witrynie hurtowo. Dla projektów z dziesiątkami widgetów to odczuwalna oszczędność zapytań HTTP.
  • Izolacja. Logika PHP, CSS i JS nie mieszają się z motywem. Znalezienie i naprawienie błędu to minuty, a nie godziny przekopywania się przez functions.php liczący 2000 linii.
  • Przenośność. Aktywują Państwo wtyczkę na innej stronie i widget działa. Bez kopiowania i ręcznego poprawiania ścieżek.
Programista przy biurku z dwoma monitorami

Wideo: pokaz na żywo od pustej wtyczki do działającego widgetu

Teoria jest dobra, ale obserwowanie kodu w działaniu jest szybsze. W tym 30-minutowym poradniku autor przechodzi całą drogę: od pustego folderu do widgetu z kontrolkami i renderowaniem.

⁉️🤔 Często zadawane pytania

Dlaczego widget nie pojawia się w panelu Elementora?

Po pierwsze: proszę sprawdzić hook rejestracji. Od Elementora 3.5 używamy elementor/widgets/register (nie elementor/widgets/widgets_registered, jest on przestarzały). Po drugie: metoda register_controls() musi być bez prefiksu _. Po trzecie: get_categories() musi zwracać tablicę z istniejącą kategorią, 'general' działa zawsze. Po czwarte: proszę wyczyścić cache WordPressa po aktywacji wtyczki.

Czym print_render_attribute_string() różni się od bezpośredniego echo w render()?

print_render_attribute_string() automatycznie stosuje filtry atrybutów, w tym edycję inline i data-atrybuty Elementora. Bezpośrednie echo tych danych nie dostarczy, edycja inline po prostu się nie włączy. Do wyprowadzania wartości ustawień proszę używać print_unescaped_setting(), poprawnie obsługuje on eskejpowanie wewnątrz kontrolek.

Czy można obejść się bez osobnego pliku JS?

Tak, jeśli widget wyłącznie renderuje znaczniki PHP bez interaktywności. Ale gdy tylko pojawiają się slidery, animacje, ładowanie AJAX lub jakakolwiek dynamika po stronie klienta, JS jest obowiązkowy. Nawet minimalny handler (jak w przykładzie powyżej) daje punkt wejścia dla przyszłej logiki, nie wymagając późniejszego przepisywania rejestracji.

Czy namespace we wtyczce jest obowiązkowy?

Formalnie nie. Ale bez namespace ryzykują Państwo konflikt nazw klas z inną wtyczką lub motywem, nazwy w rodzaju Plugin czy Widget nie są unikalne. Dla produkcji: obowiązkowo. Prefiks ElementorAwesomesauceWidgets praktycznie gwarantuje unikalność.

Jak zaktualizować stary widget napisany według przestarzałych poradników?

Plan migracji opisano w oficjalnym poradniku Elementora dotyczącym deprekacji: (1) proszę zmienić nazwy metod z prefiksem _, _register_controls()register_controls(), _content_template()content_template(); (2) proszę zastąpić hook na elementor/widgets/register, a metodę rejestracji na $manager->register(); (3) proszę zastąpić ikony fa fa-* na eicon-*, usunąć Scheme_Typography. Po poprawkach proszę podnieść MINIMUM_ELEMENTOR_VERSION do aktualnej i przetestować widget na trzech poziomach: panel edytora, podgląd live, frontend.

Co zrobić ze starymi widgetami: plan migracji na aktualne API

Jeśli mają już Państwo własne widgety napisane według poradników z lat 2019-2021, proszę nie panikować. Elementor utrzymuje kompatybilność wsteczną z handlerami deprekacji do 8 głównych wersji. Ale lepiej zaktualizować kod teraz, zanim notyfikacje o deprekacji zamienią się w błędy krytyczne.

Checklista migracji:

  • **Metody bez **_: wszędzie, gdzie widzą Państwo _register_controls i _content_template, proszę usunąć prefiks.
  • Hook rejestracji: elementor/widgets/widgets_registeredelementor/widgets/register.
  • Metoda rejestracji: $manager->register_widget_type()$manager->register().
  • Ikony: fa fa-*eicon-* (natywny zestaw Elementora) lub dashicons-* (zestaw WordPressa).
  • Typografia: Scheme_Typography → albo style globalne przez 'global' => [...], albo bezpośrednie wartości bez klucza scheme.

Po zmianach proszę przetestować widget na trzech poziomach: czy panel z kontrolkami otwiera się w edytorze, czy działa podgląd live (szablon Backbone), czy frontend renderuje się bez błędów. I proszę pamiętać o dry-run przed wdrożeniem na produkcję: Elementor po cichu pomija uszkodzone kontrolki, nie wywracając całej strony, dlatego wizualna weryfikacja jest obowiązkowa.

Stary kod w functions.php czy wtyczka od zera: co wybrać

Jeśli zaczynają Państwo nowy projekt, tylko wtyczka. Argumentów „za" functions.php nie zostało: nawet dla mikro-widgeta z 20 linijkami struktura wtyczki zwraca się przy pierwszej aktualizacji motywu.

Jeśli mają już Państwo działający kod w motywie, proszę wydzielić go do wtyczki podczas najbliższego refaktoringu. Proces jest prosty: proszę utworzyć folder i główny plik wtyczki według struktury powyżej, przenieść klasę widgeta do widgets/, zarejestrować przez elementor/widgets/register i przetestować na środowisku stagingowym. W praktyce zajmuje to 15-20 minut dla typowego widgeta.

Proszę zacząć od klasy bazowej z tego poradnika, skopiować główny plik i plugin.php jako szkielet, zastąpić nazwę widgeta własną. A gdy pojawią się pytania, proszę zajrzeć do sekcji Widgets na developers.elementor.com: omówiono tam renderowanie mediów, pracę z polami typu repeater i optymalizację wyjścia.