• Czas czytania ~8 min
  • 06.07.2023

Ostatnio miałem potrzebę wypełnienia dostępnych opcji komponentu multiselect wartościami ze zdalnego interfejsu API. ChoicesJS to wspiera, choć jego dokumentacja pozostawia wiele do życzenia. W związku z tym nowy post opisujący, w jaki sposób wykonałem moją dynamiczną pracę multiselect, wydawał się ważny.

Co dokładnie rozumiem przez "dynamiczny multiwybór", o który pytasz? Po prostu jest to komponent wybierający umożliwiający wiele wyborów, którego opcje są pobierane - dynamicznie - z innego miejsca, a nie wstępnie ładowane z góry.

Scenariusz jest taki, że moi użytkownicy muszą mieć możliwość wybrania jednego lub więcej zasobów, ale mogą mieć setki lub tysiące na liście dostępnych zasobów, więc ładowanie wszystkich opcji z góry, aby wypełnić listę opcji do wyboru, nie wchodziło w rachubę. To oczywiście znacznie uprościłoby sprawę.

Jaka jest więc pożądana funkcjonalność i jak ją zbudować?

Cieszę się, że pytasz. Pożądane zachowanie polega na tym, że gdy użytkownik wpisze wyszukiwane hasło w polu wejściowym, które przedstawia nam ChoicesJS, chcemy wywołać wywołanie interfejsu API, które pobierze dopasowane zasoby i wypełni naszą listę dostępnych opcji, umożliwiając im dokonanie jednego lub więcej wyborów. Następnie mogą opcjonalnie wyszukiwać ponownie, wybierając więcej opcji i tak dalej.

Ponieważ używamy Livewire, cała interakcja z interfejsem API będzie miała miejsce po stronie serwera, piękno tego polega na tym, że żadne poświadczenia nie muszą być ujawniane po stronie klienta, a wszelkie ciężkie manipulacje danymi będą również występować po stronie serwera.

Ok, po pierwsze, potrzebujemy komponentu

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use MyApi\Client;
class Select extends Component
{
  public $options = [];
  public $selections = [];
  public function render()
  {
    return view('livewire.select');
  }
}

livewire To podstawy, ale musimy także napisać metodę do wywołania, gdy użytkownik wykona wyszukiwanie:

public search($term)
{
  $results = Client::search($term);
  $preserve = collect($this->options)
                ->filter(fn ($option) =>
                    in_array(
                      $option['value'],
                      $this->selections
                    )
                  )
                ->unique();
  $this->options = collect($results)
                     ->map(fn ($item) => 
                             [
                               'label' => $item->name,
                               'value' => $item->id
                             ])
                     ->merge($preserve)
                     ->unique();
  $this->emit('select-options-updated', $this->options);
}

Co się tutaj dzieje?

  1. We pass the entered search phrase to the search functionality of our api client. This could also be something as basic as a whereLike query against a laravel model.
  2. We look at the existing options, and pluck out any that are currently selected.
  3. We map the relevant data from the search results into the format expected by our ChoicesJS widget, and then merge in the options that we grabbed in the previous step.
  4. Emit an event so that the JS widget can update its options list.

Całkiem proste... Ale dlaczego krok 2 i scalanie wyników z wynikami zapytania?

Cóż, ponieważ sposób, w jaki mamy wybory, nie jest świadomy wyborów, których obecnie nie ma na liście dostępnych opcji. Prawdopodobnie mógłbyś przechowywać tam cały obiekt, gdybyś naprawdę chciał, ale wtedy przewoziłbyś o wiele więcej danych przy kolejnych wywołaniach żądań / odpowiedzi ... A to jest całkiem łatwe - i schludne.

To jest strona PHP posortowana, teraz do widoku Jest nasz podstawowy framework widoku

@props([
  'options' => [],
])
@once
  @push('css')
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css" />
  @endpush
  @push('js')
    <script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
  @endpush
@endonce
<div x-data="{}" x-init="">
  <select x-ref="select"></select>
</div>

uparty. Zwróć uwagę na użycie @once i @push do wypchnięcia wymaganych css i js do odpowiednich stosów i upewnij się, że są one dodawane tylko jeden raz, więc gdybyś miał wiele instancji tego komponentu na swojej stronie, nie zanieczyszczałbyś DOM kilkoma kopiami tego samego skryptu. Wspaniały. To wszystkie takie drobne drobiazgi sprawiają, że kocham Laravel...

Ok, więc ładujemy skrypt wyborów i style, teraz musimy skierować je na nasz samotny mały element wyboru i sprawić, by to zrobił. Jeden kawałek na raz tutaj..

Rozwińmy dane x

x-data="{
  value: @entangle('selections'),
  options: {{ json_encode($options) }},
  debounce: null,
}"
  • Wartość zostaje splątana, połączona ze stanem livewire.
  • Opcje są wypełniane dowolnymi wstępnie zdefiniowanymi wartościami, które mogliśmy ustawić domyślnie w naszym komponencie PHP
  • debounce będzie używany jako cel do śledzenia setTimeout() do debounce danych wejściowych użytkownika, tak naprawdę nie musisz go definiować, ale dla jasności go uwzględniłem.

Teraz mięso implementacji, x-init

x-init="this.$nextTick(() => {
  const choices = new Choices(this.$refs.select, {
    removeItems: true,
    removeItemButton: true,
    duplicateItemsAllowed: false,
  })
  const refreshChoices = () => {
    const selection = this.value
    choices.clearStore()
    choices.setChoices(this.options.map(({ value, label }) => ({
      value,
      label,
      selected: selection.includes(value),
    })))
  }

  this.$refs.select.addEventListener('change', () => {
    this.value = choices.getValue(true)
  })
  this.$refs.select.addEventListener('search', async (e) => {
    if (e.detail.value) {
      clearTimeout(this.debounce)
      this.debounce = setTimeout(() => {
        $wire.call('search', e.detail.value)
      }, 300)
    }
  })
  $wire.on('select-options-updated', (options) => {
    this.options = options
  })
  this.$watch('value', () => refreshChoices())
  this.$watch('options', () => refreshChoices())
  refreshChoices()
})"

W porządku, dzieje się tam mnóstwo, ale nic strasznie skomplikowanego.

  1. We instantiate an instance of ChoicesJS, pointing at the select element we previously created by it's x-ref value 'select', with some basic config.
  2. Create a function that will refresh the available options whenever we call it, iterating through options and setting selected to true for any items whose value is in our selections wire model bucket.
  3. Set up an event listener to sync the selections whenever a change event fires.
  4. Set up an event listener to call our PHP search method, debounced with a 300ms timeout.
  5. Set up a listener for the livewire event that we emit when we've updated the options on the server side, and accordingly update them on the client side.
  6. Set up watchers that call our refresh function when the selections or options have changed.
  7. Finally, fire the refresh function to setup the initial state of the choicesJS widget

To wszystko jest opakowane w a $nextTick , ponieważ znaczniki i biblioteki JS / itp muszą być na stronie, zanim zadziałają.

Nie mogę przypisać sobie zasługi za to wszystko, Caleb ładnie nakreślił, jak pracować z biblioteką choicesJS wraz z alpine w integracji zapisanej na stronie alpineJS. Ale wyszukiwanie i $wire.on bity to ja.

To wszystko. Po wpisaniu w widżecie choicesJS trafi on do klienta API i pobierze wyniki, te wyniki zostaną wypełnione na liście wyborów. Po dokonaniu wyboru będzie on działał tak samo, jak gdyby przez cały czas był częścią wstępnie wypełnionej listy. Po uruchomieniu nowego wyszukiwania poprzednio wybrane elementy zostaną scalone z nowymi wynikami wyszukiwania, tak aby poprzednie wybory pozostały widoczne w widżecie choicesJS.

Prawdopodobnie zauważyłeś, że na elemencie select nie zdefiniowano wire:model ... Cóż, to dlatego, że zarządzamy stanem za pomocą Alpine, więc nie jest to konieczne. choicesJS i tak nadpisze ten element select.

Nasz ostatni komponent livewire i widok

Livewire PHP Component

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use MyApi\Client;
class Select extends Component
{
  public $options = [];
  public $selections = [];
  public function render()
  {
    return view('livewire.select');
  }
  public search($term)
  {
    $results = Client::search($term);
    $preserve = collect($this->options)
                  ->filter(fn ($option) => 
                    in_array(
                      $option['value'],
                      $this->selections
                    )
                  )
                  ->unique();
    $this->options = collect($results)
                       ->map(fn ($item) =>
                         [
                           'label' => $item->name,
                           'value' => $item->id
                         ])
                       ->merge($preserve)
                       ->unique();
    $this->emit('select-options-updated', $this->options);
  }
}

Blade Zobacz

@props([
 'options' => [],
])
@once
  @push('css')
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js/public/assets/styles/choices.min.css" />
  @endpush
  @push('js')
    <script src="https://cdn.jsdelivr.net/npm/choices.js/public/assets/scripts/choices.min.js"></script>
  @endpush
@endonce

<div
  wire:ignore
  x-data="{
    value: @entangle('selections'),
    options: {{ json_encode($options) }},
    debounce: null,
  }"
  x-init="
    this.$nextTick(() => {
      const choices = new Choices(this.$refs.select, {
        removeItems: true,
        removeItemButton: true,
        duplicateItemsAllowed: false,
     })
     const refreshChoices = () => {
       const selection = this.value
  
       choices.clearStore()
       choices.setChoices(this.options.map(({ value, label }) => ({
         value,
         label,
         selected: selection.includes(value),
       })))
     }
     this.$refs.select.addEventListener('change', () => {
       this.value = choices.getValue(true)
     })
     this.$refs.select.addEventListener('search', async (e) => {
       if (e.detail.value) {
         clearTimeout(this.debounce)
         this.debounce = setTimeout(() => {
           $wire.call('search', e.detail.value)
         }, 300)
       }
     })
     $wire.on('select-options-updated', (options) => {
       this.options = options
     })
     this.$watch('value', () => refreshChoices())
     this.$watch('options', () => refreshChoices())
     refreshChoices()
   })">
  <select x-ref="select"></select>
</div>

końcowe przemyślenia

Ten komponent mógłby, a prawdopodobnie powinien, zostać oczyszczony, aby był bardziej wielokrotnego użytku. Sprawienie, by zdarzenie było odsłuchiwane jako rekwizyt przekazany do komponentu ostrza i tak dalej, ale to powinno dać ci wystarczający przegląd, aby coś takiego działało nad twoim projektem.

Jak zawsze, ogromne podziękowania dla społeczności, która sprawia, że wszystko to jest możliwe. Laravel, Livewire, AlpineJS i Tailwind naprawdę sprawiają, że rozwój jest marzeniem - jestem wielkim fanem stosu TALL!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297