Skip to content

Eigene Feldtypen

Ein Feldtyp beschreibt sich selbst: Er liefert seinen Eintrag für die Builder-Palette, sein Inspector-Schema, sein Frontend-Markup und die Regeln zum Sanitizing und Validieren des übermittelten Werts. Wer FieldInterface implementiert und sich registriert, erscheint automatisch im Builder, im REST-Katalog und im Renderer.

Das Interface

Dingfelder\AdForm\Fields\FieldInterface verlangt 14 Methoden.

Identität und Palette

php
public function type(): string;
public function label(): string;
public function default_label(): string;
public function description(): string;
public function icon(): string;
public function category(): string;
public function supports_required(): bool;
MethodeBedeutung
type()Stabiler Typ-Schlüssel, z. B. text. Zugleich der Schlüssel in der Registry – ein zweiter Feldtyp mit demselben Wert überschreibt den ersten.
label()Lesbares Label für die Builder-Palette.
default_label()Label, das ein neu eingefügtes Feld auf dem Canvas bekommt.
description()Kurzbeschreibung in der Palette.
icon()Dashicon-Slug ohne das Präfix dashicons-, z. B. editor-textcolor.
category()Palette-Kategorie: basic, advanced, business, wordpress oder layout.
supports_required()Ob der Inspector einen Pflichtfeld-Schalter anzeigt.

Einstellungen und Inspector

php
/** @return array<string, mixed> */
public function default_settings(): array;

/** @return list<array<string, mixed>> */
public function inspector_fields(): array;

/**
 * @param array<string, mixed> $settings Raw settings.
 * @return array<string, mixed>
 */
public function sanitize_settings( array $settings ): array;

/** @return array<string, mixed> */
public function to_rest_array(): array;

default_settings() wird beim Einfügen eines Feldes in die Definition kopiert. inspector_fields() beschreibt die Steuerelemente im Inspector; jeder Eintrag hat mindestens name, path, type und label, optional help und options. Der path zeigt auf die Stelle in der Feldinstanz (label, required, key, settings.placeholder, …).

to_rest_array() liefert die Palette-/REST-Nutzlast. AbstractField baut sie bereits aus den übrigen Methoden zusammen – ergänzt um required_entitlements – sodass sie in der Regel nicht überschrieben werden muss.

Rendering und Übermittlung

php
/**
 * @param array<string, mixed> $field   Sanitized field from the definition.
 * @param array<string, mixed> $context Renderer context (values, errors, form_id).
 */
public function render_html( array $field, array $context = array() ): string;

public function sanitize_submitted_value( mixed $value ): mixed;

/**
 * @param array<string, mixed> $field Field instance.
 */
public function validate_submitted_value( mixed $value, array $field ): ?string;

validate_submitted_value() gibt null zurück, wenn der Wert in Ordnung ist, und sonst die übersetzte Fehlermeldung.

AbstractField als Basis

Dingfelder\AdForm\Fields\AbstractField implementiert alles außer type() und label(). In der Praxis wird immer davon abgeleitet – eine direkte FieldInterface-Implementierung lohnt sich nur für vollständig eigenes Markup.

Kategorie-Konstanten:

php
AbstractField::CATEGORY_BASIC;      // 'basic'
AbstractField::CATEGORY_ADVANCED;   // 'advanced'
AbstractField::CATEGORY_BUSINESS;   // 'business'
AbstractField::CATEGORY_WORDPRESS;  // 'wordpress'
AbstractField::CATEGORY_LAYOUT;     // 'layout'

Was die Basisklasse übernimmt

render_html() baut den kompletten Wrapper: CSS-Klassen (ad-form__field, ad-form__field--{type}, ad-form__field--w{width}), die Data-Attribute data-field-id, data-field-key, data-required und data-conditions, den Sichtbarkeitszustand aus dem Renderer-Kontext sowie Label, Beschreibung und Fehlermeldung mit den passenden aria-describedby-Verknüpfungen.

sanitize_settings() startet von default_settings() und schickt jeden Wert durch sanitize_setting(). sanitize_submitted_value() ruft sanitize_text_field() auf – bei Arrays für jeden Eintrag. validate_submitted_value() prüft nur die Pflichtfeld-Regel.

Erweiterungspunkte für Unterklassen

MethodeZweck
control_html( array $field, string $html_id, string $name, mixed $value, string $error, bool $required, array $context ): stringKompletter Inhalt des Wrappers inklusive Label und Fehlern
input_html( array $field, string $html_id, string $name, mixed $value, bool $required, array $described ): stringNur das Eingabe-Element – der übliche Einstiegspunkt
label_html( string $html_id, string $label, bool $required ): stringLabel-Markup
common_settings(): arrayBasis-Settings: admin_label, placeholder, default_value, description, css_class, width
identity_inspector(): arrayLabel, Required, Field key, Admin label
extra_inspector(): arrayTypspezifische Steuerelemente – hier wird meist ergänzt
style_inspector(): arrayBeschreibung, CSS-Klasse, Breite
width_options(): arrayAuswahl 100 / 75 / 66 / 50 / 33 / 25
extra_wrap_attributes( array $field, array $context ): arrayZusätzliche Attribute am Wrapper, z. B. data-formula
sanitize_setting( string $key, mixed $value ): mixedSanitizing eines einzelnen Settings-Werts
sanitize_options( mixed $value ): arrayNormalisiert {label, value}-Paare
is_empty_value( mixed $value ): boolLeer-Prüfung für die Pflichtfeld-Validierung
has_error_id( array $described ): boolOb unter den aria-describedby-IDs eine Fehler-ID ist

required_entitlements()

php
/** @return list<string> */
public function required_entitlements(): array;

Liefert die Feature-Schlüssel, die für diesen Feldtyp freigeschaltet sein müssen. AbstractField gibt eine leere Liste zurück – ein eigener Feldtyp ist also standardmäßig uneingeschränkt nutzbar. Die Methode steht bewusst auf der abstrakten Klasse und nicht auf FieldInterface, damit bestehende Interface-Implementierungen unverändert weiterlaufen. Der Wert erscheint zusätzlich als required_entitlements in to_rest_array().

Registrierung

Der Hook ad_form_register_fields feuert innerhalb von FieldRegistry::boot(), nachdem die Kern-Feldtypen registriert wurden, und bekommt die Registry als Parameter.

php
use Dingfelder\AdForm\Fields\FieldRegistry;

add_action(
	'ad_form_register_fields',
	static function ( FieldRegistry $registry ): void {
		$registry->register( new Acme\AdFormFields\PhoneField() );
	}
);

Direkt danach läuft der Filter ad_form_fields über die fertige Map type => FieldInterface. Er eignet sich zum Entfernen oder Ersetzen von Feldtypen:

php
add_filter(
	'ad_form_fields',
	static function ( array $fields ): array {
		unset( $fields['calculation'] );

		return $fields;
	}
);

Nur einmal booten

FieldRegistry::boot() läuft genau einmal pro Request und wird beim ersten has(), get(), all() oder to_rest_array() ausgelöst. Wer sich zu spät einhängt – etwa erst auf wp –, verpasst die Registrierung. ad_form_register_fields und ad_form_fields sind die einzigen verlässlichen Zeitpunkte.

Die Kern-Feldtypen sind text, email, number, textarea, select, checkbox, calculation und submit.

Vollständiges Beispiel: Telefonnummer-Feld

php
<?php
/**
 * Phone number field for AD Form.
 *
 * @package Acme\AdFormFields
 */

declare(strict_types=1);

namespace Acme\AdFormFields;

use Dingfelder\AdForm\Fields\AbstractField;
use Dingfelder\AdForm\Support\Html;

final class PhoneField extends AbstractField {

	public const TYPE = 'phone';

	public function type(): string {
		return self::TYPE;
	}

	public function label(): string {
		return __( 'Phone', 'acme-ad-form-fields' );
	}

	public function description(): string {
		return __( 'Telephone number with optional pattern check.', 'acme-ad-form-fields' );
	}

	public function icon(): string {
		return 'phone';
	}

	public function category(): string {
		return self::CATEGORY_ADVANCED;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function default_settings(): array {
		$settings              = $this->common_settings();
		$settings['min_digits'] = '6';
		$settings['autocomplete_tel'] = true;

		return $settings;
	}

	/**
	 * @return list<array<string, mixed>>
	 */
	protected function extra_inspector(): array {
		$fields   = parent::extra_inspector();
		$fields[] = array(
			'name'  => 'min_digits',
			'path'  => 'settings.min_digits',
			'type'  => 'text',
			'label' => __( 'Minimum digits', 'acme-ad-form-fields' ),
			'help'  => __( 'Digits are counted after stripping spaces, slashes and dashes.', 'acme-ad-form-fields' ),
		);
		$fields[] = array(
			'name'  => 'autocomplete_tel',
			'path'  => 'settings.autocomplete_tel',
			'type'  => 'toggle',
			'label' => __( 'Enable browser autocomplete', 'acme-ad-form-fields' ),
		);

		return $fields;
	}

	/**
	 * @param array<string, mixed> $field     Field instance.
	 * @param list<string>         $described Description/error ids.
	 */
	protected function input_html( array $field, string $html_id, string $name, mixed $value, bool $required, array $described ): string {
		$settings    = is_array( $field['settings'] ?? null ) ? $field['settings'] : array();
		$placeholder = (string) ( $settings['placeholder'] ?? '' );

		return '<input' . Html::attributes(
			array(
				'type'             => 'tel',
				'id'               => $html_id,
				'name'             => $name,
				'value'            => (string) $value,
				'placeholder'      => '' !== $placeholder ? $placeholder : null,
				'class'            => 'ad-form__control',
				'required'         => $required,
				'inputmode'        => 'tel',
				'autocomplete'     => empty( $settings['autocomplete_tel'] ) ? null : 'tel',
				'aria-invalid'     => $this->has_error_id( $described ) ? 'true' : null,
				'aria-required'    => $required ? 'true' : null,
				'aria-describedby' => array() !== $described ? implode( ' ', $described ) : null,
			)
		) . ' />';
	}

	public function sanitize_submitted_value( mixed $value ): mixed {
		$text = trim( sanitize_text_field( (string) $value ) );

		return preg_replace( '/[^0-9+()\/\s-]/', '', $text ) ?? '';
	}

	/**
	 * @param array<string, mixed> $field Field instance.
	 */
	public function validate_submitted_value( mixed $value, array $field ): ?string {
		$required = parent::validate_submitted_value( $value, $field );

		if ( null !== $required ) {
			return $required;
		}

		$text = trim( (string) $value );

		if ( '' === $text ) {
			return null;
		}

		$settings = is_array( $field['settings'] ?? null ) ? $field['settings'] : array();
		$minimum  = max( 0, (int) ( $settings['min_digits'] ?? 0 ) );
		$digits   = preg_replace( '/\D/', '', $text ) ?? '';

		if ( strlen( $digits ) < $minimum ) {
			return __( 'Please enter a valid phone number.', 'acme-ad-form-fields' );
		}

		return null;
	}

	protected function sanitize_setting( string $key, mixed $value ): mixed {
		return match ( $key ) {
			'min_digits'       => (string) max( 0, min( 20, (int) $value ) ),
			'autocomplete_tel' => ! empty( $value ),
			default            => parent::sanitize_setting( $key, $value ),
		};
	}
}

Registrierung in der Add-on-Haupt-Datei:

php
<?php
declare(strict_types=1);

use Dingfelder\AdForm\Fields\FieldRegistry;

add_action(
	'ad_form_register_fields',
	static function ( FieldRegistry $registry ): void {
		$registry->register( new Acme\AdFormFields\PhoneField() );
	}
);

Was danach automatisch funktioniert

Sobald der Feldtyp registriert ist:

  • Er erscheint in der Builder-Palette unter seiner Kategorie und im REST-Katalog (GET /wp-json/ad-form/v1/fields).
  • FieldRegistry::create_instance( 'phone', $existing_keys ) erzeugt eine neue Feldinstanz mit eindeutigem Key, stabiler ID, default_label(), default_settings() und leerer Bedingungsgruppe.
  • Der FormRenderer ruft render_html() mit dem Renderer-Kontext auf.
  • Der SubmitService ruft erst sanitize_submitted_value(), dann validate_submitted_value().
  • Bedingte Logik und Multi-Step funktionieren ohne Zutun, weil sie am Wrapper und an der Definition ansetzen, nicht am Feldtyp.

Verwandte Referenzen

Digitale Lösungen. Persönlich. Zukunftssicher.