I am relatively new to OOP so I am not sure about the terminology.
I have created a DOM element as a field of an object (eg myObject.myElement) and I appended the element to the document. The object has a .mousemove() event attached (using jQuery). I want to be able to select the object (myObject) for whom the selected element (myElement) is a field so that I can access other fields of the object. There is more than one object and the event handler is the same for elements of different objects. Is it possible to select the parent object of the element? Do I need to give the element the same fields so that I can access the same data?
I want to be able to do something like this but maybe it is not that straightforward:
$('.bubble').on({mousemove: function () {
parentObject = this.parentObject();
alert(parentObject.otherDataField);
});
The element was created like this:
function bubbleObject(value)
{
this.value = value;
this.element = document.createElement('div');
$(this.element).appendTo('.bubbles');
}
myFirstBubble = new bubbleObject(10);
mySecondBubble = new bubbleObject(100);
and I need to be able to access the value field for the object (I am making it simpler as the code is pretty long and mostly irrelevant to my issue).
Do I need to do this:
function bubbleObject(value)
{
this.value = value;
this.element = document.createElement('div');
$(this.element).appendTo('.bubbles');
this.element.value = value; // add same value to element
}
or is there a better way?
A back reference to the parent could be better than copying values:
function bubbleObject(value)
{
this.value = value;
this.element = document.createElement('div');
$(this.element).appendTo('.bubbles');
this.element.parent = this;
}
However, using this way you have to use element.parent.value instead.
Another approach (as I mentioned before) is to scan all element-containing objects if they contain a specific element. If you don't have all these objects in an array you have to scan all window components (as I said, this is expensive):
function findElement(element) {
for (obj in window) {
if (typeof obj === 'object' && obj.myElement == element) {
return obj;
}
}
return null;
}
This is just an untested draft. And it would be so much better if you have a list of parent objects which you can use instead of window.
function findElement(element) {
for (int i = 0, l = objList.length; i < l; i++) {
if (typeof objList[i] === 'object' && objList[i].myElement == element) {
return objList[i];
}
}
return null;
}
Related
I'm trying to replicate jQuery's element manipulation to a certain extent. Now what I have found to be very useful is the .first() selector. I would like to be able to chain functions like this;
getElement(selector).first().hasClass(className);
Now, there are 2 issues with how far I've gotten (Do note that my code example is minimised, so please, no comments about error-handling.)
var getElement = function(selector, parent) {
ret = document.querySelectorAll(selector);
this.element = ret;
this.hasClass = function(className) {
className.replace('.', '');
if(this.multiple())
{
console.log('Cannot use hasClass function on multiple elements');
return false;
}
};
this.first = function() {
this.element = this.element[0];
return this;
};
return this;
};
My current problem
If I call my function;
var $test = getElement('.something');
//result: nodelist with .something divs
If I call for the first element within the result;
$test.first();
//Result: First div, perfect!
However, now if I call $test again, it will replace the elements property with the result of first(), meaning I have "lost" my old values. I don't want to lose them, I only want the first() functions for that specific functionality. Then I want $test to return all elements again.
Also, recalling first() will now end up undefined, since there is only 1 element left within this as it has deleted the old elements from within the object.
Another attempt
Now, I've also tried to turn it around a bit by returning the first-child instead of the entire class object;
this.first = function() {
return this.element[0];
};
However, I will
$test.first().hasClass(className);
//Returns ERROR, method hasClass undefined
this is because .hasClass exists on the original this, which doesn't get returned anymore since I'm now returning the element.
I have tried to get something out of jQuery's library, though that just confused me more...
I have googled this subject, but with all the 'chaining methods' solutions I'm finding, all of them seem to be overwriting the original values of the object, which is not what I want to happen. One other solution actually required me to re-initiate the object over and over again, which did not seem very efficient to me... Any help is appreciated. I'm assuming I'm going about this completely the wrong way.
-- If you can help me, please do explain why your solution works. I really feel like if I understand this, my understanding of javascript can expand a lot further. I just need to get past this structural(?) issue.
A method like first() should not modify this, it should create a new object and return that. You only use return this; in methods that modify an element rather than returning information derived from the element.
this.first = function() {
return new getElement(this.element[0]);
};
And note that you have to use new getElement to create an object, not just getElement.
This also requires a change to the constructor, so it can accept either a selector string or an element:
var getElement = function(selector, parent) {
var ret = typeof selector == "string" ? document.querySelectorAll(selector) : [selector];
...
}
You should also consider doing this in proper OO fashion, by putting the methods in a prototype, rather than defining them in every object.
var getElement = function(selector, parent) {
var ret = typeof selector == "string" ? document.querySelectorAll(selector) : [selector];
this.element = ret;
};
getElement.prototype.hasClass = function(className) {
className.replace('.', '');
if (this.multiple()) {
console.log('Cannot use hasClass function on multiple elements');
return false;
}
};
getElement.prototype.first = function() {
return new getElement(this.element[0])
};
this in your outer function refers to the window / global object.
Instead, return the ret variable itself.
In the inner functions (which become the object's methods), this acts the way you expect it to.
Here's an alternative solution, which allows chaining, even after you've called the first method:
var getElement = function(selector, parent) {
var ret = typeof selector == 'string' ? document.querySelectorAll(selector)
: selector;
ret.hasClass = function(className) {
if(!this.classList) {
console.log('Cannot use hasClass function on multiple elements');
return false;
} else {
return this.classList.contains(className);
}
};
ret.first = function() {
return new getElement(this[0]);
};
return ret;
};
console.log(getElement('p').length); //2
console.log(getElement('p').first().innerHTML); //abc
console.log(getElement('p').first().hasClass('test')); //true
console.log(getElement('p').first().hasClass('nope')); //fase
console.log(getElement('p').hasClass('test')); //false (multiple elements)
<p class="test">
abc
</p>
<p>
def
</p>
Here is how I would approach this:
Create a constructor, say Search, tasked to find the elements based on the input. Using a constructor is proper OO Programming and you also have the advantage of defining methods once in the prototype and they can be accessed by all instances.
Ensure that the context (this) is an array-like object, with numeric properties and a length, so that you can easily iterate over every matched element in the traditional way (using for loops, [].forEach etc).
Create a function, say getElement, that will use the constructor and return the result without having to use the new keyword all the time. Since the function returns an instance of our constructor, you can chain the methods you want as you would normally do.
The method first uses the constructor to create a new instance instead of modifying the original, since its role is to return the first element, not delete everything but the first element.
Each time you come up with a new method you want your object to have, you can simply add it to the prototype of the constructor.
Snippet:
;(function () {
function Search (value) {
var elements = [];
/* Check whether the value is a string or an HTML element. */
if (typeof value == "string") {
/* Save the selector to the context and use it to get the elements. */
this.selector = value;
elements = document.querySelectorAll(value);
}
else if (value instanceof Element) elements.push(value);
/* Give a length to the context. */
this.length = elements.length;
/* Iterate over every element and inject it to the context. */
for (var i = 0, l = this.length; i < l; i++) this[i] = elements[i];
}
/* The method that returns the first element in a Search instance. */
Object.defineProperty(Search.prototype, "first", {
value: function () {
return new Search(this[0]);
}
});
/* The global function that uses the Search constructor to fetch the elements. */
window.getElement = (value) => new Search(value);
/* Create a reference to the prototype of the constructor for easy access. */
window.getElement.fn = Search.prototype;
})();
/* Get all elements matching the class, the first one, and the first's plain form. */
console.log(getElement(".cls1"));
console.log(getElement(".cls1").first());
console.log(getElement(".cls1").first()[0]);
/* ----- CSS ----- */
.as-console-wrapper {
max-height: 100%!important;
}
<!----- HTML ----->
<div id = "a1" class = "cls1"></div>
<div id = "a2" class = "cls1"></div>
<div id = "a3" class = "cls1"></div>
Example:
In this example, I'm adding a new method called hasClass to the prototype of the constructor.
/* The method that returns whether the first element has a given class. */
Object.defineProperty(getElement.fn, "hasClass", {
value: function (value) {
return this[0].classList.contains(value);
}
});
/* Check whether the first element has the 'cls2' class. */
console.log(getElement(".cls1").first().hasClass("cls2"));
<!----- HTML ----->
<script src="//pastebin.com/raw/e0TM5aYC"></script>
<div id = "a1" class = "cls1 cls2"></div>
<div id = "a2" class = "cls1"></div>
<div id = "a3" class = "cls1"></div>
I think the easiest would be to return a new class that contains the nodes you have selected. That would be the easiest solution, as you don't really want to mutate any of your previous selectors.
I made a small example, using some ES6 that makes a few things easier to work with, which also has a $ to initiate the selections being made.
You would notice that first of all, any selection that is made, is just calling the native document.querySelectorAll but returns a new Node class. Both first and last methods also return those elements.
Lastly, hasClass should work on all elements in the current nodes selections, so it will iterate the current node, and check all classes in there, this one returns a simple bool, so you cannot continue with the method chaining there.
Any method you wish to chain, should either:
return this object (the current node)
return an element of the this object as a new node so any further manipulations can be done there
const $ = (function(global) {
class Node extends Array {
constructor( ...nodes ) {
super();
nodes.forEach( (node, key) => {
this[key] = node;
});
this.length = nodes.length;
}
first() {
return new Node( this[0] );
}
last() {
return new Node( this[this.length-1] );
}
hasClass( ...classes ) {
const set = classes.reduce( (current, cls) => {
current[cls] = true;
return current;
}, {} );
for (let el of this) {
for (let cls of el.classList) {
if (set[cls]) {
return true;
}
}
}
return false;
}
}
global.$ = function( selector ) {
return new Node( ...document.querySelectorAll( selector ) );
};
return global.$;
}(window));
let selector = $('.foo');
let first = selector.first(); // expect 1
console.log(first[0].innerHTML);
let last = selector.last();
console.log(last[0].innerHTML); // expect 4
console.log( first.hasClass('foo') ); // expect true
console.log( first.hasClass('bar') ); // expect false
console.log( selector.hasClass('foo') ); // expect true
console.log( selector.hasClass('bar') ); // expect true
<div class="foo">1</div>
<div class="foo">2</div>
<div class="foo bar">3</div>
<div class="foo">4</div>
You can update getElement so it returns back again when you send it an element.
var getElement = function(selector, parent) {
var ret = null
if (typeof selector === "string") {
ret = document.querySelectorAll(selector);
} else {
ret = selector
}
this.element = ret;
this.hasClass = function(className) {
className.replace('.', '');
if (this.multiple()) {
console.log('Cannot use hasClass function on multiple elements');
return false;
}
};
this.first = function() {
this.element = getElement(this.element[0]);
return this;
};
return this;
};
var test = getElement(".foo");
console.log(test.first())
console.log(test.first().hasClass)
<div class="foo">1</div>
<div class="foo">2</div>
<div class="foo">3</div>
<div class="foo">4</div>
You can use .querySelectorAll(), spread element and Array.prototype.find(), which returns the first match within an array or undefined
const getElement = (selector = "", {prop = "", value = "", first = false} = {}) => {
const el = [...document.querySelectorAll(selector)];
if (first) return el.find(({[prop]:match}) => match && match === value)
else return el;
};
let first = getElement("span", {prop: "className", value: "abc", first: true});
console.log(first);
let last = getElement("span");
console.log(all);
<span class="abc">123</span>
<span class="abc">456</span>
In my code like ,
function ElementBase(name) {
this.tagName = typeof name != "" ? name : 'div';
this.createElem();
}
ElementBase.prototype = {
createElem: function() {
this.elem = document.createElement(this.tagName);
},
getIndex: function() {
var nodes = this.elem.parentNode.childNodes,
node;
var i = count = 0;
while ((node = nodes.item(i++)) && node != this.elem)
if (node.nodeType == 1) count++;
return (count);
}
};
I try to create the DOM element tag is "div".
function Div() {
this.tagName = 'div'
ElementBase.call(this, this.tagName);
}
Div.prototype = Object.create(ElementBase.prototype);
My Question is,
1) How to access the getIndex function from the html document after inserting the created objects?
example:
var div = new Div();
div.id = "d1"
document.body.appendChild(div.elem);
// After div.getIndex() working
Then some situation i need the index value of that div (id="d1") element from document.
var d= document.getElementById("d1");
d.getIndex() //not working
What mistakes i did it in above code?
thanks advance..
I think when you do document.body.appendChild(div.elem) you just do document.body.appendChild(document.createElement('div')) nothing more.
And when you do var d= document.getElementById("d1"); d is just an object return from the DOM that has nothing to do with your var div
what you can do is:
Div.prototype.getIndex.call(d);
But that doesn't actually extend your object. Actually extending a DOM object is a bad practice (check this http://perfectionkills.com/whats-wrong-with-extending-the-dom/).
Look closely at your code.
div is an instance of Div and it has a property .elem that holds the actual DOM element.
So when you do div.id = "d1", you are not setting the id of the DOM element.
var div = new Div();
div.id = 'd1'; // <div></div>
div.elem.id = 'd1'; // <div id="d1"></div>
But there's one more problem: when you do d= document.getElementById("d1"), what you get is a DOM element, not an instance of Div().
Since .getIndex() is defined on .prototype of Div(), plain old DOM elements don't have access to it.
How you solve this situation depends on what exactly you need to accomplish with your code.
Edit 1: In response to OP's comment:
document.getElementById() returns an instance of HTMLDivElement, which is fundamentally different from an instance of Div.
One solution is to use a setter method:
function Div() {
// ...
}
Div.prototype.setId = function setId(id) {
this.elem.id = id;
}
var div = new Div();
div.setId('d1'); // same as doing div.elem.id = 'd1';
another solution is to use id in the constructor function itself:
function Div(id) {
// ...
this.elem.id = id; // or you can use "this.setId(id)"
/*
if "id" is provided,
it will take that value,
else it is set to "undefined",
which is the same as not being set
*/
}
Div.prototype.setId = function setId(id) {
this.elem.id = id;
}
var div = new Div('d1'); // same as doing div.elem.id = 'd1';
div.setId('d2'); // same as doing div.elem.id = 'd2';
For example, there is an object V which takes an element id, class or a tag name as a parameter and applies functions on it, like so.
var V = function(elem) {
// logic to getElementBy ID or class or tag name and return it
if(type === '#') {
// get by id
domElem = document.getElementById(elem.slice(1));
} else if(type === '.') {
// get by class
domElem = document.getElementsByClassName(elem.slice(1));
} else if(typeof type === 'string' && (type !== '#' || type !== '.')) {
// get by elem type
domElem = document.getElementsByTagName(elem);
}
return domElem;
}
So now when you get an element, like so: var myDiv = V('#someDivElement');
it will return you the div element. Now, I also run a method on the returned object. For example, I want to run a method asdf that simply appends a paragraph tag inside the div, like so:
V.prototype.asdf = function() {
this.appendChild('<p>I am an awesome child!</p>');
return this;
}
Ideally, it works when I do:
var myDiv = V('#someDivElement');
But the below line doesn't seem to execute.
myDiv.asdf();
What am I doing wrong here?
Your V('#someDivElement'); returns domElem, but asdf is defined on prototype of V.
What you might want is the following constructor function:
var V = function(elem) {
// logic to getElementBy ID or class or tag name and return it
if(type === '#') {
// get by id
this.domElem = document.getElementById(elem.slice(1));
} else if(type === '.') {
// get by class
this.domElem = document.getElementsByClassName(elem.slice(1));
} else if(typeof type === 'string' && (type !== '#' || type !== '.')) {
// get by elem type
this.domElem = document.getElementsByTagName(elem);
}
}
var myDiv = (new V('#someDivElement')).domElem;
you are defining V as a function that returns a DOM element, then trying to add a method onto V's prototype, then just calling it and getting a value.
But you can only use the prototype when you use V as a class - with the 'new' keyword. So try this:
var V = function(elem) {
this.elem = document.getElementsByTagName(elem);
... plus whatever else here ...
}
V.prototype.asdf = function() {
this.elem.innerHTML = "Hi Dave";
};
var myElem = new V('.someselectorOrElem');
myElem.asdf();
see how V is no longer used as a function but instead as a class constructor?
But.. it's not exactly the answer you're after, you want it to work just like jquery.
So taking the code above, you could, after that, also define a function called, say, $V, which constructs one and returns it, like this:
function $V(selectorOrElem) {
return new V(selectorOrElem);
}
and call it like this:
var myElem = $V(".someSelector");
then you have jquery-like behaviour.
What you are returning is simply a plain object from JS now go back and log the real jQuery, It's returning the object of the self that helps in chaining. so you need to wrap the object in your context. that's all.
I'm trying to create a function using vanilla JS that will:
Create a new DOM element
Assign it a Class Name
Place it in the DOM either appending to an existing div or inserting it specifically into the DOM if required using "insertBefore()"
I have come up with the somewhat inelegant solution below:
function createDomElem(elem, className, parent, refElement, type) {
var a = document.createElement(elem);
if (type == "append") {
document.querySelector(parent).appendChild(a);
} else if (type == "insert") {
document.querySelector(parent).parentNode.insertBefore(a, refElement)
}
a.className = className;
};
My problems with this solution are
Too many arguments to be passed
If not passing "insert" then you don't require refElement and to avoid "type" being mistaken for "refElement" you'd have to pass "refElement" as "null" and then define type as "append"
So my question is where can I streamline this function to become more useful within my program?
I'm also dreaming of the ability to be able to push child divs into the newly created div right within this function, defining how many child divs you would want and then using a for loop to append or insert these. Would this be better placed in a new function though?
I would split the code into two parts, as they have to separate concerns. I use something similar to the following for creating DOM elements:
var DomFactory = (function (document) {
var api = {
element: function (name, attributes) {
var el = document.createElement(name);
if (attributes) {
for (var key in attributes) {
if (attributes.hasOwnProperty(key)) {
el.setAttribute(key, attributes[key]);
}
}
}
return el;
},
div: function (attributes) {
return api.element('div', attributes);
}
};
return api;
}(window.document));
Usage:
var div = DomFactory.div({ 'class': 'hero' });
var table = DomFactory.element('table', { 'class': 'table table-bordered' });
Then for positioning, you could have a generalised position function:
function attach(source, target, position) {
switch (position) {
case 'before': {
target.parentNode.insertBefore(source, target);
break;
}
case 'after': {
if (target.nextSibling) {
target.parentNode.insertBefore(source, target.nextSibling);
} else {
target.parentNode.appendChild(source);
}
}
}
}
Usage:
attach(table, div, 'before');
I was wondering if it was possible to assign values to an element object. In this case, I wish to assign the returns from the setTimeout() function to an object within an element object.
For example:
var elem = document.getElementById('target');
elem.timeouts = new Object();
elem.timeouts.sometimeout = setTimeout('...', 1000);
So I could then do:
clearTimeout(elem.timeouts.sometimeout);
I know this might seem bad practice etc, but is it possible or will it cause browsers to catch fire and turn on their user etc.
Thanks.
DOM elements are Host objects (aka non-native) and as such they can do almost anything they want. It's not guaranteed that your expandos will work. In particular IE used to have problems with them. It's highly recommended to read this article:
What’s wrong with extending the DOM by #kangax
(it is from one of the Prototype.js developers who experienced the drawbacks of such bad habits. They've rewritten the whole library just to save themselfs from more headaches)
Now if you add uniqueID to elements in non-IE browsers (IE has it by default) and then your data function becomes a simple lookup ~ O(1). The real information will be stored in a closure.
It's 2-4x faster than jQuery.data (test)
data(elem, "key", "value");
1.) Hash table
var data = (function () {
var storage = {};
var counter = 1;
return function (el, key, value) {
var uid = el.uniqueID || (el.uniqueID = counter++);
if (typeof value != "undefined") {
storage[uid] || (storage[uid] = {});
storage[uid][key] = value; // set
} else if (storage[uid]) {
return storage[uid][key]; // get
}
};
})();
2.) Array
If you want to avoid expandos all together you can use an array to hold elements (but it's slower)
var data = (function () {
var elements = [];
var storage = [];
return function (el, key, value) {
var i = elements.indexOf(el);
if (typeof value != "undefined") {
if (i == -1) {
i = elements.length;
elements[i] = el;
storage[i] = {};
}
storage[i][key] = value; // set
} else if (storage[i]) {
return storage[i][key]; // get
}
};
})();
Array.prototype.indexOf (fallback)
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (item) {
var len = this.length >>> 0;
for (var i = 0; i < len; i++) {
if (this[i] === item) {
return i;
}
}
return -1;
};
}
You're welcome! :)
It's possible, DOM elements retrieved by JS, are JS variables :) ..BTW it's not a common practice do that stuff in that way. I think the answer of #galambalazs is more deep and complete ;)
if you are using jquery, you can story it in the "data"
http://api.jquery.com/jQuery.data/
No, actually that should work just fine. From what I understand, adding that return to the object should be fine. It's better than storing it in a global container IMO.