• Время чтения ~4 мин
  • 06.07.2023

Недавно у меня возникла необходимость заполнить доступные параметры для компонента с множественным выбором значениями из удаленного API. ChoicesJS поддерживает это, хотя его документация оставляет желать лучшего. Таким образом, новый пост с подробным описанием того, как я сделал свою динамическую работу с множественным выбором, казался важным.

Что именно я имею в виду под «динамическим мультивыбором», спросите вы? Проще говоря, это компонент выбора, позволяющий выбирать несколько вариантов, параметры которого динамически извлекаются откуда-то еще, а не предварительно загружены заранее.

Сценарий здесь заключается в том, что мои пользователи должны иметь возможность выбрать один или несколько ресурсов, но у них могут быть сотни или тысячи в списке доступных ресурсов, поэтому о загрузке всех параметров заранее, чтобы заполнить список выбираемых параметров, не могло быть и речи. Это, конечно, значительно упростило бы ситуацию.

Итак, каков желаемый функционал и как его построить?

Я рад, что вы спросили. Желаемое поведение заключается в том, что, когда пользователь вводит свой поисковый запрос в поле ввода, которое представляет нам ChoicesJS, мы хотим запустить вызов API, который извлечет соответствующие ресурсы и заполнит наш список доступных опций, позволяя им сделать один или несколько вариантов. Затем они могут при желании выполнить поиск еще раз, выбрав дополнительные параметры и так далее.

Поскольку мы используем Livewire, все взаимодействие с API будет происходить на стороне сервера, прелесть этого в том, что никакие учетные данные не должны быть предоставлены на стороне клиента, и любые тяжелые манипуляции с данными также будут происходить на стороне сервера.

Хорошо, обо всем по порядку, нам нужен компонент

<?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 Это основы, но нам также нужно написать метод для вызова, когда пользователь выполняет поиск:

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);
}

Что здесь происходит?

  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.

Довольно просто... Но зачем нужен шаг 2 и объединение его результатов в результаты поискового запроса?

Ну, потому что у нас есть выборы, он не знает о вариантах, которых в настоящее время нет в списке доступных вариантов. Вы, вероятно, могли бы хранить там весь объект, если бы действительно захотели, но тогда вы бы таскали с собой гораздо больше данных о последующих вызовах запроса/ответа... А это довольно просто – и аккуратно.

Это сторона PHP отсортирована, теперь перейдем к представлению

@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>

Там наш базовый фреймворк представления заглушен. Обратите внимание на использование @once и @push для отправки необходимых css и js в соответствующие стеки и убедитесь, что они добавляются только один раз, поэтому, если у вас есть несколько экземпляров этого компонента на вашей странице, вы не будете загрязнять DOM несколькими копиями одного и того же скрипта. Славный. Именно такие маленькие тонкости заставляют меня любить Laravel...

Итак, мы загружаем сценарий выбора и стили, теперь нам нужно указать их на наш одинокий маленький элемент выбора и заставить его работать. По одному кусочку за раз здесь..

Давайте конкретизируем x-data

x-data="{
  value: @entangle('selections'),
  options: {{ json_encode($options) }},
  debounce: null,
}"
  • Значение запутывается, связывается с состоянием живого провода.
  • Опции заполняются любыми предопределенными значениями, которые мы могли установить по умолчанию в нашем компоненте PHP
  • debounce будет использоваться в качестве цели для отслеживания setTimeout() для устранения отклонения пользовательского ввода, вам не нужно его определять, но для ясности я включил его.

Теперь суть реализации, 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()
})"

Хорошо, там происходит куча, но ничего страшного.

  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

Все это завернуто в a $nextTick потому, что разметка и библиотеки JS / и т. Д. Должны быть на странице, прежде чем она будет работать.

Я не могу взять на себя ответственность за все это, Калеб красиво описал, как работать с библиотекой choicesJS вместе с alpine в написании интеграций на сайте alpineJS. Но поиск и $wire.on биты - это все я.

Вот и все. Когда вы вводите в виджет choicesJS, он попадает в клиент api и получает результаты, эти результаты будут заполнены в списке вариантов. Когда вы сделаете выбор, он будет работать так же, как если бы он был частью предварительно заполненного списка все это время. Когда вы запускаете новый поиск, ранее выбранные элементы будут объединены с новыми результатами поиска, чтобы предыдущие выборы оставались видимыми в виджете choicesJS.

Вы, наверное, заметили, что в элементе select не определен wire:model ... Ну, это потому, что мы управляем государством с помощью Alpine, так что в этом нет необходимости. choicesJS в любом случае переопределит этот элемент select.

Наш последний компонент livewire & view

Blade компонента Livewire PHP

<?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);
  }
}

Просмотр

@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>

Заключительные мысли Этот

компонент может и, вероятно, должен быть очищен, чтобы его можно было использовать повторно. Сделать так, чтобы событие прослушивалось, как реквизит, передаваемый в компонент лезвия и так далее, но это должно дать вам достаточно обзора, чтобы что-то подобное работало над вашим проектом.

Как всегда, огромное спасибо сообществу, которое делает все это возможным. Laravel, Livewire, AlpineJS и Tailwind действительно превращают разработку в мечту - я огромный, огромный поклонник стека TALL!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

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...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297