l-lin / angular-datatables

DataTables with Angular
https://l-lin.github.io/angular-datatables/
MIT License
1.57k stars 486 forks source link

ngTemplateRef + colvis-button (Turn columns "visible":false) - TypeError: Cannot read property 'appendChild' of null #1576

Closed mtrzensky closed 3 years ago

mtrzensky commented 3 years ago

IMPORTANT: I will try to deliver a stackblitz or minimal reproduction project later. I tried to create a stackblitz or use the stackblitz "angular-datatables-gitter" but I couldn't get them to work with ngTemplateRef. Please don't close this issue immediately!

:beetle: bug report

I have a datatable that gets initialized within "ngAfterViewInit". It uses "ajax" to gather data. I use the datatable "server side the angular way". I have activated the "buttons" plugin to use "colvis" and "stateSave: true".

In my use-case I need one column with a ngTemplateRef. I initialize "this.columns" and put the column with ngTemplateRef at the end, then pass it into "dtOptions". The Datatable initializes fine.

Once I turn one column before my ngTemplateRef column invisible and reload the page, the datatable crashes with this error:

TypeError: Cannot read property 'appendChild' of null
    at EmulatedEncapsulationDomRenderer2.appendChild (platform-browser.js:670)
    at angular-datatables.directive.js:79
    at Array.forEach (<anonymous>)
    at S.fn.init.rowCallback (angular-datatables.directive.js:68)
    at jquery.dataTables.js:6682
    at Function.map (jquery.min.js:2)
    at _fnCallbackFire (jquery.dataTables.js:6681)
    at _fnDraw (jquery.dataTables.js:3503)
    at _fnAjaxUpdateDraw (jquery.dataTables.js:4169)
    at jquery.dataTables.js:4009
    at callback (jquery.dataTables.js:3901)
    at SafeSubscriber._next (material-template-dt.component.ts:132)
    at SafeSubscriber.__tryOrUnsub (Subscriber.js:183)
    at SafeSubscriber.next (Subscriber.js:122)
    at Subscriber._next (Subscriber.js:72)
    at Subscriber.next (Subscriber.js:49)
    at CatchSubscriber._next (Subscriber.js:72)
    at CatchSubscriber.next (Subscriber.js:49)
    at MapSubscriber._next (map.js:35)
    at MapSubscriber.next (Subscriber.js:49)

:microscope: Minimal Reproduction

IMPORTANT: Will deliver stackblitz or project later!

x.component.ts

dataTableActions: Array<DataTableAction> = [
    {
      cmd: "edit",
      label: "Bearbeiten"
    },
    {
      cmd: "delete",
      label: "Löschen"
    },
  ];

  @ViewChild('dtActions') dtActions: TemplateRef<ActionsComponent>;

.
.
.

ngAfterViewInit(): void {
    this.columns.push(...[
      {
        title: "Name",
        data: "name"
      },
      {
        title: "Age",
        data: "age"
      },
      {
        title: "Info",
        data: "info"
      }
    ]);

    if (this.dataTableActions.length > 0) {
      this.columns.push({
        title: "Aktionen",
        data: null,
        orderable: false,
        searchable: false,
        defaultContent: "",
        ngTemplateRef: {
          ref: this.dtActions,
          context: {
            captureEvents: this.onCaptureEvent.bind(this)
          }
        }
      });
    }

    this.dtOptions = {
      language: german,
      dom: '<l>Bfrtip',
      buttons: [
        {
          extend: 'colvis',
          columns: ':not(.noVis)'
        },
        'excel',
      ],
      columnDefs: [
        {
          targets: "_all",
          className: "valign-middle",
        },
        {
          targets: [0],
          className: "text-right noVis",
        },
      ],
      stateSave: true,
      serverSide: true,
      processing: true,
      searchDelay: 600,
      ajax: (dataTablesParameters: any, callback) => {
        this.service
          .getJsonLd().subscribe(resp => {
            callback({
              recordsTotal: resp['hydra:totalItems'],
              recordsFiltered: resp['hydra:totalItems'],
              data: resp['hydra:member']
            });
          });
      },
      columns: this.columns
    };

onCaptureEvent(event: DataTableActionsEvent): void {
    // Stuff
  }

x.component.html

<ng-template #dtActions let-data="adtData" let-emitter="captureEvents">
    <app-actions [actions]="dataTableActions" [data]="data" (emitter)="emitter($event)"></app-actions>
  </ng-template>

      <table id="material--template-dt" *ngIf="columns.length" datatable [dtOptions]="dtOptions" class="table table-striped w-100"></table>

:8ball: Expected behavior

The Datatable should render fine.

:globe_with_meridians: Your Environment

:memo: Additional context

It seems like in the line 66-80 within angular-datatables.directive.js are the issue:

// Filter columns using `ngTemplateRef`
  var colsWithTemplate = columns_1.filter(function (x) { return x.ngTemplateRef && !x.ngPipeInstance; });
  colsWithTemplate.forEach(function (el) {
      var _a = el.ngTemplateRef, ref = _a.ref, context = _a.context;
      // get <td> element which holds data using index
      var index = columns_1.findIndex(function (e) { return e.data == el.data; });
      var cellFromIndex = row.childNodes.item(index);
      // render onto DOM
      // finalize context to be sent to user
      var _context = Object.assign({}, context, context === null || context === void 0 ? void 0 : context.userData, {
          adtData: data
      });
      var instance = self.vcr.createEmbeddedView(ref, _context);
      self.renderer.appendChild(cellFromIndex, instance.rootNodes[0]);
  });

I think after disabling some columns and reloading the page, there is a bug with referenced indeces. Naively spoken: The column of ngTemplateRef gets a certain index beforehand (index = 3), a column gets "visible: false, the column.length only goes to index 2 and then it tries to map everything to index 3 causing a null reference error.

mtrzensky commented 3 years ago

Sorry for the delay, here is a repo with an example: https://github.com/mtrzensky/angular-datatables-ngtemplateref-bug

To reproduce:

Is this description and the repo sufficient for you to see my issue?

shanmukhateja commented 3 years ago

Big confirmed :)

shanmukhateja commented 3 years ago

@mtrzensky could you confirm if #1578 fixed it? I'm placing the PR as a draft until the fix is confirmed.

mtrzensky commented 3 years ago

@shanmukhateja I will be able to investigate the fix early August, then I will test it and tell you :-) Thanks for your support!

shanmukhateja commented 3 years ago

@mtrzensky is the issue fixed?

mtrzensky commented 3 years ago

@shanmukhateja Sorry for reaching out late. I will test the fix this week and tell you if the issue is resolved

mtrzensky commented 3 years ago

@shanmukhateja I have problems installing your fork/commit. I tried using:

The package doesn't seem to be built locally. I get an "angular-datatables" folder within node_modules only with schematics and no "src" folder. This results in the app missing the "angular-datatables" package (imports fail)

I also wanted to copy paste the code snipped but this doesn't seem to be compatible since your file is a ".ts" file and mine from "angular-datatables: 12.0.0" only contains the ".js" files with TypeScript definition files on top. What did I do wrong?

shanmukhateja commented 3 years ago

Hi @mtrzensky

If you look at https://github.com/l-lin/angular-datatables/pull/1578/files you'll need to update the node_modules/angular-datatables/angular-datatables.directive.js file.

It could be because of as any in line 77. I don't see any other TS specific code so it should work.

Hope that helps :)

mtrzensky commented 3 years ago

Hey @shanmukhateja

Unfortunately this has not helped me :(

I tried the following:

The guide mentions that old versions may crash the "npm build". My node version: v16.4.0 My npm version: 7.18.1

Errors:


> angular-datatables@12.0.0 build
> npm run clean && npm run compile && npm run bundles && npm run schematics:build

> angular-datatables@12.0.0 clean
> rimraf -f index.{d.ts,js,js.map,metadata.json} src/*.{d.ts,js,map,metadata.json} bundles schematics/**/*.{d.ts,js,map}

> angular-datatables@12.0.0 compile
> npm run lint:code && ngc -p tsconfig-build.json

> angular-datatables@12.0.0 lint:code
> tslint ./src/**/*.ts -t verbose --exclude ./src/**/*.d.ts

angular-whitespace is deprecated. Use a formatter like Prettier for formatting purposes.
node_modules/@types/jasmine/index.d.ts:34:18 - error TS2300: Duplicate identifier 'describe'.

34 declare function describe(description: string, specDefinitions: () => void): void;
                    ~~~~~~~~

  ../../node_modules/@types/mocha/index.d.ts:2615:13
    2615 declare var describe: Mocha.SuiteFunction;
                     ~~~~~~~~
    'describe' was also declared here.
node_modules/@types/jasmine/index.d.ts:48:18 - error TS2300: Duplicate identifier 'xdescribe'.

48 declare function xdescribe(description: string, specDefinitions: () => void): void;
                    ~~~~~~~~~

  ../../node_modules/@types/mocha/index.d.ts:2636:13
    2636 declare var xdescribe: Mocha.PendingSuiteFunction;
                     ~~~~~~~~~
    'xdescribe' was also declared here.
node_modules/@types/jasmine/index.d.ts:57:18 - error TS2300: Duplicate identifier 'it'.

57 declare function it(expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number): void;
                    ~~

  ../../node_modules/@types/mocha/index.d.ts:2650:13
    2650 declare var it: Mocha.TestFunction;
                     ~~
    'it' was also declared here.
node_modules/@types/jasmine/index.d.ts:73:18 - error TS2300: Duplicate identifier 'xit'.

73 declare function xit(expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number): void;
                    ~~~

  ../../node_modules/@types/mocha/index.d.ts:2671:13
    2671 declare var xit: Mocha.PendingTestFunction;
                     ~~~
    'xit' was also declared here.
node_modules/@types/jasmine/index.d.ts:101:18 - error TS2300: Duplicate identifier 'beforeEach'.

101 declare function beforeEach(action: jasmine.ImplementationCallback, timeout?: number): void;
                     ~~~~~~~~~~

  ../../node_modules/@types/mocha/index.d.ts:2581:13
    2581 declare var beforeEach: Mocha.HookFunction;
                     ~~~~~~~~~~
    'beforeEach' was also declared here.
node_modules/@types/jasmine/index.d.ts:108:18 - error TS2300: Duplicate identifier 'afterEach'.

108 declare function afterEach(action: jasmine.ImplementationCallback, timeout?: number): void;
                     ~~~~~~~~~

  ../../node_modules/@types/mocha/index.d.ts:2599:13
    2599 declare var afterEach: Mocha.HookFunction;
                     ~~~~~~~~~
    'afterEach' was also declared here.
../../node_modules/@types/jasminewd2/index.d.ts:10:18 - error TS2300: Duplicate identifier 'it'.

10 declare function it(expectation: string, assertion?: (done: DoneFn) => Promise<void>, timeout?: number): void;
                    ~~

  ../../node_modules/@types/mocha/index.d.ts:2650:13
    2650 declare var it: Mocha.TestFunction;
                     ~~
    'it' was also declared here.
../../node_modules/@types/jasminewd2/index.d.ts:12:18 - error TS2300: Duplicate identifier 'xit'.

12 declare function xit(expectation: string, assertion?: (done: DoneFn) => Promise<void>, timeout?: number): void;
                    ~~~

  ../../node_modules/@types/mocha/index.d.ts:2671:13
    2671 declare var xit: Mocha.PendingTestFunction;
                     ~~~
    'xit' was also declared here.
../../node_modules/@types/jasminewd2/index.d.ts:13:18 - error TS2300: Duplicate identifier 'beforeEach'.

13 declare function beforeEach(action: (done: DoneFn) => Promise<void>, timeout?: number): void;
                    ~~~~~~~~~~

  ../../node_modules/@types/mocha/index.d.ts:2581:13
    2581 declare var beforeEach: Mocha.HookFunction;
                     ~~~~~~~~~~
    'beforeEach' was also declared here.
../../node_modules/@types/jasminewd2/index.d.ts:14:18 - error TS2300: Duplicate identifier 'afterEach'.

14 declare function afterEach(action: (done: DoneFn) => Promise<void>, timeout?: number): void;
                    ~~~~~~~~~

  ../../node_modules/@types/mocha/index.d.ts:2599:13
    2599 declare var afterEach: Mocha.HookFunction;
                     ~~~~~~~~~
    'afterEach' was also declared here.
../../node_modules/@types/mocha/index.d.ts:2581:13 - error TS2300: Duplicate identifier 'beforeEach'.

2581 declare var beforeEach: Mocha.HookFunction;
                 ~~~~~~~~~~

  node_modules/@types/jasmine/index.d.ts:101:18
    101 declare function beforeEach(action: jasmine.ImplementationCallback, timeout?: number): void;
                         ~~~~~~~~~~
    'beforeEach' was also declared here.
  ../../node_modules/@types/jasminewd2/index.d.ts:13:18
    13 declare function beforeEach(action: (done: DoneFn) => Promise<void>, timeout?: number): void;
                        ~~~~~~~~~~
    and here.
../../node_modules/@types/mocha/index.d.ts:2599:13 - error TS2300: Duplicate identifier 'afterEach'.

2599 declare var afterEach: Mocha.HookFunction;
                 ~~~~~~~~~

  node_modules/@types/jasmine/index.d.ts:108:18
    108 declare function afterEach(action: jasmine.ImplementationCallback, timeout?: number): void;
                         ~~~~~~~~~
    'afterEach' was also declared here.
  ../../node_modules/@types/jasminewd2/index.d.ts:14:18
    14 declare function afterEach(action: (done: DoneFn) => Promise<void>, timeout?: number): void;
                        ~~~~~~~~~
    and here.
../../node_modules/@types/mocha/index.d.ts:2615:13 - error TS2300: Duplicate identifier 'describe'.

2615 declare var describe: Mocha.SuiteFunction;
                 ~~~~~~~~

  node_modules/@types/jasmine/index.d.ts:34:18
    34 declare function describe(description: string, specDefinitions: () => void): void;
                        ~~~~~~~~
    'describe' was also declared here.
../../node_modules/@types/mocha/index.d.ts:2636:13 - error TS2300: Duplicate identifier 'xdescribe'.

2636 declare var xdescribe: Mocha.PendingSuiteFunction;
                 ~~~~~~~~~

  node_modules/@types/jasmine/index.d.ts:48:18
    48 declare function xdescribe(description: string, specDefinitions: () => void): void;
                        ~~~~~~~~~
    'xdescribe' was also declared here.
../../node_modules/@types/mocha/index.d.ts:2650:13 - error TS2300: Duplicate identifier 'it'.

2650 declare var it: Mocha.TestFunction;
                 ~~

  node_modules/@types/jasmine/index.d.ts:57:18
    57 declare function it(expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number): void;
                        ~~
    'it' was also declared here.
  ../../node_modules/@types/jasminewd2/index.d.ts:10:18
    10 declare function it(expectation: string, assertion?: (done: DoneFn) => Promise<void>, timeout?: number): void;
                        ~~
    and here.
../../node_modules/@types/mocha/index.d.ts:2671:13 - error TS2300: Duplicate identifier 'xit'.

2671 declare var xit: Mocha.PendingTestFunction;
                 ~~~

  node_modules/@types/jasmine/index.d.ts:73:18
    73 declare function xit(expectation: string, assertion?: jasmine.ImplementationCallback, timeout?: number): void;
                        ~~~
    'xit' was also declared here.
  ../../node_modules/@types/jasminewd2/index.d.ts:12:18
    12 declare function xit(expectation: string, assertion?: (done: DoneFn) => Promise<void>, timeout?: number): void;
                        ~~~
    and here.`

So I can't compile the cloned project for some reason and according to the stated versions I should have a compatible compile environment.

Maybe the tslint files are outdated due to changes to angular types? Even if I disable the linting part, I still get more ts linting errors. The list is very long so if you want it posted I can hand that in another comment. Is this another issue?

shanmukhateja commented 3 years ago

Hi,

Could you downgrade to NodeJS 12 and NPM v6? Maybe this is why the build's failing? Opinion: NodeJS 14+ and NPM6 are known to cause problems sometimes.

In my previous message I meant patching the js file relatively at <your_web_app>/node_modules/angular-datatatables/<ivy something folder>/ followed by the directive js's file.

mtrzensky commented 3 years ago

Hi,

I have some good news and some bad news. Good one's first:

Bad news:

shanmukhateja commented 3 years ago

Interesting.

I suspect we aren't synchronised to column visibility changes.

I'll update this post once I figure it out.

shanmukhateja commented 3 years ago

Hi @mtrzensky

I'm unable to reproduce the issue.

Here's what I did:

  1. Copy the below code as-is to <project_dir>/node_modules/angular-datatables/__ivy_ngcc__/src/angular-datatables.directive.js. (This code is rebased from master branch).
/**
 * @license
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://raw.githubusercontent.com/l-lin/angular-datatables/master/LICENSE
 */
import { Directive, ElementRef, Input, Renderer2, ViewContainerRef } from '@angular/core';
import { Subject } from 'rxjs';
import * as ɵngcc0 from '@angular/core';
var DataTableDirective = /** @class */ (function () {
    function DataTableDirective(el, vcr, renderer) {
        this.el = el;
        this.vcr = vcr;
        this.renderer = renderer;
        /**
         * The DataTable option you pass to configure your table.
         */
        this.dtOptions = {};
    }
    DataTableDirective.prototype.ngOnInit = function () {
        var _this = this;
        if (this.dtTrigger) {
            this.dtTrigger.subscribe(function (options) {
                _this.displayTable(options);
            });
        }
        else {
            this.displayTable(null);
        }
    };
    DataTableDirective.prototype.ngOnDestroy = function () {
        if (this.dtTrigger) {
            this.dtTrigger.unsubscribe();
        }
        if (this.dt) {
            this.dt.destroy(true);
        }
    };
    DataTableDirective.prototype.displayTable = function (dtOptions) {
        var _this = this;
        // assign new options if provided
        if (dtOptions) {
            this.dtOptions = dtOptions;
        }
        this.dtInstance = new Promise(function (resolve, reject) {
            Promise.resolve(_this.dtOptions).then(function (resolvedDTOptions) {
                // validate object
                var isTableEmpty = Object.keys(resolvedDTOptions).length === 0 && $('tbody tr', _this.el.nativeElement).length === 0;
                if (isTableEmpty) {
                    reject('Both the table and dtOptions cannot be empty');
                    return;
                }
                // Using setTimeout as a "hack" to be "part" of NgZone
                setTimeout.call(_this, function () {
                    // Assign DT properties here
                    var options = {
                        rowCallback: function (row, data, index) {
                            if (resolvedDTOptions.columns) {
                                // `colVis` extension support
                                // See: https://github.com/l-lin/angular-datatables/issues/1576
                                var visibleTableColumns_1 = _this.dt.columns().visible().toArray();
                                var columns = resolvedDTOptions.columns.filter(function (_, i) { return visibleTableColumns_1[i]; });
                                // Apply transforms
                                _this.applyNgPipeTransform(row, columns);
                                _this.applyNgRefTemplate(row, columns, data);
                                // run user specified row callback if provided.
                                if (resolvedDTOptions.rowCallback) {
                                    resolvedDTOptions.rowCallback(row, data, index);
                                }
                            }
                        }
                    };
                    // merge user's config with ours
                    options = Object.assign({}, resolvedDTOptions, options);
                    _this.dt = $(_this.el.nativeElement).DataTable(options);
                    resolve(_this.dt);
                });
            });
        });
    };
    DataTableDirective.prototype.applyNgPipeTransform = function (row, columns) {
        // Filter columns with pipe declared
        var colsWithPipe = columns.filter(function (x) { return x.ngPipeInstance && !x.ngTemplateRef; });
        colsWithPipe.forEach(function (el) {
            var pipe = el.ngPipeInstance;
            // find index of column using `data` attr
            var i = columns.findIndex(function (e) { return e.data === el.data; });
            // get <td> element which holds data using index
            var rowFromCol = row.childNodes.item(i);
            // Transform data with Pipe
            var rowVal = $(rowFromCol).text();
            var rowValAfter = pipe.transform(rowVal);
            // Apply transformed string to <td>
            $(rowFromCol).text(rowValAfter);
        });
    };
    DataTableDirective.prototype.applyNgRefTemplate = function (row, columns, data) {
        var _this = this;
        // Filter columns using `ngTemplateRef`
        var colsWithTemplate = columns.filter(function (x) { return x.ngTemplateRef && !x.ngPipeInstance; });
        colsWithTemplate.forEach(function (el) {
            var _a = el.ngTemplateRef, ref = _a.ref, context = _a.context;
            // get <td> element which holds data using index
            var i = columns.findIndex(function (e) { return e.data === el.data; });
            var cellFromIndex = row.childNodes.item(i);
            // render onto DOM
            // finalize context to be sent to user
            var _context = Object.assign({}, context, context === null || context === void 0 ? void 0 : context.userData, {
                adtData: data
            });
            var instance = _this.vcr.createEmbeddedView(ref, _context);
            _this.renderer.appendChild(cellFromIndex, instance.rootNodes[0]);
        });
    };
    DataTableDirective.ctorParameters = function () { return [
        { type: ElementRef },
        { type: ViewContainerRef },
        { type: Renderer2 }
    ]; };
    DataTableDirective.propDecorators = {
        dtOptions: [{ type: Input }],
        dtTrigger: [{ type: Input }]
    };
DataTableDirective.ɵfac = function DataTableDirective_Factory(t) { return new (t || DataTableDirective)(ɵngcc0.ɵɵdirectiveInject(ɵngcc0.ElementRef), ɵngcc0.ɵɵdirectiveInject(ɵngcc0.ViewContainerRef), ɵngcc0.ɵɵdirectiveInject(ɵngcc0.Renderer2)); };
DataTableDirective.ɵdir = /*@__PURE__*/ ɵngcc0.ɵɵdefineDirective({ type: DataTableDirective, selectors: [["", "datatable", ""]], inputs: { dtOptions: "dtOptions", dtTrigger: "dtTrigger" } });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && ɵngcc0.ɵsetClassMetadata(DataTableDirective, [{
        type: Directive,
        args: [{
                selector: '[datatable]'
            }]
    }], function () { return [{ type: ɵngcc0.ElementRef }, { type: ɵngcc0.ViewContainerRef }, { type: ɵngcc0.Renderer2 }]; }, { dtOptions: [{
            type: Input
        }], dtTrigger: [{
            type: Input
        }] }); })();
    return DataTableDirective;
}());
export { DataTableDirective };

//# sourceMappingURL=angular-datatables.directive.js.map
  1. Run npm run start and open http://localhost:4200
  2. Click on "Column visibility" button and then click on "Aktionen" button. You should see "Aktionen" column of the table disappears.
  3. Again click on "Aktionen" button. You should see "Aktionen" column of the table re-appears with ngTemplateRef buttons rendered.

What do you see?

mtrzensky commented 3 years ago

Hi, I will test it eod or tomorrow and then tell you my observations. I will test it against the testcase project listed here and the real project I'm working in to verify each observations

mtrzensky commented 3 years ago

Regarding your last ticket: The difference with my observation was that I tested an older version before the refactoring of that file. I have to update it first and then patch it. I will post results later.

mtrzensky commented 3 years ago

@shanmukhateja

I reproduced your steps as you have told with the exact copied code and made following observations:

4a) If I do it exactly like you did, it works:

4b) But what I meant happens (the stated bug with no rendered button), when I do it this way:

It seems the button won't get rendered when the column initially is not loaded (i.e. due to a page refresh)

shanmukhateja commented 3 years ago

Hi @mtrzensky

bug confirmed. Let me see if I can work something out this weekend :)

shanmukhateja commented 3 years ago

@mtrzensky I checked the documentation of ColVis and I noticed the extension is marked as legacy.

After discussing with @l-lin it's been decided we don't provide support for legacy extensions.

According to DataTables authors, it is recommended to use Buttons extension. See here

As such, I'm closing this issue as well as the draft PR. Sorry for the inconvenience :(

mtrzensky commented 3 years ago

@shanmukhateja Alright. I changed colvis with "columnsToggle" and get the same result. Shall I open another issue so the context is seperated from this legacy context? I can also update the issue showcase, if you want.

shanmukhateja commented 3 years ago

@mtrzensky yes and yes - an updated repo project as well as new issue would help.

ccisco077 commented 7 months ago

The same issue happens to me when using columns, and you hide some using the DataTables built-in extensions

{ title: 'Actions', data: null, defaultContent: '', ngTemplateRef: { ref: this.demoNg, context: { // needed for capturing events inside <ng-template> captureEvents: self.onCaptureEvent.bind(self) } } }