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