Go Back   CodingForums.com > :: Client side development > JavaScript programming

Before you post, read our: Rules & Posting Guidelines

Reply
 
Thread Tools Rate Thread
Enjoy an ad free experience by logging in. Not a member yet? Register.
Old 01-06-2013, 01:49 PM   PM User | #1
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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>
xelawho is offline   Reply With Quote
Old 01-06-2013, 07:44 PM   PM User | #2
Logic Ali
Regular Coder

 
Logic Ali's Avatar
 
Join Date: Sep 2010
Location: London
Posts: 959
Thanks: 0
Thanked 198 Times in 193 Posts
Logic Ali will become famous soon enoughLogic Ali will become famous soon enough
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>
Logic Ali is offline   Reply With Quote
Users who have thanked Logic Ali for this post:
xelawho (01-06-2013)
Old 01-06-2013, 11:47 PM   PM User | #3
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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?
xelawho is offline   Reply With Quote
Old 01-07-2013, 12:40 AM   PM User | #4
Logic Ali
Regular Coder

 
Logic Ali's Avatar
 
Join Date: Sep 2010
Location: London
Posts: 959
Thanks: 0
Thanked 198 Times in 193 Posts
Logic Ali will become famous soon enoughLogic Ali will become famous soon enough
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});
}
Logic Ali is offline   Reply With Quote
Old 01-07-2013, 10:36 AM   PM User | #5
rnd me
Senior Coder

 
rnd me's Avatar
 
Join Date: Jun 2007
Location: Urbana
Posts: 3,455
Thanks: 9
Thanked 466 Times in 450 Posts
rnd me is a jewel in the roughrnd me is a jewel in the roughrnd me is a jewel in the rough
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.
__________________
my site (updated 5/13)
STATS (2013/5) HTML5:90.2% MOB:14% IE7:0.5% IE8:8.6% IE9:9.8% IE10:10%

Last edited by rnd me; 01-07-2013 at 10:44 AM..
rnd me is offline   Reply With Quote
Old 01-07-2013, 04:54 PM   PM User | #6
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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?
xelawho is offline   Reply With Quote
Old 01-07-2013, 09:16 PM   PM User | #7
rnd me
Senior Coder

 
rnd me's Avatar
 
Join Date: Jun 2007
Location: Urbana
Posts: 3,455
Thanks: 9
Thanked 466 Times in 450 Posts
rnd me is a jewel in the roughrnd me is a jewel in the roughrnd me is a jewel in the rough
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 5/13)
STATS (2013/5) HTML5:90.2% MOB:14% IE7:0.5% IE8:8.6% IE9:9.8% IE10:10%
rnd me is offline   Reply With Quote
Old 01-07-2013, 10:15 PM   PM User | #8
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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});
xelawho is offline   Reply With Quote
Old 01-08-2013, 07:19 AM   PM User | #9
rnd me
Senior Coder

 
rnd me's Avatar
 
Join Date: Jun 2007
Location: Urbana
Posts: 3,455
Thanks: 9
Thanked 466 Times in 450 Posts
rnd me is a jewel in the roughrnd me is a jewel in the roughrnd me is a jewel in the rough
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 5/13)
STATS (2013/5) HTML5:90.2% MOB:14% IE7:0.5% IE8:8.6% IE9:9.8% IE10:10%
rnd me is offline   Reply With Quote
Users who have thanked rnd me for this post:
xelawho (01-08-2013)
Old 01-08-2013, 05:30 PM   PM User | #10
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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?
xelawho is offline   Reply With Quote
Old 01-09-2013, 09:09 AM   PM User | #11
rnd me
Senior Coder

 
rnd me's Avatar
 
Join Date: Jun 2007
Location: Urbana
Posts: 3,455
Thanks: 9
Thanked 466 Times in 450 Posts
rnd me is a jewel in the roughrnd me is a jewel in the roughrnd me is a jewel in the rough
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 5/13)
STATS (2013/5) HTML5:90.2% MOB:14% IE7:0.5% IE8:8.6% IE9:9.8% IE10:10%
rnd me is offline   Reply With Quote
Old 01-09-2013, 09:48 PM   PM User | #12
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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.
xelawho is offline   Reply With Quote
Old 01-11-2013, 01:19 AM   PM User | #13
rnd me
Senior Coder

 
rnd me's Avatar
 
Join Date: Jun 2007
Location: Urbana
Posts: 3,455
Thanks: 9
Thanked 466 Times in 450 Posts
rnd me is a jewel in the roughrnd me is a jewel in the roughrnd me is a jewel in the rough
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 5/13)
STATS (2013/5) HTML5:90.2% MOB:14% IE7:0.5% IE8:8.6% IE9:9.8% IE10:10%
rnd me is offline   Reply With Quote
Old 01-11-2013, 04:43 AM   PM User | #14
xelawho
Senior Coder

 
xelawho's Avatar
 
Join Date: Nov 2010
Posts: 2,437
Thanks: 52
Thanked 453 Times in 451 Posts
xelawho will become famous soon enoughxelawho will become famous soon enough
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
xelawho is offline   Reply With Quote
Old 01-11-2013, 10:17 AM   PM User | #15
rnd me
Senior Coder

 
rnd me's Avatar
 
Join Date: Jun 2007
Location: Urbana
Posts: 3,455
Thanks: 9
Thanked 466 Times in 450 Posts
rnd me is a jewel in the roughrnd me is a jewel in the roughrnd me is a jewel in the rough
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 5/13)
STATS (2013/5) HTML5:90.2% MOB:14% IE7:0.5% IE8:8.6% IE9:9.8% IE10:10%
rnd me is offline   Reply With Quote
Reply

Bookmarks

Jump To Top of Thread


Thread Tools
Rate This Thread
Rate This Thread:

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT +1. The time now is 10:28 PM.


Advertisement
Log in to turn off these ads.