Question

I've started working with angular js and have a problem that requires getting the current state of the DOM inside of my controller. Basically I'm building a text editor inside of an contenteditable div. Revisions to the text in the div can come from an external service(long polling pushes from the server) as well as the user actually typing in the field. Right now the revisions from the server are manipulating my angular model, which then updates the view through an ng-bind-html-unsafe directive. The only problem with this is that this blows away the users current cursor position and text selection.

I've figured out a way around the problem, but it requires directly manipulating dom elements in my controller, which seems to be discouraged in angular. I'm looking for either validation of my current method, or reccomendations on something more "angulary".

Basically what I've done is added two events to my model, "contentChanging" and "contentChanged". The first is fired right before I update the model, the second right after. In my controller I subscribe to these events like this.

//dmp is google's diff_match_patch library
//rangy is a selection management library http://code.google.com/p/rangy/wiki/SelectionSaveRestoreModule
var selectionPatch;
var selection;
scope.model.on("contentChanging", function() {
    var currentText = $("#doc").html();       
    selection = rangy.saveSelection();
    var textWithSelection = $("#doc").html();
    selectionPatch = dmp.patch_make(currentText, textWithSelection);
});
scope.model.on("contentChanged", function() {
    scope.$apply();
    var textAfterEdit = $("#doc").html();
    $("#doc").html(dmp.patch_apply(selectionPatch, textAfterEdit)[0]);
    rangy.restoreSelection(selection);
});

So basically, when the content is changing I grab the current html of the editable area. Then I use the rangy plugin which injects hidden dom elements into the document to mark the users current position and selection. I take the html without the hidden markers and the html with the markers and I make a patch using google's diff_match_patch library(dmp).

Once the content is changed, I invoke scope.$apply() to update the view. Then I get the new text from the view and apply the patch from earlier, which will add the hidden markers back to the html. Finally I use range to restore the selection.

The part I don't like is how I use jquery to get the current html from the view to build and apply my patches. It's going to make unit testing a little tricky and it just doesn't feel right. But given how the rangy library works, I can't think of another way to do it.

Was it helpful?

Solution

Here's a simple example of how you would start:

<!doctype html>
<html ng-app="myApp">
<head>
    <script src="http://code.angularjs.org/1.1.2/angular.min.js"></script>
    <script type="text/javascript">
    function Ctrl($scope) {
        $scope.myText = "Here's some text";
    }

    angular.module("myApp", []).directive('texteditor', function() {
        return {
            restrict: 'E',
            replace: true,
            template: '<textarea></textarea>',
            scope: {
                text: '=' // link the directives scopes `text` property
                          // to the expression inside the text attribute
            },
            link: function($scope, elem, attrs) {
                elem.val($scope.text);
                elem.bind('input', function() {
                    // When the user inputs text, Angular won't know about
                    // it since we're not using ng-model so we need to call 
                    // $scope.$apply() to tell Angular run a digest cycle
                    $scope.$apply(function() {
                        $scope.text = elem.val();
                    });
                });
            }
        };
    });
    </script>
</head>
<body>
    <div ng-controller="Ctrl">
        <texteditor text="myText"></texteditor>
        <p>myText = {{myText}}</p>
    </div>
</body>
</html>

It's just binding to a textarea, so you would replace that with your real text editor. The key is to listen to changes on the text in your text editor, and update the value on your scope so that the outside world know that the user changed the text inside the text editor.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top