Question

I currently have 2 directives:

  1. An editor directive that creates a textbox and a toolbar div.
  2. A bold button which is transcluded into the editor

Markup

<editor content="text">
  <bold-button>B</bold-button>
</editor>

Editor directive

.directive('editor', function () {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    scope: {
      'content': '='
    },
    template: '<div class="editor">' +
                '<div class="toolbar" ng-transclude></div>' +
                '<textarea ng-model="content"></textarea>' +
              '</div>',
    controller: function ($scope, $element, $attrs) { },
    link: function (scope, elem, attrs, ctrl) {
      var editor = new Editor();
      editor.onPostUpdateDom = function () {
        scope.content = elem.find('textarea').val();
      };
      // Expose editor to other directives
      ctrl.editor = editor;
    };
});

Bold button directive

.directive('boldButton', function () {
  return {
    require: '^editor',
    restrict: 'E',
    replace: true,
    transclude: true,
    scope: {},
    template: '<button type="button" ng-click="format()" ng-transclude>'+
              '</button>',
    link: function (scope, elem, attrs, editorCtrl) {
      scope.format = function () {
        editorCtrl.editor.formatCommand("bold");
      };
    };
});

The editor directive uses a third party plugin that provides a formatCommand() method which changes the textarea value.

The bold button directive fires this method via the editor's controller.

Now, every time the plugin changes the DOM it raises an onPostUpdateDOM event which I use to grab the new value and assign it to scope within the editor directive:

scope.content = elem.find('textarea').val();

This works really well. The button is pressed and the values changes.

However the plugin also provides DOM manipulation via keyboard shortcuts. When the DOM is changed the scope.content = elem.find('textarea').val(); line doesn't work because it occurred outside of Angular. Wrapping that line in an $apply works but then the formatCommand() call from the button directive throws an "apply is already in progress" error.

Is this a case for a "safe apply" anti-pattern?!

Was it helpful?

Solution

These lines in the editor directive:

editor.onPostUpdateDom = function () {
  scope.content = elem.find('textarea').val();
};

are the problem, the onPostUpdateDom event handler fires either from inside, or outside of the Angular world and it is out of the control of the developer due to the reliance on the third party plugin.

To solve this we essentially need a safe apply to handle Angular and non-Angular events:

see: AngularJS : Prevent error $digest already in progress when calling $scope.$apply()

So the answer is to wrap the code in a $timeout.

i.e.

editor.onPostUpdateDom = function () {
  $timeout(function () {
    // anything you want can go here and will safely be run on the next digest.
    scope.content = elem.find('textarea').val();
  });
};
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top