thedevdojo / voyager

Voyager - The Missing Laravel Admin
https://voyager.devdojo.com
MIT License
11.72k stars 2.67k forks source link

"Tags" as a formfield #5870

Open Omais-Rana opened 6 days ago

Omais-Rana commented 6 days ago

Laravel version

10.0

PHP version

8.1

Voyager version

1.6

Description of problem

Desperately needed a formfield type that would be helpful in creating tags and I know there could be the approach of storing a string and exploding via separation through commas but that's not neat at all and a bit complex as well. So I made a custom formfield that accepts tags at the time of input and stores it as an array. It works similary to the Multiple Dropdown formfield but instead of having to declare options beforehand and only being able to choose from those, this approach lets you create them dynamically. Its not the cleanest looking but it works. I hope voyager makes a default tags formfield in near future.

Proposed solution

Steps to reproduce:

  1. Make a folder named "FormFields" inside Laravel's "App" folder so the directory looks like this "App/FormFields"
  2. Inside FormFields folder create a php file named "TagsFormField.php" and use this code
<?php
namespace App\FormFields;
use TCG\Voyager\FormFields\AbstractHandler;

class TagsFormField extends AbstractHandler
{
    protected $codename = 'tags';

    public function createContent($row, $dataType, $dataTypeContent, $options)
    {
        return view('voyager::formfields.tags', [
            'row'             => $row,
            'dataType'        => $dataType,
            'dataTypeContent' => $dataTypeContent,
            'options'         => $options,
        ]);
    }
}
  1. Now go to "vendor\tcg\voyager\resources\views\formfields" and create a blade file named "tags.blade.php". Paste this code there
@php
    $uniqueId = uniqid();
    $currentTags = !empty($dataTypeContent->{$row->field}) ? json_decode($dataTypeContent->{$row->field}) : [];
    $currentTags = is_array($currentTags) ? $currentTags : [];
@endphp

<div id="{{ $row->field }}-tag-container-{{ $uniqueId }}">
    @foreach ($currentTags as $tag)
        <span class="badge badge-primary mr-1">
            {{ $tag }}
            <button type="button" class="btn btn-sm btn-danger ml-1"
                onclick="removeTag('{{ $uniqueId }}', '{{ $tag }}')">
                X
            </button>
        </span>
    @endforeach
</div>

<div class="form-group">
    <input type="text" class="form-control" id="{{ $row->field }}-tag-input-{{ $uniqueId }}">
    <input type="hidden" name="{{ $row->field }}" id="{{ $row->field }}-hidden-{{ $uniqueId }}"
        value='{{ json_encode($currentTags) }}'>
    <small class="form-text text-muted">Press Enter to add tags</small>
</div>

<script>
    document.addEventListener('DOMContentLoaded', function() {
        function initializeTagInput(uniqueId) {
            const tagContainer = document.getElementById('{{ $row->field }}-tag-container-' + uniqueId);
            const tagInput = document.getElementById('{{ $row->field }}-tag-input-' + uniqueId);
            const tagsHidden = document.getElementById('{{ $row->field }}-hidden-' + uniqueId);
            let tags = {!! json_encode($currentTags) !!};

            updateTagDisplay();

            tagInput.addEventListener('keydown', function(e) {
                if (e.key === 'Enter' && tagInput.value.trim() !== '') {
                    e.preventDefault();
                    const tag = tagInput.value.trim();
                    if (!tags.includes(tag)) {
                        tags.push(tag);
                        updateTagDisplay();
                    }
                    tagInput.value = '';
                }
            });

            function updateTagDisplay() {
                tagContainer.innerHTML = '';
                tags.forEach(tag => {
                    const tagElement = document.createElement('span');
                    tagElement.classList.add('badge', 'badge-primary', 'mr-1');
                    tagElement.textContent = tag;

                    const removeButton = document.createElement('button');
                    removeButton.type = 'button';
                    removeButton.classList.add('btn', 'btn-sm', 'btn-danger', 'ml-1');
                    removeButton.innerHTML = 'X';
                    removeButton.onclick = function() {
                        removeTag(tag);
                    };

                    tagElement.appendChild(removeButton);
                    tagContainer.appendChild(tagElement);
                });

                tagsHidden.value = JSON.stringify(tags);
            }

            function removeTag(tagToRemove) {
                tags = tags.filter(tag => tag !== tagToRemove);
                updateTagDisplay();
            }

            window.removeTag = function(id, tag) {
                if (id === uniqueId) {
                    removeTag(tag);
                }
            };
        }

        initializeTagInput('{{ $uniqueId }}');
    });
</script>
  1. Now the last step is to go to "App/Providers/AppServiceProvider.php" and paste this code
    
    <?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider; use App\FormFields\TagsFormField; use TCG\Voyager\Facades\Voyager; use Illuminate\Support\Facades\View; use App\Http\ViewComposers\CartComposer;

class AppServiceProvider extends ServiceProvider { /**

Alternatives considered

No response

Additional context

Screenshots attached

Screenshot 2024-06-26 101336

Screenshot 2024-06-26 101455

Screenshot 2024-06-26 101636