SrAxi
SrAxi

Reputation: 20005

Angular - Datatables: TypeError: Cannot read property 'nodeName' of null

This one is tricky, I'll try to explain myself as best as I can.

Brief explanation:

I have DataTables library integrated in my Angular project, had it since I purchased the theme of the project several months ago. The Theme has been updated, and so I proceeded with the update of my project with latest versions.

The strange thing is that what's not working is DataTables, and DataTables have not been changed!

Code breakdown:

From X component I'm triggering a method of my shared service IBOsService.

When this method is triggered, my DatatableComponent imports the datatables library in a Promise.

Never had issues with this, until now.

In DatatableComponent:

this.tablesService.initTableData$.subscribe(() => {
                if (!this.datatableInitialized) {
                    log.info('Starting script import promise');

                    Promise.all([
                        System.import('script-loader!my-plugins/datatables-bundle/datatables.min.js')
                    ]).then(values => {
                        log.data('success', JSON.stringify(values));
                        this.render();
                    }, reason => {
                        log.error('error', JSON.stringify(reason));
                    });
                }
            }
        );

In my console I see: DataTables success [{}]. Therefore, I understand that promise was success.

So then we enter this.render(); method.

The method goes on until this line here:

const _dataTable = element.DataTable(options);

The thing is that in my IDE, I can navigate to all variables, methods, etc... But I cannot navigate to DataTable(), therefore I guess is just not recognizing this method and that's why it throws error.

But, as the script is loaded in the promise, is normal that the IDE doesn't have the mapping of DataTables methods...

Full Component Code:

import { Component, Input, ElementRef, AfterContentInit, OnInit, Injectable, OnDestroy } from '@angular/core';
import { IBOsService } from '../../../+ibos/ibos.service';
import { Subscription } from 'rxjs/Subscription';
import { Log, Level } from 'ng2-logger';
import { logConfig } from '../../../../environments/log_config';

const log = Log.create('DataTables');
log.color = logConfig.dataTable;

declare let $: any;

@Component({

    selector: 'sa-datatable',
    template: `
        <table class="dataTable {{tableClass}}" width="{{width}}">
            <ng-content></ng-content>
        </table>
    `,
    styles: [
        require('my-plugins/datatables-bundle/datatables.min.css')
    ]
})
@Injectable()
export class DatatableComponent implements OnInit, OnDestroy {

    @Input() public options: any;
    @Input() public filter: any;
    @Input() public detailsFormat: any;

    @Input() public paginationLength: boolean;
    @Input() public columnsHide: boolean;
    @Input() public tableClass: string;
    @Input() public width = '100%';

    public datatableInitialized: boolean;

    public subscription: Subscription;

    constructor(private el: ElementRef, private tablesService: IBOsService) {
        this.tablesService.refreshTable$.subscribe((tableParams) => {
                this.filterData(tableParams);
            }
        );

        this.tablesService.initTableData$.subscribe(() => {
                if (!this.datatableInitialized) {
                    log.info('Starting script import promise');

                    Promise.all([
                        System.import('script-loader!my-plugins/datatables-bundle/datatables.min.js')
                    ]).then(values => {
                        log.data('success', JSON.stringify(values));
                        this.render();
                    }, reason => {
                        log.error('error', JSON.stringify(reason));
                    });
                }
            }
        );
    }

    ngOnInit() {
    }

    render() {
        log.info('Starting render!');

        const element = $(this.el.nativeElement.children[0]);
        let options = this.options || {};

        log.info('1 render!');

        let toolbar = '';
        if (options.buttons) {
            toolbar += 'B';
        }
        log.info('2 render!');

        if (this.paginationLength) {
            toolbar += 'l';
        }

        if (this.columnsHide) {
            toolbar += 'C';
        }

        log.info('3 render!');

        if (typeof options.ajax === 'string') {
            const url = options.ajax;
            options.ajax = {
                url: url,
                // complete: function (xhr) {
                //
                // }
            };
        }

        log.info('4 render!');

        options = $.extend(options, {

            'dom': '<\'dt-toolbar\'<\'col-xs-12 col-sm-6\'f><\'col-sm-6 col-xs-12 hidden-xs text-right\'' + toolbar + '>r>' +
            't' +
            '<\'dt-toolbar-footer\'<\'col-sm-6 col-xs-12 hidden-xs\'i><\'col-xs-12 col-sm-6\'p>>',
            oLanguage: {
                'sSearch': `<span class='input-group-addon'><i class='glyphicon glyphicon-search'></i></span>`,
                'sLengthMenu': '_MENU_'
            },
            'autoWidth': false,
            retrieve: true,
            responsive: true,
            initComplete: (settings, json) => {
                element.parent()
                    .find('.input-sm')
                    .removeClass('input-sm')
                    .addClass('input-md');
            }
        });

        log.info('5 render! element', JSON.stringify(element));
        log.info('5.1 render! options', JSON.stringify(options));

        const _dataTable = element.DataTable(options);

        log.info('5.2 render! _dataTable', JSON.stringify(_dataTable));

        if (this.filter) {
            // Apply the filter
            element.on('keyup change', 'thead th input[type=text]', function () {
                console.log('searching?');
                _dataTable
                    .column($(this).parent().index() + ':visible')
                    .search(this.value)
                    .draw();
            });
        }

        log.info('6 render!');

        if (!toolbar) {
            element.parent().find('.dt-toolbar')
                .append(
                    '<div class="text-right">' +
                    '<img src="assets/img/logo.png" alt="SmartAdmin" style="width: 111px; margin-top: 3px; margin-right: 10px;">' +
                    '</div>'
                );
        }

        log.info('7 render!');

        if (this.detailsFormat) {
            const format = this.detailsFormat;
            element.on('click', 'td.details-control', function () {
                const tr = $(this).closest('tr');
                const row = _dataTable.row(tr);
                if (row.child.isShown()) {
                    row.child.hide();
                    tr.removeClass('shown');
                } else {
                    row.child(format(row.data())).show();
                    tr.addClass('shown');
                }
            });
        }

        log.info('8 render!');

        this.datatableInitialized = true;
    }

    filterData(tableParams) {
        console.log('reloading DT... With these parameters: ' + JSON.stringify(tableParams));

        const element = $(this.el.nativeElement.children[0]);
        const table = element.find('table.dataTable');

        log.data('current table element is: ', JSON.stringify(table));

        Object.keys(tableParams).forEach(function (key) {
            log.warn('current key: ', JSON.stringify(key));
            table.DataTable().column(`${key}:name`).visible(tableParams[key]);
        });

        table.DataTable().ajax.reload();
    }

    ngOnDestroy() {
        if (this.subscription) {
            this.subscription.unsubscribe();
        }
    }
}

You'll see I've invaded the component with logs, it helped me understand where my Component was failing.

Full Console Log:

enter image description here

Interesting info:

If I call the filterData(tableParams) method... The DataTable renders with no problems.

This tells me 2 things:

  1. DataTable() is not the issue.
  2. The issue is only on first time rendering, then, on updating, I encounter no problems.

I'm terribly sorry for my Wall of text... But I did some Sherlock Holmes by my own and couldn't find the solution.

If you need clarification or more details let me know.

Thanks in advance!

Update:

After debugging for days, I have found a possible source of error:

enter image description here

The thing is that s is null.

Maybe someone more experienced on DataTables or that encountered this issue could give me a hint on what's going on.

I'll be here debugging... ;)

Update 2:

After more debugging I found out that DataTables is not making the first Ajax call.

I'm using server side DataTables, with an Ajax call in POST.

Here is the code of my options object:

this.options = {
    dom: 'Bfrtip',
    processing: true,
    serverSide: true,
    pageLength: 20,
    searchDelay: 1200,
    ajax: {
        url: this.jsonApiService.buildURL('/test_getUsers.php', 'remote'),
        type: 'POST',
        data: function (d) {
            Object.assign(d, IBOsTable.params);
            log.data('DT options obj. New params are: ', JSON.stringify(IBOsTable.params));
            return d;
        }
    },
    columns: this.initColumns,
};

This is not making the Ajax call on initialization but it's making it on table.DataTable().ajax.reload();.

This tells me that the code is not incorrect or broken (on reload() works like a charm)...

I'm still not undesrtanding fully why the initialization of this DataTables is not working... But I believe that I'm close enough!

Upvotes: 3

Views: 5196

Answers (1)

SrAxi
SrAxi

Reputation: 20005

I finally found the source of the error!

If you read my Updates you'll find out I figured out that my DataTables was not making the first Ajax call, but on reload() was working perfectly:

That was because a JavaScript error in initialisation wasn't allowing the Ajax call to be made. Hint: TypeError: Cannot read property 'nodeName' of null.

After speaking to Allan (from DataTables), he gave me the hint that the error may be caused because of number of columns of thead not being equal to the number of columns of tbody.

So I checked my .html and I found out that I had some empty <thead> and <tfoot> tags. I commented them... And worked!

enter image description here

I have ran over and over every possible solution:

  • Dependencies
  • jQuery Library within Angular 2 App
  • Many different ways of instancing the DataTable server side
  • Changing approaches in code: TypeScript, jQuery, Ng2, etc...

At the end I just needed to clear the div where my DataTable was being loaded into in order to avoid this issue.

Fun fact: With that .html my App worked for 3/4 months with no problems, no problems with DataTables until now...

Upvotes: 2

Related Questions