Search code examples
javascriptalpine.js

Adapt a multi input with JavaScript and Alpine JS


I have a multi input made with alpine js, is not mine, I search for one on Google and like this one. So, the problem is that 3 inputs on my form needs to be multi-options available, but the first input options repeat for all inputs (They should be different for each input).

I know is because that the input was implemented for 1 multi input per form, but I need to adapt it and I don't know have any idea of what to do. (I'm a Django dev, I don't have knowledge in alpine js at all and my knowledge of JavaScript is pretty basic).

If a pure soul can help me to adapt it or, at least, guide me how to do it, it would help. Thanks.

Dependencies: Tailwind (lasted stable), Alpine JS (lasted stable)

<section class="w-full">
<select x-cloak id="select" style="display:none" class="text-gray-400">
  <option value="1">Option 2</option>
  <option value="2">Option 3</option>
  <option value="3">Option 4</option>
  <option value="4">Option 5</option>
</select>

<label class="w-full text-sm">
  <span class="text-gray-700 dark:text-gray-400">Materia/s</span>
  <div x-data="dropdown()" x-init="loadOptions()" class="h-fit w-full flex flex-col items-center mx-auto">
    <input name="values" type="hidden" x-bind:value="selectedValues()">
    <div class="inline-block relative w-full">
      <div class="flex flex-col items-center relative">
        <div x-on:click="open" class="w-full svelte-1l8159u">
          <div class="mt-1 p-2 flex rounded border dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-400 dark:focus:shadow-outline-gray svelte-1l8159u">
            <div class="flex flex-auto flex-wrap grow gap-1">
              <template x-for="(option,index) in selected" :key="options[option].value">
                <div
                  class="flex justify-center gap-1 items-center font-medium pl-1 pr-0.5 rounded text-purple-700 bg-purple-100 border border-purple-300">
                  <div class="text-xs font-normal leading-none max-w-full flex-initial" x-model="options[option]" x-text="options[option].text"></div>
                  <div class="flex flex-auto flex-row-reverse">
                    <div x-on:click="remove(index,option)">
                      <svg xmlns="http://www.w3.org/2000/svg" role="button" viewBox="0 0 20 20" fill="currentColor" class="size-4 fill-current">
                        <path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
                      </svg>
                    </div>
                  </div>
                </div>
              </template>
              <div x-show="selected.length == 0" class="flex-1">
                <input placeholder="..."
                  class="bg-transparent p-0 border-none appearance-none outline-none h-full w-full text-gray-400 text-sm"
                  x-bind:value="selectedValues()">
              </div>
            </div>
            <div
              class="w-fit pl-2 border-l flex items-center border-gray-600 svelte-1l8159u">
              <button type="button" x-show="isOpen() === true" x-on:click="open"
                class="cursor-pointer outline-none focus:outline-none">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 cursor-pointer outline-none focus:outline-none">
                  <path fill-rule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
                </svg>
              </button>
              <button type="button" x-show="isOpen() === false" @click="close"
                class="cursor-pointer outline-none focus:outline-none">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 cursor-pointer outline-none focus:outline-none">
                  <path fill-rule="evenodd" d="M9.47 6.47a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L10 8.06l-3.72 3.72a.75.75 0 0 1-1.06-1.06l4.25-4.25Z" clip-rule="evenodd" />
                </svg>  
              </button>
            </div>
          </div>
        </div>
        <div class="w-full px-4">
          <div x-show.transition.origin.top="isOpen()"
            class="absolute shadow top-100 text-gray-400 bg-gray-700 z-40 w-full left-0 rounded max-h-select overflow-y-auto svelte-5uyqqj"
            x-on:click.away="close">
            <div class="flex flex-col w-full">
              <template x-for="(option,index) in options" :key="option">
                <div>
                  <div class="cursor-pointer w-full border-gray-600 rounded-b border-b hover:bg-purple-700"
                    @click="select(index,$event)">
                    <div x-bind:class="option.selected ? 'border-purple-600' : ''"
                      class="flex w-full items-center p-2 pl-2 hover:text-gray-100 relative">
                      <div class="w-full items-center flex">
                        <div class="mx-2 leading-6" x-model="option" x-text="option.text"></div>
                      </div>
                    </div>
                  </div>
                </div>
              </template>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</label>
</section>

JS code:

function dropdown() {
  return {
    options: [],
    selected: [],
    show: false,
    open() { this.show = true },
    close() { this.show = false },
    isOpen() { return this.show === true },
    select(index, event) {

      if (!this.options[index].selected) {

        this.options[index].selected = true;
        this.options[index].element = event.target;
        this.selected.push(index);

      } else {
        this.selected.splice(this.selected.lastIndexOf(index), 1);
        this.options[index].selected = false
      }
    },
    remove(index, option) {
      this.options[option].selected = false;
      this.selected.splice(index, 1);
    },
    loadOptions() {
      const options = document.getElementById('select').options;
      for (let i = 0; i < options.length; i++) {
        this.options.push({
          value: options[i].value,
          text: options[i].innerText,
          selected: options[i].getAttribute('selected') != null ? options[i].getAttribute('selected') : false
        });
      }
    },
    selectedValues() {
      return this.selected.map((option) => {
        return this.options[option].value;
      })
    }
  }
 }

Solution

  • I handle to fix it:

    1. Need to make a personal method to load options and unique ID for every select:

      <select x-cloak id="select1" style="display:none" class="text-gray-400">
        <!-- options -->
      </select>
      
      loadOptions1() {
         var options = document.getElementById('select1').options;
         ...
      
      # Then, repeat the same process for select2, select3...
      
      <select x-cloak id="select2" style="display:none" class="text-gray-400">
        <!-- options -->
      </select>
      
      loadOptions2() {
         var options = document.getElementById('select2').options;
         ...
      
    2. Assign the new method to every select on x-init property:

      <div x-data="dropdown()" x-init="loadOptions1()" class="h-fit w-full flex flex-col items-center mx-auto">
      ...
      
      <div x-data="dropdown()" x-init="loadOptions2()" class="h-fit w-full flex flex-col items-center mx-auto">
      ...
      
    3. Change the name property to have a unique one for every select, so you catch the data with POST request:

      <input name="value1" type="hidden" x-bind:value="selectedValues()" readonly="readonly">
      ...
      
      <input name="value2" type="hidden" x-bind:value="selectedValues()" readonly="readonly">
      ...