8

This may seem like a silly question but I need to know how to watch the entire DOM of a page and recompile it any time it changes. Essentially this is what AngularJS does by default by use of databindings, but I need this to happen anytime anything in the DOM is changed, not just bindings. The reason why is because I have an app that is built entirely in HTML, Javascript and PHP. It's a single page application, it has a main page and injects PHP into a DIV wrapper within that page.

I want to make some modifications to it but want to keep my code completely separate from the original code. To do this I need to be able to recompile the DOM anytime a new PHP file with it's own DOM structure is injected. What I have so far does not appear to be working.

app.directive("watch", function () {
    return function (scope, element, attr) {
        scope.$watch("watch", function(oldValue, newValue) {
            if(newValue) {
                console.log("there is a new value");
                console.log("the new value is " + newValue);
             }
         });
     }
});

I add the watch attribute the the <body> tag but it doesn't seem to work, when the dom is changed nothing gets logged. Ultimately I'd like to replace the console.log with $compile, but I first need to get the watch to work. Can someone point me to what I'm doing wrong?

richbai90
  • 4,994
  • 4
  • 50
  • 85
  • This all seems like a very bad design. Why not have Angular load the PHP and put it in the div with `ng-inlude` or routing? – Ben Lesh Oct 10 '13 at 19:34
  • I didn't build the original application, had I done so that's how I would have done it. As it is I'm just trying to make some changes to what is already there. – richbai90 Oct 10 '13 at 19:35
  • Certainly you could add an ng-app="myModule" to the tag in the original app? – Ben Lesh Oct 10 '13 at 19:37

5 Answers5

32

It's a bad idea, but I'll humor you.

So as I've stated in my comments above, it's a bad idea to do what you're trying to do. That said, if you still want to go this way, you can $watch just about anything by passing a function in as the first parameter.

The code below will check to see if the HTML inside the <body> tag has changed. Please note, that this is a HORRIBLE idea.

$scope.$watch(function () {
   return document.body.innerHTML;
}, function(val) {
   //TODO: write code here, slit wrists, etc. etc.
});

The watch above will fire anytime ANYTHING in the HTML changes.

  • when a value changes in an input
  • when the selection changes on a dropdown
  • when an attribute changes in any element in the html.
  • etc.

Additional Info: As of right now, in a whole lot of browsers, there's not really a good way to monitor for new DOM elements which have been loaded. The best way is still to trigger something at the moment the new DOM elements were added.

Community
  • 1
  • 1
Ben Lesh
  • 107,825
  • 47
  • 247
  • 232
  • I appreciate you humoring me, I do want to do this the right way. I could use ng-include portal.php for instance, the problem is I don't know how I'd do this correctly without without rewriting the whole thing. The application I'm dealing with is a cluster and I don't really know of a better way to go. – richbai90 Oct 10 '13 at 19:46
  • I guess if I knew more specifics of why you needed to do this I could give you a less snarky answer. haha. – Ben Lesh Oct 10 '13 at 19:46
  • `As of right now, in almost all browsers, there's not really a good way to monitor for new DOM elements which have been loaded.` - what about `MutationObserver`? Other than IE, it has pretty good support – Ian Oct 10 '13 at 19:51
  • Well basically they have built hundreds of pages of PHP code that is used for the sole purpose of writing html and javascript. There is no separation of code, it is not restful so there are no requests, basically the buttons fire javascript functions that return php classes that rebuild the dom. – richbai90 Oct 10 '13 at 19:56
  • @Ian doesn't work in Android Mobile, Opera Mini (who cares? lol) or any version of IE less than 11. (that's a big slice of browsers) I guess "almost all" was overstating it. – Ben Lesh Oct 10 '13 at 19:56
  • 1
    I can see how this would be a terrible idea. I will need to find a different way. – richbai90 Oct 10 '13 at 20:14
  • 1
    This is nice for last resort debugging. – wag2639 Jun 04 '14 at 21:22
11

As mentioned by Steinway Wu, MutationObserver is the way to go. Here is a snippet :

.directive('myDirective', function() {
    return {
        ...
        link: function(scope, element, attrs) {
            var observer = new MutationObserver(function(mutations) {
                // your code here ...
            });
            observer.observe(element[0], {
                childList: true,
                subtree: true
            });
        }
    };
});
aaaaahaaaaa
  • 454
  • 5
  • 15
2

I created a small utility service

here is the code

in GenericService.js we have this

angular.module('GenericServiceModule', [])
    .factory('$utilsService', function () {

        var utilService = {};

        utilService.observeDomChange = function (domElement, doThisOnChange, observationOptions) {
            var defaultOptions = {
                // attributes: true, // attribute changes will be observed | on add/remove/change attributes
                // attributeOldValue: true, // will show oldValue of attribute | on add/remove/change attributes | default: null
                // we need this because in .find() sizzle modifies id temporarily, https://github.com/jquery/jquery/issues/2620
                //attributeFilter: ['style'], // filter for attributes | array of attributes that should be observed, in this case only style

                // characterData: true, // data changes will be observed | on add/remove/change characterData
                // characterDataOldValue: true, // will show OldValue of characterData | on add/remove/change characterData | default: null

                childList: true, // target childs will be observed | on add/remove
                subtree: true // target childs will be observed | on attributes/characterData changes if they observed on target

            };
            var options = observationOptions || defaultOptions;

            var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
            var observer = new MutationObserver(function anElementIWatchChanged(mutations) {
                doThisOnChange();
            });

            // removing the dom element won't be triggered
            observer.observe(domElement, options);
        };

        return utilService;
    });

and simply you can use it like

$utilsService.observeDomChange($element[0], doSomething());

here is an example of a usage

angular.module(
...
.directive('myCustomDirective', function ($utilsService) {
    return {
        restrict: 'A',
        //...
        controller: function ($scope, $element) {
            //...
            $scope.$on('$viewContentLoaded', function (event, viewName) {
                $utilsService.observeDomChange($element[0], doSomething());
            }
        }
    }
}
Basheer AL-MOMANI
  • 14,473
  • 9
  • 96
  • 92
0

As mentioned here (Mutation Events), you can add listeners for DOM mutation events like DOMSubtreeModified.

e.g. element.addEventListener("DOMSubtreeModified", function (ev) {...}, false)

Update

And actually they recommend using Mutation Observer instead.

Steinway Wu
  • 1,288
  • 1
  • 12
  • 18
0

I know this is an old topic, but I experienced a similar problem when trying to detect changes in the content of an element filled with HTML code dynamically from the model using ng-bind-html.

What about associating some custom data to the children of your ng-bind-html element using jqLite's data() to detect if the content was replaced (and therefore the custom data lost)?.

This would be the code of a directive to watch the changes of an element filled with with ng-bind-html:

.directive("myOnDomContentChanges", function($window) {
    var DATA_KEY_NAME = "myOnDomContentChangesKey";
    return function(scope, element, attrs){
        scope.$watch(function(){
            var res = null;
            var children = element.children();
            if(children && children.length > 0){
                var childData = children.data(DATA_KEY_NAME);
                res = (typeof childData !== "undefined" ? childData : false );
            }
            return res;
        }, function(watchData){
            if(watchData === false){ //Only run when false
                var children = element.children();
                if(children && children.length > 0){
                    children.data(DATA_KEY_NAME, true); //Set custom data to true
                }
                //New HTML binding detected
                //Write your code here
            }
        });
    };
})

I hope it helps.
Comments are welcome.
  Rafa

rbarriuso
  • 787
  • 8
  • 30