After each click, I intend to empty object editProductList. My code below is instead creating an additional new object editProductList instead of emptying the original editProductList. How do I ensure I'm emptying editProductList instead of creating a new editProductList after clicking it once more?
After the first clicking on the 'devices' column, then #edit_product_add_btn,
I'm logging:
product name, qty: Remote Tag 6
After the second clicking on the 'devices' column, then #edit_product_add_btn, the previous object remains, and it updates both the original object and new one at the same time
product name, qty: Remote Tag 7
product name, qty: Remote Tag 6
Why is it creating an additional object of the same editProductList instead of emptying the original one?
EditableGrid.prototype.mouseClicked = function(e) {
var editProductList = {};
$.ajax({
//...
success: function(response) {
editProductList = JSON.parse(response);
console.log('editProductList direct from DB: ', editProductList);
//adding products into editProductList
$('#edit_product_add_btn').on('click', function(e) {
var in_editProductList = false;
for (var product in editProductList) {
if (editProductList.hasOwnProperty(product)) {
if (editProductList[product].name === productType) {
//...
console.log('product name, qty: ', editProductList[product].name, editProductList[product].qty);
in_editProductList = true;
break;
}
}
}
if (in_editProductList === false) {
//...
var new_product_obj = { name: productType, qty: qty };
editProductList[Object.size(editProductList)] = new_product_obj;
}
});
});
}
After deconstructing your code example it became clear that you want to maintain a shopping cart.
If the user adds a product that also is already in the cart, it should simply increase the quantity of the existing item.
If the user adds a new product, it should append it to the cart.
In both cases the screen should be updated accordingly.
So there are two tasks here. Maintain a list and update the screen.
For updating the screen it is helpful to use an HTML templating library. This helps code readability and reduces the risk surface (no more manual HTML building from strings = less XSS risk). I used Mustache.js in the following example.
It is also helpful to separate the tasks so function size stays manageable.
Note how I use a custom jQuery event (dataUpdated) to decouple screen updating from list maintenance:
$(function () {
var editProductList = [];
var productListItems = Mustache.parse('{{#.}}<li class="list-group-item"><span class="badge">{{qty}}</span>{{name}}<button type="button" class="close" aria-hidden="true">×</button></li>{{/.}}');
EditableGrid.prototype.mouseClicked = function (e) {
if (this.getColumnName(columnIndex) == 'devices') {
$.post('get_requested_devices.php', {
table: this.name,
request_id: this.getRowId(rowIndex)
})
.done(function (response) {
editProductList = response;
$('#edit_product_list').trigger("dataUpdated");
});
}
};
$('#edit_product_list').on("dataUpdated", function () {
var listItems = Mustache.render(productListItems, editProductList);
$('#edit_product_list').empty().append(listItems);
});
$('#edit_product_add_btn').on('click', function (e) {
var qty = parseInt($('#edit_product_qty').val().trim(), 10);
var name = $('#edit_product_type').text().trim();
var existingProduct;
if (qty > 0) {
existingProduct = $.grep(editProductList, function (product) {
return product.name === name;
});
if (existingProduct) {
existingProduct.qty += qty;
} else {
editProductList.push({
name: name,
qty: qty
});
}
$('#edit_product_list').trigger("dataUpdated");
} else {
alert('Enter a number greater than 0');
}
});
});
Warning The above code contains references to two undefined global variables (columnIndex and rowIndex). I have no idea where they come from, I just carried them over from your code. It is a bad idea to maintain global variables for a number of reasons, the biggest one being that many nasty bugs can be traced back to global variables. Try to replace those references, either by function results or by local variables.
Recommendation This situation is the perfect use case of MVVM libraries like Knockout.js. They are designed to completely take over all UI updating for you, so all you need to do is to maintain the data model of the shopping cart. You might want to consider switching.
It's not so clear what you are trying to do.
You seem to be defining a click handler as a method of EditableGrid. So the user will need to click something for the ajax call to be executed, and then click #edit_product_add_btn to load the results into a variable which is local to the first handler? Presumably you are doing something with editProductList after the ajax call comes back, if not loading it at all is pointless, since you can't access it from anywhere else. Perhaps you want this.editProductList instead, so it is accesible from the rest of the "class"?
Are you sure you don't mean to do something like this?
EditableGrid.prototype.mouseClicked = function(e) {
var self = this;
$.ajax({
//...
success: function(response) {
self.editProductList = JSON.parse(response);
}
});
};
About mutating the current object instead of creating a new one: I don't think you gain much from trying to do this. The old object will be garbage collected as soon as the variable points to the new one. You are creating an object as soon as you do JSON.parse, so there's no escaping that.
If I missundersood your intentions, please clarify your question.
Related
I have a firebaseObject (MyFirebaseService.getCurrentUser()) bind to $scope.user.
After binding successful, I loop tho the object to see if the object contain "associatedCourseId" equal to some value ($stateParams.id). If does, the $scope.finishLessonCount count up. The problem is, when I add new Object inside the firebaseObject (that bindto user) via other page OR inside firebase, the finishLessonCount value won't change as what I expect for 3 way binding. I need to refresh the page to see the finishLessonCount reflect the true value. What is wrong? I want the finishLessonCount change using the compare function as I add more finishedLessons into the firebaseObject. Please see code below:
MyFirebaseService.getCurrentUser().$bindTo($scope, "user").then(function(){
for (var key in $scope.user.finishedLessons) {
if ($scope.user.finishedLessons.hasOwnProperty(key)) {
if ($scope.user.finishedLessons[key].associatedCourseId == $stateParams.id) {
$scope.finishLessonCount++;
}
}
};
console.log ($scope.finishLessonCount);
});
UPDATE 1 according to #Kato solution:
I decide to use Extending firebaseOject way to solute this problem. But still, it does not. I did not use factory here to simplify thing since I need to pass in courseId to do the operation. Here is my code:
function countLessons(lessons, courseId) {
var count = 0;
for(var key in lessons) {
if( lessons[key].associatedCourseId == courseId) {
count++;
}
}
return count;
}
var UserWithLessonsCounter = $firebaseObject.$extend({
$$updated: function(snap) {
var changed = $firebaseObject.prototype.$$updated.call(this, snap);
this.lessonCount = countLessons(this.finishedLessons, $stateParams.id);
}
});
var refTemp = new Firebase($rootScope.baseUrl + "users/" + $rootScope.userId);
var userTemp = new UserWithLessonsCounter(refTemp);
userTemp.$bindTo($scope, "userTemp").then(function(){
console.log($scope.userTemp);
});
userTemp.$watch(function() {
console.log("Does this run at all? " + $scope.userTemp.lessonCount);
});
I update the user object, the lessonCount value did not change unless I refresh the page. And the console.log inside $watch did not run at all. What is wrong?
The promise returned by $bindTo is called exactly once. It's not an event listener. You can't listen to this to get updated each time there is a change.
Please read the guide, start to finish, and read about Angular's $watch method before continuing down this route, as with some fundamental knowledge, this should not have been your first instinct.
A beginner approach would be to utilize $watch:
MyFirebaseService.getCurrentUser().$bindTo($scope, "user");
$scope.$watch('user', function() {
for (var key in $scope.user.finishedLessons) {
if ($scope.user.finishedLessons.hasOwnProperty(key)) {
if ($scope.user.finishedLessons[key].associatedCourseId == $stateParams.id) {
$scope.finishLessonCount++;
}
}
};
console.log ($scope.finishLessonCount);
});
Or, having familiarized with the AngularFire API, one might pick $scope.user.$watch() in place of the scope method, which would prove more efficient.
Having written a large portion of the AngularFire code, I would pick the $extend tool, which was added precisely for use cases like this:
// making some assumptions here since you haven't included
// the code for your firebase service, which does not seem SOLID
app.factory('UserWithLessonsCounter', function($firebaseObject) {
return $firebaseObject.$extend({
$$updated: function(snap) {
var changed = $firebaseObject.prototype.$$updated.call(this, snap);
this.lessonCount = countLessons(this.finishedLessons);
return changed;
}
});
});
function countLessons(lessons) {
var count = 0;
for(var key in lessons) {
if( lessons.hasOwnProperty(key) ) {
count++;
}
}
return count;
}
And now in your controller:
app.controller('...', function($scope, UserWithLessonsCounter) {
var ref = new Firebase(...);
var user = new UserWithLessonCounter(ref);
user.$bindTo($scope, 'user');
user.$watch(function() {
console.log($scope.user.lessonCount);
});
});
I'm creating a custom combobox which uses jQuery validator.
At first they all are gray except the first (it means Country). When I choose 'Slovenská republika' the second combobox is enabled.
They all are instances of a a custom autocomplete combobox widget.
To enable the validation I use this code (which is called within _create: function(){..})
There you can find $.validator.addClassRules(); and $.validator.addMethod(). I also added the appropriate class so it really does something.
_registerCustomValidator: function(){
var uniqueName = this._getUniqueInstanceNameFromThisID(this.id);
var that = this;
console.log(this.id);//this prints 5 unique ids when the page is being loaded
$.validator.addMethod(uniqueName, function(value,element){
if(!that.options.allowOtherValue){
return that.valid;
}
console.log(that.id);//this always prints the ID of the last combobox StreetName
return true;
}, "Error message.");
var o = JSON.parse('{"'+uniqueName+'":"true"}');
$.validator.addClassRules("select-validator", o);
}
//this.id is my own property that I set in _create
Problem: When I change the value of any instance of combobox, it always prints the ID of the last instance StreetName, but it should belong to the one that has been changed.
I thought it might be because of registering $.validator.addMethod("someName",handler) using such a fixed string, so now I pass a uniqueName, but the problem remains.
Therefore the validation of all instances is based on the property allowOtherValue of the last instance.
I don't understand why it behaves so. Does anyone see what might be the problem?
EDIT:
see my comments in the following code
_registerCustomValidator is a custom function within a widget factory.
//somewhere a global var
var InstanceRegistry = [undefined];
//inside a widget factory
_registerCustomValidator: function(){
var i=0;
while(InstanceRegistry[i] !== undefined) ++i;
InstanceRegistry[i] = this.id;
InstanceRegistry[i+1] = undefined;
var ID = i; //here ID,i,InstanceRegistry are correct
$.validator.addMethod(uniqueName, function(value,element){
//here InstanceRegistry contains different values at different positions, so its correct
console.log("ID=="+ID);//ID is always 5 like keeping only the last assiged value.
var that = InstanceRegistry[ID];
if(!that.options.allowOtherValue){
return that.valid;
}
return true;
}, "Error message");
var o = JSON.parse('{"'+uniqueName+'":"true"}');
$.validator.addClassRules("select-validator", o);
}
It looks like a sneaky combination of closure logic and reference logic. The callback in $.validator.addMethod is enclosing a reference to this which will equal the last value of this when $.validator.addMethod. (Or something like that?)
Glancing at your code, it's not clear to me what this is in this context. So I can't really offer a concrete solution. But one solution might be to create some kind of global registry for your thises. Then you could do something along the lines of:
_registerCustomValidator: function(){
var uniqueName = this._getUniqueInstanceNameFromThisID(this.id);
$.validator.addMethod(uniqueName, function(value,element) {
var instance = InstanceRegistry[uniqueName];
if(! instance.options.allowOtherValue){
return instance.valid;
}
return true;
}, "Error message.");
var o = JSON.parse('{"'+uniqueName+'":"true"}');
$.validator.addClassRules("select-validator", o);
}
The registry could be keyed to uniqueName or id, just so long as it is a value getting enclosed in your callback.
I have a custom shopping cart object that I created and put it in the lib folder.
ShoppingCart = function ShoppingCart() {
this.Items = new Array();
this.grandTotal = 0.00;
}
ShoppingCart.prototype.addItem = function(Item){
this.Items.push(Item);
this.Items.sort();
this.calculateTotal();
}
I initialized the shopping cart and store it as Session.set('shoppingCart') during the page created phase.
Template.loginStatus.created = function() {
Session.set('loginShown',false);
if(!Session.get('shoppingCart')){ //set default if session shopping cart not exist
var cart = new ShoppingCart();
Session.setDefault('shoppingCart',cart);
}
Then when user click add item to cart, it will trigger this logic:
var cart = Session.get('shoppingCart');
cart.addItem(item);
Session.set('shoppingCart',cart);
Somehow, it does not work. When I take a look ad the chrome console it says undefined is not a function, pointing at cart.addItem(item) line. If I change it to this, it will work , but of course since everytime new shopping cart is created, I cannot accumulate items in the cart.
var cart = new ShoppingCart();
cart.addItem(item);
Session.set('shoppingCart',cart);
How should I store and retrieve the object from session properly? It looks like the returned object from the Session.get() somehow not considered as ShoppingCart. Did I miss any type cast?
As #Peppe L-G mentioned, you can only store EJSONs in Session. To store your custom object, you need to be able to manually transform it to and from EJSONs. Example:
_.extend(ShoppingCart, {
fromJSON: function(json) {
var obj = new ShoppingCart();
obj.grandTotal = json.grandTotal;
obj.Items = json.Items;
return obj;
},
});
_.extend(ShoppingCart.prototype, {
toJSON: function() {
return {
grandTotal: this.grandTotal,
Items: this.Items,
};
},
});
Then you can save it to Session:
Session.set('shoppingCart', cart.toJSON());
and restore:
ShoppingCart.fromJSON(Session.get('shoppingCart'));
I ran into the same problem. Essentially what is happening Meteor Sessions (and Collections) can only store EJSON types, so your ShoppingCart custom type is retrieved from the Session as a normal Object.
While you can manually transform to and from EJSONs, you may end up needing to do this repeatedly in a lot of different places. If your ShoppingCart is a member of another object, you'll have to also manually transform the member. It's better to use EJSON.addType to tell Meteor how to handle it automatically anywhere you store or retrieve an object of that type.
There's a great demo of this here: https://www.eventedmind.com/feed/meteor-create-a-custom-ejson-type. Full docs are also here: http://docs.meteor.com/#/full/ejson. But a short version is this:
Add a method to your custom type called typeName:
ShoppingCart.prototoype.typeName = function(){
return "ShoppingCart";
};
Add another method called toJSONValue:
ShoppingCart.prototype.toJSONValue = function(){
/* return a JSON compatible version of your object */
};
And finally, add the custom type to EJSON with:
EJSON.addType("ShoppingCart", function fromJSONValue(value){
/* return an object of your custom type from the JSON object 'value' */
};
NOTE: the "Type Name" in steps 1 and 3 must match exactly.
I am designing a Javascript-based Ohms law calculator (voltage, resistance, current) using knockout.js.
I want the ability of the user being able to select what is calculated, , e.g. voltage, resistance, or current, given the other two parameters, via radio buttons.
So my question is, can you change a ko.observable into a ko.computed and vise versa, after ko.applyBindings() has been called?
My initial attempts say no, I have tried this and slaved over the non-working code for hours, trying to get it to work.
You can't do it that way, but you can make all of them read/write ko.computeds that store a "shadow" value when written to and return that value when read from if they aren't the selected quantity (and return a calculated value if they aren't)
You dont even need a writable computed for this like ebohlman suggests
A simple demo
http://jsfiddle.net/K8t7b/
ViewModel = function() {
this.selected = ko.observable("1");
this.valueOne = ko.observable(1);
this.valueTwo = ko.observable(5);
this.result = ko.computed(this.getResult, this);
}
ViewModel.prototype = {
getResult: function() {
if(this.selected() === "1") {
return this.valueOne() - this.valueTwo();
}
return this.valueOne() + this.valueTwo();
}
};
ko.applyBindings(new ViewModel());
edit: hm, if you want the result to be presented in the correct value textbox you need to make them writable computed like ebohlman suggets
As ebohlman mentioned, the vital thing I was missing was shadow-variables, and the use of separate read/write procedures (a recently added feature to knockout) for ko.computed.
The code for one of the three variables is:
this.voltageS = ko.observable();
this.voltage = ko.computed({
read: function () {
if(this.calcWhat() == 'voltage')
{
console.log('Calculating voltage');
if(this.currentS == null)
return;
if(this.resistanceS == null)
return;
this.voltageS(this.currentS()*this.resistanceS());
return(this.currentS()*this.resistanceS());
}
else
{
console.log('Reading from voltage');
return this.voltageS();
}
},
write: function (value) {
console.log('Writing to voltage');
this.voltageS(value)
},
owner: this
});
I have created a JSFiddle here, which demonstrates being able to switch between which variable is calculated.
Another key part to this code is that on read, if it did happen to be the selected variable, as well as calculating it from the other two, I also had to write this result back to the shadow variable. This preventing some of the variables from mysteriously dissapearing/reappearing when the selected variable was changed.
I'm starting to do a lot of jQuery programming. I'm finding that my code is becoming a little hard to maintain and is not all that readable. My javascript is being included on every page of our web application, even though it is only used on one page. This has the benefits of having the javascript cached before loading the page, but I'm worried about accidentally creating functions with identical names (created by other programmers on my team).
I'm new to javascript, so there may be some basics that I'm missing. What are some techniques that I can apply to the following code, as well as other code in the future to make my jQuery code more maintainable and easy to read?
<script type="text/javascript">
$(document).ready(function(){
$('#registration-information .copy-button').click(copyField);
});
function copyField(e) {
e.preventDefault();
var $tr = $(this).closest('tr');
var originalText = jQuery.trim($tr.find('.contact-data').text());
var newText = jQuery.trim($tr.find('.registration-data').text());
var $button = $tr.find('.copy-button');
var $loadingIcon = $('<span class="loading-icon"></span>').hide().insertAfter($button);
var $undoLink = $('<a class="undo-link" href="#">Undo</a>').hide().insertAfter($button);
var field = $button.attr('id');
$undoLink.click(function(e){
e.preventDefault();
undoCopyField($tr, originalText);
});
$button.hide();
$loadingIcon.show();
$.ajax({
url: '/registrations/copy-field-to-contact',
data: {
id: '<?php echo $registration->ID ?>',
field: field,
data: newText
},
success: function(data){
if (data.success) {
$loadingIcon.hide();
$tr.find('.contact-data').html(newText);
$tr.find('td.form_field_label').removeClass('mismatched');
$tr.effect('highlight', 1000, function(){
$undoLink.fadeIn();
});
} else {
displayErrorMessage(data.error);
$loadingIcon.hide();
$button.show();
}
},
error: function(){
displayErrorMessage('Unknown reason');
$loadingIcon.hide();
$button.show();
}
});
}
function undoCopyField($tr, originalText) {
var $button = $tr.find('.copy-button');
var $loadingIcon = $tr.find('.loading-icon');
var $undoLink = $tr.find('.undo-link');
var field = $button.attr('id');
$undoLink.hide();
$loadingIcon.show();
$.ajax({
url: '/registrations/copy-field-to-contact',
data: {
id: '<?php echo $registration->ID ?>',
field: field,
data: originalText
},
success: function(data){
if (data.success) {
$undoLink.remove();
$loadingIcon.hide();
$tr.find('.contact-data').html(originalText);
$tr.find('td.form_field_label').addClass('mismatched');
$tr.effect('highlight', 1000, function(){
$tr.find('.copy-button').fadeIn();
});
} else {
displayErrorMessage(data.error);
$loadingIcon.hide();
$undoLink.show();
}
},
error: function(){
displayErrorMessage('Unknown reason');
$loadingIcon.hide();
$undoLink.show();
}
});
}
function displayErrorMessage(message) {
alert('Sorry, there was an error while trying to save your changes: ' + message);
}
</script>
Update: There are numerous sections of this code sample with chunks of code that are almost identical. Specifically the AJAX calls. Those two blocks are essentially the same, except for the actions that take place after the call has completed. I'd really like to figure out a way to DRY up those sections.
Two tips:
Use namespaces for your code to avoid name conflicts. There are of course no real namespaces in Javascript, but you can fake them using objects.
`
var MyCode=MyCode||{
copyField:function (e){
},
someOtherFunction:function(){
}
};
$(document).ready(function(){
MyCode.copyField(...);
});
Put your javascript code one or more separate files (one per namespace), and use third party libraries like combres to combine them into one file, minify them and take care of proper caching. It saves a lot of bandwidth and is cleaner than distributing all kinds of javascript functions across different pages.
One way to make your jQuery code cleaner and more maintainable is to break it into reusable jQuery plugins. This allows you to encapsulate related functionality in a single file. Each plugin is effectively a namespace so you will avoid function name collisions. You can also pass arguments to the plugin to customize the behaviour on a page by page or case by case basis.
There is a pretty good guide and template for writing plugins here
The only real readability issue I see here is that you could declare your variables without using the var each time by using a comma after each variable:
var $tr = $(this).closest('tr'),
originalText = jQuery.trim($tr.find('.contact-data').text()),
newText = jQuery.trim($tr.find('.registration-data').text());
I've always found this a bit easier to read then just a series of vars. To the eye, it looks like a code block that is started with var and ends when the indent returns.
Aside from that, it all looks good.
As a start...
Test drive the code using something like jsunit.
Create small well named classes, with small well named methods. The name of the class will describe it's responsibilities. The name of the method will describe it's responsibilities as well.
Refactor to remove duplication.
Limit the use of global scope.
Read Clean Code: A Handbook of Agile Software Craftsmanship [Paperback]
Robert C. Martin (Editor)
You can use YUI namespace model,
create a top level object (namespace)
lets var STW = {};
use this namespace function to create other classes or namespace
STW.namespace = function () {
var a = arguments, o = null, i, j, d;
for (i = 0; i < a.length; i = i + 1) {
d = ("" + a[i]).split(".");
o = STW;
for (j = (d[0] == "STW") ? 1 : 0; j < d.length; j = j + 1) {
o[d[j]] = o[d[j]] || {};
o = o[d[j]];
}
}
return o;
}
lets takes some example
in your first file
use STW.namespace("STW.namespace1");
STW.Namespace1.class1=function(){
//your code
}
in other files
STW.namespace("STW.namespace1.namespace2");
STW.namespace1.namespace2.class2=function(){
//your code
}
The stuff in the $.ajax parens is just a single argument like in any c-based function.
{} is JS's literal representation of an object. So if both cases have the same stuff between {} you could do this:
options = {
url: '/registrations/copy-field-to-contact',
data: {
id: '<?php echo $registration->ID ?>',
field: field,
data: newText
},
success: function(data){
if (data.success) {
$loadingIcon.hide();
$tr.find('.contact-data').html(newText);
$tr.find('td.form_field_label').removeClass('mismatched');
$tr.effect('highlight', 1000, function(){
$undoLink.fadeIn();
});
} else {
displayErrorMessage(data.error);
$loadingIcon.hide();
$button.show();
}
},
error: function(){
displayErrorMessage('Unknown reason');
$loadingIcon.hide();
$button.show();
}
}
And then for both:
$.ajax(options);
Now if there's a variation in the options for your next ajax call:
Example #1 - just change the URL to google
options.url = "http://www.google.com";
$.ajax(options);
Example #2 - change id property of the data property (which is another object)
options.data.id = "newID";
A word of caution on JQuery's selector syntax. Grabbing directly by ID is ideal. Grabbing just by class can be really slow in older versions of IE (which have no native getByClassName methods so there's more interpreter-level looping going on). The ideal way to write a selector narrows down to the closest parent ID available first and if you can be explicit add the tag name to the follow-up class selector to narrow down further (getElementsByTagName is native and speedy).
So if we could rely on .copy-button being an input tag:
$('#registration-information input.copy-button')
or we could rely on it being an input or an anchor tag
$('#registration-information input.copy-button, #registration-information a.copy-button')
those might be considerably faster options in IEs 6 and maybe 7 if there's a ton of HTML elements inside #registration-information. I try to give IDs to all unique containers likely to stay that way such that I've always got IDs handy for more efficient JQuery selectors.
But whatever you do, avoid these:
$('.copy-button')
That can choke IE 6 even on a largish html doc if there's enough of it going around. If you plan on using the results of a selector more than once, save it to a var. It's so concise it's easy to forget that these can be fairly resource intensive operations in some cases.