In general custom HTML element attributes are discouraged but in some cases one have the choice between either having a collection of nodes that one saves and traverse for comparison or setting a property. As for the former that is often enough not a good choice.
Usually I would use dataset for this, but what if that is not a good option due to the script being used on any site where one have no control of what that site itself does?
Say one want to tag all <video> elements with a custom status property could one use Symbol() attached directly to the element? I have tested using Symbol() on dataset but it seems to be lost sometimes. As in: I can set it, log it, next call it is sometimes there, sometimes not. (Which also makes a use-case harder to make) Not sure why. Might have something to do with it being a media element? Symbol() is perhaps not valid as "dataset property name"?
Question the is if it is OK to set it directly on the element?
const symbol = Symbol.for("test");
// in some function:
// instead of:
some_element.dataset[symbol] = "foo";
// do:
some_element[symbol] = "foo";
Have not had any trouble with the latter (as in property disappearing).
Or could one perhaps use something like:
Object.defineProperty(element, symbol, {
readonly: true,
value: "foo"
});
If your worry is that the site you have no control over could theoretically modify the data attribute - the same could occur with a symbol if the other site was really intent on breaking things.
const element = document.querySelector('div');
// Your code
const symbol = Symbol.for("test");
element[symbol] = "foo";
// Site's code
const [sym] = Object.getOwnPropertySymbols(element);
element[sym] = "bar";
// Your code, later
console.log(element[symbol]);
<div></div>
or, the site itself could use Symbol.for too.
const element = document.querySelector('div');
// Your code
const symbol = Symbol.for("test");
element[symbol] = "foo";
// Site's code
element[Symbol.for('test')] = "bar";
// Your code, later
console.log(element[symbol]);
<div></div>
Defining the property as readonly could work, but it'd also make you unable to alter the property later, which you might find would eventually be a problem.
A better solution would be to, instead of putting a property onto the object, to create your own collection that associates the elements with your custom values. If your own collection is scoped to only your own script (like with an IIFE), it'll then be impossible for other scripts to interfere with. This is a great place for a WeakMap. For example:
// Your code
(() => {
const map = new WeakMap();
const element = document.querySelector('div');
// Set the value:
map.set(element, 'foo');
// Retrieve the value:
console.log(map.get(element));
})();
<div></div>
The WeakMap values will continue to work and be unmodifiable by outside sources as long as the elements still exist in memory.
Related
I'm aware this isn't "best practice" but I'm curious why this code doesn't work. For reference, I'm using Chrome 63 at the moment.
Typing getElementById everywhere is obnoxious. Suppose I have the following code:
var D = document;
D.getID = D.getElementById;
D.getTags = D.getElementsByTags;
HTMLElement.prototype.getID = HTMLElement.prototype.getElementById;
HTMLElement.prototype.getTags = HTMLElement.prototype.getElementsByTags;
If I try to use these new names, e.g. D.getID("foo").getID("bar"), then I get the error D.getID("foo").getID("bar") is not a function. What's the deal? (To be sure, D.getID("foo") works just fine)
Since IDs are supposed to be unique in a document, there's no HTMLElement.prototype.getElementById() method. The only object that has this method is document.
You can add this method to HTMLElement by assigning the function that's bound to document:
HTMLElement.prototype.getID = document.getElementById.bind(document);
Note that even though you can use this to call the method from a particular element, it will search the entire document, not just within that element. You could use querySelector to limit it:
HTMLElement.prototype.getID = function(id) {
return this.querySelector('#' + id);
}
Your code for getTags just uses the wrong name for the long method. It should be:
HTMLElement.prototype.getTags = HTMLElement.prototype.getElementsByTagName
There's no need for the assignments to D.getID and D.getTags, since Document inherits from HTMLElement.
I need to change on the fly the value set on every node using the innerHTML.
The closest solution I found is:
...
Object.defineProperty(Element.prototype, 'innerHTML', {
set: function () {
// get value (ok)
var value = arguments[0];
// change it (ok)
var new_value = my_function(value);
// set it (problem)
this.innerHTML = new_value; // LOOP
}
}
...
But obviously it's an infinite loop.
Is there a way to call the original innerHTML set?
I also try the Proxy way but i could not make it work.
More details:
I am working on an experimental project which uses a reverse proxy to generate and add CSP policies to a website, so:
the owner of the website will be aware of these "overwrites"
i needed to handle any js code client generated which could trigger the
policy
i need to modify it before the Content Security Policy engine evalution! (this is the main problem which requires this "non so good" solution)
Obligatory warning:
Overriding the setter and getter for any property of Element.prototype is bound to be bad idea in any production-level code. If you use any libraries that rely on innerHTML to work as it should or if there are other developers in the project that don't know of these changes, things might get weird. You will also loose the ability to use innerHTML "normally" in other parts of the app.
That said, as you haven't provided any information about why you would want to do this, I'm just going to assume that you know about the caveats and you still want to override the browser's own functionality, perhaps for development purposes.
Solution: You are overriding the browser's native setter for the Element.prototype.innerHTML, but you also need the original setter to achieve your goal. This can be done using Object.getOwnPropertyDescriptor, which is sort of the "counterpart" of Object.defineProperty.
(function() {
//Store the original "hidden" getter and setter functions from Element.prototype
//using Object.getOwnPropertyDescriptor
var originalSet = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML').set;
Object.defineProperty(Element.prototype, 'innerHTML', {
set: function (value) {
// change it (ok)
var new_value = my_function(value);
//Call the original setter
return originalSet.call(this, new_value);
}
});
function my_function(value) {
//Do whatever you want here
return value + ' World!';
}
})();
//Test
document.getElementById('test').innerHTML = 'Hello';
<div id="test"></div>
There's no straightforward way to do this with an arbitrary HTML string, no.
A problem is you're using an arbitrary HTML string. The only way currently to set arbitrary HTML on an element is with innerHTML. You'd have to find a different way to set arbitrary HTML on an element, for example appending the HTML to a temporary node and grabbing its contents:
// Attempt: build a temporary element, append the HTML to it,
// then grab the contents
var div = document.createElement( 'div' );
div.innerHTML = new_value;
var elements = div.childNodes;
for( var i = 0; i < elements.length; i++ ) {
this.appendChild( elements[ i ] );
}
However this suffers the same problem, div.innerHTML = new_value; will recurse forever because you're modifying the only entry point to arbitrary HTML setting.
The only solution I can think of is to implement a true, complete HTML parser that can take an arbitrary HTML string and turn it into DOM nodes with things like document.createElement('p') etc, which you could then append to your current element with appendChild. However that would be a terrible, overengineered solution.
All that aside, you shouldn't do this. This code will ruin someone's day. It violates several principles we've come to appreciate in front end development:
Don't modify default Object prototypes. Anyone else who happens to run this code, or even run code on the same page (like third party tracking libraries) will have the rug pulled out from under them. Tracing what is going wrong would be nearly impossible - no one would think to look for innerHTML hijacking.
Setters are generally for computed properties or properties with side effects. You're hijacking a value and changing it. You face a sanitization problem - what happens if someone sets a value a second time that was already hijacked?
Don't write tricky code. This code is unquestionably a "tricky" solution.
The cleanest solution is probably just using my_function wherever you need to. It's readable, short, simple, vanilla programming:
someElement.innerHTML = my_function(value);
You could alternatively define a method (I would do method over property since it clobbers the value from the user), like:
Element.prototype.setUpdatedHTML = function(html) {
this.innerHTML = my_function(html);
}
This way when a developer comes across setUpdatedHTML it will be obviously non-standard, and they can go looking for someone hijacking the Element prototype more easily.
I made a small jsperf to test selecting an element with jQuery: http://jsperf.com/testing-class-selector-vs-data-selector2
I found the results to be shocking. It says that it is 80% slower to use:
var foo = $('[data-ui=foo]');
versus
var foo = $('.ui-foo');
Shouldn't these be equally as performant? Both looking for an exact match on a string inside of a specific area of the DOM either in 'class' or 'data-ui'?
This
var foo = $('[data-ui=foo]');
scans the whole DOM tree but this
var foo = $('.ui-foo');
simply returns list of elements of that class. Usually browsers maintain collection of elements under each class. For optimization purposes.This collection gets populated once per DOM parsing time (and class attribute updates).
I have wriiten
document.createElement("p");
document.createElement("p")
How does Javascript intepreter knows do distinct between those two?
Id ? ( what is the Js property )? maybe something else ?
In this particular case:
document.createElement("p");
document.createElement("p")
the JavaScript runtime doesn't worry about the elements at all, because you didn't save the values returned. They're thrown away.
If you had written,
var p1 = document.createElementById('p');
var p2 = document.createElementById('p');
well then you've got two separate variables, and so you're keeping track of the difference.
It's important to be aware that the JavaScript interpreter itself doesn't really care too much about your DOM nodes. That's not really it's business. If you call methods and create objects, it's your problem to keep track of them.
Let's pick another real-life example: How does a human distinguish one orange from another one, in a basket? I look inside the basket, and notice that there're multiple orange-coloured, ball-shaped items
The JavaScript interpreter internally keeps track of the created objects. When an object isn't referred by anything (variables), the built-in Garbage Collector destroys the object.
It doesn't. createElement returns the object reference but if you don't assign it to anything or directly use it, it's lost.
var firstp = document.createElement("p");
var secondp = document.createElement("p");
Then you can assign an id or whatnot:
firstp.id = "first";
Keep in mind though, that this is not in the DOM yet. You have to insert it somewhere before it can be seen by the user/found with getElementById.
For that you can do something like this:
document.body.appendChild(firstp);
It doesn't. When you call document.createElement(), you create an element and it's inserted into the DOM. JavaScript can then grab certain p elements by ID, for example, or grab other p (and other) elements with getElementsByClassName().
If you'd assigned the returned values to variables, you'd be able to keep track of which one was which by referencing different variables. This is you distinguishing between them, not JavaScript, though.
You can refer to them by their index in the array of elemnts:
document.getElementsByTagName('p')[0]
You can assign id to them
document.getElementsByTagName('div')[0].id = 'first'
document.getElementsByTagName('div')[1].id = 'second'
And refer to them by id
document.getElementById('first')
It is up to you how you do it really...
For some reason if I type in:
var i = document.getElementById('fake').style.backgroundPosition;
I get undefined.
However,
document.getElementById('fake').style.backgroundPosition;
by itself, returns the right answer.
Any ideas?
When I use that line of code on an element that actually exists in Chrome, I get i equal to an empty string. You can see that here in this jsFiddle: http://jsfiddle.net/jfriend00/b6syd/.
So, if you are getting undefined, then probably your object "fake" doesn't exist when you're running the code.
element.style.backgroundPosition will only return to you an actual inline style settings or programmatically values set for that particular object directly. It won't return computed styles from stylesheets. If you want to get the current style setting for that attribute, then you need to use something like this:
var i = window.getComputedStyle(document.getElementById('fake'), null).getPropertyValue('background-position');
And, it's different in IE. This is where a framework like jQuery or YUI is very useful.
In jQuery, it would just be this:
var i = $("#fake").css('backgroundPosition');