yajra / laravel-datatables

jQuery DataTables API for Laravel
https://yajrabox.com/docs/laravel-datatables
MIT License
4.75k stars 858 forks source link

Using livewire/livewire components in a yajra/laravel-datatables table #2401

Closed Namoshek closed 3 years ago

Namoshek commented 4 years ago

Note: This is not a bug report or an issue. Prior to trying it out myself, I searched for the keyword livewire in the issues and did not find what I was looking for. So I thought, I leave a hint for future readers with a similar question. This issue may be closed at any time.

I always wanted to have dynamic components within my tables, but unfortunately with vuejs, the default option provided by Laravel, this isn't possible. The vuejs core parses the DOM once when loading the page and switches then to a virtual DOM which is unaware of changes in the actual DOM. Therefore, when returning vuejs components from a DataTable, they are not recognized by vuejs and are simply not activated. And the alternative, plain JavaScript, was never an option for me either.

When learning about livewire/livewire, I thought it might be a suitable alternative for exactly this scenario. And it turns out, it is! (As long as you add a tiny but necessary configuration to your tables.)

The only thing's I had to add to my DataTables configuration are the following two options:

{
    deferLoading: true,
    drawCallback: function(settings) {
        if (window.livewire) {
            window.livewire.rescan();
        }
    }
}

The deferLoading option was necessary for me because I'm also using a self-written logic to save and restore the state of my tables, especially the column search state. Without the option, my tables performed three draw cycles when loading a page, which seemed to break the livewire logic (it happened quite fast, maybe that's why). If you are not using custom logic in the initComplete callback, you most likely don't need this option.

The drawCallback is the actual sweetness needed to get things working. And it is straight forward as well. Whenever we finish drawing the table (which happens when loading new data, e.g. on search, sort or page change), we ask livewire to look for new components and initialize them. That's it. livewire will even make sure to subscribe to new Laravel Echo channels, if any of the newly recognized components listens for some events. Unfortunately though, it won't unsubscribe when components (i.e. table data) are replaced with new ones. For me, this isn't an issue though.

yajra commented 4 years ago

Thanks for sharing, I haven't tried livewire yet and surely will use this as a ref when I got the chance to play with it.

fabioi commented 4 years ago

Hello @Namoshek I am a bit new with LiveWire. I am trying to render a Livewire component on a custom 'actions' column but without success. I would like to have a livewire button component loaded in a custom datatable 'actions' column.

 $query = $this->userRepository->getIndexQuery();

        return DataTables::of($query)
            ->addColumn('roles', function (User $user) {
                             return $user->roles->implode('name', ', ');
                         })
            ->addColumn('actions', function (User $user) {
                            // render livewire component (button)
                         })
            ->rawColumns(['actions'])
            ->make(true);

Do you have any hints maybe? thank you a lot

Namoshek commented 4 years ago

You just need to return the html which renders the livewire component from the closure, similar to what you would use in a view.

fabioi commented 4 years ago

Thanks for your reply! I tried this way but cannot make it work:

Notes:

On the Laravel Controller I tried these two ways:

return the view:

 $query = $this->userRepository->getIndexQuery();

        return DataTables::of($query)
                         ->addColumn('roles', function (User $user) {
                             return $user->roles->implode('name', ', ');
                         })
                         ->addColumn('actions', function (User $user) {
                             return view('livewire.block-user-button');
                         })
                         ->rawColumns(['actions'])
                         ->toJson();

Return directly the HTML:

$query = $this->userRepository->getIndexQuery();

        return DataTables::of($query)
                         ->addColumn('roles', function (User $user) {
                             return $user->roles->implode('name', ', ');
                         })
                         ->addColumn('actions', function (User $user) {
                         return <<<'HTML'
<div>
    <input wire:model="buttonText" wire:click="myAction" type="text">
</div>
HTML;

                         })
                         ->rawColumns(['actions'])
                         ->toJson();

But unfortunately clicking the button does not trigger the component PHP function.

Do you have any other possible hint maybe? Would be super nice to use livewire to handle these button functionality

Namoshek commented 4 years ago

Your HTML is not a Livewire component, it's the template of the component. You need to return the call to the component, for example <livewire:counter> when we stick with the example component from the quickstart guide.

fabioi commented 4 years ago

Thank you for your suggestion @Namoshek!.

I have been able to make the Livewire component work in a Datatable column this way:


 $query = $this->userRepository->getIndexQuery();

        return DataTables::of($query)
                         ->addColumn('roles', function (User $user) {
                             return $user->roles->implode('name', ', ');
                         })
                         ->addColumn('actions', function (User $user) {
                             return Livewire::mount('block-user-button', ['user' => $user])->html();
                         })
                         ->rawColumns(['actions'])
                         ->toJson();
yajra commented 3 years ago

I was able to work with livewire now and the code suggestions here works great. I also added a new api on html builder to integrate with livewire as per suggestion of @Namoshek 🎊

    public function html()
    {
        return $this->builder()
                    ->setTableId('videos-table')
                    ->columns($this->getColumns())
                    ->minifiedAjax()
                    ->drawCallbackWithLivewire() // enable livewire integration
...

Tried implementing this with wire:poll for automatic loading of status from a job queue. Great work around for a realtime checking of row status with dataTables. ^_^

Namoshek commented 3 years ago

Cool stuff, thank you!

fabioi commented 3 years ago

super nice! Thank you @yajra :D

jhourlad commented 3 years ago

Hello, guys. It's still a blur on my side. Can anybody suggest how this issue is dealt with if we are doing livewire inside custom colums? Something similar to this code:

      const dt = table.DataTable( {
         columns: [
           { data: 'id', render: (data, type, row, meta) => {
             return `<a href="#" as="button" wire:click.prevent="edit(${data})">Edit</a>`;
           }},
         ],
         ...
      });

With that code, function edit() is not being called. Thanks!

Namoshek commented 3 years ago

@jhourlad If you use custom JavaScript render functions, the Blade component tags like <livewire:counter /> are not parsed and rendered correctly.

See my earlier comment as how to render Livewire components. You need to use a raw column which renders the Livewire component tag. And you need to add the new builder helper method yajra showed here.

kiotipot1 commented 3 years ago

This is how i add my button but it is still don't work

![Uploading dataTables_button.PNG…]()

front datatable dataTable_front any suggestion on how that is the proper way of displaying the datables? by the way i'm a newbie in laravel i just only starting to study it this day i badly need your help

fdelvals commented 3 years ago

Hello, guys. It's still a blur on my side. Can anybody suggest how this issue is dealt with if we are doing livewire inside custom colums? Something similar to this code:

      const dt = table.DataTable( {
         columns: [
           { data: 'id', render: (data, type, row, meta) => {
             return `<a href="#" as="button" wire:click.prevent="edit(${data})">Edit</a>`;
           }},
         ],
         ...
      });

With that code, function edit() is not being called. Thanks!

I have been stuck with your same problem. Does anyone have a working example? I can't figure it out with the guidelines given.

thank you

AtefR commented 3 years ago

was anyone able to make the livewire click work?

Namoshek commented 3 years ago

was anyone able to make the livewire click work?

You mean a clickable button for example, like <button wire:click="foo">? Absolutely, it requires no special handling.

AtefR commented 3 years ago

was anyone able to make the livewire click work?

You mean a clickable button for example, like <button wire:click="foo">? Absolutely, it requires no special handling.

It doesn't work with me though, there's no requests happening in Network tab when clicking and not performing the click

View:

<div>
    <button wire:click="increment">+</button>
    <h1>{{ $count }}</h1>
</div>

Component:

class Actions extends Component
{
    public $count = 0;

    public function increment()
    {
        $this->count++;
    }

    public function render()
    {
        return view('livewire.permission.actions');
    }
}

datatable

    public function dataTable($query)
    {
        return datatables()
            ->eloquent($query)
            ->editColumn('created_at', function ($data) {
                return $data->created_at->format('Y/m/d');
            })
            ->editColumn('action', function ($data) {
                return Livewire::mount('permission.actions', ['permission' => $data->id])->html();
            })
            ->rawColumns(['action']);
    }

and scripts are like this on layout

@livewireScripts
<script src="{{ mix('js/app.js') }}"></script>
@stack('scripts')
</body>
</html>
Namoshek commented 3 years ago

How does the relevant part of your DataTable look like? Have you added the draw callback, which Yajra added to the package?

AtefR commented 3 years ago

How does the relevant part of your DataTable look like? Have you added the draw callback, which Yajra added to the package?

yes, I did add it

    public function dataTable($query)
    {
        return datatables()
            ->eloquent($query)
            ->editColumn('created_at', function ($data) {
                return $data->created_at->format('Y/m/d');
            })
            ->editColumn('action', function ($data) {
                return Livewire::mount('permission.actions', ['permission' => $data->id])->html();
            })
            ->rawColumns(['action']);
    }

    public function html()
    {
        return $this->builder()
            ->setTableId('permissions-table')
            ->columns($this->getColumns())
            ->drawCallbackWithLivewire()
            ->addAction(['width' => '80px'])
            ->orderBy(1);
    }
Namoshek commented 3 years ago

My code looks a bit different, but I swapped it for your style (didn't know the Livewire::mount()->html() yet) and it works as well.

Are you rendering the scripts for your DataTable? Or is this some manually written stuff?

<div>
    {{ $dataTable->table([], true) }}
</div>

@push('scripts')
    {!! $dataTable->scripts() !!}
@endpush
AtefR commented 3 years ago

My code looks a bit different, but I swapped it for your style (didn't know the Livewire::mount()->html() yet) and it works as well.

Are you rendering the scripts for your DataTable? Or is this some manually written stuff?

<div>
    {{ $dataTable->table([], true) }}
</div>

@push('scripts')
    {!! $dataTable->scripts() !!}
@endpush

this is weird, this is how the view

<x-app-layout>

    <div class="row">
        <div class="col-12">
            <div class="card">
                <div class="card-body">
                    <div class="row mb-2">
                        <div class="col-sm-4">
                            @livewire('permission.create')
                        </div>
                    </div>

                    <div class="table-responsive">
                        <div class="table-responsive">
                            {{ $dataTable->table(['class' =>'table table-centered table-striped dt-responsive nowrap w-100'], true) }}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    @push('scripts')
        {!! $dataTable->scripts() !!}
    @endpush
</x-app-layout>

what's your livewire version might be some bug in the newer versions..

Namoshek commented 3 years ago

Latest, this is weird. I guess the browser is also not showing any click event handlers for the button?

AtefR commented 3 years ago

Latest, this is weird. I guess the browser is also not showing any click event handlers for the button?

no requests happens when I click the button, also the component doesn't show up on the chrome livewire extension. the HTML only is being injected to the view. Do I have to pass scripts in drawCallbackWithLivewire()?

how did u type ur code before knowing about Livewire::mount()->html()

Namoshek commented 3 years ago

I'm using ->editColumn('action', 'some.view.name') in my DataTable and <livewire:my.component :foo="$model" :bar="true" /> in the view for example. But as said, I tried your style and it works the same (and is definitely nicer 👍).

mahendra2448 commented 2 years ago

I was able to work with livewire now and the code suggestions here works great. I also added a new api on html builder to integrate with livewire as per suggestion of @Namoshek 🎊

    public function html()
    {
        return $this->builder()
                    ->setTableId('videos-table')
                    ->columns($this->getColumns())
                    ->minifiedAjax()
                    ->drawCallbackWithLivewire() // enable livewire integration
...

Tried implementing this with wire:poll for automatic loading of status from a job queue. Great work around for a realtime checking of row status with dataTables. ^_^

Hello, where you create this html() method? is it inside the component?

Namoshek commented 2 years ago

@mahendra2448 You'll need yajra/laravel-datatables-html for this. The usage is described in the documentation.