My entire JavaScript library is constructed in an object literal namespace. It contains each page's logic from init() to common functions, controls, validation, etc. The page's init() function gets fired based on the page's body id.
The problem I have is in my controls section. I have an object literal named AddressEntry. This object literal contains the functionality that deals with my AddressEntry ASP.NET UserControl. It works just fine with one AddressEntry control on the page, but when there's more than one, only the last one on the page works as intended.
Here's my AddressEntry object literal (trimmed down to pertinent information):
SFAIC.ctrls.AddressEntry = {
inputs : {
city : undefined,
cityState : undefined,
hidden : {
city : undefined,
state : undefined
},
},
fn : {
root : undefined,
citySelected : function($ddl) {
var $selected = $ddl.find(":selected");
this.root.inputs.hidden.city.val($selected.val());
this.root.inputs.hidden.state.val($selected.text().split(", ")[1]);
}
},
init : function(addressId) {
var self = this,
fn = self.fn,
inputs = self.inputs,
hidden = inputs.hidden;
inputs.city = SFAIC.fn.getContentElement(addressId + "_ddlCity", SFAIC.$updatePanel);
inputs.cityState = SFAIC.fn.getContentElement(addressId + "_ddlCityStateLocked", SFAIC.$updatePanel);
hidden.city = SFAIC.fn.getContentElement(addressId + "_txtCity", SFAIC.$updatePanel);
hidden.state = SFAIC.fn.getContentElement(addressId + "_txtState", SFAIC.$updatePanel);
fn.root = this;
inputs.city.change(function() { fn.citySelected($(this)); });
inputs.cityState.change(function() { fn.citySelected($(this)); });
}
};
Then, the page object literal would look something like this (again, trimmed down):
SFAIC.pages.salesProcess.Applicant = {
init : function() {
SFAIC.ctrls.AddressEntry.init("AddressGarage");
SFAIC.ctrls.AddressEntry.init("AddressMailing");
}
};
It's obvious to me that AddressMailing init overrides the AddressGarage init. What can I do to objectify the SFAIC.ctrls.AddressEntry object literal to be able to handle as many AddressEntry UserControls that I may have on the page without them overriding each other?
An object literal is by definition a single instance of an object. Maybe you should be creating a object using prototypes that way you can create multiple instances of the same object.
A HACK would be to create a second object literal that is nearly the same but with a different name.
Here's the solution I came up with:
SFAIC.ctrls.AddressEntry = function(addressId) {
var ctrl = {
inputs : {
city : undefined,
cityState : undefined,
hidden : {
city : undefined,
state : undefined
},
},
fn : {
root : undefined,
citySelected : function($ddl) {
var $selected = $ddl.find(":selected");
this.root.inputs.hidden.city.val($selected.val());
this.root.inputs.hidden.state.val($selected.text().split(", ")[1]);
}
},
init : function(addressId) {
var self = this,
fn = self.fn,
inputs = self.inputs,
hidden = inputs.hidden;
inputs.city = SFAIC.fn.getContentElement(addressId + "_ddlCity", SFAIC.$updatePanel);
inputs.cityState = SFAIC.fn.getContentElement(addressId + "_ddlCityStateLocked", SFAIC.$updatePanel);
hidden.city = SFAIC.fn.getContentElement(addressId + "_txtCity", SFAIC.$updatePanel);
hidden.state = SFAIC.fn.getContentElement(addressId + "_txtState", SFAIC.$updatePanel);
fn.root = this;
inputs.city.change(function() { fn.citySelected($(this)); });
inputs.cityState.change(function() { fn.citySelected($(this)); });
}
};
ctrl.init();
return ctrl;
};
SFAIC.pages.salesProcess.Applicant = {
init : function() {
var garage, mailing;
garage = SFAIC.ctrls.AddressEntry("AddressGarage");
mailing = SFAIC.ctrls.AddressEntry("AddressMailing");
}
};
Well, I don't know if I'm late to post a possible solution, but here I go anyway.
I don't know your DOM structure or JS objects, but what I would do something like this:
Get the all DOM elements or JS objects of type AddressEntry, by some class or special attribute. You probably would have to change something elsewhere in order to achieve this.
Cycle each element and call the "init" function using "Call". That way each object or element would be bound to the initialization you are doing.
It would look something like this (not tested!!):
SFAIC.pages.salesProcess.Applicant = {
init : function() {
$('.addresses').each(function (i) {
SFAIC.ctrls.AddressEntry.init.call(this, this.attr('id'); });
}
};
The code might be bugged or lacking few more lines to make it work, but I think the idea is clear. Hope it helps and you can apply it to your case.
Related
I'm writing simple slider for my website. This slider contains list items. I want to use OOP approach.
My actual code:
var miniSlider = function(objId)
{
this.obj = $("#" + objId);
this.obj.settings = [];
this.obj.settings['items'] = $('ul li', this.obj).length;
this.pagerNext = this.obj.find("i.next");
this.pagerPrev = this.obj.find("i.prev");
this.pagerNext.on("click", function() {
alert(this.obj.settings['items']);
});
};
I can invoke a few other sliders (yes, that's why I introduced a class):
miniSlider("mini-slider");
The problem is that when I'm in jQuery this.pagerNext.on("click", function() { }); this is no longer my object but - it's become a clicked element. How can I access this.obj.settings after click in a well done way (and with multi sliders support)?
EDIT:
Here is a full code created with a cooperation with SOF community :)
var MiniSlider = function(objId)
{
this.obj = $("#" + objId);
this.obj.settings = {
items: $("ul li", this.obj).length,
autoChangeTime: 8000
};
this.obj.activeElement = null;
this.pagerNext = this.obj.find("i.next");
this.pagerPrev = this.obj.find("i.prev");
var self = this;
this.pagerNext.on("click", function() {
self.obj.activeElement = $('li.active', self.obj);
if(self.obj.settings.items > 0)
{
if(self.obj.activeElement.is(':last-child'))
{
$('li.active', self.obj).removeClass('active');
$('li', self.obj).first().addClass('active');
}
else
{
self.obj.activeElement.next().addClass('active').prev().removeClass('active');
}
}
});
this.pagerPrev.on("click", function()
{
self.obj.activeElement = $('li.active', self.obj);
if(self.obj.settings.items > 0)
{
if(self.obj.activeElement.is(':first-child'))
{
self.obj.activeElement.removeClass('active');
$('li', self.obj).last().addClass('active');
}
else
{
self.obj.activeElement.prev().addClass('active').next().removeClass('active');
}
}
});
this.obj.parent().on('mouseenter mouseleave', function(e) {
if (e.type == 'mouseenter')
{
$(this).addClass('stop');
}
else
{
$(this).removeClass('stop');
}
});
setInterval(function() {
if(self.obj.settings.items > 0 && !self.obj.parent().hasClass("stop"))
{
self.pagerNext.click();
}
}, this.obj.settings.autoChangeTime);
};
and invoke:
new MiniSlider("mini-slider");
Alex gave you the solution to the this problem in your callback, but there is another problem in your code.
You are calling the miniSlider() function without a new operator:
miniSlider("mini-slider");
That means that inside the function, this is not a unique object, but is actually the window object!
You need to use the new operator to create an individual object for each call:
new miniSlider("mini-slider");
But you should also change the name of this function to follow the JavaScript convention that constructors begin with a capital letter. Call it MiniSlider and use it like so:
new MiniSlider("mini-slider");
If you follow this convention (which most experienced JavaScript programmers do), it will help you remember when to use new. If the function begins with a capital letter, it's a constructor and you need to use new with it. Otherwise, you don't.
If you'd like to be able to use your constructor without new, that is also possible with a bit more code, e.g.:
function MiniSlider( objId ) {
if( this == window ) return new MiniSlider( objId );
// the rest of your constructor code goes here
}
But generally people don't bother with that and just use the initial capital letter on the constructor as a reminder to use new.
Also, as a suggestion, I like to use a meaningful name when I save this in a variable, and then I use that name consistently instead of using this at all. Doing it this way it might look like:
var miniSlider = function(objId) {
var slider = this;
slider.obj = $("#" + objId);
slider.obj.settings = [];
slider.obj.settings['items'] = $('ul li', slider.obj).length;
slider.pagerNext = slider.obj.find("i.next");
slider.pagerPrev = slider.obj.find("i.prev");
slider.pagerNext.on("click", function() {
alert(slider.obj.settings['items']);
});
};
Why do I prefer that approach over using this in most places and another variable like self where you need it? This way I don't have to remember which to use: I can always use slider in the code instead of this. (Of course you could use self or any other name; I just like to have a more meaningful name if I'm going to the trouble of making up a name at all.)
Another minor problem in the code is in these two statements:
slider.obj.settings = [];
slider.obj.settings['items'] = $('ul li', slider.obj).length;
You shouldn't use an Array when you are going to be giving it named properties like this. Use an Object instead. Arrays should only be used when you have numeric indexes like 0, 1, 2, etc. And with an object literal you can set the property at the same time:
slider.obj.settings = {
items: $('ul li', slider.obj).length
};
Also, when you use that property:
alert(slider.obj.settings['items']);
you can write it more simply as:
alert(slider.obj.settings.items);
Either way it does the same thing.
Save a reference to this in a local variable, and use that variable instead of this in the nested function.
var self = this;
this.pagerNext.on("click", function() {
alert(self.obj.settings['items']);
});
I'm just wondering how to remove a property from knockout viewModel. Specifically, a computed one. I have a simple viewModel
function viewModel(){
var self = this;
self.name = ko.observable("John");
self.lastname = ko.observable("Doe");
self.age = ko.observable("22");
self.fullName = ko.computed(function(){
return self.name() + self.lastname();
});
self.fullNameAndAge = ko.computed(function(){
return self.name() + self.lastname() + ': ' + self.age();
});
};
The data is going to be sent to the server, but I want to exclude the computed data from the viewModel.
I thought something like this would get all the computed data and remove it, but didn't find anything like it.
for (observableKey in viewModel) {
if (ko.isComputed(viewModel[observableKey])
{
delete viewModel[observableKey];
}
}
Knockout can return a regular object from which you then can remove anything you want.
var plainJs = ko.toJS(viewModel);
delete plainJs.fullName;
Documented here.
You can loop through keys like this:
for (var key in obj) {
if(ko.isComputed(obj[key]))
{
delete obj[key];
}
}
EDIT
Here is a working fiddle.In fiddle click over the button and check the console, there you can see 2 objects, first one is before removing computed obervables and second one is after removing computed observables.
My preferred approach for this sort of problem is to create a specific transport object for whatever your JSON call requires.
var userSaveRequest = function(data) {
var self = this;
self.name = data.name;
self.lastname = data.lastname;
// etc, etc
}
Then, from my calling code, something like this to get the JSON.
// inside viewModel
self.saveUser = function(){
var queryData = ko.mapping.toJSON(
new userSaveRequest(ko.mapping.toJS(self))
);
// use querydata in AJAX
}
Also, it's worth remembering just how damn flexible Javascript is. You can create an object of your own design on the fly if need be.
var queryData = ko.mapping.toJSON(
{
name: self.name(),
lastname: self.lastname()
}
);
Hey all I have a question. I'm writing a small Js Object to make it easier for me to manage what page I'm on in order for me to be able to load proper scripts/styles per page. I am running into a situation that I just dont understand. I have a property currentPage that will obviously enough be set to the current page but if I just set it straight from another property I previously defined, it returns a reference error, but if I put it into a function that returns the same thing, it works. I'm not sure why that is. Can someone explain this to me? I'm not a hardcore JS Developer I just figure things out as I go, so is this something specific to JS? Here's a code sample of what I mean :
var self = PageInfo = {
locationArray : window.location.pathname.toString().split("/"),
printOutPath : function(){
console.log(self.locationArray.length);
},
//ref. error to locationArray
parentDirectory : self.locationArray[self.locationArray.length -3],
currentPage : function() {
return self.locationArray[self.locationArray.length -2]; // works
}
};
When you use JavaScript object literal syntax (creating an object with the curly braces {}) the values that go with each property are expressions that get evaluated at the moment the object is created. They can't reference properties of that same object because the object doesn't exist yet.
Note that within your object's methods you can use this instead of creating the self variable. As long as you call the methods using dot syntax like this:
PageInfo.currentPage()
...within the method this will automatically reference the object so you can do this:
var PageInfo = {
locationArray : window.location.pathname.toString().split("/"),
printOutPath : function(){
console.log(this.locationArray.length);
},
currentPage : function() { return this.locationArray[this.locationArray.length -2];}
};
alert( PageInfo.currentPage() );
Further reading: https://developer.mozilla.org/en/JavaScript/Guide/Working_with_Objects
When you define an object, you can't refer to the object until it has been created. By using a function, you're delaying the lookup of self.locationArray until the object has been created.
Object is assigned to self and PageInfo only after the execution of the statement.
So do it after the statement.
var self = PageInfo = {
locationArray : window.location.pathname.toString().split("/"),
printOutPath : function(){
console.log(self.locationArray.length);
},
currentPage : function() { return self.locationArray[self.locationArray.length -2]; // works
}
};
self.parentDirectory = self.locationArray[self.locationArray.length -3];
It will update PageInfo also
Use this inside functions to make it more OO
var self = PageInfo = {
locationArray : window.location.pathname.toString().split("/"),
printOutPath : function(){
console.log(this.locationArray.length);
},
currentPage : function() { return this.locationArray[this.locationArray.length -2]; // works
}
};
self.parentDirectory = self.locationArray[self.locationArray.length -3];
You can also create a function to set parentDirectory
var self = PageInfo = {
locationArray : window.location.pathname.toString().split("/"),
printOutPath : function(){
console.log(this.locationArray.length);
},
parentDirectory:"",
setParentDirectory: function() {
this.parentDirectory = this.locationArray[this.locationArray.length -3];
},
currentPage : function() { return this.locationArray[this.locationArray.length -2]; }
};
self.setParentDirectory();
Ok I'm not sure the title of this post is the correct way to refer to what I mean and I'm pretty sure I already know the answer to this question but I just wanted some clarification.
If I have an oject like this
var myObj = {
settings : {
domObj = document.getElementById('elem1');
},
myFunc1 : function () {
return this.domObj;
},
myFunc2 : function () {
return this.domObj;
}
}
myObj.myFunc1();
myObj.myFunc2();
Is the domObj cached the first time it is accessed or is the dom traversed in both functions? I am attempting to access the Dom only once but not sure if this is a possible solution.
Assuming you really meant this:
var myObj = {
settings : function() {
domObj = document.getElementById('elem1');
},
myFunc1 : function() {
return this.domObj;
},
myFunc2 : function() {
return this.domObj;
}
};
the answer is that "domObj" is a global variable because you forgot the var keyword. Now, you may have meant this:
var myObj = {
domObj: null,
settings : function() {
this.domObj = document.getElementById('elem1');
},
myFunc1 : function() {
return this.domObj;
},
myFunc2 : function() {
return this.domObj;
}
};
in which case "domObj" is just a property of "myObj". It'd get set if you call
myObj.settings();
Assuming your doing "this.domObj =" and the other corrections you've noted; yes; the DOM element is cached in this.domObj. The only time the DOM is actually traversed is when you're calling DOM traversal methods. Assigning a DOM element to a variable/object property works exactly the same as any other assignment.
In trying to make my Javascript unobtrusive, I'm using onLoads to add functionality to <input>s and such. With Dojo, this looks something like:
var coolInput = dojo.byId('cool_input');
if(coolInput) {
dojo.addOnLoad(function() {
coolInput.onkeyup = function() { ... };
});
}
Or, approximately equivalently:
dojo.addOnLoad(function() {
dojo.forEach(dojo.query('#cool_input'), function(elt) {
elt.onkeyup = function() { ... };
});
});
Has anyone written an implementation of Ruby's andand so that I could do the following?
dojo.addOnLoad(function() {
// the input's onkeyup is set iff the input exists
dojo.byId('cool_input').andand().onkeyup = function() { ... };
});
or
dojo.byId('cool_input').andand(function(elt) {
// this function gets called with elt = the input iff it exists
dojo.addOnLoad(function() {
elt.onkeyup = function() { ... };
});
});
I don't know Dojo, but shouldn't your first example read
dojo.addOnLoad(function() {
var coolInput = dojo.byId('cool_input');
if(coolInput)
coolInput.onkeyup = function() { ... };
});
Otherwise, you might end up trying to access the element before the DOM has been built.
Back to your question: In JavaScript, I'd implement andand() as
function andand(obj, func, args) {
return obj && func.apply(obj, args || []);
}
Your example could then be written as
dojo.addOnLoad(function() {
andand(dojo.byId('cool_input'), function() {
this.onkeyup = function() { ... };
});
});
which isn't really that much shorter than using the explicit if statement - so why bother?
The exact syntax you want is not possible in JavaScript. The way JavaScript executes would need to change in a pretty fundamental fashion. For example:
var name = getUserById(id).andand().name;
// ^
// |-------------------------------
// if getUserById returns null, execution MUST stop here |
// otherwise, you'll get a "null is not an object" exception
However, JavaScript doesn't work that way. It simply doesn't.
The following line performs almost exactly what you want.
var name = (var user = getUserById(id)) ? user.name : null;
But readability won't scale to larger examples. For example:
// this is what you want to see
var initial = getUserById(id).andand().name.andand()[0];
// this is the best that JavaScript can do
var initial = (var name = (var user = getUserById(id)) ? user.name : null) ? name[0] : null;
And there is the side-effect of those unnecessary variables. I use those variables to avoid the double lookup. The variables are mucking up the context, and if that's a huge deal, you can use anonymous functions:
var name = (function() {return (var user = getUserById(id)) ? user.name : null;})();
Now, the user variable is cleaned-up properly, and everybody's happy. But wow! what a lot of typing! :)
You want dojo.behavior.
dojo.behavior.add({
'#cool_input': {
onKeyUp: function(evt) { ... }
}
});
How about something like this:
function andand(elt, f) {
if (elt)
return f(elt);
return null;
}
Call like this:
andand(dojo.byId('cool_input'), function(elt) {
// this function gets called with elt = the input iff it exists
dojo.addOnLoad(function() {
elt.onkeyup = function() { ... };
});
});
As far as I know there isn't a built-in JavaScript function that has that same functionality. I think the best solution though is to query by class instead of id and use dojo.forEach(...) as you will be guaranteed a non-null element in the forEach closure.
You could always use the JavaScript equivalent:
dojo.byId('cool_input') && dojo.byId('cool_input').whateverYouWantToDo(...);
I've never used dojo, but most javascript frameworks (when dealing with the DOM) return the calling element when a method is called from the element object (poor wording, sorry). So andand() would be implicit.
dojo.addOnLoad(function() {
dojo.byId('cool_input').onkeyup(function(evt) { /*event handler code*/
});
});
For a list:
Array.prototype.andand = function(property, fn) {
if (this.filter(property).length > 0) this.map(fn);
}