Webcomponents and CakePHP FormHelper
Webcomponents are starting to get more traction now that they are fully supported across browsers. I have recently been rebuilding my personal todo list software Docket with HTMX and Webcomponents. While this won’t be an introduction to webcomponents — I found this article and this one to be helpful in learning how to build webcomponents. For this article, I wanted to share how webcomponents can be integrated with CakePHP’s FormHelper
.
Replacing React libraries with Web components
My need for this came up while replacing react-select
components during the htmx conversion. I wanted to retain the UX improvements that react-select offered like rich item option content, and autocomplete filtering. However, I didn’t want to pull in many dependencies. This excluded jQuery based libraries, and a few other ‘vanilla JS’ projects. I found several webcomponent libraries that looked good like shoelace. However, I was drawn to learning web components from the ‘ground floor’ so I could better understand the sharp corners that these libraries were solving in addition to providing consistent design and UX.
After prototyping the webcomponent logic, I wanted to integrate the necessary markup with the templates that FormHelper
uses. I wanted to see how well webcomponents paired with the Widget
extensions that FormHelper
has.
The select-box component HTML
The select-box
webcomponent can be found on GitHub . The HTML for this component looks like:
- <select-box name="" type="projectpicker" id="project-id" val="1" tabindex="-1">
- <input type="text" name="project_id" style="display:none">
- <select-box-current selectedhtml="" open="false">
- <span class="select-box-value">Home</span>
- <input type="text" class="select-box-input">
- </select-box-current>
- <select-box-menu val="1" style="display: none;" filter="" current="0">
- <select-box-option value="1" selected="true" aria-selected="true" aria-current="true">
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
- <path fill="#218fa7" d="M8 4a4 4 0 1 1 0 8a4 4 0 0 1 0-8Z"></path>
- </svg>
- Home
- </select-box-option>
- <select-box-option value="2" aria-selected="false">
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
- <path fill="#b86fd1" d="M8 4a4 4 0 1 1 0 8a4 4 0 0 1 0-8Z"></path>
- </svg>
- Work
- </select-box-option>
- </select-box-menu>
- </select-box>
With the HTML roughed in and working on the client side I wanted to wire up this UX component to FormHelper
with the goal of being able to create inputs with a small amount of PHP code:
- echo $this->Form->control('project_id', ['type' => 'projectpicker', 'projects' => $projects]);
Widget Class
CakePHP’s FormHelper
provides extension points that enable you to integrate custom input widget logic and markup. We start off by implementing the Cake\View\Widget\WidgetInterface
. The widget class for my project input looks like:
- <?php
- declare(strict_types=1);
- namespace App\View\Widget;
- use Cake\View\Form\ContextInterface;
- use Cake\View\StringTemplate;
- use Cake\View\View;
- use Cake\View\Widget\BasicWidget;
- use RuntimeException;
- class ProjectPickerWidget extends BasicWidget
- {
- /**
- * Data defaults.
- *
- * @var array<string, mixed>
- */
- protected $defaults = [
- 'name' => '',
- 'disabled' => null,
- 'val' => null,
- 'projects' => [],
- 'tabindex' => '-1',
- 'templateVars' => [],
- 'inputAttrs' => [],
- ];
- public function __construct(private StringTemplate $templates, private View $view)
- {
- }
- {
- $data = $this->mergeDefaults($data, $context);
- throw new RuntimeException('`projects` option is required');
- }
- $selected = $data['val'] ?? null;
- $projects = $data['projects'];
- $inputAttrs = $data['inputAttrs'] ?? [];
- $data['projects'],
- $data['data-validity-message'],
- $data['oninvalid'],
- $data['oninput'],
- $data['inputAttrs']
- );
- $inputAttrs += ['style' => 'display:none'];
- $options = [];
- foreach ($projects as $project) {
- // Generate the option body
- $optionBody = $this->view->element('icons/dot16', ['color' => $project->color_hex]) . h($project->name);
- $optAttrs = [
- 'selected' => $project->id == $selected,
- ];
- $options[] = $this->templates->format('select-box-option', [
- 'value' => $project->id,
- 'text' => $optionBody,
- 'attrs' => $this->templates->formatAttributes($optAttrs, ['text', 'value']),
- ]);
- }
- // Include a 'hidden' text input that is manipulated by the webcomponent.
- $hidden = $this->templates->format('input', [
- 'name' => $data['name'],
- 'value' => $selected,
- 'type' => 'text',
- 'attrs' => $this->templates->formatAttributes($inputAttrs),
- ]);
- $attrs = $this->templates->formatAttributes($data);
- return $this->templates->format('select-box', [
- 'templateVars' => $data['templateVars'],
- 'attrs' => $attrs,
- 'hidden' => $hidden,
- ]);
- }
- {
- return [$data['name']];
- }
- }
Next, we add the widget to FormHelper
in our AppController::beforeRender
:
- <?php
- $this->viewBuilder()
- ->addHelper('Form', [
- 'templates' => 'formtemplates',
- 'widgets' => [
- 'projectpicker' => [ProjectPickerWidget::class, '_view'],
- ],
- ]);
Finally, because I wanted to separate the markup from the Widget logic, I’m using custom templates. The templates for my project picker look like:
- <?php
- return [
- 'select-box-option' => '<select-box-option value="{{value}}"{{attrs}}>{{text}}</select-box-option>',
- 'select-box' => <<<HTML
- <select-box name="{{name}}"{{attrs}}>
- {{hidden}}
- <select-box-current>
- <span class="select-box-value"></span>
- <input type="text" class="select-box-input" />
- </select-box-current>
- <select-box-menu>{{options}}</select-box-menu>
- </select-box>
- HTML,
- ];
With all of this done, we can make custom inputs wrapped in the standard HTML that FormHelper
creates. I think this could be increasingly valuable as folks discover the power and simplicity that webcomponents offer.
Makes me wanna dig more into HTMX and web components ! Thanks that was very interesting
Max on 1/5/24