Hello and welcome to our community! Is this your first visit?
Register
Enjoy an ad free experience by logging in. Not a member yet? Register.
Page 1 of 2 12 LastLast
Results 1 to 15 of 20
  1. #1
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts

    unpacking a mutation observer callback

    hello,

    I'm working on a greasemonkey script (so we can assume FF 16+) and am looking at Mutation Observers because sadly that seems to be the easiest way to do what I need to do.

    I'm having trouble understanding how to read the results of the callback, though, and there is very little in the way of documentation or examples out there. Here's what I have seen:
    handy but basic
    way too technical

    In the simple example below, all I want to observe is when the div changes to display block. With the DOMAttrModified event listener it would give you a newValue which you could use. But as far as I can get into the mutation observer is when it tells me there has been a style change.

    The bonus of the callback appears to be that it waits for all the mutations to take place, so it isn't firing multiple events (see example - two style changes, the block one happens first... presumably... but only one alert)

    But just to be super cautious, I'd like to be able to know what that style change was, so in this example I want to know when the style changes to display:block, not just test for display:block at the end of the mutation.

    But I can't get my head around it. Am I missing something simple? Am I being too cautious?

    thanks in advance...

    Code:
    <!DOCTYPE html>
    <html>
    <head>
    <style>
    #thediv{
    display:none
    }
    </style>
    </head>
    <body>
    
    <div id="thediv">hello</div>
    <input type ="button" value="show/hide" onclick="showHide()"/> 
    <script type="text/javascript">
    var target = document.getElementById("thediv");
    
    function showHide(){
    target.style.display=target.style.display=="block"?"none":"block";
    target.style.backgroundColor="red";
    }
    
    var myobserver = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
            console.log("key: "+mutation.type);
    		for (i in mutation){
    		console.log("val: "+mutation[i])
    				}
        });
    if( target.style.display ==="block") {
    alert("changed to block")
    	}
    });
    myobserver.observe(target, {attributes: true});
    </script>
    </body>
    </html>

  • #2
    Senior Coder Logic Ali's Avatar
    Join Date
    Sep 2010
    Location
    London
    Posts
    1,028
    Thanks
    0
    Thanked 207 Times in 202 Posts
    Quote Originally Posted by xelawho View Post
    I'm having trouble understanding how to read the results of the callback,
    ...
    In the simple example below, all I want to observe is when the div changes to display block. With the DOMAttrModified event listener it would give you a newValue which you could use. But as far as I can get into the mutation observer is when it tells me there has been a style change.
    Just going by https://developer.mozilla.org/en-US/...tationObserver , you just read the MutationRecord properties.

    In the case of a style change, it's seems it's just 'style' that's returned in attributeName rather than its changed property, so you have then to test the property you want. This is an example I built for testing:
    Code:
    <html>
    <head>
    <title>MutationObserver</title>
    </head>
    <body>
    
    <div id='theDiv' style='display:block'>The Div</div>
    
    <input type=button value='Change' onclick='f("theDiv")'>
    
    <script type="text/javascript" >
    
    function f( id )
    {
      var elem = document.getElementById( id );
     
      elem.style.display = ( elem.style.display=='block' ? 'none' : 'block' );
    }
    
    (function()
    {
      var elem = document.getElementById( 'theDiv' ),
          lastStyle = elem.style.display;
      
      var obs = new MutationObserver( function( mutations ) 
      {
        mutations.forEach( function( mutation ) 
        {
          if( mutation.type == 'attributes' && mutation.attributeName == 'style' && lastStyle != mutation.target.style.display )
          {
            lastStyle = mutation.target.style.display;
           
            alert( "Style change; display now set to: " + lastStyle );
          }   
        });   
      });
      
      obs.observe( elem, { attributes: true, childList: false, characterData: false } );
    
    })();
    
    </script>
    </body>
    </html>

  • Users who have thanked Logic Ali for this post:

    xelawho (01-06-2013)

  • #3
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts
    Thanks, Ali,

    but I think this is what's annoying me, that you have to do this extra check:
    Code:
    lastStyle != mutation.target.style.display
    really (in my eyes) you should just be able to do something like

    Code:
    <!DOCTYPE html>
    <html>
    <head>
    <style>
    #thediv{
    display:none
    }
    </style>
    </head>
    <body>
    
    <div id="thediv">hello</div>
    <input type ="button" value="show/hide" onclick="showHide()"/> 
    <script type="text/javascript">
    var target = document.getElementById("thediv");
    
    function showHide(){
    target.style.display=target.style.display=="block"?"none":"block";
    target.style.backgroundColor="red";
    }
    
    var myobserver = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
        if(mutation.target.style.display==="block") {
    	alert("changed to block")
    	}
    	});
    });
    
    myobserver.observe(target, {attributes: true});
    </script>
    </body>
    </html>
    but then it fires the event twice. See what I mean?

  • #4
    Senior Coder Logic Ali's Avatar
    Join Date
    Sep 2010
    Location
    London
    Posts
    1,028
    Thanks
    0
    Thanked 207 Times in 202 Posts
    The ability to specify the monitoring of a particular property could have a high processing overhead, which I read was one problem that stalled the introdution of this facility.

    Provided that nothing else can change an attribute on your element, I expect you could get around the multiple firing this:
    Code:
    function showHide()
    {
      target.style.display=target.style.display=="block"?"none":"block";
      myobserver.disconnect();
      target.style.backgroundColor="red";
      myobserver.observe(target, {attributes: true});
    }

  • #5
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,373
    Thanks
    11
    Thanked 592 Times in 572 Posts
    Quote Originally Posted by xelawho View Post
    Thanks, Ali,

    but I think this is what's annoying me, that you have to do this extra check:?
    you can define a new property, like element.display, and then use getters and setters to instantly push the updates to the actual element.style.display property. that also gives you the event you need, and with zero CPU cost and no polling.

    if you want to get ambitious, you can create a style2 property on each element, or Element.prototype, use a naive for-in on document.body.style to get the names of each known style property, and patch them to/from style2 by using Object.defineProperty(elm, property, {get: xxx, set:xxx}) in a loop.

    this would give you a whole shadow style object with the chance to dispatch external functions before and/or after the actual element.style change is set, with object handles to the changing element.
    Last edited by rnd me; 01-07-2013 at 10:44 AM.
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/9/03) IE7:0.1, IE8:4.6, IE11:9.1, IE9:3.1, IE10:3.0, FF:17.2, CH:46, SF:11.4, NON-MOUSE:38%

  • #6
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts
    Quote Originally Posted by rnd me View Post
    you can define a new property, like element.display, and then use getters and setters to instantly push the updates to the actual element.style.display property. that also gives you the event you need, and with zero CPU cost and no polling.
    this sounds good (the second sounds good, too, but a little over my head). Sorry for being thick, but can you give me an example of how the first one would look?

  • #7
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,373
    Thanks
    11
    Thanked 592 Times in 572 Posts
    Quote Originally Posted by xelawho View Post
    this sounds good (the second sounds good, too, but a little over my head). Sorry for being thick, but can you give me an example of how the first one would look?
    here is an example of the first routine i described.
    it sets a shadow style property on one element, document.body in this example.
    you could move the code to the middle of a loop or [].map() call and apply it to several tags at once.
    it only handles one property at a time, ideal for simple binding of a few specific styles on a few specific tags.

    Code:
    document.documentElement.innerHTML="" // clear the doc of CSS and style attribs (for this demo)
    
    Object.defineProperty(document.body, "color", {
      get: function(){return this.style.backgroundColor; },
      set: function(c){ 
    	alert("before attrib change: "+this._color); //can call fn/handler here instead of alert()
    	this.style.backgroundColor=this._color=c;
    	alert("after attrib change: "+this._color);  
      },
    });
    
    document.body.color="red" // "red"
    
    console.log( document.body.color == document.body.style.backgroundColor ) // true
    
    document.body.style.backgroundColor // "red"


    this version binds ALL valid css properties on one element to a shadow style object (style2).
    this is ideal for complete style control over some specific elements.

    Code:
    document.documentElement.innerHTML="" // clear the doc of CSS and style attribs (for this demo)
    
    var elm=document.body;
    var style2=elm.style2={}; //make a shadow style object
    
    Object.keys(elm.style).map(function(prop){
    
      Object.defineProperty( style2, prop, {
        get: function(){  return elm.style[prop]; },
        set: function(c){ 
              alert("before: " +elm.style[prop]); 
              elm.style[prop]=c; 
              alert("after: " +elm.style[prop]); 
       }
      });//end define
    
    });//end map
    
    document.body.style2.backgroundColor = "red";


    lastly, this version attempts to bind ALL css properties on ALL elements.
    this issue here is recyclying the "this" keyword; we need it to mean the element and the shadow style object, which is impossible.

    the way around that is to use the element itself as a shadow style object, substituting "_"+property to avoid clashing names.

    this is good for having before / after change events on all elements upon any style change:

    Code:
    document.documentElement.innerHTML="" // clear the doc of CSS and style attribs (for this demo)
    
    var elm=Element.prototype;
    
    Object.keys(document.body.style).map(function(prop){
    
      Object.defineProperty( elm, "_"+prop, {
        get: function(){return this.style[prop]; },
        set: function(c){ 
             alert("before: " +this.style[prop]); 
             this.style[prop]=c; 
             alert("after: " +this.style[prop]); 
        }
      });//end define
    
    });//end map
    
    document.body._backgroundColor = "red";

    since Object.defineProperty() is supposed to work in IE8's DOM, and mutations events don't for sure, and given the performance benefits of not polling or using mutation events, i think the pattern ought to be a great solution.
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/9/03) IE7:0.1, IE8:4.6, IE11:9.1, IE9:3.1, IE10:3.0, FF:17.2, CH:46, SF:11.4, NON-MOUSE:38%

  • #8
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts
    cool. Works like a charm.

    Just to double check, this is what you mean, right...?
    Code:
    var target = $("#the_form")[0];
    
    Object.defineProperty(target, "display", {
      get: function(){return this.style.display; },
      set: function(d){ 
    	alert("before attrib change: "+this._display); //can call fn/handler here instead of alert()
    	this.style.display=this._display=d;
    	alert("after attrib change: "+this.display);  
      },
    });
    
    
    // create an observer instance
    var myobserver = new MutationObserver(function(mutations) {
    alert(target.display);
    });
    
    myobserver.observe(target, {attributes: true});

  • #9
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,373
    Thanks
    11
    Thanked 592 Times in 572 Posts
    Quote Originally Posted by xelawho View Post
    cool. Works like a charm.

    Just to double check, this is what you mean, right...?
    if you're not needing an extra style object, you can simplify the definition part for better performance and readability:

    Code:
    Object.defineProperty(target, "display", {
      get: function(){return this.style.display; },
      set: function(d){ 
    	alert("before attrib change: "+this.style.display);
    	this.style.display=d;
    	alert("after  attrib change: "+this.style.display);  
      },
    });
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/9/03) IE7:0.1, IE8:4.6, IE11:9.1, IE9:3.1, IE10:3.0, FF:17.2, CH:46, SF:11.4, NON-MOUSE:38%

  • Users who have thanked rnd me for this post:

    xelawho (01-08-2013)

  • #10
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts
    thanks for your time and patience, rnd me. Although I suspect that what I want isn't exactly possible. I have to listen for the change to happen, and once it does happen I just want to be notified once. So it seems in this case that all the solutions really are just a variation on the theme suggested by Logic Ali - set a flag, and check if the new value is the same as the old value and if it is, change the value of the flag.

    I guess I was looking for something like when using mutation events you could do

    Code:
    elem.addEventListener ('DOMAttrModified',function(event){ 
    if(event.attrName=="style"&&event.newValue.indexOf("block")!=-1)){
    //do some stuff
    Or am I completely missing the point and giving up too easily?

  • #11
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,373
    Thanks
    11
    Thanked 592 Times in 572 Posts
    I had to think about this for a min or two; nice.


    i think your code would be more managble in a re-usable function, which isn't that difficult to work-up. the question, is do you wan't power or compatibility? if you can re-use the existing style object, all tools for animation should drop right in. That's power. If this isn't something that needs a bevy of existing tools, having a new element property by which to manipulate style isn't a big deal. Indeed, you code uses object.display instead of object.style.display.

    the power version works in webkit, but not in FF:, which is sad because this version is invisible: it replaces element.style with a stand-in shadow.

    Code:
    function watchStyle(element, property, changeCallback){
    
        if(!element.style2){
    	element.style2=element.style;
    	delete element.style;
    	element.style={};	
       }
    
     delete element.style[property];
    
     Object.defineProperty( element.style, property, { configurable: true,
       get: function(){return element.style2[property]; },
       set: function(val){ 
    	var cStyle=element.style2[property];
    	if( val==cStyle){return false;} // throw away duplicates
    	element.style2[property]=val; // set actual style object
    
    	 changeCallback.call(element, property, val);  
       },
     });
    
    }
    
    
    // Let's test it:
    watchStyle(document.body, "color", function demoCB(prop, value){
      alert(prop +" changed to "+value);
    });
    
    
    // invoke watcher
    document.body.style.color="blue";
    
    // invoke watcher
    document.body.style.color="red";
    
    // invoke watcher (NOT!)
    document.body.style.color="red";
    
    // invoke watcher
    document.body.style.color="blue";
    there reason firefox rejects it is becasue it won't allow element.style to be deleted or replaced in whole, but webkit does...

    giving up the cool factor, we can still accomplish the goal cross browser by using a property other than style. let's call it style2 for lack of imagination.

    once you setup the subscription using watchStyle(), you then set style2.display instead of style.display, or whatever property you are monitoring:

    Code:
    function watchStyle(element, property, changeCallback){
      var pool= (element.style2=element.style2||{});
      delete pool[property];
     Object.defineProperty(pool, property, { configurable: true,
       get: function(){return element.style[property]; },
       set: function(val){ 
    	var cStyle=element.style[property];
    	if(val==cStyle){return false;} // throw away duplicates
    	element.style[property]=val;
    	changeCallback.call(element, property, val, cStyle);  
       },
     });
    
    }
    
    watchStyle(document.body, "color", function demoCB(prop, value, previousValue){
      alert(prop +" changed to "+value+" on "+this.outerHTML.split(">")[0].slice(0,25));
    });
    
    //demo test: should alert "red", then "blue", then be done.
    document.body.style2.color="red";
    document.body.style2.color="blue";
    document.body.style2.color="blue"; // but not this time, it's a duplicate...
    tested ff, ch, ie9
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/9/03) IE7:0.1, IE8:4.6, IE11:9.1, IE9:3.1, IE10:3.0, FF:17.2, CH:46, SF:11.4, NON-MOUSE:38%

  • #12
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts
    Quote Originally Posted by rnd me View Post
    I had to think about this for a min or two
    hopefully the means it was a decent question

    But either I'm not understanding, or you think I'm trying to do the opposite of what I'm trying to do.

    What I need is to listen for actual changes, called by functions that I have no control over, so this doesn't help:
    Code:
    document.body.style2.color="red";
    because what I need to know is when this happens:
    Code:
    document.body.style.color="red";
    Being that you've stuck with me thus far, perhaps I can take a minute to explain the actual circumstance...

    On the page, the user clicks a button, a hidden div appears and is populated by ajax with a whole lot of other stuff, including another button. I want to put a listener on that button. But adding the listener to it when the user clicks the first button fails because the 2nd button doesn't exist yet.

    I had a ray of hope with jQuery's live() because it adds listeners to dynamically created elements. But it also adds them at the end (I'm guessing that this is in keeping with LIFO), and the point of adding the listener was to validate a form, so what was happening was that the form was submitted, then the validation code was run. Again, too late.

    So this was the plan - listen for when the div display turns to "block", then add the listener because by that time the 2nd button exists. But this brought on the new problem - if the code notified me more than once, multiple listeners got set, meaning the validation code ran multiple times for one click of the button.

    At the moment, what I'm seeing is that putting a setTimeout inside the click listener on the first button that puts the listener on the second button is surprisingly effective - in fact the timeout can be 0, because as I understand it (having read it somewhere) a timeout of any length guarantees that the function is placed at the end of the queue (or is this the stack? anyway...) which means that it won't be applied while the page is busy populating the div and once it is populated is exactly when I want the listener to be applied.

    So that's my workable workaround at the moment. But I do recognize that it's kinda low-rent and would love to know if there is a good way to do this.

  • #13
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,373
    Thanks
    11
    Thanked 592 Times in 572 Posts
    ahh. the only browser in which i could redefine element.style was chrome.


    you might look into on or delegate instead of the older live method:

    http://api.jquery.com/on/


    Example: Cancel a form submit action and prevent the event from bubbling up by returning false:

    Code:
    $("form").on("submit", false)
    Example: Cancel only the default action by using .preventDefault().

    Code:
    $("form").on("submit", function(event) {
      event.preventDefault();
    });
    Example: Stop submit events from bubbling without preventing form submit, using .stopPropagation().

    Code:
    $("form").on("submit", function(event) {
      event.stopPropagation();
    });
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/9/03) IE7:0.1, IE8:4.6, IE11:9.1, IE9:3.1, IE10:3.0, FF:17.2, CH:46, SF:11.4, NON-MOUSE:38%

  • #14
    Senior Coder xelawho's Avatar
    Join Date
    Nov 2010
    Posts
    2,981
    Thanks
    56
    Thanked 557 Times in 554 Posts
    thanks. Unfortunately the page I'm trying to greasemonkey is using jQ 1.6 and delegate is like live in that it works, but fires too late to catch the form before it's sent

  • #15
    Senior Coder rnd me's Avatar
    Join Date
    Jun 2007
    Location
    Urbana
    Posts
    4,373
    Thanks
    11
    Thanked 592 Times in 572 Posts
    Quote Originally Posted by xelawho View Post
    thanks. Unfortunately the page I'm trying to greasemonkey is using jQ 1.6 and delegate is like live in that it works, but fires too late to catch the form before it's sent
    doh!

    perhaps the event attributes are not usednbeforesubmit and the like.
    these should fire before delagates.
    my site (updated 13/9/26)
    BROWSER STATS [% share] (2014/9/03) IE7:0.1, IE8:4.6, IE11:9.1, IE9:3.1, IE10:3.0, FF:17.2, CH:46, SF:11.4, NON-MOUSE:38%


  •  
    Page 1 of 2 12 LastLast

    Posting Permissions

    • You may not post new threads
    • You may not post replies
    • You may not post attachments
    • You may not edit your posts
    •