I'm trying to acquire a reference for a specific QUnit DOM-element, as soon as this element is created. I can get it by using window.setTimeout, but is there an event-driven way to do it?
I have tried various approaches, but only the least satisfying (window.setTimeout) actually works:
window.onload = function() {
var qunitTestrunnerToolbar_element = document.getElementById("qunit-testrunner-toolbar");
console.log("from window.onload: qunitTestrunnerToolbar_element: ", qunitTestrunnerToolbar_element);
};
returns null
document.addEventListener("DOMContentLoaded", function(event) {
var qunitTestrunnerToolbar_element = document.getElementById("qunit-testrunner-toolbar");
console.log("from document.addEventListener(DOMContentLoaded): qunitTestrunnerToolbar_element: ", qunitTestrunnerToolbar_element);
});
returns null
document.addEventListener("load", function(event) {
var qunitTestrunnerToolbar_element = document.getElementById("qunit-testrunner-toolbar");
console.log("from document.addEventListener(load): qunitTestrunnerToolbar_element: ", qunitTestrunnerToolbar_element);
});
is never executed
window.setTimeout(function() {
var qunitTestrunnerToolbar_element = document.getElementById("qunit-testrunner-toolbar");
console.log("from window.setTimeout: qunitTestrunnerToolbar_element: ", qunitTestrunnerToolbar_element);
}, 2000);
returns the DOM-reference
The code can be befiddled at this jsfiddle.
Note that the fiddle only logs the one successful DOM-reference. The others are somehow silenced.
This is how it looks when executed locally:
There is the DOMMutationObserver, which allows you to subscribe to changes to the DOM.
var mutationObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var id = 'qunit-testrunner-toolbar',
added = mutation.addedNodes,
removed = mutation.removedNodes,
i, length;
for (i = 0, length = added.length; i < length; ++i) {
if (added[i].id === id) {
// qunit-testrunner-toolbar was added
}
}
for (i = 0, length = removed.length; i < length; ++i) {
if (removed[i].id === id) {
// qunit-testrunner-toolbar was removed
}
}
});
});
mutationObserver.observe(target, {childList: true});
If you need to stop listening for changes (as the amount of changes to the DOM can be huge ;) ), you can simply use mutationObserver.disconnect().
If the #qunit-testrunner-toolbar is not the node that is added (but instead is part of another structure), the checks above will not work. If that is the case you can replace the added[i].id === id with something like
if (added[i].querySelector('#qunit-testrunner-toolbar')) {
// it was added
}
I should at least mention the now deprecated Mutation Events, which seem more convenient to implement but were pretty slow and inaccurate.
document.addEventListener('DOMNodeInserted', function(e) {
if (e.target.id === 'qunit-testrunner-toolbar') {
}
});
Tempting to use, but don't as it is deprecated.
Everything you put in JSFiddle's javascript is already wrapped in onload event (so window.onload will never be fired again - it is already fired). Change it to "Javascript" -> "Load time" -> "No wrap - in <head>" and you will get all of your events fired.
Related
I have a script in my code
<script src="https://geodata.solutions/includes/statecity.js"></script>
which is making an ajax call. This script is used to fetch states and cities and loads the value in select. How do I check whether that particular call is complete as it is in this external script and I want to set value of select using javascript/jquery
I am currently using setTimeout for setting select value and delaying it randomly for 6 seconds however it's not the right approach. Also, I have tried putting the code to set value in $(document).ready() but the api call returns the values later
setTimeout(function(){
jQuery("#stateId").val('<?php echo $rowaddress['state']; ?>').change();
setTimeout(function(){
jQuery("#cityId").val('<?php echo $rowaddress['city']; ?>').change();
}, 3000);
}, 6000);
spy on jQuery.ajax:
jQuery.ajax = new Proxy(jQuery.ajax, {
apply: function(target, thisArg, argumentsList) {
const req = target.apply(thisArg, argumentsList);
const rootUrl = '//geodata.solutions/api/api.php';
if (argumentsList[0].url.indexOf(rootUrl) !== -1) {
req.done(() => console.log(`request to ${argumentsList[0].url} completed`))
}
return req;
}
});
Having a look through the code for statecity.js, I've just seen that:
jQuery(".states").prop("disabled",false);
is executed upon completion of initial loading. This is on line 150 of the source code.
You could monitor the disabled attribute of the .states selector to be informed when the activity is completed using the handy JQuery extension watch.
To detect the completion of the event, just watch the disabled property of the .states item:
$('.states').watch('disabled', function() {
console.log('disabled state changed');
// Add your post-loading code here (or call the function that contains it)
});
Note that this is extremely hacky. If the author of statecity.js changes their code, this could stop working immediately or could behave unexpectedly.
It is always very risky to rely on tinkering in someone else's code when you have no control over changes to it. Use this solution with caution.
Unfortunately, the original link to the watch extension code seems to have expired, but here it is (not my code but reproduced from author):
// Function to watch for attribute changes
// http://darcyclarke.me/development/detect-attribute-changes-with-jquery
$.fn.watch = function(props, callback, timeout){
if(!timeout)
timeout = 10;
return this.each(function(){
var el = $(this),
func = function(){ __check.call(this, el) },
data = { props: props.split(","),
func: callback,
vals: [] };
$.each(data.props, function(i) { data.vals[i] = el.attr(data.props[i]); });
el.data(data);
if (typeof (this.onpropertychange) == "object"){
el.bind("propertychange", callback);
} else {
setInterval(func, timeout);
}
});
function __check(el) {
var data = el.data(),
changed = false,
temp = "";
for(var i=0;i < data.props.length; i++) {
temp = el.attr(data.props[i]);
if(data.vals[i] != temp){
data.vals[i] = temp;
changed = true;
break;
}
}
if(changed && data.func) {
data.func.call(el, data);
}
}
}
My question is really "Is the lapsed listener problem preventable in javascript?" but apparently the word "problem" causes a problem.
The wikipedia page says the lapsed listener problem can be solved by the subject holding weak references to the observers. I've implemented that before in Java and it works nicely, and I thought I'd implement it in Javascript, but now I don't see how. Does javascript even have weak references? I see there are WeakSet and WeakMap which have "Weak" in their names, but they don't seem to be helpful for this, as far as I can see.
Here's a jsfiddle showing a typical case of the problem.
The html:
<div id="theCurrentValueDiv">current value: false</div>
<button id="thePlusButton">+</button>
The javascript:
'use strict';
console.log("starting");
let createListenableValue = function(initialValue) {
let value = initialValue;
let listeners = [];
return {
// Get the current value.
get: function() {
return value;
},
// Set the value to newValue, and call listener()
// for each listener that has been added using addListener().
set: function(newValue) {
value = newValue;
for (let listener of listeners) {
listener();
}
},
// Add a listener that set(newValue) will call with no args
// after setting value to newValue.
addListener: function(listener) {
listeners.push(listener);
console.log("and now there "+(listeners.length==1?"is":"are")+" "+listeners.length+" listener"+(listeners.length===1?"":"s"));
},
};
}; // createListenable
let theListenableValue = createListenableValue(false);
theListenableValue.addListener(function() {
console.log(" label got value change to "+theListenableValue.get());
document.getElementById("theCurrentValueDiv").innerHTML = "current value: "+theListenableValue.get();
});
let nextControllerId = 0;
let thePlusButton = document.getElementById("thePlusButton");
thePlusButton.addEventListener('click', function() {
let thisControllerId = nextControllerId++;
let anotherDiv = document.createElement('div');
anotherDiv.innerHTML = '<button>x</button><input type="checkbox"> controller '+thisControllerId;
let [xButton, valueCheckbox] = anotherDiv.children;
valueCheckbox.checked = theListenableValue.get();
valueCheckbox.addEventListener('change', function() {
theListenableValue.set(valueCheckbox.checked);
});
theListenableValue.addListener(function() {
console.log(" controller "+thisControllerId+" got value change to "+theListenableValue.get());
valueCheckbox.checked = theListenableValue.get();
});
xButton.addEventListener('click', function() {
anotherDiv.parentNode.removeChild(anotherDiv);
// Oh no! Our listener on theListenableValue has now lapsed;
// it will keep getting called and updating the checkbox that is no longer
// in the DOM, and it will keep the checkbox object from ever being GCed.
});
document.body.insertBefore(anotherDiv, thePlusButton);
});
In this fiddle, the observable state is a boolean value, and you can add and remove checkboxes that view and control it, all kept in sync by listeners on it.
The problem is that when you remove one of the controllers, its listener doesn't go away: the listener keeps getting called and updating the controller checkbox and prevents the checkbox from being GCed, even though the checkbox is no longer in the DOM and is otherwise GCable. You can see this happening in the javascript console since the listener callback prints a message to the console.
What I'd like instead is for the controller DOM node and its associated value listener to become GCable when I remove the node from the DOM. Conceptually, the DOM node should own the listener, and the observable should hold a weak reference to the listener. Is there a clean way to accomplish that?
I know I can fix the problem in my fiddle by making the x button explicitly remove the listener along with the DOM subtree, but that doesn't help in the case that some other code in the app later removes part of the DOM containing my controller node, e.g. by executing document.body.innerHTML = ''. I'd like set things up so that, when that happens, all the DOM nodes and listeners I created get released and become GCable. Is there a way?
Custom_elements offer a solution to the lapsed listener problem. They are supported in Chrome and Safari and (as of Aug 2018), are soon to be supported on Firefox and Edge.
I did a jsfiddle with HTML:
<div id="theCurrentValue">current value: false</div>
<button id="thePlusButton">+</button>
And a slightly modified listenableValue, which now has the ability to remove a listener:
"use strict";
function createListenableValue(initialValue) {
let value = initialValue;
const listeners = [];
return {
get() { // Get the current value.
return value;
},
set(newValue) { // Set the value to newValue, and call all listeners.
value = newValue;
for (const listener of listeners) {
listener();
}
},
addListener(listener) { // Add a listener function to call on set()
listeners.push(listener);
console.log("add: listener count now: " + listeners.length);
return () => { // Function to undo the addListener
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
console.log("remove: listener count now: " + listeners.length);
};
}
};
};
const listenableValue = createListenableValue(false);
listenableValue.addListener(() => {
console.log("label got value change to " + listenableValue.get());
document.getElementById("theCurrentValue").innerHTML
= "current value: " + listenableValue.get();
});
let nextControllerId = 0;
We can now define a custom HTML element <my-control>:
customElements.define("my-control", class extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const n = nextControllerId++;
console.log("Custom element " + n + " added to page.");
this.innerHTML =
"<button>x</button><input type=\"checkbox\"> controller "
+ n;
this.style.display = "block";
const [xButton, valueCheckbox] = this.children;
xButton.addEventListener("click", () => {
this.parentNode.removeChild(this);
});
valueCheckbox.checked = listenableValue.get();
valueCheckbox.addEventListener("change", () => {
listenableValue.set(valueCheckbox.checked);
});
this._removeListener = listenableValue.addListener(() => {
console.log("controller " + n + " got value change to "
+ listenableValue.get());
valueCheckbox.checked = listenableValue.get();
});
}
disconnectedCallback() {
console.log("Custom element removed from page.");
this._removeListener();
}
});
The key point here is that disconnectedCallback() is guaranteed to be called when the <my-control> is removed from the DOM whatever reason. We use it to remove the listener.
You can now add the first <my-control> with:
const plusButton = document.getElementById("thePlusButton");
plusButton.addEventListener("click", () => {
const myControl = document.createElement("my-control");
document.body.insertBefore(myControl, plusButton);
});
(This answer occurred to me while I was watching this video, where the speaker explains other reasons why custom elements could be useful.)
You can use mutation observers which
provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature which was part of the DOM3 Events specification.
An example of how this can be used can be found in the code for on-load
if (window && window.MutationObserver) {
var observer = new MutationObserver(function (mutations) {
if (Object.keys(watch).length < 1) return
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].attributeName === KEY_ATTR) {
eachAttr(mutations[i], turnon, turnoff)
continue
}
eachMutation(mutations[i].removedNodes, function (index, el) {
if (!document.documentElement.contains(el)) turnoff(index, el)
})
eachMutation(mutations[i].addedNodes, function (index, el) {
if (document.documentElement.contains(el)) turnon(index, el)
})
}
})
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
attributeFilter: [KEY_ATTR]
})
}
I have a function which "types" out a header title as though it is being typed on the screen.
The typer only starts typing once a particular section of my site is "active" or is seen on the screen.
At present, it takes the outputID aka the area where this text will be typed into. There are two instances of this function being run, each with different outputIDs - I only want the function to run once per outputID.
This is how the function is initially called.
<h2 id="typer-get-in-touch" class="typer" data-text="Get in Toche^^^^^ Touch"></h2>
if(anchorLink == 'contact'){
var outputID = $("#typer-get-in-touch");
textTyping(outputID);
}else if(anchorLink == 'expertise'){
var outputID = $("#typer-expertise");
textTyping(outputID);
}
This is the textTyping function
function textTyping(outputID){
$(outputID).show();
var textString = $(outputID).data("text");
var textArray = textString.split("");
var texttypeing = setInterval(
function() {
typeOutText(outputID,textArray);
}, 170);
function typeOutText(outputID,textArray) {
if (textArray[0] == "^"){
outputID.text(function(index, text){
return text.replace(/(\s+)?.$/, '');
});
textArray.shift();
}else {
if (textArray.length > 0) {
outputID.append(textArray.shift());
} else {
clearTimeout(texttypeing);
}
}
}
}
My issue at present is that the function runs multiple types, and continues to type each time the original anchorLink trigger is achieved. The result is that is writes the title many times e.g:
Get In TouchGet In TouchGet In Touch
Each time the section is navigated to, the typing starts again.
How can I run this function only ONCE per outputID? So once the outputID has been used, the function can no longer run for that data?
JSFiddle of non-working example: https://jsfiddle.net/qLez8zeq/
JSFiddle of mplungjan's solution: https://jsfiddle.net/qLez8zeq/1/
Change
function textTyping(outputID){
$(outputID).show();
var textString = $(outputID).data("text");
to
function textTyping(outputID){
var textString = $(outputID).data("text");
if (textString=="") return;
$(outputID).data("text","");
$(outputID).show();
FIDDLE
What you need to do is to bind the event handler for each ID and then unbind it after it's been triggered the first time. Since you're already using jQuery, you can use the "one" method to do exactly this for each outputID:
$( "#typer-get-in-touch" ).one( "click", function() {
textTyping(outputID);
});
I suppose you could store your processed outputIds into an array and then check if the given outputId is present in the array before starting?
Define your array, check for the existence, if not found, do code example:
var processedIds = [];
function textTyping(outputID) {
var foundItem = false;
for (var i = 0; i < processedIds.length; i++)
{
if (processedIds[i] == outputID) {
foundItem = true;
break;
}
}
if (!foundItem) {
//the rest of your code goes here
}
}
You can add some check at the beginning of your function:
var called = {};
function textTyping(outputID) {
if (called[outputID]) {
return;
}
called[outputID] = true;
// your code
}
I create a hammer instance like so:
var el = document.getElementById("el");
var hammertime = Hammer(el);
I can then add a listener:
hammertime.on("touch", function(e) {
console.log(e.gesture);
}
However I can't remove this listener because the following does nothing:
hammertime.off("touch");
What am I doing wrong? How do I get rid of a hammer listener? The hammer.js docs are pretty poor at the moment so it explains nothing beyond the fact that .on() and .off() methods exist. I can't use the jQuery version as this is a performance critical application.
JSFiddle to showcase this: http://jsfiddle.net/LSrgh/1/
Ok, I figured it out. The source it's simple enough, it's doing:
on: function(t, e) {
for (var n = t.split(" "), i = 0; n.length > i; i++)
this.element.addEventListener(n[i], e, !1);
return this
},off: function(t, e) {
for (var n = t.split(" "), i = 0; n.length > i; i++)
this.element.removeEventListener(n[i], e, !1);
return this
}
The thing to note here (apart from a bad documentation) it's that e it's the callback function in the on event, so you're doing:
this.element.addEventListener("touch", function() {
//your function
}, !1);
But, in the remove, you don't pass a callback so you do:
this.element.removeEventListener("touch", undefined, !1);
So, the browser doesn't know witch function are you trying to unbind, you can fix this not using anonymous functions, like I did in:
Fiddle
For more info: Javascript removeEventListener not working
In order to unbind the events with OFF, you must:
1) Pass as argument to OFF the same callback function set when called ON
2) Use the same Hammer instance used to set the ON events
EXAMPLE:
var mc = new Hammer.Manager(element);
mc.add(new Hammer.Pan({ threshold: 0, pointers: 0 }));
mc.add(new Hammer.Tap());
var functionEvent = function(ev) {
ev.preventDefault();
// .. do something here
return false;
};
var eventString = 'panstart tap';
mc.on(eventString, functionEvent);
UNBIND EVENT:
mc.off(eventString, functionEvent);
HammerJS 2.0 does now support unbinding all handlers for an event:
function(events, handler) {
var handlers = this.handlers;
each(splitStr(events), function(event) {
if (!handler) {
delete handlers[event];
} else {
handlers[event].splice(inArray(handlers[event], handler), 1);
}
});
return this;
}
Here's a CodePen example of what Nico posted. I created a simple wrapper for "tap" events (though it could easily be adapted to anything else), to keep track of each Hammer Manager. I also created a kill function to painlessly stop the listening :P
var TapListener = function(callbk, el, name) {
// Ensure that "new" keyword is Used
if( !(this instanceof TapListener) ) {
return new TapListener(callbk, el, name);
}
this.callback = callbk;
this.elem = el;
this.name = name;
this.manager = new Hammer( el );
this.manager.on("tap", function(ev) {
callbk(ev, name);
});
}; // TapListener
TapListener.prototype.kill = function () {
this.manager.off( "tap", this.callback );
};
So you'd basically do something like this:
var myEl = document.getElementById("foo"),
myListener = new TapListener(function() { do stuff }, myEl, "fooName");
// And to Kill
myListener.kill();
Basically I have some event listeners and their handling function defined as follows:
<div id="postTextBlock"/>
<div id="postImageBlock"/>
<div id="postQuoteBlock"/>
<div id="postLinkBlock"/>
document.getElementById('postTextBlock').addEventListener('click', function() { showPostType(postTextBlock) }, false);
document.getElementById('postImageBlock').addEventListener('click', function() { showPostType(postImageBlock) }, false);
document.getElementById('postQuoteBlock').addEventListener('click', function() { showPostType(postQuoteBlock) }, false);
document.getElementById('postLinkBlock').addEventListener('click', function() { showPostType(postLInkBlock) }, false);
var showPostType = (function () {
var postTypes = new Array('postTextBlock', 'postImageBlock', 'postQuoteBlock', 'postLinkBlock')
return function(type) {
for (var i = 0; i < postTypes.length; i++) {
(function(index) { alert(document.getElementById(postTypes[index])) })(i)
}
}
})()
When I run this I will get 5 alerts. One for each of the postTypes defined in my array and a final null for what I'm guessing is postTypes[5]. Why is it executing the code with i = 5 when I have set the for loop to terminate when i = 5 (postTypes.length = 4).
Edit:
I added the html that it references as well as the full array values. Hopefully this clears some stuff up about the code not working.
You know your code sample doesn't work? I took a stab at what it's --supposed-- to do.
http://jsfiddle.net/8xxQE/1/
document.getElementById('postTextBlock').addEventListener('click', function() {
showPostType('postTextBlock'); //Argument does nothing
}, false);
document.getElementById('postImageBlock').addEventListener('click', function() {
showPostType('postImageBlock'); //Argument does nothing
}, false);
The arguments passed above were not included, based on the function code they did nothing anyways.
var showPostType = (function() {
var postTypes = new Array('postTextBlock', 'postImageBlock')
return function(/*type argument removed isn't referenced*/) {
var l = postTypes.length;
for (; l--;) {
(function(index) {
console.log(index, postTypes[index]);
alert(document.getElementById(postTypes[index]))
})(l);
}
}
})()
I added some trickery as just an example of a better way to write a for loop. Your closure works fine, I think you are doing something else to cause this code to not work as expected. Why would this error run 4 times, there's only two items in the array. My example ran exactly twice every time I clicked a div, as you can see on JSFiddle.
The div's id is "postLInkBlock", but you're searching for "postLinkBlock". That's the null.