I currently have 2 directives:
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?!
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();
});
};