Skip to content

Allt om WordPress, webbutveckling — och mer därtill

🛠️ Anpassade Elementor-widgets: hel cykel från plugin till kontroller

🛠️ Anpassade Elementor-widgets: hel cykel från plugin till kontroller

Temauppdateringen slog sönder det anpassade blocket som visade kampanjer på startsidan. Låter det bekant? Koden ligger i functions.php, stilar är utsmetade över style.css, och JS-hanteraren är fastspikad i footer.php. Byt tema eller gör en större Elementor-uppdatering, så måste du pussla ihop allt detta från grunden.

Problemet är inte att "koden är dålig". Problemet är att den ligger på fel ställe. Anpassade modifieringar som trycks in i temat är permanent knutna till det: att överföra till en annan sajt är omöjligt, felsökning av konflikter är plågsamt, och villkorlig inläsning av assets existerar inte.

Rätt tillvägagångssätt är att paketera den anpassade widgeten som ett separat plugin. Det är precis vad den officiella Elementor-dokumentationen rekommenderar för alla icke-triviala anpassningar. Kodisolering, temaoberoende, korrekt versionshantering och JS-inläsning endast på sidor med widgeten. I den här guiden, hela cykeln: från plugin-struktur till en live-widget med inline-redigering, på det aktuella Elementor-API:et (3.x/4.x).

Kodredigerare på en datorskärm

💡 Snabb översikt:

  • Registrera ett separat plugin för widgeten, isolera koden från temat en gång för alla.
  • Bygg huvudklassen med kompatibilitetskontroller: Elementor aktivt, version inte lägre än 3.5, PHP från 7.4.
  • Skapa widgetklassen: ärv Widget_Base, definiera register_controls() och renderingsmetoden.
  • Koppla upp inline-textredigering via add_inline_editing_attributes() och en Backbone-mall.
  • Lägg till anpassade fält: bildväljare från mediebiblioteket, dropdowns, knappgrupper, typografi.
  • Haka fast JavaScript-hanteraren på elementor/frontend/init, ingångspunkten för all klientlogik.

Plugin-struktur och huvudklass

Vi skapar pluginet Elementor Awesomesauce. Minimal filstruktur:

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

Huvudplugin-fil, standard WordPress-header plus en laddarklass med kompatibilitetskontroller. Detta är exakt den struktur som beskrivs i Elementors guide för första tillägg. Koden nedan fungerar i Elementor 3.5+ och Elementor 4.x: metoder utan det föråldrade _-prefixet, konstanter för moderna PHP-versioner.

Lägg till denna kod i elementor-awesomesauce.php. Filen ska ligga i /wp-content/plugins/elementor-awesomesauce/. Gör en fullständig säkerhetskopia av sajten innan aktivering.

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

Vad som är viktigt här. Konstanten MINIMUM_ELEMENTOR_VERSION, 3.5.0. Från och med denna version började nya konventioner för hook- och metodnamn utan _-prefixet att gälla. För Elementor 4.x (aktuell version i juni 2026) fungerar samma kod utan ändringar, widget-API:et i 4.x bröts inte. PHP, minimum 7.4, men i praktiken är 8.x redan de facto-standard. Klassen deklareras final: detta är ingångspunkten, inget behov av att ärva från den.

Plugin-klass: singleton och widget-registrering

Fil plugin.php, en singleton som laddar JS-assets och registrerar widgeten via hooken elementor/widgets/register. Detta är den huvudsakliga förändringen jämfört med gamla guider: hooken elementor/widgets/widgets_registered förklarades föråldrad från version 3.5.0, metoden register_widget_type() ersattes med 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();

Notera: register_widgets tar en $widgets_manager-parameter. I den nya hooken skickas managern in direkt, inte hämtas via ElementorPlugin::instance()->widgets_manager. Renare och utan den extra importen.

Widget-klass: arv, kontroller och rendering

Fil widgets/awesomesauce.php, hjärtat i pluginet. Ärv Widget_Base, definiera namn, titel, ikon och kategori. Metoden register_controls() lägger till tre textfält: enkelrad, flerrad och WYSIWYG. Metoden render() matar ut markup på frontend, och content_template() definierar Backbone-mallen för live-förhandsvisning i editorn.

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}

Vad som har ändrats jämfört med föråldrade guider. Metoderna register_controls() och content_template() skrivs utan prefixet _, detta är en ändring sedan Elementor 3.1. För utdata i render(), använd print_render_attribute_string() och print_unescaped_setting() istället för direkt echo, ett modernt tillvägagångssätt som rekommenderas sedan Elementor 3.x och fungerar i 4.x. Ikon ersatt från fa fa-pencil (Font Awesome 4, borttagen ur kärnan) till eicon-pencil från Elementors egna ikonuppsättning.

Anpassade fält: media, select, typografi

Textfält begränsar inte möjligheterna. Vi går igenom fyra kontrolltyper som hanterar de flesta verkliga scenarier.

Mediafält

Controls_Manager::MEDIA lägger till standardbildval från WordPress mediebibliotek. Metoden Utils::get_placeholder_image_src() fungerar i aktuella versioner och tillhandahåller en grå platshållare om ingen bild har valts.

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

Rullgardinslista

Controls_Manager::SELECT, val från fördefinierade värden. Nedan är ett exempel för HTML-rubriknivå:

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

Knappgrupp

Controls_Manager::CHOOSE visar en rad med ikoner för visuellt val. Ikoner, endast från eicon-*-uppsättningen, inte 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);

Typografi via gruppkontroll

Gruppkontrollen för typografi tillhandahåller hela uppsättningen: teckensnitt, storlek, teckenavstånd, vikt, allt med responsiva brytpunkter. Mer information i dokumentationen för Group_Control_Typography. Viktig punkt: Scheme_Typography har varit föråldrat sedan Elementor 3.x, använd det inte. Länka istället till globala stilar eller utelämna nyckeln scheme helt.

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

Varför ett plugin och inte functions.php

En anpassad widget i temat fungerar. Men bara tills du byter tema. I ett separat plugin får du tre viktiga fördelar:

  • Villkorlig inläsning. Elementor anropar widget_scripts endast när widgeten faktiskt visas på sidan, inte över hela webbplatsen. För projekt med dussintals widgets är detta en märkbar besparing i HTTP-anrop.
  • Isolering. PHP-logik, CSS och JS blandas inte med temat. Att hitta och åtgärda en bugg tar minuter, inte timmar av grävande genom 2000 rader i functions.php.
  • Portabilitet. Aktivera pluginet på en annan webbplats, widgeten fungerar. Ingen kopiering och manuell redigering av sökvägar.
Utvecklare vid ett skrivbord med två skärmar

Video: livedemonstration från tomt plugin till fungerande widget

Teori är bra, men att se kod i aktion går snabbare. I denna 30-minutersguide går författaren igenom hela processen: från en tom mapp till en widget med kontroller och rendering.

⁉️🤔 Vanliga frågor

Varför syns inte widgeten i Elementor-panelen?

Kontrollera först registreringshooken. Sedan Elementor 3.5 används elementor/widgets/register (inte elementor/widgets/widgets_registered, den är föråldrad). För det andra: metoden register_controls() måste vara utan prefixet _. För det tredje: get_categories() måste returnera en array med en befintlig kategori, 'general' fungerar alltid. För det fjärde: rensa WordPress cache efter att du har aktiverat pluginet.

Vad är skillnaden mellan print_render_attribute_string() och direkt echo i render()?

print_render_attribute_string() tillämpar automatiskt attributfilter, inklusive inline-redigering och Elementors dataattribut. Direkt echo ger inte denna data, inline-redigering aktiveras helt enkelt inte. För att skriva ut inställningsvärden, använd print_unescaped_setting(), den hanterar escaping korrekt inuti kontroller.

Klarar man sig utan en separat JS-fil?

Ja, om widgeten enbart renderar PHP-uppmärkning utan interaktivitet. Men så fort reglage, animationer, AJAX-laddning eller någon klientdynamik förekommer är JS obligatoriskt. Även en minimal hanterare (som i exemplet ovan) ger en ingångspunkt för framtida logik, utan att kräva att registreringen skrivs om senare.

Är en namespace obligatorisk i pluginet?

Formellt sett nej. Men utan en namespace riskerar du att få klassnamnskonflikter med ett annat plugin eller tema, namn som Plugin eller Widget är långt ifrån unika. För produktion, obligatoriskt. Prefixet ElementorAwesomesauceWidgets garanterar praktiskt taget unikhet.

Hur uppdaterar man en gammal widget skriven enligt föråldrade guider?

Migreringsplanen beskrivs i den officiella Elementor-guiden för föråldrad funktionalitet: (1) byt namn på metoder med prefixet _, _register_controls()register_controls(), _content_template()content_template(); (2) ersätt hooken med elementor/widgets/register, och registreringsmetoden med $manager->register(); (3) ersätt fa fa-*-ikoner med eicon-*, ta bort Scheme_Typography. Efter redigeringarna, höj MINIMUM_ELEMENTOR_VERSION till aktuell och testa widgeten på tre nivåer: redigeringspanel, liveförhandsvisning, frontend.

Vad man gör med gamla widgets: migreringsplan till det aktuella API:et

Om du redan har anpassade widgets skrivna enligt guider från 2019-2021, få inte panik. Elementor upprätthåller bakåtkompatibilitet med hanterare för föråldrad kod i upp till 8 huvudversioner. Men det är bättre att uppdatera koden nu, innan varningar om föråldrad kod förvandlas till fatala fel.

Migreringschecklista:

  • **Metoder utan **_: överallt där du ser _register_controls och _content_template, ta bort prefixet.
  • Registreringshook: elementor/widgets/widgets_registeredelementor/widgets/register.
  • Registreringsmetod: $manager->register_widget_type()$manager->register().
  • Ikoner: fa fa-*eicon-* (Elementors egna uppsättning) eller dashicons-* (WordPress uppsättning).
  • Typografi: Scheme_Typography → antingen globala stilar via 'global' => [...], eller direkta värden utan nyckeln scheme.

Efter ändringarna, testa widgeten på tre nivåer: öppnas kontrollpanelen i redigeraren, fungerar liveförhandsvisningen (Backbone-mall), renderas frontenden utan fel. Och kom ihåg torrkörning innan driftsättning till prod: Elementor hoppar tyst över trasiga kontroller utan att krascha hela sidan, så visuell verifiering är obligatorisk.

Gammal kod i functions.php eller plugin från grunden: vad ska man välja

Om du startar ett nytt projekt, endast ett plugin. Det finns inga argument kvar "för" functions.php: även för en mikrowidget på 20 rader lönar sig plugin-strukturen vid första temauppdateringen.

Om du redan har fungerande kod i temat, extrahera den till ett plugin vid nästa refaktorisering. Processen är okomplicerad: skapa en mapp och en huvudplugin-fil enligt strukturen ovan, flytta widget-klassen till widgets/, sätt upp registrering via elementor/widgets/register och testa på staging. I praktiken tar detta 15-20 minuter för en typisk widget.

Börja med basklassen från denna guide, kopiera huvudfilen och plugin.php som ett skelett, ersätt widgetnamnet med ditt eget. Och när frågor uppstår, kolla Widgets-sektionen på developers.elementor.com: den täcker mediarendering, arbete med repeater-fält och optimering av utdata.