omines / datatables-bundle

DataTables bundle for Symfony
https://omines.github.io/datatables-bundle/
MIT License
251 stars 113 forks source link

[rookie question] How to customize datatable.js without modify it? #213

Closed polochon777 closed 3 years ago

polochon777 commented 3 years ago

Hello, first, I apologize for this maybe dummy questions but I did a lot of google and I don't find any answer. I'm also a rookie in web development, I do my best but maybe I've not understood at 100% how to use this great bundle.

Q1: How to add multiple search fields from a symfony custom form? So, I have a symfony form (extends of AbstractType) with several field: a search area, but also several filters based on products characteristics: brand, family, origin (country). When the user perform a query, I want to have all these fields to display only relevant products in my table. I don't use at all the search field from datatable. The only way I've found is to modify my public/bundles/datatables/js/datatable.js file to add some custom code:

                            request._dt = config.name;
                            /* __Pol__ */
                            request.catalogue = catalogueId;
                            request.searchText = $('#searchText').val();
                            request.origins = $('#origins').val();
                            request.families = $('#families').val();
                            request.brands = $('#brands').val();
                            /* \__Pol__ */
                            $.ajax(typeof config.url === 'function' ? config.url(dt) : config.url, {
                                method: config.method,
                                data: request
                            }).done(function (data) {
                               drawCallback(data);
                            })

But I'm afraid I will have to merge this code each time the bundle is updated, I don't think this is the right way to manage this. Is there another solution?

Q2) I wish to have a "detailRow" that could be hide/show based on a click on an icon. Same problem than for Q1, my code is working but I have to modify again the /bundles/datatables/js/datatable.js file at the end of the promise:

return new Promise((fulfill, reject) => {
            // Perform initial load
            $.ajax(typeof config.url === 'function' ? config.url(null) : config.url, {
                method: config.method,
                data: {
                [...]
                }
            }).done(function(data) {
               [...]

                /* __Pol__ 
                    Display detail row
                */

                // Array to track the ids of the details displayed rows
                var detailRows = [];

                $('#datatable tbody').on('click', 'tr td.details-control', function () {
                    var tr = $(this).closest('tr');
                    var row = dt.row(tr);
                    var idx = $.inArray(tr.attr('id'), detailRows);

                    if (row.child.isShown()) {
                        tr.removeClass('details');
                        row.child.hide();

                        // Remove from the 'open' array
                        detailRows.splice(idx, 1);
                    }
                    else {
                        tr.addClass('details');
                        row.child(format(row.data())).show();

                        // Add to the 'open' array
                        if (idx === -1) {
                            detailRows.push(tr.attr('id'));
                        }
                    }
                });
            /* __\Pol__ */

            }).fail(function(xhr, cause, msg) {
                console.error('DataTables request failed: ' + msg);
                reject(cause);
            });

I've read carefully the doc https://omines.github.io/datatables-bundle/#javascript but I don't found any event on which I can bind this code. Is there a best solution?

Sorry again for these questions which are not really issues, but I really wish to know if I'm facing to the bundle limitations or if I miss something in the symfony/js/bundle behavior. Thanks a lot,

Pol

pluk77 commented 3 years ago

We are also using a Symfony form to display our 'filters'. Note that we make use of a DTO in the form that takes care of the validation of the form values.

The trick here is to store the form values after validation in session so the call-back takes those values from session and passes them on to the query or other adapter that fetches the data.

As GET is only used when accessing the page initially, we use that to set the defaults for the form when returning to the page.

We add the form values to the post-back data via $.ajaxSetup({beforeSend Note that when adding the form values to the post-back data, we use the form naming convention which enures the data is treated as a valid form submission. To ensure the clear button works, we submit the blank button value as well, which ensures the request is validated even if the filter is cleared.

Hope this helps with Q1.

Our controller looks something like this:

public function index(Request $request, SessionInterface $session)
{
    $QueueEntryFilterForm = $this->setupIndexFilters($session, $request, 'QUEUEENTRY_FILTER_4');

    // Get the filters.
    $searchTerms = $session->get('QUEUEENTRY_FILTER_4', []);

    // Get the table.
    $table = $this->createDataTable()
    ->///// Add table fields here //////
    ->createAdapter(QueueServiceArrayAdapter::class, [
                'facility' => $this->getFacility()] + $searchTerms)
    ->handleRequest($request);

    if ($table->isCallback()) {
        return $table->getResponse();
    }
    return $this->render('queue/index.html.twig', [
            'QueueEntryFilterForm' => $QueueEntryFilterForm->createView(),
            'datatable' => $table
        ]);
}

private function setupIndexFilters(SessionInterface $session, Request $request, $sessionToken): FormInterface
    {
        $dto = new QueueEntryFilterDTO();

        $defaults = $session->get($sessionToken, null);
        if($defaults !== null && $request->isMethod('get')) {
            // setting the default ensures the filter form has the correct values when returning to the page.
            // other option is to reset the whole filter by resetting the session upon a GET
            // no need to do this on a POST to prevent unnecessary queries
            $dto->setDefaults($this->getDoctrine(), 
                    (!empty($defaults['healthService']) ? $defaults['healthService'] : null), 
                    (!empty($defaults['healthUnit']) ? $defaults['healthUnit'] : null), 
                    (!empty($defaults['practitioner']) ? $defaults['practitioner'] : null), 
                    (!empty($defaults['status']) ? $defaults['status'] : null));
        }

        // Get the form
        $filterForm = $this->createForm(QueueEntryFilterForm::class, $dto, array('facility' => $this->getFacility(),'csrf_protection' => false));
        $filterForm->handleRequest($request);
        if ($filterForm->isSubmitted() && $filterForm->isValid()) {
            // Add patient's details.
            $session->set($sessionToken, $dto->getSearchTerms());
        }

        return $filterForm;
    }

The JS on the page looks like this:


import '../../../public/bundles/datatables/js/datatables';

$(document).ready(function() {

    $(document).on('click', '#queue_entry_filter_form_btnFilter', function(e){
        e.preventDefault(); 
        getQueue();
    });

    $(document).on('click', '#queue_entry_filter_form_btnClear', function(e){
        e.preventDefault(); 
        $("#QueueEntryFilterForm select option:selected").removeAttr("selected");
        $("#QueueEntryFilterForm select option:selected").prop("selected", false);
        getQueue();
    });
});

function getQueue() {
    $.ajaxSetup({
            beforeSend: function(jqXHR, settings) {
                settings.data = settings.data+serealizeSelects('healthService')+serealizeSelects('practitioner')+serealizeSelects('healthUnit')+serealizeSelects('status')+"&queue_entry_filter_form[btnFilter]=";
                return true;
            }
        });
    $('#queue').html('Loading...');
    $('#queue').initDataTables(tableMetaData, {
        lengthMenu: [[10, 25, 50], [10, 25, 50]]
    });
    return false;
}

/**
 * Convert select to array with values
 */    
function serealizeSelects (select)
{
    var string = '';
    $('#'+select+' > :selected').each(function() {
        string += '&queue_entry_filter_form['+select+'][]='+$(this).val();
    });
    return string;
}
polochon777 commented 3 years ago

Thank you a lot for this very detailed answer! I think I understand the spirit of the answer, can you confirm that I don't miss something?

1) you set the DTO with default values - setupIndexFilters function (in fact, I had created one but without know this class was called a "DTO", thank you a lot I know the word now) 2) you create the table (in your controller) 3) thanks to the beforeSend event, you are able to catch the values entered or selected by the users in your filters. These values will be sent in the AJAX request (this is the most confused part in my mind) 4) form is submitted by the user 5) if the form is valid, you set the values in $_SESSION 6) you fetch the data from DB with the values you stored previously in $_SESSION 7) you display the table

Am I right? If yes, the trick with storing the values after sumbission in $_SESSION instead take them directly from request is to be sure to get validated data?

Pol

pluk77 commented 3 years ago

FYI, if you want to read up more on it: DTO stands for Data Transfer Objects and they are used to transfer data between domains. They are specifically handy when dealing with forms and requests argument resolvers. When used correctly you prevent an Entity from ever being in an invalid state as you use the DTO to transfer the data between the user and your application, and only after the DTO is properly validated will you transfer the data to the entity.

That aside, you seem to be correct in your steps.

Although the session is mainly there to allows you to store the values so you can set them as a default upon returning to the controller method via a GET request later in time which in my opinion gives a better user experience.

If that is not required, you can leave the session stuff out. All the form values are added to the request on each call-back via the beforeSend event so you can validate them in the DTO via the form and use the validated values in your query.

Hope this helps you.

polochon777 commented 3 years ago

Hello, I've just did the modifications as you suggested and it works perfectly, thanks again!

Then, I've also understand why my "detailRow" was not working (question n°2 in my first post). My first view was correct, the right place for the additional code was in the "draw" event of datatable. But I didn't look at the issue deeply enough so I put the code in the bundle js file as a pig. The root cause was an issue in my code that sent 2 ajax call at table initialization, which leads to 2 consecutive "draw" event. Because in this event I register a click event with $('#datatable tbody').on('click', 'tr td.details-control', function (), the click was submitted twice, which leads to an quick hide/show at same time. Remove the unwanted ajax call solved the issue, and put a off('click') before the on('click') registration made this fix 100% bullett proof (I hope!)

Thanks for your support. Pol