zaster's avatar

Livewire - Many to Many - Alternative to Select2

Select2 is based on jquery and bootstrap. What would be the best/better approach to achieve this when using the TALL Stack.

0 likes
12 replies
Snapey's avatar

depends what you want to do with it

zaster's avatar

@snapey

Customer and Company has a Many to Many relationship

and I want to assign companies to a customer

zaster's avatar

@snapey I also need to remove the companies from the customer when a particular company is no longer required to be linked to a customer

Snapey's avatar

and a select box is the best way to do that?

Foks's avatar

I think he'd like to have to search ability in the select box @snapey

1 like
zaster's avatar

@foks thank you for clearing things out.

@snapey I will give this a try and update, If it works for multiple values, then this should be ok.

Snapey's avatar

it doesn't work for multiple values, but that's not how i would select multiple companies

Snapey's avatar

As you are doing this in livewire, I would not select multiple companies. I would select one and add it to the list of companies already attached to the customer.

Using the article mentioned before, create a type-ahead search box, then when you find the right one, select it and in the component, do the attach.

What the article doesn't mention is that you ideally need a hidden field for the selected company. When the user selects the company, put its id in the hidden field using alpine.

Here, I use the techniques in the article to create a school picker.

Autocomplete.php (parent class)

<?php

namespace App\Http\Livewire;

use Livewire\Component;

abstract class Autocomplete extends Component
{
    public $results;
    public $search;
    public $selected;
    public $showDropdown;
    public $initial;

    abstract public function query();

    public function mount()
    {
        $this->showDropdown = false;
        $this->results = collect();
    }

    public function updatedSelected()
    {
        $this->emitSelf('valueSelected', $this->selected);
    }

    public function updatedSearch()
    {
        if (strlen($this->search) < 2) {
            $this->results = collect();
            $this->showDropdown = false;
            return;
        }

        if ($this->query()) {
            $this->results = $this->query()->get();
        } else {
            $this->results = collect();
        }

        $this->selected = '';
        $this->showDropdown = true;
    }

    public function render()
    {
        return view('livewire.autocomplete');
    }


}

The above is common to any auto complete component

The 'school' picker instance

<?php

namespace App\Http\Livewire;

use App\School;

class SchoolPicker extends Autocomplete
{
    protected $listeners = ['valueSelected'];

    public $fieldname = 'school';

    public function valueSelected(School $school)
    {
        $this->selected = $school->id;
        $this->emitUp('schoolSelected', $school);
    }

    public function query()
    {
        return School::where('name', 'like', '%' . $this->search . '%')->orderBy('name');
    }

    public function resultPresenter($result)
    {
        return sprintf(
            '%s, %s - %s',
            $result->name,
            $result->postcode,
            $result->region,
        );
    }

}

and the generic autocomplete view

<div class="">
    <style>
        .autocomplete .results ul {
            border:1px solid #ccc;
            font-size:14px;
            padding:5px;
            margin-top:0;
        }

        .autocomplete .results li {
            cursor:pointer;
            margin-top:4px; margin-bottom:4px;
            padding:0px 5px;
        }

        .autocomplete .results li:hover {
            background-color:#ccc;
        }
        .autocomplete .results .bg-highlight {
            background-color:#ccc;
        }

    </style>
    <div class="autocomplete">
        <div x-data="{
          open: @entangle('showDropdown'),
          search: @entangle('search'),
          selected: @entangle('selected'),
          highlightedIndex: 0,
          highlightPrevious() {
            if (this.highlightedIndex > 0) {
              this.highlightedIndex = this.highlightedIndex - 1;
              this.scrollIntoView();
            }
          },
          highlightNext() {
            if (this.highlightedIndex < this.$refs.results.children.length - 1) {
              this.highlightedIndex = this.highlightedIndex + 1;
              this.scrollIntoView();
            }
          },
          scrollIntoView() {
            this.$refs.results.children[this.highlightedIndex].scrollIntoView({
              block: 'nearest',
              behavior: 'smooth'
            });
          },
          updateSelected(id, name) {
            this.selected = id;
            this.search = name;
            this.open = false;
            this.highlightedIndex = 0;
          },
      }">
            <input type="hidden" name="{{ $fieldname }}_id" wire:model="selected" />
            <div x-on:value-selected="updateSelected($event.detail.id, $event.detail.name)">
                <span>
                    <div>
                        <input 
                            type="text" name="{{ $fieldname }}"
                            class="form-control"
                            autocomplete="off"
                            wire:model.debounce.300ms="search" 
                            x-on:keydown.arrow-down.stop.prevent="highlightNext()" 
                            x-on:keydown.arrow-up.stop.prevent="highlightPrevious()" 
                            x-on:keydown.enter.stop.prevent="$dispatch('value-selected', {
                                id: $refs.results.children[highlightedIndex].getAttribute('data-result-id'),
                                name: $refs.results.children[highlightedIndex].getAttribute('data-result-name')
                            })">
                    </div>
                </span>
    
                <div x-show="open" x-on:click.away="open = false" class="results">
                    <ul x-ref="results">
                        @forelse($results as $index => $result)
                            <li wire:key="{{ $result->id }}" x-on:click.stop="$dispatch(`value-selected`, {
                                    id: {{ $result->id }},
                                    name: `{{ $result->name }}`
                            })" :class="{
                                'bg-highlight': {{ $index }} === highlightedIndex,
                                'text-white': {{ $index }} === highlightedIndex
                                }" data-result-id="{{ $result->id }}" data-result-name="{{ $result->name }}">
                            <span>
                                {!! $this->resultPresenter($result) !!}
                            </span>
                        </li>
                        @empty
                        <li>No results found</li>
                        @endforelse
                    </ul>
                </div>
            </div>
        </div>
    </div>
</div>

This unfortunately was on a bootstrap project so I had to throw in some styles into the component. In TALL stack you would apply the tailwind css classes direct

2 likes
jlrdw's avatar

You can build your own select option drop down, or even have a table in a modal where user can select from. I normally use a searchable table, and a modal is so easy to do.

2 likes

Please or to participate in this conversation.