Block Entwicklung

Aus wissen.becker-heiko.de

Hier sammle ich ein paar Infos zur Blockentwicklung für das Projekt Gutenberg in WordPress. Da Blöcke immer in einem PlugIn untergebracht werden müssen, ist eventuell auch der Artikel PlugIn-Entwicklung für dich interessant.

Als Leitfaden dient auch gerne das offizielle Block-Editor Handbuch von WordPress.

Die Zielstellung

Hier werde ich einen WordPress-Block entwickeln, der eine Infobox erstellt.

  • Diese Infobox erhält eine Texteingabe für die Überschriftenleiste.
  • Der Content der Infobox soll durch die Verwendung von den in WP enthaltenen Standard-Blöcken erstellt werden können.
  • Außerdem kann der Redakteur in der Block-Einstellung ein Styling auswählen (Anmerkung, Info und Warnung).
  • Der Block wird in einem eigenen Plugin integriert bzw. entwickelt.
WP-Block Infobox - Info
HB Infobox - Info
WP-Block Infobox - Anmerkung
HB Infobox - Anmerkung
WP-Block Infobox - Warnung
HB Infobox - Warnung
Infobox - Einstellungen
Infobox - Einstellungen

Voraussetzungen

Ich gehe davon aus, dass ihr

  • eine lokale WordPress-Installation laufen und
  • Node sowie NPM (kommt in der Regel mit der Node-Installation mit) installiert habt.

Einen Block erstellen

Hier wählen wir den einfachen und schnellen Weg und setzen Create Guten Block von Ahmad Awais ein. Dabei handelt es sich um ein Zero Configuration Dev-Toolkit für WordPress Gutenberg Blocks, sodass wir React, Webpack und Babel nicht selbst konfigurieren müssen.

Die Installation erfolgt über die Kommandozeile. Begebe dich dazu als Erstes in das Plugin-Verzeichnis deiner lokalen WordPress Website.

cd wp-content/plugins

Anschließend kannst du dein Block Plugin mit dem Befehl npx create-guten-block erstellen. Dem Kommando wird außerdem der Name für das WordPress Plugin übergeben. Du kannst einen beliebigen Namen für dein PlugIn wählen. Hier verwenden wir hb-blocks.

npx create-guten-block hb-blocks

Das Tool legt für uns den Plugin-Ordner an und installiert alle benötigten Node Module. Die Installation kann allerdings ein paar Minuten in Anspruch nehmen.

Plugin erstellung.png

Das Toolkit hat das Plugin erstellt, welches bereits einen Dummy-Block enthält..

Block Plugin in WordPress aktivieren

Nach Abschluss der Installation kannst du das Plugin in deinem WordPress Backend unter "Plugins" aktivieren:

Plugin aktivieren
Plugin aktivieren

Dummy-Block in Beitrag einfügen

Das Toolkit erstellt einen Dummy-Block. Im Gutenberg Editor kannst du nach CGB Block suchen, um diesen einzufügen. Der Block zeigt nur etwas Text, ohne Eingabefelder oder Optionen. Wir werden später den Namen ändern und den Block editierbar machen.

Dummy-Block einfügen

Struktur des WordPress Block Plugins

Die PlugIn-Struktur
Die PlugIn-Struktur

Unser PlugIn trägt den Namen hb-blocks. Im Verzeichnis /wp-content/plugins wird also das Verzeichnis hb-blocks (PlugIn-Hauptverzeichnis) erstellt. Im Unterverzeichnis /src wird das Verzeichnis infobox erstellt. Darin befinden sich die eigentlichen Daten für die Blockentwicklung.

PHP Dateien

Im PlugIn-Hauptverzeichnis werden in der plugin.php die Blöcke initialisiert, bzw auf die init.php im Verzeichnis /src weitergeleitet:

<?php
/**
 * Plugin Name: HB Blocks
 * Plugin URI: https://becker-heiko.de/
 * Description: HB Blocks - A collection of blocks for the Gutenberg editor.
 * Author: Heiko Becker
 * Author URI: https://becker-heiko.de/
 * Version: 0.1.0
 * License: GPL2+
 * License URI: https://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain: hb-blocks
 * Domain Path: /languages
 *
 * @package CGB
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Block Initializer.
 */
require_once plugin_dir_path( __FILE__ ) . 'src/init.php';

In der /src/init.php werden die Styles und Scripte für die Blöcke initialisiert. Aus Gründen der Übersichtlichkeit habe ich die meisten Kommentare entfernt:

<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}
function hb_blocks_cgb_block_assets() { 
        // Register block styles for both frontend + backend.
	wp_register_style(
		'hb_blocks-cgb-style-css',
		plugins_url( 'dist/blocks.style.build.css', dirname( __FILE__ ) ), 
		is_admin() ? array( 'wp-editor' ) : null, null
	);

        // Register block editor script for backend.
	wp_register_script(
		'hb_blocks-cgb-block-js', 
		plugins_url( '/dist/blocks.build.js', dirname( __FILE__ ) ), 
		array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor' ), 
		null, 
		true
	);

        // Register block editor styles for backend.
	wp_register_style(
		'hb_blocks-cgb-block-editor-css', 
		plugins_url( 'dist/blocks.editor.build.css', dirname( __FILE__ ) ), 
		array( 'wp-edit-blocks' ), 
		null 
	);

        // WP Localized globals. Use dynamic PHP stuff in JavaScript via `cgbGlobal` object.
	wp_localize_script(
		'hb_blocks-cgb-block-js',
		'cgbGlobal',
		[
			'pluginDirPath' => plugin_dir_path( __DIR__ ),
			'pluginDirUrl'  => plugin_dir_url( __DIR__ ),
		]
	);

        // Register Gutenberg block on server-side.
	register_block_type(
		'cgb/block-hb-blocks', array(
			'style'         => 'hb_blocks-cgb-style-css',
			'editor_script' => 'hb_blocks-cgb-block-js',
			'editor_style'  => 'hb_blocks-cgb-block-editor-css',
		)
	);
}

add_action( 'init', 'hb_blocks_cgb_block_assets' );

Ordner dist/

In diesem Verzeichnis finden sich die JavaScript- und css-Dateien, die während des Build-Vorganges für die produktive Verwendung des Plugins erstellt werden.

Ordner src/

In diesem Verzeichnis finden sich die für die Blockentwicklung zuständigen Dateien.

Das Verzeichnis /block beinhaltet den Dummy-Block. Dieses Verzeichnis nutze ich gerne als Basis für meine eigenen Blöcke. Darum lösche ich es erst, wenn das PlugIn veröffentlicht werden soll.

Im Verzeichnis /infobox entwickeln wir den Infobox-Block aus dieser Infosammlung.

Block Development mit create-guten-block

Die /src/infobox/block.js registriert den Block und wird so aussehen:

/**
 * WordPress dependencies
 */
const { registerBlockType } = wp.blocks;
const { __ } = wp.i18n;

/**
 * Internal dependencies
 */
import './style.scss';
import Edit from './edit';
import save from './save';

/**
 * Register block
 */
registerBlockType( 'hb-blocks/infobox',
	{
		title: __( 'HB Infobox', 'hb-blocks' ),
		icon: 'edit-page',
		description: 'Create your own infoblock.',
		category: 'widgets',

		attributes: {
			title: {
				type: 'string',
				source: 'html',
				selector: '.hb-infobox-header',
			},
			selectedStyle: {
				type: 'string',
				default: 'info',
			},
		},

		supports: {
			align: [ 'wide' ],
		},

		/**
		 * @see ./edit.js
		 */
		edit: Edit,

		/**
		 * @see ./save.js
		 */
		save,
	}
);

Frontend Markup festlegen

Die /src/infobox/save.js wird so aussehen:

/**
 * WordPress dependencies
 */
const { RichText } = wp.editor;
const { InnerBlocks } = wp.editor;

/**
 * The save function defines the way in which the different attributes should
 * be combined into the final markup, which is then serialized by the block
 * editor into `post_content`.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save
 *
 * @return {WPElement} Element to render.
 */
export default function save( { attributes } ) {
	const {
		title,
		selectedStyle,
	} = attributes;
	return (
		<div className={ 'hb-infobox-wrapper hb-infobox-block-style-' + selectedStyle }>
			<RichText.Content
				tagName="p"
				className={ 'hb-infobox-header-' + selectedStyle }
				value={ title }
			/>
			<div className={ 'hb-infobox-content-' + selectedStyle }>
				<InnerBlocks.Content />
			</div>
		</div>
	);
}

Backend Markup festlegen

Die /src/infobox/edit.js wird so aussehen:

/**
 * WordPress dependencies
 */
const { __ } = wp.i18n;
const { RichText } = wp.editor;
const { InnerBlocks } = wp.editor;
const { InspectorControls } = wp.editor;
const { PanelBody } = wp.components;
const { SelectControl } = wp.components;

// eslint-disable-next-line valid-jsdoc
/**
 * The edit function describes the structure of your block in the context of the
 * editor. This represents what the editor will render when the block is used.
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit
 *
 * @return {WPElement} Element to render.
 */
export default function Edit( props ) {
	const {
		attributes,
		className,
		setAttributes,
	} = props;

	function changeTitle( newTitle ) {
		setAttributes( { title: newTitle } );
	}

	return [
		// eslint-disable-next-line react/jsx-key
		<InspectorControls>
			<PanelBody title={ __( 'Choose Style', 'hb-blocks' ) }>
				<SelectControl
					label={ __( 'Choose info box style', 'hb-blocks' ) }
					value={ props.attributes.selectedStyle }
					onChange={ ( selectedStyle ) => {
						props.setAttributes( { selectedStyle } );
					} }
					options={ [
						{ value: 'annotation', label: 'Anmerkung' },
						{ value: 'info', label: 'Info' },
						{ value: 'warning', label: 'Warnung' },
					] }
				/>
			</PanelBody>
		</InspectorControls>,
		// eslint-disable-next-line react/jsx-key
		<div className={ 'hb-infobox-wrapper hb-infobox-style-' + attributes.selectedStyle }>
			<RichText
				tagName="p"
				value={ attributes.title }
				className={ 'hb-infobox-header-' + attributes.selectedStyle }
				onChange={ changeTitle }
				placeholder={ __( 'Placeholder Text', 'hb-blocks' ) }
				keepPlaceholderOnFocus
			/>
			<div className={ 'hb-infobox-content-' + attributes.selectedStyle }>
				<InnerBlocks />
			</div>
		</div>,
	];
}

Das Styling

Die /src/infobox/style.scss beinhaltet das Styling für die Infobox (Frontend + Backend) und wird so aussehen:

.hb-infobox-wrapper {
  border: 1px solid;
  border-color: rgb(164, 162, 162);
  border-radius: 5px;
  box-shadow: 5px 10px 10px rgba(136, 136, 136, 0.22);
}

.editor-styles-wrapper p {
  margin-top: 0 !important;
  margin-bottom: 0 !important;
}

.hb-infobox-header-info {
  background-color: blue;
  font-weight: 600;
  padding: 5px;
  color: white;
  margin: 0;
  border-radius: 4px;
  border-bottom-left-radius: 0px;
  border-bottom-right-radius: 0px;
}

.hb-infobox-header-annotation {
  background-color: green;
  font-weight: 600;
  padding: 5px;
  color: white;
  margin: 0;
  border-radius: 4px;
  border-bottom-left-radius: 0px;
  border-bottom-right-radius: 0px;
}

.hb-infobox-header-warning {
  background-color: red;
  font-weight: 600;
  padding: 5px;
  color: white;
  margin: 0;
  border-radius: 4px;
  border-bottom-left-radius: 0px;
  border-bottom-right-radius: 0px;
}

.hb-infobox-content-annotation {
  background-color: rgba(172, 238, 116, 0.59);
  margin: 0;
  padding: 10px 5px;
}

.hb-infobox-content-warning {
  background-color: rgba(255, 0, 0, 0.2);
  margin: 0;
  padding: 10px 5px;
}

.hb-infobox-content-info {
  background-color: rgba(148, 171, 239, 0.41);
  margin: 0;
  padding: 10px 5px;
}

Den WordPress Block mit Komponenten erweitern

Ausgangsbasis war ja der Dummy-Block. Hier beschreibe ich nun die notwendigen Ergänzungen, wie sie in den oben aufgeführten Dateien schon zu sehen sind.

RichText-Komponente integrieren
Eingabefeld erstellen
Blöcke innerhalb eines Blocks erlauben mit InnerBlocks
Sidebar Einstellungen hinzufügen mit InspectorControls
Dropdown für die Styling-Auswahl einfügen mit SelectControl
Toolbar-Einstellungen verfeinern

Finale Optimierungen

Multilingualer Block

Anmerkungen

Absichern der php-Dateien

Um einen Zugriff von außerhalb unserer WP-Installation zu unterbinden, füge folgenden Codeschnipsel am Anfang jeder PHP-Datei ein:

<?php
// kein Plugin-Zugriff bei direktem Aufruf
if ( ! defined( 'ABSPATH' )  ) {
    exit;
}
?>

Damit wird überprüft, ob die Variable "ABSPATH" gesetzt ist. Dies erfolgt zur Laufzeit von WP. Ist das nicht der Fall, wird die Ausführung abgebrochen.

Dashicons

https://developer.wordpress.org/resource/dashicons

In Zeile 4 findest Du ein Beispiel zur Einbindung eines WP-DashIcons.

registerBlockType( 'gutenberg-examples/example-01-basic-esnext', {
    apiVersion: 2,
    title: 'Example: Basic (esnext)',
    icon: 'universal-access-alt',
    category: 'design',
    example: {},
    edit() {},
    save() {},
} );

Auf der oben verlinkten Übersicht wird das Icon mit dashicons-universal-access-alt angegeben. Beim Einbinden entfällt der Teil dashicons-!