rappasoft/laravel-livewire-tables

[Feature Request]: Custom Component Without Database Column

CyberPunkCodes opened this issue · 7 comments

Overview

I am trying to create an "Action" column with view, edit, and delete actions at the end of the table rows. Instead of using text, using/rendering the html. Using the text of <i class="fa-solid fa-eye me-2"> should show the FontAwesome eyeball icon. It just prints the text. Yes, I have the library loaded. The LinkComponent doesn't process html.

Detailed explanation

I know to use a ButtonGroupColumn to use multiple LinkColumns, which would allow for the 3 links (view, edit, delete). However, it's still limited to text links because it's 3 LinkColumns. I don't see any way to use/render html instead of text for the anchor of the links.

I then tried ComponentColumn. I figured I could just pass it to my own component view and be on my way. Unfortunately, the ComponentColumn is coupled to the table's column in the database and kept throwing errors about missing an action column. I don't need it bound to a column. I need it to behave like the LinkColumn does. I don't know if you would call it an Anonymous Column or Decoupled Column or what.

Made My Own

Since I struck out with the LinkColumn and ComponentColumn, I decided to make my own component column named CustomComponentColumn. It is a copy of the ComponentColumn, but using the LinkColumn as a reference, I figured out how to bypass the use/need of a table column. So it's now a "decoupled" or "anonymous" column.

I then noticed that only 2 parameters are passed to the component, $slot and $attributes. If I wanted to pass the user's id for example, it would be in the attributes array and accessed via $attributes['user_id'] in the component's view file. We don't typically use variables like that. I would expect to do $user_id or pass the whole user object and do $user->id. I noticed the "old" way of using the ComponentColumn was to pass the view directly along with with(). So I added a with() callback to pass in an array of data to send to the view.

Notes

I wrote all of that to explain the issue and the "use case". If I didn't miss something with this repo on how to use html in the LinkColumn and there isn't already a way to pull this off, then hopefully we can discuss this, and I can create a Pull Request. I don't want to create a PR if I missed something silly or it isn't likely to get merged.

Note: in my CustomComponentColumn class, I left some commented code so you can see what I changed from the original ComponentColumn class. Also there are 2 methods that would need to be added to Rappasoft\LaravelLivewireTables\Views\Traits\Helpers\ComponentColumnHelpers and 1 method to add to Rappasoft\LaravelLivewireTables\Views\Traits\Configuration\ComponentColumnConfiguration. They are commented.

CustomComponentColumn:

<?php

namespace App\Services;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\HtmlString;
use Illuminate\View\ComponentAttributeBag;
use Rappasoft\LaravelLivewireTables\Exceptions\DataTableConfigurationException;
use Rappasoft\LaravelLivewireTables\Views\Column;
use Rappasoft\LaravelLivewireTables\Views\Traits\Configuration\ComponentColumnConfiguration;
use Rappasoft\LaravelLivewireTables\Views\Traits\Helpers\ComponentColumnHelpers;

class CustomComponentColumn extends Column
{
    use ComponentColumnHelpers,
        ComponentColumnConfiguration;

    protected string $componentView;

    protected $attributesCallback;

    protected $slotCallback;

    protected $withCallback;

    public function __construct(string $title, string $from = null)
    {
        parent::__construct($title, $from);

        // disables the need for database column, else throws error that column not found
        $this->label(fn () => null);
    }

    // add to: Rappasoft\LaravelLivewireTables\Views\Traits\Helpers\ComponentColumnHelpers
    public function getWithCallback()
    {
        return $this->withCallback;
    }

    // add to: Rappasoft\LaravelLivewireTables\Views\Traits\Helpers\ComponentColumnHelpers
    public function hasWithCallback(): bool
    {
        return $this->withCallback !== null;
    }

    // add to: Rappasoft\LaravelLivewireTables\Views\Traits\Configuration\ComponentColumnConfiguration
    public function with(callable $callback): self
    {
        $this->withCallback = $callback;

        return $this;
    }

    public function getContents(Model $row)
    {
        // since we used label() in constructor, this triggered
        // if ($this->isLabel()) {
        //     throw new DataTableConfigurationException('You can not use a label column with a component column');
        // }

        if (false === $this->hasComponentView()) {
            throw new DataTableConfigurationException('You must specify a component view for a component column');
        }

        $attributes = [];
        //$value = $this->getValue($row);   // we don't have a value because we aren't using a db column
        $slotContent = '';

        if ($this->hasAttributesCallback()) {
            $attributes = call_user_func($this->getAttributesCallback(), $row, $this);

            if (! is_array($attributes)) {
                throw new DataTableConfigurationException('The return type of callback must be an array');
            }
        }

        if ($this->hasSlotCallback()) {
            $slotContent = call_user_func($this->getSlotCallback(), $row, $this);
            if (is_string($slotContent)) {
                $slotContent = new HtmlString($slotContent);
            }
        }

        $withData = [];

        if ( $this->hasWithCallback() ) {
            $withData = call_user_func($this->getWithCallback(), $row, $this);
        }

        $view = view($this->getComponentView(), [
            'attributes' => new ComponentAttributeBag($attributes),
            'slot' => $slotContent,
        ]);

        if ( isset($withData) && is_array($withData) && ! empty($withData) ) {
            foreach ( $withData as $withKey => $withValue) {
                $view->with($withKey, $withValue);
            }
        }

        return $view;

        // return view($this->getComponentView(), [
        //     'attributes' => new ComponentAttributeBag($attributes),
        //     'slot' => $slotContent,
        // ]);
    }
}

UserTable extends DataTableComponent:

CustomComponentColumn::make('Action')
                ->component('livewire.datatables.action-column')
                ->attributes(fn ($row) => [
                    'foo' => 'bar',
                    // 'type' => Str::endsWith($value, 'example.org') ? 'success' : 'danger',
                    // 'dismissible' => true,
                ])
                ->with(fn ($row) => [
                    'viewLink' => route('users.view', $row),
                    'editLink' => route('users.edit', $row),
                ]),

livewire.datatables.action-column component view:

<div>
    <a href="{{ $viewLink }}"><i class="fa-solid fa-eye me-2"></i></a>
    <a href="{{ $editLink }}"><i class="fa-solid fa-pen-to-square"></i></a>
</div>

PS: I didn't want to pollute this any more with the delete code. Obviously, it would be a form with delete method and csrf. This shows the point... though, is it possible to delete the record/row and trigger a refresh so it re-populates the data in the table listing? That's my next question lol.

Final Result:

CustomColumnComponent

Turns out I was on an older version of this package and Livewire v2... I don't know how other than I started working on this a few weeks ago and something along the way held me back on Livewire v2?... I just upgraded to v3. Most seems to work except my edit forms and an error on the CustomComponentColumn is reporting being incompatible.

Please give me some time to sort this out and I will report back.

@CyberPunkCodes , I have to admit to having only glanced at this! Always happy to add more options into the package!

Looking at what you're suggesting, is this technically just adding a "with" option to a Component Column to pass additional data into it? And also allowing you to use "label" style Columns (i.e. those that don't have a corresponding database table).

Just to help me fully understand any limitations with the current approach, I'm assuming you're aware of the following:

// Column has a related database field
Column::make("Email")
    ->format(function ($value) {
        return view('components.alert')
            ->with('attributes', new ComponentAttributeBag([
                'type' => Str::endsWith($value, 'example.org') ? 'success' : 'danger',
                'dismissible' => true,
            ]))
            ->with('slot', $value);
    }),

If your Component isn't doing much (or you're using anonymous components)

// Action doesn't have a related database field, so we use a "label" method
            Column::make('Action')
            ->label(
                fn ($row, Column $column) => view('livewire.tables.actions')->withValue(
                    [
                        'viewLink' => route('users.view', $row),
                        'editLink' => route('users.edit', $row),
                    ]
                )
            )->html(),

I use a similar approach in my tables to generate a View/Edit/Delete "Actions" column, and put the logic into a anonymous component.

Using a "label" Column just stops the table from trying to find a related database field.

@CyberPunkCodes - if you reach out on the official Discord, then I'll give you any pointers needed ahead of you opening up a PR etc

Feature already exists - closing.

@lrljoe Thank you for your reply. The 2nd example is what I was looking for, an anonymous column without a database column. I initially tried the link column then the component usage and overlooked label.

For anyone else stumbling across this, withValue() is not correct. It should just be with().

It looks like it is technically documented, it's just buried in the label() section and very easy to overlook. I think maybe a dedicated section could be used. Something like "Anonymous Columns" or something fitting under the "Columns" section on the left of the docs page.

Since it's some simple docs, I can write up a PR for the docs (I assume you have a repo, haven't looked) so you can see if it makes sense to add.

The docs are in the "docs" section of the repo, the markdown just gets parsed for the Rappasoft site.

withValue(string) and withValues(array) are valid and function, it'll just make the data available the blade as $value or $values respectively.

Worth noting that I've been planning to do the same for the docs for the Columns as for the Filters, and properly split them out. But welcome any input on docs always

@lrljoe I have created PR #1556 that adds an "Anonymous Columns" section to the documentation. I wasn't sure what you meant by "been planning to do the same for the docs for the Columns as for the Filters", so I just added a new page under the columns section. Feel free to modify as you see fit.

Thanks for your help :)