I am struggling to make the pattern below work. I'm not interested in using a library.
function _createElement(tagNm, attr){
var el = document.createElement(tagNm);
for(var at in attr){
el.setAttribute(at, attr[at]);
}
}
//below function is not correct just for giving you idea
function _append(ele){
this.appendChild(ele)
return this;
}
// I would like to be able to achive following chaining patter with this.
var div = document.getElementById('div');
div._append( _createElement('div', {
id : 'parent', className: 'parent'
})).appendChile( _createElement('div', {
id : 'child', className: 'child'
}));
For something like that to work, you're going to have to have some sort of object to be the focal point of the chained calls. The object would be the value of this, in other words, in your "_append" function. An alternative would be to have your functions extend the native DOM object prototypes, but that won't work in older IE versions (and maybe not even newer ones; I'm not sure).
You could perhaps root everything in a wrapper around document.getElementById(). You'd set up a "class" object for all your append() and appendChild and whatever other functions you'd like to gather up for this facility. Those functions would all go on the prototype for that:
function chainingWrapper(elem) {
this.targetElement = elem;
}
chainingWrapper.prototype = {
append: function(arg) {
// do the append operation
return this;
},
appendChild: function(arg) {
// do the appendChild operation
return this;
},
// ...
};
Now you'll have a utility to start things off:
function forElement(id) {
return new chainingWrapper(document.getElementById(id));
}
So then you could do:
var div = forElement("myDiv");
div.appendChild(xyz).append(abc);
Inside the functions like "append" you'll be able to refer to that <div> as this.targetElement. There are a zillion other interesting things you could do with this sort of structure, of course.
The issue that you're having is that div (a DOM element) doesn't have a method called _append. You can't reliably modify the prototype of DOM elements, so you can't add the method (and it would be a bad idea even if you could).
Instead, you need to create a wrapper object, and create the append method on that:
function Appender(id) {
if (!(this instanceof Appender)) { // if the new keyword wasn't used
return new Appender(el);
}
this.element = document.getElementById(id); // create an instance variable of the element with the id passed
this.append = function(child) { // proxy the appendChild function
this.element.appendChild(child);
return this; // return the Appender object for chaining
};
}
You could then use this as follows:
Appender('div').append(_createElement('div', {
id : 'parent', className: 'parent'
})).append( _createElement('div', {
id : 'child', className: 'child'
}));
NB If you want to follow this approach, you're going to need to learn quite a bit more about Javascript's object model.
Even if you don't want to use it, you should defently take a look at some JavaScript library, like jQuery. They do exactly what you try to achieve.
jQuery does this, by putting the DOMElement inside a Wrapper and then call functions on that wrapper. These functions manipulate the given DOMObject underneath.
Your example would look like this:
var div = document.getElementById('div');
$(div).append( _createElement('div', {id : 'parent', className: 'parent'})...
And your try to implement jQuery's "$" function. The code can be found here.
It's possible to extend the DOM. Here's a working example:
HTMLElement.prototype.append = function(ele) {
return this.appendChild(ele); // returns the new child
}
function _createElement(tagNm, attr){
var el = document.createElement(tagNm);
for (var at in attr) {
el.setAttribute(at, attr[at]);
}
return el;
}
var div = document.getElementById('myDiv');
div.append(_createElement('div', {
id : 'parent', className: 'parent'
})).appendChild( _createElement('div', {
id : 'child', className: 'child'
})).appendChild(document.createTextNode("test"));
But it's probably not a good idea:
In Javascript, can you extend the DOM?
http://perfectionkills.com/whats-wrong-with-extending-the-dom/
What others are saying is correct. You need to some how add the method _append to your object. For example, jQuery returns a jQuery object after each call. The jQuery object has functions like .each, .ajax etc etc. It's a big wrapper.
Something like this will get you the chaining you're looking for:
function _createElement(tagNm, attr){
var el = document.createElement(tagNm);
for(var at in attr){
el.setAttribute(at, attr[at]);
}
return el;
}
function customObj(Id) {
var obj = document.getElementById(Id);
obj._append = function(el) { this.appendChild(el); return this; }
return obj;
}
// I would like to be able to achive following chaining patter with this.
var div = customObj('test');
div._append( _createElement('div', {
id : 'parent', className: 'parent'
}))._append( _createElement('div', {
id : 'child', className: 'child'
}));
Related
I am new to javascript and I was messing around with it. I was checking out jquery and I wanted to see if I could create something to do the same things. This is my js file
//returns a dom
var $ = function(selector) {
return document.querySelector(selector);
};
//adds something to the dom
var append = function(value) {
this.innerHTML += value;
};
//clears dom
var empty = function() {
while (this.lastChild) this.removeChild(this.lastChild);
};
When I call $('#test') I get the dom with id test. When I call $('#test').append('hi there') it works. However when I try $('#test').empty() I get a Uncaught TypeError: $(...).empty is not a function Any idea why? If I am doing something comletely wrong please let me know. Thanks
Your functions aren't added to the prototype chain of DOM elements, (that wouldn't be a good idea), so they can't be called as methods of DOM elements.
append works, because the DOM node already had a method called append, and it has nothing to do with the function you stored in a variable called append.
jQuery works by creating a container object that holds a collection of DOM elements and has it's own methods. EG:
var $ = function(selector) {
var objects = document.querySelectorAll(selector);
return {
append: function ( ) {
// do something to `objects` here
},
empty: function ( ) {
},
};
};
I am new to jQuery and just learning new stuff. I was just reading through Chris Coyer's article and came across the following code :
$.fn.faq = function(options) {
return this.each(function(i, el) {
var base = el,
$base = $(el);
console.log(options);
base.init = function() {
// Do initialization stuff
$base
.find("dd")
.hide()
.end()
.find("dt")
.click(function() {
var ans = $(this).next();
if (ans.is(":visible")) {
base.closeQ(ans);
} else {
base.openQ(ans);
}
})
};
base.openQ = function(ans) {
// Open panel
ans.show();
// Do callback
options.qOpen.call();
};
base.closeQ = function(ans) {
// Open panel
ans.hide();
// Do callback
options.qClose.call();
};
base.init();
});
};
$("dl").faq({
qOpen: myQuestionOpenCallback,
qClose: myQuestionCloseCallback
});
function myQuestionOpenCallback() {
alert("answer opened!");
}
function myQuestionCloseCallback() {
alert("answer closed!");
}
Now I didn't quite understand this part of the code:
return this.each(function(i, el) {
The second line in the code, what exactly is i and el? I don't see anywhere these parameters being passed into the each function.
I asked a senior colleague of mine and got the following answer:
Many plugins start that way. Since most plugins are chainable, they
have to return this. And they also have to loop through the elements
from the selector,
return this.each(function(i, el) {
does them both. A loop, then the return.
but I still didn't quite understand.
The JS Fiddle can be found here.
Inside a jQuery plugin, this refers to the jQuery object representing what you called the plugin on. For example, in the case of this faq plugin, if I call $('#someDiv').faq({ ... });, this will be the same as $('#someDiv') inside the plugin function.
So because it is a jQuery object representing a selection of DOM nodes, you can call the jQuery method .each() on it, which takes a function that gets given two parameters when it is called for each DOM node in the selection:
The index (0, 1, 2 and so on)
The DOM node itself
.each() also returns the thing it was called on, so you end up returning the $('#someDiv') object from the plugin. That's great, because then we can call some other jQuery method on it straight afterwards ("chaining"). e.g. $('#someDiv').faq({ ... }).hide(); to hide it immediately.
https://api.jquery.com/jquery.each/
i : index of the element.
el : the DOM element (not a jQuery object).
I am trying to extend a jquery element with custom properties and functions so I can track my elements and get their custom properties anytime.
at the moment I have done this:
jQuery.fn.designerElement = function (tpl, cls) {
this.__template = tpl
this.des__class = cls;
this.compile = function () {
this.html(this.__template(this));
return this;
}
return this;
};
var template = Handlebars.compile(
'<div id="' + +new Date() + '" class="{{des__class}}"></div>'
);
var el = $('<div></div>').designerElement(template, "form-container");
el.attr('id', "test");
el.compile();
$('body').append(el);
Now if I call $('#test').compile() it say says the method is undefined.
Here is a fiddle: http://jsfiddle.net/jmorvan/HLVj4/
To explain my context, I need the methods and properties available directly on the object for some dataBindings to work, thats why i can't use .data(). It seemed to me jquery plugin would be the best approach but I am definitly missing something here.
So just to be clear I would need to be able to access properties like this: $('#test').__template as well as functions.
thanks for your time!
I think you already know what is going on but here's a short explanation: With $('#test') you are creating a new jQuery object. It contains the same element but you are defining properties for the jQuery object, not the element. So what you are asking is if there is a way to add the functionality to the element but with the jQuery object attached. In jQuery data() can after all be used to do this:
jQuery.fn.designerElement = function (tpl, cls) {
this.__template = tpl
this.des__class = cls;
this.compile = function () {
this.html(this.__template(this));
return this;
}
this.data('this', this);
return this;
};
And retrieve that object with:
var test = $('#test').data('this');
Here is the fiddle
Another solution is to store all these objects in a globally available array or JSON object.
Let's say I have a setup like this.
var Account = function(data) {
this.data = data;
this.domElement = (function(){ code that generates DOM element that will represent this account })();
this.domElement.objectReference = this;
}
Account.prototype = {
show: function() { this.domElement.classList.remove('hidden'); },
hide: function() { this.domElement.classList.add('hidden') }
}
My question is about the last line: this.domElement.objectReference = this;
It would be a useful thing to have because then I can add event listeners to my DOM element and still get access to the object itself. I can add methods that would affect my DOM element, such as hide() and show(), for instance, without having to resort to modifying visibility of the DOM element using CSS directly.
I tested this code and it works like I want it to, but I'm curious whether this would cause memory leaks or some other unpleasantness or if it's an acceptable thing to do?
Thank you!
Luka
I know this has been answered by #PaulS. already, but I find the answer counter intuitive (returning a DOM element from the Account constructor is not expected) and too DOM-centric, but at the same time the implementation is very simple, so I am not sure what to think ;)
Anyway, I just wanted to show a different way of doing it. You can store Account instances in a map and give them a unique id (perhaps they have one already), then you store that id as a data attribute on the DOM element. Finally you implement a getById function or something similar to retrieve the account instance by id from the listeners.
That's pretty much how jQuery's data works.
Here's an example with delegated events like you wanted from the comments.
DEMO
var Account = (function (accounts, id) {
function Account(data) {
accounts[this._id = ++id] = this;
this.el = createEl.call(this);
}
Account.prototype = {
constructor: Account,
show: function() { this.el.classList.remove('hidden'); },
hide: function() { this.el.classList.add('hidden'); }
};
function createEl() {
var el = this.el = document.createElement('div');
el.className = 'account';
el.innerHTML = el.dataset.accountId = this._id;
return el;
}
Account.getById = function (id) {
return accounts[id];
};
Account.init = function () {
//add delegate listeners
document.addEventListener('click', function (e) {
var target = e.target,
account = Account.getById(target.dataset.accountId);
if (!account) return;
account.hide();
});
};
return Account;
})({}, 0);
//once DOM loaded
Account.init(); //start listening to events
var body = document.body;
body.appendChild(new Account().el);
body.appendChild(new Account().el);
Why not have domElement as a variable, and return it from your function? To keep the reference to your constructed Object (but only where this is as expected), you could do a if (this instanceof Account) domElement.objectReference = this;
You've now saved yourself from circular references and can access both the Node and the Object. Doing it this way around is more helpful if you're expecting to lose the direct reference to your Account instance, but expect to need it when "looking up" the Node it relates to at some later time.
Code as requested
var Account = function (data) {
var domElement; // var it
this.data = data;
domElement = (function(){/* ... */}()); // use var
if (this instanceof Account)
domElement.objectReference = this; // assign `this`
return domElement;
};
// prototype as before
Returned element is now the Node, not the Object; so you'd access the Account instance like this
var domElement = new Account();
domElement.objectReference.show(); // for example
In my opinion there is nothing good about referencing the object inside of the object itself. The main reason for this is complexity and obscurity.
If you would point out how exactly are you using this domElement.objectReference later in the code, I am sure that I or someone else would be able to provide a solution without this reference.
I want to create a jquery slider using oop. This is the structure of my code :
var photoGallery = new Slider();
function Slider(){ this.init(); }
Slider.prototype = {
el : {
nav : $('.slideshowNav', this.main),
navItems : $('.items', this.nav),
main : $('.slideshow')
}
,init: function(){
// some code here
}//init
// rest of functions here
}
My problem is that i have two <div class="slideshow"> in same page and my code is wrong according to this situation.
I know that i can pass a parameter to init() and all my problems will fly away, but i use the this.el elements also in other functions so this is not a solution.
I want to know how i can add another element to this.el coming from Slider() parameter (I know, there is no parameter now)
Or, does somebody has an idea how i can make the structure which looks like
$('.slideshow').each(...)
?
I need my code working with multiple elements on a single page
Here is how many jquery plugins start:
$.fn.myplugin = function(opts) {
// Let each closure has its own options
var myplugin = new Myplugin(opts);
return this.each(myplugin);
};
function Myplugin(opts) {}
This way, people can call your code like this:
$('.slideshow').myplugin();
And each element will be handled separately. this.el will be able to be called easily in each of your instance with no issue.
You can try something like this (untested):
var galleries = [];
$('.slideshow').each(function(index, elem) {
galleries.push(new Slider(elem));
});
And then change your Slider implementation to something like this:
function Slider(elem) {
this.elem = elem;
this.init();
}
Slider.prototype = {
el : {
nav : $('.slideshowNav', this.main),
navItems : $('.items', this.nav),
main : $('.slideshow')
}
,init: function() {
// some code here
}//init
// rest of functions here
}
The new thing with this approach is, that you have a this.elem inside your Slider instance which you can refer to. You will have to adapt some of your code to refer to this.elem inside your Slider prototype to work, but this should get you started.