vitalets / angular-xeditable

Edit in place for AngularJS
http://vitalets.github.io/angular-xeditable
MIT License
1.91k stars 403 forks source link

Manually created $scope should $destroy on end editing, or $watchers will leak #364

Closed S-YOU closed 7 years ago

S-YOU commented 9 years ago

Imgur

DiegoZoracKy commented 9 years ago

Came here to open this same issue. Was reported here on #108 too. Any news about that?

omarw commented 8 years ago

What Chrome extension is that in your screenshot?

S-YOU commented 8 years ago

angular watchers - https://chrome.google.com/webstore/detail/angular-watchers/nlmjblobloedpmkmmckeehnbfalnjnjk?hl=en

NickAb commented 8 years ago

Is there are any workaround or a quick fix to this?

NickAb commented 8 years ago

I made a temporary fix setting onhide to (typescript code)

function () {
            var last = this.scope.$$watchers.pop();
            this.scope.$$watchers = [last];
        };

But i am new to angular and not sure if it is a correct way that will free memory, etc.

Also, based on code, I think there are might be a problems not only with watchers, but with binds too. Directive bind to some events, but never class unbind.

kenfdev commented 8 years ago

I think this leak would be fixed with these commits.

Fix scope memory leak 46b71a7cadc169093a8464261b9c5545b87b0155 Change $data on scope parent to propagate change 7e73a3b9d459cab3960455bede46af7c3f043e71

But the pull request (#158) is mixed with many other fixes and is not merged.

These changes at least worked for my situation.

First, change the self.render function.

// src/js/editable-element/controller.js
self.render = function() {
...
      self.inputEl.addClass('editable-input');
      self.inputEl.attr('ng-model', '$data'); // <- change this to self.inputEl.attr('ng-model', '$parent.$data');

      // add directiveName class to editor, e.g. `editable-text`
      self.editorEl.addClass(editableUtils.camelToDash(self.directiveName));
...

and then change the self.show function.

// src/js/editable-element/controller.js
     //show
    self.show = function() {
...

      // compile (needed to attach ng-* events from markup)
      var newScope = $scope.$new(); // <- add this line
      $compile(self.editorEl)(newScope);
...
    };

and finally destroy the created scope inside self.hide

// src/js/editable-element/controller.js
    //hide
    self.hide = function() {

      // destroy the scope to prevent memory leak
      self.editorEl.scope().$destroy(); // <- add this line

      self.controlsEl.remove();
      self.editorEl.remove();
      $element.removeClass('editable-hide');
...
    };

I haven't checked the side effects but it seems to be working. If anybody can confirm this change, I'll create a pull request.

Updated(2016/06/20) Struck out my comment. The above approach does not work.

jawadatgithub commented 8 years ago

Dear participants,

I have deadline after 10 hours from now, I need this leak bug fixed and released, I have part of the SPA that uses form with ~400 xeditable fields (that's ~ 2000 Watches everytime user click Edit), please update ASAP, otherwise I will be forced to make custom changes in my local version of the component which I prefer not to happen.

thanks for the awesome component.

eugef commented 8 years ago

@jawadatgithub You are more than welcome to do changes in your fork and submit the PR

kenfdev commented 8 years ago

The modification I made above does not work. It is referenced in issue #265 and the fix has been reverted in this commit (1fcdd97ab886b67fee7e47cfe7d0a7e2da9cc6da) . It's probably because creating a new scope with $scope.new() creates an additional layer and access from directives like editableRadioList have to be modified to ng-model="$parent.$parent.$data" . This modification has pretty much a big impact and I'm not sure if it's even the right approach. Does anybody have any ideas on how we can fix this??

For example, I'm thinking that we have to change all the directives to reference another $parent above to access the right $data when we create a new $scope.

// radiolist
angular.module('xeditable').directive('editableRadiolist', [
  'editableDirectiveFactory',
  'editableNgOptionsParser',
  function(editableDirectiveFactory, editableNgOptionsParser) {
    return editableDirectiveFactory({
      directiveName: 'editableRadiolist',
      inputTpl: '<span></span>',
      render: function() {
        this.parent.render.call(this);
        var parsed = editableNgOptionsParser(this.attrs.eNgOptions);
        // changed the `ng-model` below to `$parent.$parent.$data`
        var html = '<label ng-repeat="'+parsed.ngRepeat+'">'+
          '<input type="radio" ng-disabled="' + this.attrs.eNgDisabled + '" ng-model="$parent.$parent.$data" value="{{'+parsed.locals.valueFn+'}}">'+
          '<span ng-bind="'+parsed.locals.displayFn+'"></span></label>';

        this.inputEl.removeAttr('ng-model');
        this.inputEl.removeAttr('ng-options');
        this.inputEl.html(html);
      },
      autosubmit: function() {
        var self = this;
        self.inputEl.bind('change', function() {
          setTimeout(function() {
            self.scope.$apply(function() {
              self.scope.$form.$submit();
            });
          }, 500);
        });
      }
    });
}]);

Am I on the right road or am I missing something??

Thanks in advance.

ckosloski commented 8 years ago

@kenfdev have you tried building the latest code? I have put in some changes that reduced the memory leak that haven't been released yet. You could see if that helps you out.

kenfdev commented 8 years ago

@ckosloski thx for the reply. Yes, I've built the latest with grunt build and I believe the code you're mentioning is as below.

      // Manually remove the watcher on 'has-error' to prevent a memory leak on it.
      for (var i = 0, len = $scope.$$watchers.length; i < len; i++) {
        if ($scope.$$watchers[i] !== undefined && $scope.$$watchers[i].last && $scope.$$watchers[i].last !== undefined &&
            typeof $scope.$$watchers[i].last === 'object' && "has-error" in $scope.$$watchers[i].last) {
          $scope.$$watchers.splice(i, 1);
          break;
        }
      }

This indeed reduces a few $$watchers but not all. For example, the view used in my project creates about 400 $$watchers and ending editable mode only reduces about 40 of them. Creating a new scope and destroying the scope reduces the same amount of $$watchers created.

kenfdev commented 8 years ago

@ckosloski This is an example of the $$watchers leaking in the newest build .

watchers

The code is as below.

<div ng-app="app" ng-controller="RadiolistCtrl" style="padding: 50px;">
  <div>
    <a href="#" editable-radiolist="user.status" e-ng-options="s.value as s.text for s in statuses">
      user status: {{ user.status }}
    </a>
  </div>

  <div>
    <a href="#" editable-radiolist="user.status2" e-ng-options="s.value as s.text for s in statuses">
      user status2: {{ user.status2 }}
    </a>
  </div>

</div>
<script>
  var app = angular.module("app", ["xeditable"]);

  app.run(function (editableOptions) {
    editableOptions.theme = 'bs3';
  });

  app.controller('RadiolistCtrl', function ($scope, $filter) {
    $scope.user = {
      status: 2,
      status2: 1
    };

    $scope.statuses = [{
      value: 1,
      text: 'status1'
    }, {
      value: 2,
      text: 'status2'
    }];
  });
</script>
kenfdev commented 8 years ago

I've created a PR #489 . I hope this problem gets a step closer to being solved.

jawadatgithub commented 8 years ago

Another bug might be caused by this bug is $data content keep reference of previous edit items:

i.e., html: <form editable-form name="editableForm" onshow="pauseVideo()" onbeforesave="beforeSavingEditedText($data)"> <span ng-repeat="word in tsWordsArrayForEdit track by $index" e-style="width: 110px;height: inherit;" e-name="{{$index}}" editable-text="word"></span> ......</form> js:

scope.beforeSavingEditedText = function(data) { //data object contains items of previous scope.tsWordsArrayForEdit array items. };

one first edit: if scope.tsWordsArrayForEdit.length = 100 and on second edit, scope.tsWordsArrayForEdit.length = 80

then after form submit $data return 100 items, in which last 20 items came from scope.tsWordsArrayForEdit old content! in below image, every item after 318, is coming from previous content!

image

I think this should make this opened issue get higher priority because it highlight data corruption not only performance issue.

meanwhile, is there a way to destroy the form and recreate it on each edit? can you share an example? or in case there is better workaround till this get fixed please share?

kenfdev commented 8 years ago

@jawadatgithub Maybe I can look at the problem. Could you create a plunkr??? I'll try and reproduce it. I have a feeling that this should be a different issue though.

ckosloski commented 8 years ago

Is this issue fixed now that #489 has been merged?

ckosloski commented 7 years ago

@eugef this can be closed, it was fixed by #489