* wrapping a with one row per field. Hidden inputs * and datalist elements are emitted before the table; notices before the card, * submit button after the fields. * * Usage: * echo RsvFormBuilder::create('settings', $action, 'PATCH', 'Saved.') * ->heading('Settings') * ->text('name', 'Name', required: true) * ->email('email', 'Email') * ->submit('Save'); */ class RsvFormBuilder { private string $form_id; /** Where the form submits, and how RsvAdminForm should send it. */ private string $action; private string $rest_method; private string $success_msg; /** Optional heading shown inside the card. */ private string $heading = ''; /** @var string[] Rendered before the table (hidden inputs, datalists). */ private array $before = []; /** @var string[] WP admin notice banners rendered before the card. */ private array $notices = []; /** @var string[] elements inside the table. */ private array $rows = []; /** @var string[] Rendered after the table (submit button). */ private array $after = []; private function __construct() {} /** * @param string $rest_method Verb sent via data-method (POST, PUT, PATCH…). * @param string $success_msg Message RsvAdminForm shows on success. */ public static function create( string $id, string $action, string $rest_method = 'POST', string $success_msg = '' ): static { $builder = new static(); $builder->form_id = $id; $builder->action = $action; $builder->rest_method = $rest_method; $builder->success_msg = $success_msg; return $builder; } /** Heading shown inside the card, above the fields. */ public function heading(string $text): static { $this->heading = $text; return $this; } // ------------------------------------------------------------------------- // Input fields — each becomes a in the table // ------------------------------------------------------------------------- public function text( string $id, string $label, string $desc = '', bool $required = false, string $value = '' ): static { $req = $required ? 'required' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function email( string $id, string $label, string $desc = '', bool $required = false, string $value = '', ?string $list_id = null ): static { $req = $required ? 'required' : ''; $list = $list_id !== null ? 'list="' . esc_attr($list_id) . '"' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function password( string $id, string $label, string $desc = '', bool $required = false, string $placeholder = '' ): static { $req = $required ? 'required' : ''; $ph = $placeholder !== '' ? 'placeholder="' . esc_attr($placeholder) . '"' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function number( string $id, string $label, string $desc = '', bool $required = false, string|int $value = '', ?int $min = null, ?int $max = null ): static { $req = $required ? 'required' : ''; $min_attr = $min !== null ? 'min="' . $min . '"' : ''; $max_attr = $max !== null ? 'max="' . $max . '"' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function date( string $id, string $label, string $desc = '', bool $required = false, string $value = '' ): static { $req = $required ? 'required' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function time( string $id, string $label, string $desc = '', bool $required = false, string $value = '' ): static { $req = $required ? 'required' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function checkbox( string $id, string $label, string $desc = '', bool $checked = false ): static { $c = $checked ? 'checked' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } /** * @param array $options Associative: value => display text. */ public function select( string $id, string $label, array $options, string $desc = '', bool $required = false, string $selected = '' ): static { $req = $required ? 'required' : ''; $opts = $this->build_options($options, $selected); $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function textarea( string $id, string $label, string $desc = '', bool $required = false, string $value = '', int $rows = 5 ): static { $req = $required ? 'required' : ''; $ctrl = ''; return $this->row($id, $label, $ctrl, $desc); } public function custom(string $label, callable $fn) : static { $this->rows[] = '' . '' . '' . ''; return $this; } /** * Groups multiple inputs into a single row with a shared label. * * The callable receives an RsvFormGroup instance; inputs added to it * are laid out as a flex row inside the row's ' . '' . '' . ''; return $this; } // ------------------------------------------------------------------------- // Non-field outputs // ------------------------------------------------------------------------- /** Hidden input — no row, emitted before the table. */ public function hidden(string $id, string|int $value): static { $this->before[] = ''; return $this; } /** WordPress nonce field, emitted inside the form before the table. */ public function nonce(string $action, string $name): static { $this->before[] = wp_nonce_field($action, $name, true, false); return $this; } /** * element for email/text suggestions — emitted before the table. * * @param string[] $values */ public function datalist(string $id, array $values): static { $options = ''; foreach ($values as $v) { $options .= ''; return $this; } /** * WordPress admin notice — emitted before the table. * * @param string $type One of: success | error | warning | info */ public function notice(string $type, string $message, bool $dismissible = true): static { $classes = 'notice notice-' . esc_attr($type) . ($dismissible ? ' is-dismissible' : ''); $this->notices[] = '

' . esc_html($message) . '

'; return $this; } /** Submit button — emitted after the table as

. */ public function submit(string $label, string $css_class = 'button-primary', string $name = ''): static { $name_attr = $name !== '' ? 'name="' . esc_attr($name) . '"' : ''; $this->after[] = '

'; return $this; } // ------------------------------------------------------------------------- // Rendering // ------------------------------------------------------------------------- public function render(): string { $inner = implode('', $this->before); if (!empty($this->rows)) { $inner .= '
' . esc_html($label) . '' . $fn() . '
. * * Example: * ->group('Availability Range', fn($g) => $g * ->time('start_time', 'Start') * ->time('end_time', 'End') * ) */ public function group(string $label, callable $fn): static { $group = RsvFormGroup::create(); $fn($group); $this->rows[] = '
' . esc_html($label) . '' . $group->render() . '
. */ public function note(string $text): static { $this->rows[] = '

' . esc_html($text) . '

' . implode('', $this->rows) . '
'; } $inner .= implode('', $this->after); $success = $this->success_msg !== '' ? ' data-success-msg="' . esc_attr($this->success_msg) . '"' : ''; $form = '
' . $inner . '
'; $heading = $this->heading !== '' ? '

' . esc_html($this->heading) . '

' : ''; return implode('', $this->notices) . '
' . $heading . $form . '
'; } public function output(): void { echo $this->render(); } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- private function row(string $id, string $label, string $control_html, string $desc): static { $d = $desc !== '' ? '

' . esc_html($desc) . '

' : ''; $this->rows[] = '' . '' . '' . $control_html . $d . '' . ''; return $this; } /** * @param array $options */ private function build_options(array $options, string $selected): string { $html = ''; foreach ($options as $value => $text) { $is_selected = (string) $value === $selected ? 'selected' : ''; $html .= ''; } return $html; } }