Skip to content
This repository was archived by the owner on Feb 2, 2025. It is now read-only.

Wen I create datatables and rows with angularjs $compile then increases memory leak in every dtInstante.reload() #1119

Closed
falinsin opened this issue Oct 24, 2017 · 3 comments

Comments

@falinsin
Copy link

falinsin commented Oct 24, 2017

First thing first:
Version of my angular-datatables lib is: v0.6.3-dev
Version of my jquery datatable lib is: DataTables 1.10.9

My code creating a angular-datatable :

$scope.dtOptionsCases = DTOptionsBuilder.newOptions()
	        	.withOption('order', [[4, 'asc']])
	            .withOption('ajax', {
	                url: pathToDataBaseServer + 'caso/getAllCasesInDataTable',
	                type: 'POST',
	                dataType: "json",
	                contentType: "application/json",
 	                xhrFields: {
 	                    withCredentials: true
 	                },
	                headers: corsHeaders,
	                data: function (objData) { //Dato Solicitud
	                    objData.params = {
	                        customerId: $scope.currentContactId//parseInt(DashboardDataSrv.currentContactId),
	                    }; //Inyecto en el objeto otros valores a mandar en el JSON como: "params.kindOfCustomer=1"
	                    return JSON.stringify(objData);
	                },
	                dataSrc: function (json) { //Dato Respuesta
	                	LogFileServices.info("[" + new Date().toLocaleString() + "] >> " + $scope.controllerName + "(" + $scope.templateName + ") >> Global Scope Definition >> response from " + pathToDataBaseServer + "caso/getAllCasesInDataTable "
	                			+ " >> json : " + json);
	                	if($scope.contactDetailsLoaded ){
		                	$scope.setCaseManagementSectionLoaded(true);
		                	$scope.caseData.loaded = true;
	                	}
	                    if (json.aaData == null) {
	                        return [];
	                    } else {
	                        return json.aaData;
	                    }
	                },
	                error: function(data){
	                	if(data != null && data.readyState == 0 && data.status == 0 && data.statusText == "error"){
	                		NotificationSrv.danger("Ha habido un error cargando la tabla de casos. Actualiza los datos.");
	                		LogFileServices.error("[" + new Date().toLocaleString() + "] >> Error de red en datatables getAllCasesInDataTable" + JSON.stringify(data));
	                	} else {
	                		NotificationSrv.danger("Ha habido un error cargando los casos:" + data.statusText);
	                		LogFileServices.error("[" + new Date().toLocaleString() + "] >> Error datatables getAllCasesInDataTable" + JSON.stringify(data));
	                	}
	                	//Queremos que aunque haya fallado las demas secciones se carguen
	                	if($scope.contactDetailsLoaded ){
		                	$scope.setCaseManagementSectionLoaded(true);
		                	$scope.caseData.loaded = true;
	                	}
                		$scope.dataTableFailure = true;
	                }
	            })
	            .withDataProp('data')
	            .withOption('processing', true)
	            .withOption('serverSide', true)
	            .withOption('createdRow', function (row, data, dataIndex) { //Para que AngularJS rendereé los tags HTML insertados como String
	                // Recompiling so we can bind Angular directive to the DT
			$compile(angular.element(row).contents())($scope);

	            })
	            .withOption('headerCallback', function (header) { //Para que AngularJS rendereé los tags HTML insertados como String
	                if (!$scope.headerCompiled) {
	                    // Use this headerCompiled field to only compile header once
	                    $scope.headerCompiled = true;
	                    $compile(angular.element(header).contents())($scope);
	                }
	            })
	            .withPaginationType('simple_numbers')
	            .withDisplayLength(9)
                .withOption('autoWidth', true)
	            .withOption('lengthChange', false)
	            .withOption('scrollCollapse', true)
	            .withOption('scrollY', "100%")
	            .withOption('ordering', true)
	            .withOption('info', false);

So far alll is working and in this code the only important thing is I am using angular $compile in order to use ng-click in buttons in every row

The problem is when I reload the datable instance:

In every reload of this datatable instance, I do it like this:
$scope.dtOptionsCases.reloadData(null,false);

The big problems is in memory there are a lot of increasing amount of wathers (from every detached row, becouse these rows have ng-click compiled) A lot of big memory leaks are created evere reload.

How can I clear old rows before reload new rows. Is there a way to do this.

We are in a big problem with a client because of this and We´d apreciate any help.

Sorry my bad english and a lot of Thanks in advance for your help.

@falinsin
Copy link
Author

More info about.
I've noticed that if we use angularJs $compile on a datatable createdRow()
When reload reloadData grow numbre of detached watchers and unattended DOM stuff

Even when use pagination is growing this becouse in paginations datatable execute createdRow()

Is it possible destroy previous $compiles Rows before enter in createdRows and avoid this Big memory leak?

@l-lin
Copy link
Owner

l-lin commented Oct 28, 2017

Check #1118.

@szmalec
Copy link

szmalec commented Jun 12, 2019

More info about.
I've noticed that if we use angularJs $compile on a datatable createdRow()
When reload reloadData grow numbre of detached watchers and unattended DOM stuff

Even when use pagination is growing this becouse in paginations datatable execute createdRow()

Is it possible destroy previous $compiles Rows before enter in createdRows and avoid this Big memory leak?

I had a similar problem - I had event handlers in my angularjs components dynamically compiled in datatables rows; when datatable was reloaded or resorted, then event handlers were not removed. I have tried with:

$element.on('$destroy', function () { /* deinit here */ });

but $destroy event was not triggered.
So I have created directive which solved this problem (more clarifications in code below):

    // Directive is used to fix memory leaks related problems in scenario when angularjs $compile is used to dynamically render datatables 
    // row content(see $compile(angular.element(row).contents())($scope) in 'createdRow' callback: https://datatables.net/reference/option/createdRow).
    // When datatables is reloaded (or rows are resorted) then angularjs directives compiled in old datatables rows are NOT destroyed.
    // This is probably due to way that datatables are removing old nodes. I have discovered that it is performed using body.children().detach() (see _fnDraw in jquery.dataTables.js)...
    // As you can see detach() jQuery method calls jQuery remove() method with keepData parameter set to true. When keepData is true then jQuery does NOT call jQuery.cleanData() method (see https://github.com/jquery/jquery/blob/2.2.4/src/manipulation.js#L212).
    // When jQuery.cleanData() is NOT called then angularjs will NOT trigger '$destroy' event which could be used to eg. deinitialize directive components compuled in old datatables rows...
    //
    // This directive should wrap datatables for which $compile is used. There also should be '$destroy' event handler in all angularjs components used in dynamically compiled datatables rows, eg.:
    //      app.directive('xxx', function() {
    //          return {
    //              // ...
    //              link: function ($scope, $element, $attrs, $ctrl) {
    //                  // ...
    //                  $element.on('$destroy', function () {
    //                      $scope.$destroy();
    //                  });
    //                  $scope.$on('$destroy', function () {
    //                      // TODO: Add deinit code (eg. detach event handlers, etc.)
    //                  });
    //                  // ...
    //              },
    //              // ...
    //          };
    //      });
    // TODO-readings: https://www.dwmkerr.com/fixing-memory-leaks-in-angularjs-applications/
    app.directive('axDtTriggerDestroyOnRowRemoved', ['$log', function ($log) {    // ax-dt-trigger-destroy-on-row-removed
        return {
            restrict: 'E',
            link: function ($scope, $element, $attrs, $ctrl) {
                // Idea of using MutationObserver taken from:  https://stackoverflow.com/questions/44935865/detect-when-a-node-is-deleted-or-removed-from-the-dom-because-a-parent-was/44937162#44937162
                var observer = new MutationObserver(function (mutations) {
                    mutations.forEach(function (mutation) {
                        // DEV HINT: https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
                        // mutation.type: Returns "attributes" if the mutation was an attribute mutation, "characterData" if it was a mutation to a CharacterData node, and "childList" if it was a mutation to the tree of nodes.
                        // mutation.target: For childList, it is the node whose children changed.
                        if (mutation.removedNodes.length > 0 && mutation.type === 'childList' && $.fn.dataTable.isDataTable($(mutation.target).parents('table'))) {
                            var removedNodes = Array.from(mutation.removedNodes);
                            removedNodes.forEach(function (node) {
                                // eg. node => <tr class="ng-scope odd" role="row"><td>....</td></tr>
                                if (node.tagName === 'TR') {
                                    var elementsWithDestroySupport = $(node)
                                        .find('*')                      // find all children
                                        .filter(function (idx, itm) {   // find child directive nodes for which $destroy event could be triggered
                                            var events = $._data(itm, 'events');
                                            return events && events.$destroy;
                                        });
                                    // DEV HINT: See angular.js code for jQuery.cleanData = function(elems) // => https://github.com/angular/angular.js/blob/v1.6.10/src/Angular.js#L1944
                                    if (elementsWithDestroySupport.length)
                                        angular.forEach(elementsWithDestroySupport, function (el, idx) {
                                            angular.element(el).triggerHandler('$destroy');
                                        });
                                }
                            });
                        }
                    });
                });

                observer.observe($element[0], { subtree: true, childList: true });

                // Directive deinit code
                $element.on('$destroy', function () {
                    $log.debug('[axDtTriggerDestroyOnRowRemoved] Element destroy => ', $scope, $element);

                    observer.disconnect();
                    observer = undefined;
                });
            }
        };
    }]);

I am wondering if it helps in your scenario (it for sure helped me to solve problems with detaching event handlers in components $compile-d in createdRow datatables callback).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants