Is it possible in JavaScript to listen for a change of attribute value? For example:
var element=document.querySelector('…');
element.addEventListener( ? ,doit,false);
element.setAttribute('something','whatever');
function doit() {
}
I would like to respond to any change in the something attribute.
I have read up on the MutationObserver object, as well as alternatives to that (including the one which uses animation events). As far as I can tell, they are about changes to the actual DOM. I’m more interested in attribute changes to a particular DOM element, so I don’t think that’s it. Certainly in my experimenting it doesn’t seem to work.
I would like to do this without jQuery.
Thanks
You need MutationObserver, Here in snippet I have used setTimeout to simulate modifying attribute
var element = document.querySelector('#test');
setTimeout(function() {
element.setAttribute('data-text', 'whatever');
}, 5000)
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === "attributes") {
console.log("attributes changed");
// Example of accessing the element for which
// event was triggered
mutation.target.textContent = "Attribute of the element changed";
}
console.log(mutation.target);
});
});
observer.observe(element, {
attributes: true //configure it to listen to attribute changes
});
<div id="test">Dummy Text</div>
Additionally, mutation.target property gives the reference to mutated/changed node.
This question is already answered, but I'd like to share my experiences, because the mutation observer did not bring me the insights in needed.
Note This is some kind of hacky solution, but for (at least) debugging purposes quite good.
You can override the setAttribute function of a particalar element. This way you can also print the callstack, and get an insight of "who" changed the attribute value:
// select the target element
const target = document.querySelector("#element");
// store the original setAttribute reference
const setAttribute = target.setAttribute;
// override setAttribte
target.setAttribute = (key: string, value: string) => {
console.trace("--trace");
// use call, to set the context and prevent illegal invocation errors
setAttribute.call(target, key, value);
};
Related
:)
I'm using the NicEdit WYSIWYG editor. The DIV is patched with a hidden textarea field.
The problem is that all changes are just synced onSubmit().
Now I need a way to listen for changes. For example to change backgound color of the field on changing content (inputs) (flagged as unsaved). But there is no addEventListener for changed InnerHTML, as far as I know.
So maybe it's possible to use a callback function like a domMutationObserver. But I have no ideas how to deal with that.
js code:
let domField = document.querySelector('.contents .subpage[data-pagename="Index"][data-culture="en-US"] .nicEdit-main');
I can detect the selector of the editor div. So how to detect changes inside this element/node?
I read something about an "observer" to detect dom mutations. Maybe somebody can give me a simple and easy example.
Thx for everything. ;)
EDIT: And I hope we all agree that I won't set an interval (just to save browser performance). But yeah that could be a way. So watching for mutations or changing innerHTML of my DIV element.
pseudo code:
domElement.innerHTML.watch('change', elem => {
elem.classList.add('changed'); //or unsaved
});
After doing some research, I managed to create an Observer.
Unfortunately, this fires with every single DOM change during runtime.
js code:
let domElement = document.querySelector('.contents .subpage[data-pagename="Index"] .culture[data-culture="de-DE"] .nicEdit-main');
let observer = new window.MutationObserver((e) => {
console.log(domElement.innerHTML);
});
observer.observe(domElement, {
characterData: true,
subtree: true
});
In the meantime, I think that watching the blur event would be enough. Then it just should be checked whether the "new" innerHTML differs from the "old" (or initially loaded) innerHTML.
js code:
let domElement = document.querySelector('.contents .subpage[data-pagename="Index"] .culture[data-culture="de-DE"] .nicEdit-main');
domElement.addEventListener('blur', (e) => {
//check .innerHTML changed (compared to init value)
alert('onblur');
});
Of course, this isn't a DOM observer / watching, but it costs the browser significantly less performance, if not each separate change fires, but only leave-focus interaction of the user and a manually check for .innerHTML changes.
maybe Proxy could solve it.when some properties of style has changed,set will invoke.
var app = document.getElementById('app')
var handle = {
get(target, property, value, receive) {
return target[property]
},
set(target, property, value, receive) {
// callback()
Reflect.set(target, property, value)
}
}
var a = new Proxy(app, handle)
a.innerHTML = 2
a.style.background = "red"
a.style.height = "200px"
a.style.width = "200px"
I need to accurately measure the dimensions of text within my web app, which I am achieving by creating an element (with relevant CSS classes), setting its innerHTML then adding it to the container using appendChild.
After doing this, there is a wait before the element has been rendered and its offsetWidth can be read to find out how wide the text is.
Currently, I'm using setTimeout(processText, 100) to wait until the render is complete.
Is there any callback I can listen to, or a more reliable way of telling when an element I have created has been rendered?
The accepted answer is from 2014 and is now outdated. A setTimeout may work, but it's not the cleanest and it doesn't necessarily guarantee that the element has been added to the DOM.
As of 2018, a MutationObserver is what you should use to detect when an element has been added to the DOM. MutationObservers are now widely supported across all modern browsers (Chrome 26+, Firefox 14+, IE11, Edge, Opera 15+, etc).
When an element has been added to the DOM, you will be able to retrieve its actual dimensions.
Here's a simple example of how you can use a MutationObserver to listen for when an element is added to the DOM.
For brevity, I'm using jQuery syntax to build the node and insert it into the DOM.
var myElement = $("<div>hello world</div>")[0];
var observer = new MutationObserver(function(mutations) {
if (document.contains(myElement)) {
console.log("It's in the DOM!");
observer.disconnect();
}
});
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});
$("body").append(myElement); // console.log: It's in the DOM!
The observer event handler will trigger whenever any node is added or removed from the document. Inside the handler, we then perform a contains check to determine if myElement is now in the document.
You don't need to iterate over each MutationRecord stored in mutations because you can perform the document.contains check directly upon myElement.
To improve performance, replace document with the specific element that will contain myElement in the DOM.
There is currently no DOM event indicating that an element has been fully rendered (eg. attached CSS applied and drawn). This can make some DOM manipulation code return wrong or random results (like getting the height of an element).
Using setTimeout to give the browser some overhead for rendering is the simplest way. Using
setTimeout(function(){}, 0)
is perhaps the most practically accurate, as it puts your code at the end of the active browser event queue without any more delay - in other words your code is queued right after the render operation (and all other operations happening at the time).
This blog post By Swizec Teller, suggests using requestAnimationFrame, and checking for the size of the element.
function try_do_some_stuff() {
if (!$("#element").size()) {
window.requestAnimationFrame(try_do_some_stuff);
} else {
$("#element").do_some_stuff();
}
};
in practice it only ever retries once. Because no matter what, by the next render frame, whether it comes in a 60th of a second, or a minute, the element will have been rendered.
You actually need to wait yet a bit after to get the after render time. requestAnimationFrame fires before the next paint. So requestAnimationFrame(()=>setTimeout(onrender, 0)) is right after the element has been rendered.
In my case solutions like setTimeout or MutationObserver weren't totaly realiable.
Instead I used the ResizeObserver. According to MDN:
Implementations should, if they follow the specification, invoke
resize events before paint and after layout.
So basically the observer always fires after layout, thus we should be able to get the correct dimensions of the observed element.
As a bonus the observer already returns the dimensions of the element. Therefore we don't even need to call something like offsetWidth (even though it should work too)
const myElement = document.createElement("div");
myElement.textContent = "test string";
const resizeObserver = new ResizeObserver(entries => {
const lastEntry = entries.pop();
// alternatively use contentBoxSize here
// Note: older versions of Firefox (<= 91) provided a single size object instead of an array of sizes
// https://bugzilla.mozilla.org/show_bug.cgi?id=1689645
const width = lastEntry.borderBoxSize?.inlineSize ?? lastEntry.borderBoxSize[0].inlineSize;
const height = lastEntry.borderBoxSize?.blockSize ?? lastEntry.borderBoxSize[0].blockSize;
resizeObserver.disconnect();
console.log("width:", width, "height:", height);
});
resizeObserver.observe(myElement);
document.body.append(myElement);
This can also we wrapped in a handy async function like this:
function appendAwaitLayout(parent, element) {
return new Promise((resolve, reject) => {
const resizeObserver = new ResizeObserver((entries) => {
resizeObserver.disconnect();
resolve(entries);
});
resizeObserver.observe(element);
parent.append(element);
});
}
// call it like this
appendAwaitLayout(document.body, document.createElement("div")).then((entries) => {
console.log(entries)
// do stuff here ...
});
The MutationObserver is probably the best approach, but here's a simple alternative that may work
I had some javascript that built the HTML for a large table and set the innerHTML of a div to the generated HTML. If I fetched Date() immediately after setting the innerHTML, I found that the timestamp was for a time prior to the table being completely rendered. I wanted to know how long the rendering was taking (meaning I needed to check Date() after the rendering was done). I found I could do this by setting the innerHTML of the div and then (in the same script) calling the click method of some button on the page. The click handler would get executed only after the HTML was fully rendered, not just after the innerHTML property of div got set. I verified this by comparing the Date() value generated by the click handler to the Date() value retrieved by the script that was setting the innerHTML property of the div.
Hope someone finds this useful
suppose your element has classname class="test"
The following function continue test if change has occured
if it does, run the function
function addResizeListener(elem, fun) {
let id;
let style = getComputedStyle(elem);
let wid = style.width;
let hei = style.height;
id = requestAnimationFrame(test)
function test() {
let newStyle = getComputedStyle(elem);
if (wid !== newStyle.width ||
hei !== newStyle.height) {
fun();
wid = newStyle.width;
hei = newStyle.height;
}
id = requestAnimationFrame(test);
}
}
let test = document.querySelector('.test');
addResizeListener(test,function () {
console.log("I changed!!")
});
when you make for example
var clonedForm = $('#empty_form_to_clone').clone(true)[0];
var newForm = $(clonedForm).html().replace(/__prefix__/g, next_index_id_form);
// next_index_id_form is just a integer
What am I doing here?
I clone a element already rendered and change the html to be rendered.
Next i append that text to a container.
$('#container_id').append(newForm);
The problem comes when i want to add a event handler to a button inside newForm, WELL, just use ready event.
$(clonedForm).ready(function(event){
addEventHandlerToFormButton();
})
I hope this help you.
PS: Sorry for my English.
According to #Elliot B.'s answer, I made a plan that suits me.
const callback = () => {
const el = document.querySelector('#a');
if (el) {
observer.disconnect();
el.addEventListener('click', () => {});
}
};
const observer = new MutationObserver(callback);
observer.observe(document.body, { subtree: true, childList: true });
I am trying to detect when an elements display property has changed. Initially the element has no display property. I want to alert when the element has changed. I've tried the following the code, but nothing alerts.
document.getElementById('xxx').addEventListener('DOMAttrModified', function(e){
if (e.attrName === 'style') {
if (e.newValue === 'block') {
alert('test!');
}
}
}, false);
setTimeout(function () {
var xxx = document.getElementById('xxx');
xxx.style.display = 'block';
console.log(xxx);
}, 5000);
Here's a practical example showing off how mutation observers can be used. Do note that I used the WebKit prefixed MutationObserver implementation, but you mentioned you're using Chrome anyway, so you can at least test this and if necessary adapt it to your needs.
First we create the MutationObserver and provide it with a callback. This callback will be called with 2 parameters - an array of MutationRecord instances, and secondly an instance of MutationObserver (the one which was responsible for the callback being called). This is useful when attaching a single callback to multiple MutationObservers. Note that it receives an array of MutationRecords rather than being called once for every MutationRecord. This allows bundling them up, which was necessary to improve performance.
var observer = new WebKitMutationObserver(function(mutations) {
console.log(mutations);
});
Now we create (an optional) config object, to specify what type of mutations we're interested in. In this case, mutations of an attributes.
var config = { attributes: true };
Note that, although we define it here as a simple object, this will actually be used as a MutationObserverInit object.
Now, finally, we tell the observer to observe a node:
observer.observe(document.getElementById('xxx'), config);
In case you're interested, I adapted your example into a jsFiddle demo.
I need to intercept any changes in the content of a cell inside my webpage.
The following code shows me that addEventListener does not work.
function modifyText() {
alert("!");
}
var el=document.getElementById("mycell");
el.innerHTML="a"
el.addEventListener("change", modifyText, false);
// After next instruction I expect an alert message but it does not appear...
el.innerHTML="Z";
The code is just a toy example. In my real case the changes in the page (and therefore in the cell, too) are made by a webapp that I have NO control over.
There is a modern way to catch innerhtml changes:
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe
Example:
// identify an element to observe
elementToObserve = window.document.getElementById('y-range').children[0];
// create a new instance of 'MutationObserver' named 'observer',
// passing it a callback function
observer = new MutationObserver(function(mutationsList, observer) {
console.log(mutationsList);
});
// call 'observe' on that MutationObserver instance,
// passing it the element to observe, and the options object
observer.observe(elementToObserve, {characterData: false, childList: true, attributes: false});
childList mutation fires on innerHTML change.
You can't listen to a DOM element change that way. change event is mostly for inputs
There is some other new DOM 3 events that would help you on this.
Here is some:
DOMCharacterDataModified //Draft
DOMSubtreeModified
Does this Most efficient method of detecting/monitoring DOM changes? help?
It seems like there aren't any 100% cross browser solutions, and one of the workarounds is to poll the elements of interest to see if their innerHTML.length changes!
I have an ajax callback which injects html markup into a footer div.
What I can't figure out is how to create a way to monitor the div for when it's contents change. Placing the layout logic I'm trying to create in the callback isn't an option as each method (callback and my layout div handler) shouldn't know about the other.
Ideally I'd like to see some kind of event handler akin to $('#myDiv').ContentsChanged(function() {...}) or $('#myDiv').TriggerWhenContentExists( function() {...})
I found a plugin called watch and an improved version of that plugin but could never get either to trigger. I tried "watching" everything I could think of (i.e. height property of the div being changed via the ajax injection) but couldn't get them to do anything at all.
Any thoughts/help?
The most effective way I've found is to bind to the DOMSubtreeModified event. It works well with both jQuery's $.html() and via standard JavaScript's innerHTML property.
$('#content').bind('DOMSubtreeModified', function(e) {
if (e.target.innerHTML.length > 0) {
// Content change handler
}
});
http://jsfiddle.net/hnCxK/
When called from jQuery's $.html(), I found the event fires twice: once to clear existing contents and once to set it. A quick .length-check will work in simple implementations.
It's also important to note that the event will always fire when set to an HTML string (ie '<p>Hello, world</p>'). And that the event will only fire when changed for plain-text strings.
You can listen for changes to DOM elements (your div for example) by binding onto DOMCharacterDataModified tested in chrome but doesn't work in IE see a demo here
Clicking the button causes a change in the div which is being watched, which in turn fills out another div to show you its working...
Having a bit more of a look Shiki's answer to jquery listen to changes within a div and act accordingly looks like it should do what you want:
$('#idOfDiv').bind('contentchanged', function() {
// do something after the div content has changed
alert('woo');
});
In your function that updates the div:
$('#idOfDiv').trigger('contentchanged');
See this as a working demo here
There is a neat javascript library, mutation-summary by google, that lets you observe dom changes concisely. The great thing about it, is that if you want, you can be informed only of the actions that actually made a difference in the DOM, to understand what I mean you should watch the very informative video on the project's homepage.
link:
http://code.google.com/p/mutation-summary/
jquery wrapper:
https://github.com/joelpurra/jquery-mutation-summary
You might want to look into the DOMNodeInserted event for Firefox/Opera/Safari and the onpropertychange event for IE. It probably wouldn't be too hard to utilize these events but it might be a little hack-ish. Here is some javascript event documentation: http://help.dottoro.com/larrqqck.php
Now we can use a MutationObserver ; Well, apparently we must.
Use of Mutation Events is deprecated. Use MutationObserver instead.
jquery.min.js:2:41540
From https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver :
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
observer.disconnect();