Stroing child elements and events as properties of DOM Object in Javascript - javascript

How bad would it be if I could cache DOM selections and store them as property directly on DOM parent element. Say i have
(function(element){
element['content'] = element.querySelector(".tooltip__content");
//maybe event this
element['content']['handlers']['click'] = function(e){...};
}(document.querySelector("tooltip"));
This way I can "cache" DOM selection and have references to event handlers to use with removeEventListener.
How bad is it? What are the alternatives? Thank you.
Of course i can store it in seperate variables but maybe there is more structural organized way?

Related

Struggling to bind and match DOM elements with vanilla JS

I prefer to bind all my DOM elements at the top of my code so that the ID or class reference is only hard coded once, even though the element is used multiple times within the code. Something like this:
var startButton = "#startButton";
var closeButtons = ".button.close";
That works well with selectors, such as document.querySelector. The challenge occurs when I'm about to compare an element ID to one of the static variables above, like for instance during a click event:
e.target.id == startButton
Since the variable startButton contains a sharp, I would have to do something like this:
e.target.id == startButton.replace('#', '')
This is obviously not the ideal way of handling things, so I'm wondering if there's a better way to bind DOM elements for later use?
Please note that I did consider retrieving the elements using a selector right away, like document.querySelector, but that prevents me from manipulating the elements later on.
EDIT: My note above turns out to be wrong. It's totally possible to manipulate the elements later on. So the question is whether it's better to store selectors in variables, or to store nodes in variables. As pointed out below, the DOM element might not exist while it's being declared, so it might be better to store the selector in a variable, like I'm doing above.
For the specific example in your question, you can use .matches():
if (e.target.matches(startButton))
The .matches() DOM API takes a selector as its argument.

Knockoutjs cleanup on DOM removal

I will be using knockoutjs in a single page application and I am concerned whether the observables remain in memory even if any DOM elements that the bindings were applied to, are removed.
What I think is that I will need to handle this in my application, by calling
ko.cleanNode(DOMElement)
on each DOM element that used observables, before removing them from the document.
I just need someone to confirm that this is the case
Thanks
CleanNode doesn't remove the observables, it just unbinds them from the UI elements. You'd also need to remove any references to your view models by setting them to null in order for them to be garbage collected, something like:
var myVM = new myViewModel();
ko.applyBindings(myVM, DOMElement);
//All your other stuff
ko.cleanNode(DOMElement);
myVM = null;

Override Marionette.Region's getEl method in Backbone.Marionette

I've set up a project where I've extended a Backbone.Marionette.Layout that contains two different regions. This layout can be used as a component throughout the application. In particular, the regions are set up like the following.
regions : {
masterRegion : { selector: '[data-region=master]' },
slaveRegion: { selector: '[data-region=slave]' }
},
In particular, I'm using a data-region selector to inject the view I'm interested in.
When such a layout is used in a tree structure views are duplicated since getEl function adresses the wrong region to inject the view. Obviously it's my fault and within Marionette (v1.1.0) doc the following is written.
override the getEl function if we have a parentEl this must be
overridden to ensure the selector is found on the first use of the
region. if we try to assign the region's el to parentEl.find(selector)
in the object literal to build the region, the element will not be
guaranteed to be in the DOM already, and will cause problems
where getEl is defined as
getEl: function(selector){
return Marionette.$(selector);
}
So, my question is the following. What does this mean? How can I override this method? Where is the correct to perform such an override?
Hope it's clear.
Here's my understanding of this:
the points below apply to the case where the layout is contained within another element ("if we have a parentEl")
the first time you use a region, Marionette needs to select the proper DOM element to populate, according to the selector string ("ensure the selector is found on the first use of the region")
you can't simply look for the selector in the parentEl ("if we try to assign the region's el to parentEl.find(selector) in the object literal"), because the DOM element we want isn't necessarily in the DOM yet ("the element will not be guaranteed to be in the DOM already")
In other words, the first time you use a region (e.g. with a call to the show method), Marionette needs to build a region instance and associate it with the correct DOM element (specified by the selectorattribute).
However, before Marionette can look for the DOM element within the containing parent element, it must ensure that all required DOM elements (most importantly the one we're looking for) have loaded.
Does that make more sense to you?
Edit based on flexaddicted's comment.
Could you suggest me a the correct way to achieve this? Is there any
manner to override the method below?
I don't think you need to override this method. The comment indicates why the DOM element is fetched that way instead of by direct assignment when the region is built, but it should still work properly with a tree structure (since parents can still be determined properly).
I think the problem might be with your region selector: as it is "generic", it can potentially match multiple elements (as opposed to selecting with an id attribute that should match only 1 element), and could be matching a DOM element you're not expecting such as a child view. (This of course depends on when Marionette looks at the DOM to fetch the selector.)
Also, I'd consider using a composite view for your tree structure needs if possible. See http://davidsulc.com/blog/2013/02/03/tutorial-nested-views-using-backbone-marionettes-compositeview/ and http://lostechies.com/derickbailey/2012/04/05/composite-views-tree-structures-tables-and-more/

Why should y.innerHTML = x.innerHTML; be avoided?

Let's say that we have a DIV x on the page and we want to duplicate ("copy-paste") the contents of that DIV into another DIV y. We could do this like so:
y.innerHTML = x.innerHTML;
or with jQuery:
$(y).html( $(x).html() );
However, it appears that this method is not a good idea, and that it should be avoided.
(1) Why should this method be avoided?
(2) How should this be done instead?
Update:
For the sake of this question let's assume that there are no elements with ID's inside the DIV x.
(Sorry I forgot to cover this case in my original question.)
Conclusion:
I have posted my own answer to this question below (as I originally intended). Now, I also planed to accept my own answer :P, but lonesomeday's answer is so amazing that I have to accept it instead.
This method of "copying" HTML elements from one place to another is the result of a misapprehension of what a browser does. Browsers don't keep an HTML document in memory somewhere and repeatedly modify the HTML based on commands from JavaScript.
When a browser first loads a page, it parses the HTML document and turns it into a DOM structure. This is a relationship of objects following a W3C standard (well, mostly...). The original HTML is from then on completely redundant. The browser doesn't care what the original HTML structure was; its understanding of the web page is the DOM structure that was created from it. If your HTML markup was incorrect/invalid, it will be corrected in some way by the web browser; the DOM structure will not contain the invalid code in any way.
Basically, HTML should be treated as a way of serialising a DOM structure to be passed over the internet or stored in a file locally.
It should not, therefore, be used for modifying an existing web page. The DOM (Document Object Model) has a system for changing the content of a page. This is based on the relationship of nodes, not on the HTML serialisation. So when you add an li to a ul, you have these two options (assuming ul is the list element):
// option 1: innerHTML
ul.innerHTML += '<li>foobar</li>';
// option 2: DOM manipulation
var li = document.createElement('li');
li.appendChild(document.createTextNode('foobar'));
ul.appendChild(li);
Now, the first option looks a lot simpler, but this is only because the browser has abstracted a lot away for you: internally, the browser has to convert the element's children to a string, then append some content, then convert the string back to a DOM structure. The second option corresponds to the browser's native understanding of what's going on.
The second major consideration is to think about the limitations of HTML. When you think about a webpage, not everything relevant to the element can be serialised to HTML. For instance, event handlers bound with x.onclick = function(); or x.addEventListener(...) won't be replicated in innerHTML, so they won't be copied across. So the new elements in y won't have the event listeners. This probably isn't what you want.
So the way around this is to work with the native DOM methods:
for (var i = 0; i < x.childNodes.length; i++) {
y.appendChild(x.childNodes[i].cloneNode(true));
}
Reading the MDN documentation will probably help to understand this way of doing things:
appendChild
cloneNode
childNodes
Now the problem with this (as with option 2 in the code example above) is that it is very verbose, far longer than the innerHTML option would be. This is when you appreciate having a JavaScript library that does this kind of thing for you. For example, in jQuery:
$('#y').html($('#x').clone(true, true).contents());
This is a lot more explicit about what you want to happen. As well as having various performance benefits and preserving event handlers, for example, it also helps you to understand what your code is doing. This is good for your soul as a JavaScript programmer and makes bizarre errors significantly less likely!
You can duplicate IDs which need to be unique.
jQuery's clone method call like, $(element).clone(true); will clone data and event listeners, but ID's will still also be cloned. So to avoid duplicate IDs, don't use IDs for items that need to be cloned.
It should be avoided because then you lose any handlers that may have been on that
DOM element.
You can try to get around that by appending clones of the DOM elements instead of completely overwriting them.
First, let's define the task that has to be accomplished here:
All child nodes of DIV x have to be "copied" (together with all its descendants = deep copy) and "pasted" into the DIV y. If any of the descendants of x has one or more event handlers bound to it, we would presumably want those handlers to continue working on the copies (once they have been placed inside y).
Now, this is not a trivial task. Luckily, the jQuery library (and all the other popular libraries as well I assume) offers a convenient method to accomplish this task: .clone(). Using this method, the solution could be written like so:
$( x ).contents().clone( true ).appendTo( y );
The above solution is the answer to question (2). Now, let's tackle question (1):
This
y.innerHTML = x.innerHTML;
is not just a bad idea - it's an awful one. Let me explain...
The above statement can be broken down into two steps.
The expression x.innerHTML is evaluated,
That return value of that expression (which is a string) is assigned to y.innerHTML.
The nodes that we want to copy (the child nodes of x) are DOM nodes. They are objects that exist in the browser's memory. When evaluating x.innerHTML, the browser serializes (stringifies) those DOM nodes into a string (HTML source code string).
Now, if we needed such a string (to store it in a database, for instance), then this serialization would be understandable. However, we do not need such a string (at least not as an end-product).
In step 2, we are assigning this string to y.innerHTML. The browser evaluates this by parsing the string which results in a set of DOM nodes which are then inserted into DIV y (as child nodes).
So, to sum up:
Child nodes of x --> stringifying --> HTML source code string --> parsing --> Nodes (copies)
So, what's the problem with this approach? Well, DOM nodes may contain properties and functionality which cannot and therefore won't be serialized. The most important such functionality are event handlers that are bound to descendants of x - the copies of those elements won't have any event handlers bound to them. The handlers got lost in the process.
An interesting analogy can be made here:
Digital signal --> D/A conversion --> Analog signal --> A/D conversion --> Digital signal
As you probably know, the resulting digital signal is not an exact copy of the original digital signal - some information got lost in the process.
I hope you understand now why y.innerHTML = x.innerHTML should be avoided.
I wouldn't do it simply because you're asking the browser to re-parse HTML markup that has already been parsed.
I'd be more inclined to use the native cloneNode(true) to duplicate the existing DOM elements.
var node, i=0;
while( node = x.childNodes[ i++ ] ) {
y.appendChild( node.cloneNode( true ) );
}
Well it really depends. There is a possibility of creating duplicate elements with the same ID, which is never a good thing.
jQuery also has methods that can do this for you.

In DOM is it OK to use .notation for getting/setting attributes?

In DOM, is it OK to refer to an element's attributes like this:
var universe = document.getElementById('universe');
universe.origin = 'big_bang';
universe.creator = null;
universe.style.deterministic = true;
? My deep respect for objects and their privacy, and my sense that things might go terribly wrong if I am not careful, makes me want to do everything more like this:
var universe = document.getElementById('universe');
if(universe.hasAttribute('origin')) then universe.origin = 'big_bang';
etc...
Is it really necessary to use those accessor methods? Of course it may be more or less necessary depending on how certain I am that the elements I am manipulating will have the attributes I expect them to, but in general do the DOM guys consider it OK to use .notation rather than getters and setters?
Thanks!
For XML documents, you must use getAttribute/setAttribute/removeAttribute etc. There is no mapping from JavaScript properties to DOM attributes.
For HTML documents, you can use getAttribute et al to access attributes, but it's best not to because IE6-7 has difficulties with it. The DOM Level 2 HTML properties are not only more reliable, but also easier to read.
It's unclear whether you're using XML or HTML documents here. Clearly origin is not an HTML attribute; ‘custom’ elements and attributes like this should not be included in HTML documents. But it's unclear what universe.style.deterministic refers to; you wouldn't get a CSS style lookup mapped without an HTML style attribute.
Yes, it's fine ;-)
If there's an attribute in the DOM, you can set it or get it, directly.
No private or read-only elements or anything. By the way, JavaScript doesn't have a 'then' keyword.
Due to cross browser issues I always use getAttribute and setAttribute:
if(!universe.getAttribute('origin'))
{
universe.setAttribute('origin', 'big_bang');
}
I don't recall the specifics but I have had problems with the property style universe.origin and dynamically created DOM elements.
No, it's not fine to do so. Most properties of DOM objects can be overwritten. You won't ruin the browser's behavior, since it doesn't use the DOM API. But you will ruin your JS scripts if they attempt to use the overwritten property in its original meaning.
My own way of doing things, when I have several attributes to attach to an object (as opposed to a single flag or link), is to create a custom object and then link it from the DOM element:
var Universe = {
origin: "big_bang",
creator: null,
style: { deterministic: true }
};
document.getElementById('universe')._universe = Universe;

Categories

Resources