Skip to content

Commit 33e93d1

Browse files
mjauvinLukeTowers
andauthored
Add image/icon support for FormBuilder::select (#222)
Co-authored-by: Luke Towers <git@luketowers.ca>
1 parent 3de97f6 commit 33e93d1

2 files changed

Lines changed: 285 additions & 3 deletions

File tree

src/Html/FormBuilder.php

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,15 @@ protected function setQuickTextAreaSize(array $options): array
407407

408408
/**
409409
* Create a select box field with empty option support.
410+
*
411+
* Supports several formats for the $list parameter:
412+
* - Simple format: ['value' => 'Label']
413+
* - With icon/image: ['value' => ['Label', 'icon-name']] or ['value' => ['Label', 'image.png']]
414+
* - With optgroups: ['Group Name' => ['value' => 'Label', ...]]
415+
* - Mixed format combining all of the above
416+
*
417+
* Icons are detected when the second array element doesn't contain a dot (.).
418+
* Images are detected when the second array element contains a dot (.).
410419
*/
411420
public function select(string $name, array $list = [], string|array|null $selected = null, array $options = []): string
412421
{
@@ -482,10 +491,15 @@ public function selectMonth(string $name, string|array|null $selected = null, ar
482491

483492
/**
484493
* Get the select option for the given value.
494+
*
495+
* Determines whether to create a single option or an optgroup based on the $display parameter:
496+
* - If $display is an array with string keys, creates an optgroup
497+
* - If $display is an array with numeric keys (e.g., ['Label', 'icon']), creates a single option with icon/image
498+
* - If $display is a string, creates a simple option
485499
*/
486500
public function getSelectOption(string|array $display, string $value, string|array|null $selected = null): string
487501
{
488-
if (is_array($display)) {
502+
if (is_array($display) && array_keys($display) !== [0,1]) {
489503
return $this->optionGroup($display, $value, $selected);
490504
}
491505

@@ -508,16 +522,33 @@ protected function optionGroup(array $list, string $label, string|array|null $se
508522

509523
/**
510524
* Create a select element option.
525+
*
526+
* If $display is an array in the format ['Label', 'icon-or-image'], adds data attributes:
527+
* - data-icon: added if the second element doesn't contain a dot (e.g., 'icon-refresh')
528+
* - data-image: added if the second element contains a dot (e.g., 'image.png')
511529
*/
512-
protected function option(string $display, string $value, string|array|null $selected = null): string
530+
protected function option(string|array $display, string $value, string|array|null $selected = null): string
513531
{
514532
$selectedAttr = $this->getSelectedValue($value, $selected);
515533

516534
$options = [
517-
'value' => e($value),
535+
'value' => $value,
518536
'selected' => $selectedAttr
519537
];
520538

539+
if (is_array($display)) {
540+
$label = array_get($display, 0);
541+
$data = array_get($display, 1);
542+
543+
if (is_string($data) && $data !== '') {
544+
if (strpos($data, '.') !== false) {
545+
$options['data-image'] = $data;
546+
} else {
547+
$options['data-icon'] = $data;
548+
}
549+
}
550+
$display = $label;
551+
}
521552
return '<option' . $this->html->attributes($options) . '>' . e($display) . '</option>';
522553
}
523554

tests/Html/FormBuilderTest.php

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,255 @@ public function testSelectWithEmptyOption()
340340
$this->assertStringContainsString('<option value="1">Option 1</option>', $result);
341341
$this->assertStringContainsString('<option value="2">Option 2</option>', $result);
342342
}
343+
344+
/**
345+
* @testdox can create a select element with icon data attributes.
346+
*/
347+
public function testSelectWithIcon()
348+
{
349+
$result = $this->formBuilder->select(
350+
name: 'my-select',
351+
list: [
352+
'1' => 'Regular Option',
353+
'2' => ['Option With Icon', 'icon-refresh'],
354+
],
355+
selected: null,
356+
options: []
357+
);
358+
359+
$this->assertElementIs('select', $result);
360+
$this->assertElementAttributeEquals('name', 'my-select', $result);
361+
$this->assertStringContainsString('<option value="1">Regular Option</option>', $result);
362+
$this->assertStringContainsString('<option value="2" data-icon="icon-refresh">Option With Icon</option>', $result);
363+
}
364+
365+
/**
366+
* @testdox can create a select element with image data attributes.
367+
*/
368+
public function testSelectWithImage()
369+
{
370+
$result = $this->formBuilder->select(
371+
name: 'my-select',
372+
list: [
373+
'1' => 'Regular Option',
374+
'2' => ['Option With Image', 'myImage.jpeg'],
375+
],
376+
selected: null,
377+
options: []
378+
);
379+
380+
$this->assertElementIs('select', $result);
381+
$this->assertElementAttributeEquals('name', 'my-select', $result);
382+
$this->assertStringContainsString('<option value="1">Regular Option</option>', $result);
383+
$this->assertStringContainsString('<option value="2" data-image="myImage.jpeg">Option With Image</option>', $result);
384+
}
385+
386+
/**
387+
* @testdox can create a select element with image data attributes.
388+
*/
389+
public function testSelectWithSelectedImage()
390+
{
391+
$result = $this->formBuilder->select(
392+
name: 'my-select',
393+
list: [
394+
'1' => 'Regular Option',
395+
'2' => ['Option With Image', 'myImage.jpeg'],
396+
],
397+
selected: '2',
398+
options: []
399+
);
400+
401+
$this->assertElementIs('select', $result);
402+
$this->assertElementAttributeEquals('name', 'my-select', $result);
403+
$this->assertStringContainsString('<option value="1">Regular Option</option>', $result);
404+
$this->assertStringContainsString('<option value="2" selected="selected" data-image="myImage.jpeg">Option With Image</option>', $result);
405+
}
406+
407+
/**
408+
* @testdox can create a select element with optgroups.
409+
*/
410+
public function testSelectWithOptgroups()
411+
{
412+
$result = $this->formBuilder->select(
413+
name: 'my-select',
414+
list: [
415+
'Group 1' => [
416+
'g1-opt1' => 'Group 1 Option 1',
417+
'g1-opt2' => 'Group 1 Option 2',
418+
],
419+
'Group 2' => [
420+
'g2-opt1' => 'Group 2 Option 1',
421+
'g2-opt2' => 'Group 2 Option 2',
422+
],
423+
],
424+
selected: null,
425+
options: []
426+
);
427+
428+
$this->assertElementIs('select', $result);
429+
$this->assertElementAttributeEquals('name', 'my-select', $result);
430+
$this->assertStringContainsString('<optgroup label="Group 1">', $result);
431+
$this->assertStringContainsString('<optgroup label="Group 2">', $result);
432+
$this->assertStringContainsString('<option value="g1-opt1">Group 1 Option 1</option>', $result);
433+
$this->assertStringContainsString('<option value="g1-opt2">Group 1 Option 2</option>', $result);
434+
$this->assertStringContainsString('<option value="g2-opt1">Group 2 Option 1</option>', $result);
435+
$this->assertStringContainsString('<option value="g2-opt2">Group 2 Option 2</option>', $result);
436+
$this->assertStringContainsString('</optgroup>', $result);
437+
}
438+
439+
/**
440+
* @testdox can create a select element with optgroups containing icons and images.
441+
*/
442+
public function testSelectWithOptgroupsAndIconsImages()
443+
{
444+
$result = $this->formBuilder->select(
445+
name: 'my-select',
446+
list: [
447+
'option1' => 'Regular option',
448+
'option2' => ['Option With Image', 'myImage.jpeg'],
449+
'Group1' => [
450+
'group1-opt1' => 'OptGroup Option1 regular option',
451+
'group1-opt2' => ['OptGroup Option2 with icon', 'icon-refresh'],
452+
'group1-opt3' => ['OptGroup Option3 with image', 'otherImage.png'],
453+
],
454+
'Group2' => [
455+
'group2-opt1' => 'OptGroup2 Option1',
456+
'group2-opt2' => 'OptGroup2 Option2',
457+
],
458+
],
459+
selected: null,
460+
options: []
461+
);
462+
463+
$this->assertElementIs('select', $result);
464+
$this->assertElementAttributeEquals('name', 'my-select', $result);
465+
466+
// Regular options
467+
$this->assertStringContainsString('<option value="option1">Regular option</option>', $result);
468+
$this->assertStringContainsString('<option value="option2" data-image="myImage.jpeg">Option With Image</option>', $result);
469+
470+
// Optgroups
471+
$this->assertStringContainsString('<optgroup label="Group1">', $result);
472+
$this->assertStringContainsString('<optgroup label="Group2">', $result);
473+
474+
// Options inside optgroups
475+
$this->assertStringContainsString('<option value="group1-opt1">OptGroup Option1 regular option</option>', $result);
476+
$this->assertStringContainsString('<option value="group1-opt2" data-icon="icon-refresh">OptGroup Option2 with icon</option>', $result);
477+
$this->assertStringContainsString('<option value="group1-opt3" data-image="otherImage.png">OptGroup Option3 with image</option>', $result);
478+
$this->assertStringContainsString('<option value="group2-opt1">OptGroup2 Option1</option>', $result);
479+
$this->assertStringContainsString('<option value="group2-opt2">OptGroup2 Option2</option>', $result);
480+
}
481+
482+
/**
483+
* @testdox can create a select element with backward compatibility for simple string options.
484+
*/
485+
public function testSelectBackwardCompatibility()
486+
{
487+
$result = $this->formBuilder->select(
488+
name: 'my-select',
489+
list: [
490+
'1' => 'Option 1',
491+
'2' => 'Option 2',
492+
'3' => 'Option 3',
493+
],
494+
selected: '2',
495+
options: []
496+
);
497+
498+
$this->assertElementIs('select', $result);
499+
$this->assertElementAttributeEquals('name', 'my-select', $result);
500+
$this->assertStringContainsString('<option value="1">Option 1</option>', $result);
501+
$this->assertStringContainsString('<option value="2" selected="selected">Option 2</option>', $result);
502+
$this->assertStringContainsString('<option value="3">Option 3</option>', $result);
503+
$this->assertStringNotContainsString('data-icon', $result);
504+
$this->assertStringNotContainsString('data-image', $result);
505+
}
506+
507+
/**
508+
* @testdox can create a select element with backward compatibility for optgroup integer keys
509+
*/
510+
public function testSelectBackwardCompatibilityOptgroupIdItemsKeys()
511+
{
512+
// this simulates grouped options base on a model with ids as keys
513+
$result = $this->formBuilder->select(
514+
name: 'my-select',
515+
list: [
516+
'Group1' => [
517+
1 => 'Option 1',
518+
2 => 'Option 2',
519+
],
520+
'Group2' => [
521+
3 => 'Option 3',
522+
4 => 'Option 4',
523+
],
524+
],
525+
selected: 2,
526+
options: []
527+
);
528+
529+
$this->assertElementIs('select', $result);
530+
$this->assertElementAttributeEquals('name', 'my-select', $result);
531+
532+
// Optgroups
533+
$this->assertStringContainsString('<optgroup label="Group1">', $result);
534+
$this->assertStringContainsString('<optgroup label="Group2">', $result);
535+
536+
// Options inside optgroups
537+
$this->assertStringContainsString('<option value="1">Option 1</option>', $result);
538+
$this->assertStringContainsString('<option value="2" selected="selected">Option 2</option>', $result);
539+
$this->assertStringContainsString('<option value="3">Option 3</option>', $result);
540+
$this->assertStringContainsString('<option value="4">Option 4</option>', $result);
541+
$this->assertStringNotContainsString('data-icon', $result);
542+
$this->assertStringNotContainsString('data-image', $result);
543+
}
544+
545+
/**
546+
* @testdox show case where backward compatibility is broken (expected)
547+
*/
548+
public function testSelectBackwardCompatibilityBrokenOptGroup()
549+
{
550+
// optgroup syntax with two items with integer keys starting at zero are seen as a regular option with an icon
551+
$result = $this->formBuilder->select(
552+
name: 'my-select',
553+
list: [
554+
'Group1' => [
555+
0 => 'Option 1',
556+
1 => 'Option 2',
557+
],
558+
],
559+
options: []
560+
);
561+
562+
$this->assertElementIs('select', $result);
563+
$this->assertElementAttributeEquals('name', 'my-select', $result);
564+
565+
// Options inside optgroups
566+
$this->assertStringContainsString('<option value="Group1" data-icon="Option 2">Option 1</option>', $result);
567+
$this->assertStringContainsString('data-icon', $result);
568+
}
569+
570+
/**
571+
* @testdox properly escapes HTML in option labels and values.
572+
*/
573+
public function testSelectHtmlEscaping()
574+
{
575+
$result = $this->formBuilder->select(
576+
name: 'my-select',
577+
list: [
578+
'<script>' => 'Normal Label',
579+
'safe-value' => ['<b>Bold Label</b>', 'icon-test'],
580+
],
581+
selected: null,
582+
options: []
583+
);
584+
585+
$this->assertElementIs('select', $result);
586+
587+
$this->assertStringContainsString('value="&lt;script&gt;"', $result);
588+
$this->assertStringContainsString('&lt;b&gt;Bold Label&lt;/b&gt;', $result);
589+
590+
// Ensure dangerous tags are not rendered as raw HTML
591+
$this->assertStringNotContainsString('value="<script>"', $result);
592+
$this->assertStringNotContainsString('<b>Bold Label</b>', $result);
593+
}
343594
}

0 commit comments

Comments
 (0)