CRUD icon indicating copy to clipboard operation
CRUD copied to clipboard

[Pro] HTML in Select2 fields

Open pxpm opened this issue 1 year ago • 1 comments

Discussed in https://github.com/Laravel-Backpack/community-forum/discussions/1109

Originally posted by giovdi August 7, 2024 Hi everyone! Today, I was wondering if it's possible to write HTML into a Select2 dropdown with Backpack. Well, the short answer (I think) is no... but...

I tried looking at the code that generates the Select2 Javascript for my specific case, and it is probably pretty easy to do.

In this example, I added the following code blocks to backpack/pro/resources/views/fields/relationship/select.blade.php.

    // make sure the $field['value'] takes the proper value
    $current_value = old_empty_or_null($field['name'], []) ??  $field['value'] ?? $field['default'] ?? [];

+   if(isset($field['html']) && !is_array($field['html'])) {
+       $field['html'] = [true];
+   }
+   $activeHtml = !empty($field['html']) ? true : false;
+
+   if ($activeHtml) {
+       $field['html']['base_element'] = $field['html']['base_element'] ?? 'span';
+   }

    if (!empty($current_value) || is_int($current_value)) {
        switch (gettype($current_value)) {
        @if($field['multiple'])
        multiple
        @endif

+       data-html="{{ var_export($field['allows_null']) }}"
+       @if($activeHtml)
+         data-html-element="{{ $field['html']['base_element'] }}"
+       @endif
        >
    function bpFieldInitRelationshipSelectElement(element) {
        const $placeholder = element.attr('data-placeholder');
        const $multiple = element.attr('data-field-multiple')  == 'false' ? false : true;
+       const $html = element.attr('data-html')  == 'true' ? true : false;
+       const $htmlElement = element.attr('data-html-element');
        const $allows_null = (element.attr('data-column-nullable') == 'true') ? true : false;
        var $select2Settings = {
                theme: 'bootstrap',
                multiple: $multiple,
                placeholder: $placeholder,
                allowClear: $allowClear,
                dropdownParent: $isFieldInline ? $('#inline-create-dialog .modal-content') : $(document.body)
            };
+           if ($html) {
+               $select2Settings.templateSelection = function(elem) {
+                   if (!elem.id) {
+                       return elem.text;
+                   }
+
+                   return $('<'+$htmlElement+'>'+elem.text+'</'+$htmlElement+'>');
+               }
+               $select2Settings.templateResult = function(elem) {
+                   if (!elem.id) {
+                       return elem.text;
+                   }
+
+                   return $('<'+$htmlElement+'>'+elem.text+'</'+$htmlElement+'>');
+               }
+           }
        if (!$(element).hasClass("select2-hidden-accessible"))

At this point, I can add the following option to the CRUD:

CRUD::field([
	'name' => 'variants',
	'type' => "relationship",
	'inline_create' => true,
	'init_rows' => 1,
	'min_rows' => 1,
	'subfields' => [
		[
			'name' => 'price',
			'label' => 'Price',
			'type' => 'number',
			'prefix' => "&euro;",
			'attributes' => ["step" => "any"],
			'wrapper' => [
				'class' => 'form-group col-md-3'
			],
		],
		[
			'name' => 'weight_initial',
			'label' => 'Weight',
			'type' => 'number',
			'suffix' => "grams",
			'wrapper' => [
				'class' => 'form-group col-md-3'
			],
		],
	],
	'pivotSelect' => [
+		'html' => true,
		'wrapper' => [
			'class' => 'form-group col-md-6'
		],
		'options' => (function ($query) {
			return $query->orderBy('variant_name', 'ASC')->get();
		})
	],
]);

...and with the following accessor:

public function identifiableAttribute()
{
	return 'variant_name_full';
}

protected function variantNameFull(): Attribute
{
	return Attribute::make(
		get: function () {
			$ret = '';
			if (!empty($this->product->filament_material)) {
				$ret .= '<span class="badge badge-primary">'.$this->product->filament_material->filament_material.'</span> ';
			}
			if (!empty($this->attributes['color'])) {
				$ret .= '<span class="badge" style="border:1px solid black; background-color:'.$this->attributes['color'].'">&nbsp;</span>';
			}
			$ret .= " ".$this->product->manufacturer->manufacturer_name . ' - ' . $this->product->product_name . ' - ' . $this->attributes['variant_name'];
			if (!empty($this->code)) {
				$ret .= ' ('.$this->code.')';
			}
			return $ret;

		},
	);
}

...here is the result! Screenshot 2024-08-07 222805

Not so bad, isn't it?

As you can see from the code blocks, a <span> tag surrounds the HTML option, but you can customize it with the base_element property, as follows: 'html' => ['base_element' => 'div']. You can't avoid this because if you mix plain text options and HTML ones, the $(elem.text); fails with a JS error, so at least one HTML tag is required.

Please let me know your suggestions... or if I just lost 2 hours of my life for some option that I don't know 😄 Unfortunately, I don't have enough time to create a full PR in a reasonable time, so please, if it's a good idea, take care of it :)

Thanks a lot!

pxpm avatar Aug 16 '24 14:08 pxpm

I was 1 minute into reading it and was ready to say "NO" but, holy shit... can this be useful! Like for example... when you have a selector for people - to show the avatar too. Or show both the name and their email, but in a nice way, not in the standard John Doe - [email protected].

I like this, I think it's a nice COULD-DO. Buuuut.... I would mean having to do this in all our select2s right 🤦‍♂️ So it would take quite some time to actually implement it in Backpack PRO 😔

tabacitu avatar Aug 27 '24 08:08 tabacitu