Unable to reference dom object - javascript

I am trying to set a reference to a DOM Element with the following code. For some reason anything called immediately after the reference is made works perfectly fine but calling it anywhere else in my application nothing happens. I don't get any errors like "unable to set innerHTML of undefined" which is the weirdest part. Immediately after the declaration it works fine later it doesn't do anything, yet other elements referenced in the same manner and in the same function work fine.
var dom = new function() {
this.signInA = document.getElementById("signInPin");
this.orderEntry = document.getElementById("orderEntry");
this.menuGroup = document.getElementById("openGroup");
}
<div id="orderEntry">
<div id="openGroup">
</div>
</div>
then later i am calling
dom.openGroup.innerHTML="TEST";
But nothing is happening. This of course is just a snippet of the application, yet ive already searched through the entire document to check every other refrence to DOM and specifically dom.openGroup. dom.orderEntry works just fine through the entire application and dom.openGroup is only working in the immediate vacinity of this declaration.

The problem I found was that the innerHTML method when changed deleted all nested DOM objects and therefore, any references to the child objects where also deleted.
So in hindsight I should either only use innerHTML on very simple objects that won't cascade down, and use other methods to append new elements into these more complex objects.

Related

Why isn's dom updates detected by subsequent dom reads?

I'm coding a small Vue app. I've got an element which has a data-range property written like this:
:data-range="form.appearence.height_min + '/7'"
form.appearence.height_min will change based on a select element values, selected by the user.
After every select change, I'll read again the data-range and do things based on it.
// from the vue app, a watcher
'form.appearence.xps':function(val, oldval){
// this will properly change the model and the dom as well
this.$set(this.form.appearence, 'height_min', xps_map[val]);
this.$emit('xps-updated');
}
// then from another script
this.options.vue.$on('xps-updated', function(){
this.options.vue.$nextTick(function(){
console.log($('#test5').data('range')) // issue: this value doesn't change
}.bind(this))
}.bind(this));
My issue is that the range value does change on dom, I can see it from console, but javascript will always read the initial value... For example, at start was 3/7, then it gets changed to 5/7, but $('#test5').data('range') will still read 3/7. Why?
Ok, I'll answer by myself. I found out that jquery objects do not follow Vue's dom updates, at least in this case. Therefore, even if dom gets updated by Vue, $('#test5').data('range') will always give the initial value.
Instead, by getting the 'real' element with vanilla js, like
let range = document.querySelector('#test5').dataset.range;
Will always return the updated value.

Javascript return an empty array but with two elements in it

I am little confused. When i try to get elements like document.getElementsByClassName('html5gallery-tn-image-0') what i get is that:
These are the elements i am searching for but instead the array says that it is empty. Can you explain me please why is that way and can i reach the elements in this array? Thank you in advance!
You have several things going on that explains this behaviour:
You are performing the console.log at a moment that there are no such elements yet in the document. Probably the JavaScript executes before the document is ready. This explains why temp[0] is undefined.
The HTMLCollection returned by getElementsByClassName is not an array and has some magical behaviour: it is a live collection. So if the document gets an additional element of that class, it will magically appear in that temp collection without you touching it!
When you log an object to console, the Chrome dev tools will not collect all the object properties at that moment, but asynchronously. This means that although at the time of logging the collection was empty, it no longer is when you click on the dev tools to see what is in the collection.
See the first two points illustrated in this script:
var temp = document.getElementsByClassName('html5gallery-tn-image-0');
console.log(temp.length); // 0
// add the class to the element
mydiv.className = "html5gallery-tn-image-0";
console.log(temp.length); // 1
<div id="mydiv"></div>
Solution
Move your JavaScript code so that it only executes when the document is completely loaded. Either:
Move your Javascript to the end of the body tag, or
wrap the code in a document.addEventListener('DOMContentLoaded', function () { ... }); callback.
In your console.log it returns with two image elements on it, so it not empty.
getElementsByClassName returns a HTMLCollection, which has a few special properties. One of them is, that the content of that array-like thing gets updated as soon as the DOM gets updated. Additionally in Chrome the part which shows "[]" got calculated as soon as you pressed enter, but the content (those two elements) get evaluated only when you expand the output.

Elegant way around HTMLCollections updating dynamically

Today, I discovered something in Javascript that looked like "strange behavior" to me. Let's assume the following minimal example:
HTML:
<div id="test">
<span>1</span>
<span>2</span>
</div>
JS:
var div = document.getElementById('test');
var spans = div.getElementsByTagName('span');
div.removeChild(spans[0]);
div.removeChild(spans[1]);
(Fiddle: http://jsfiddle.net/SkYJg/)
Now, when running the script, I get an error:
TypeError: Argument 1 of Node.removeChild is not an object.
Looking closer, it turned out that spans[1] is null after the first one was removed. And indeed, the following code
var div = document.getElementById('test');
var spans = div.getElementsByTagName('span');
console.log(spans.length);
div.removeChild(spans[0]);
console.log(spans.length);
div.removeChild(spans[1]);
yields 2at the first log operation, but 1 the second time.
Now, it's pretty clear what happens here: after the first ?span? was removed from DOM, it's not part fo that HTMLCollection stored inside spans anymore either.
I always was under the impression, that the HTMLCollection-Object holds references to all objects that it contains. I didn't modify the collection anywhere after creating it. So I thought (but it was wrong, obviously) that the collection would behave like an array: references stay there until I delete/modify them manually.
I looked into the specification at MDN. And, indeed, richt at the top it says: HTMLCollections in the HTML DOM are live; they are automatically updated when the underlying document is changed.
The only way I could think of to prevent this is to loop over the collectino before doing anything with it, copying all references to an array, and use the array to access them afterwards. But that just looks so horribly bulky to me... is there a nicer solution? Like some way to make the collection static or to copy it without looping?
(in the minimal example I could just remove spans[0] twice, of course, but it isn't that simple in reality).
[Edit]: After seeing #Teemu's answer, I repeat: it's NOT that simple in my real code (that one is just too complex to show it here completely). I assure you, I really need random access to all elements that were inside that Collection, deleted or not.
A back-gate would be to use querySelectorAll() instead of getElementsByTagName(), it returns a non-live NodeList.
You're not using a "reference" when trying to remove the tag, just pointing the first or the second element of a collection. To use reference, you should create tags with ID and than point it by ID. The key of an Array is a third part, that's why it will be updated.
On the other hand, is a fact that JavaScript is objected-oriented sometimes, and other times it is just a script.

What happens if a JavaScript event-listener is called and target element is missing?

For the moment, we're loading site-wide event-listeners from a single common.js file for a Rails project. We're aware of (most of) the trade-offs involved there, and are just trying to mitigate them. Once our basic architecture takes hold, we may move them off to separate files by controller or by view.
For the moment, the quick question is how we can activate them only when necessary, which begs the mangled, pseudo-zen question:
if an event-listener is declared in a forest when nobody is around to hear it, does it still make a sound?
In other words, if one declares a basic listener (i.e., nothing persistent like .live() or .delegate()) in the JavaScript for a given page, and the target element is not actually present on that given page, does anything really happen, other than the few cycles devoted to evaluating it and checking the DOM for the element? Is it active in memory, looking for that element? Something else? It never seems to throw an error, which is interesting, given that in other contexts a call like that would generate a null/nil/invalid type of error.
For instance:
$(document).ready(function () {
$('#element').bind('blur keyup', function);
}
Assume that #element isn't present. Does anything really happen? Moreover is it any better to wrap it in a pre-filter like:
$(document).ready(function () {
if ($('#element')) {
$('#element').bind('blur keyup', function);
}
Or, are the .js interpreters in the browsers smart enough to simply ignore a basic listener declared on an element that's not present at $(document).ready? Should we just declare the initial, simple form above, and leave it at that, or will checking for the element first somehow save us a few precious resources and/or avoid some hidden errors we're not seeing? Or is there another angle I'm missing?
JQuery was designed to work with 0+ selected elements.
If no elements were selected, nothing will happen.
Note that you will never get null when using jQuery selector. For example:
$('#IDontExist') // != null
$('#IDontExist').length === 0 // true (it's ajQuery object with
// zero selected elements).
The docs says:
If no elements match the provided selector, the new jQuery object is "empty"; that is, it contains no elements and has .length property of 0.
$('#element') if results into empty set then jQuery will not do anything.
Since jQuery always returns an object we can can call the methods on an empty set also but internally it will do the checking before applying it's logic.
Even if you want to check if the element exists before attaching the event handler you can use length property of jQuery object.
if ($('#element').length > 0) {
$('#element').bind('blur keyup', function);
}

onchange attribute won't call function

I have an HTML document (here), which creates an iframe-based media player for a collection of songs within albums (I just used letters to define these albums and songs in the mymusic array, for simplicity).
Focusing on the top 3 iframes, the way I have set out the user interaction is to generate the HTML for forms of available albums and songs using Javascript, and write them to the iframes in the body. If you run it and make a selection in the Albums menu, you will see that the options in the Songs menu correspond with the mymusic array, so this works.
However, when I choose a song, the function nowplaying(trackindex,albumindex) should be called using an onchange event in the Songs form, the same way as in the form generated using showinitial() ... but the function does not get called.
I have ruled out the coding of nowplaying itself as a cause, because even when I change nowplaying to alert("hello"), it does not get called. So this leads me to think the problem is with the onchange attribute in "anything", but I can't see the problem. The way I coded it is no different to before, and that worked fine, so why won't this work?
Any help would be much appreciated!
Firebug is your friend....
i is not defined
function
onchange(event) {
parent.nowplaying(this.SelectedIndex,
i); }(change )
onchange is getting called, but i is not defined when calling nowplaying.
This is the result of this line:
p+="<html><head></head><body><form><select onchange='parent.nowplaying(this.SelectedIndex,i);' size='";
which is using "i" in the string, when it should append it as a variable:
p+="<html><head></head><body><form><select onchange='parent.nowplaying(this.SelectedIndex," + i + ");' size='";
To clarify, i is defined when anything(i) is called, but you aren't writing i into the code, just the letter "i". When nowplaying(this.SelectedIndex,i) is called, i is no longer defined, because you aren't inside of the anything() function anymore. You need to expand i when you append the html to p, so that the value is there and not the variable i.
function anything(i){
p+="...<select onchange='parent.nowplaying(this.SelectedIndex,i);'...";
Your onchange event handler is set from a string. When run, it will not have access to i, which is a local variable from the anything function that has long since gone away.
The simple fix would be:
p+="...<select onchange='parent.nowplaying(this.SelectedIndex,'+i+');'...";
which turns the current value of i at string-making time into an integer literal inside the string.
However, it's not generally a good idea to be creating code from strings. It's normally better to write the event handler as a normal function object:
// You will need the below workaround to get the iframe document in IE too
//
var iframe= document.getElementById('songs');
var idoc= 'contentDocument' in iframe? iframe.contentDocument : iframe.contentWindow.document;
idoc.open();
idoc.write(s);
idoc.close();
idoc.getElementsByTagName('select')[0].onchange= function() {
// This is a closure. The 'i' variable from the parent 'anything' function is
// still visible in here
//
parent.nowplaying(this.selectedIndex, i);
};
However you would generally want to avoid setting handlers from one frame on a different one. I'm not really sure what the iframes are gaining you here other than headaches. Why not just simply use positioned divs with overflow? You can still rewrite their content through innerHTML if you need to... though I would prefer to populate them using DOM methods, to avoid all the HTML-injection problems your current script has.

Categories

Resources