JQuery: How to cache DOM? - javascript

I used Firefug to profile my web application and found that the following function is called, and needs to be called, literally hundreds of times per user visit. So I want to optimize it since Firebug says it uses the most resources/times.
function highlightChildDiv(parentDiv) {
/* find the closest (hlisting) home listing to the middle of the scrollwindow & highlight */
var scrollElemPos = parentDiv.offset();
var highlightDiv = $(document.elementFromPoint(
scrollElemPos.left + parentDiv.width() / 2,
scrollElemPos.top + parentDiv.height() / 2)
).closest('#parentDiv div.childClass');
if (highlightDiv.hasClass("HighlightRow")) {
return; // if the div is already highlighted, return
} else {
$('#parentDiv div.childClass').removeClass("HighlightRow");
highlightDiv.addClass('HighlightRow');
}
}
Seems to me that one of the most un-optimized statements is .closest('#parentDiv div.childClass');, but I'm sure there is other things to improve.
Question: Does anyone have any JQuery performance tips on how I can optimize the code above given that this function is run literally hundreds of times per user visit.

First thought, eliminate the dead statement in the if clause.
if (!highlightDiv.hasClass("HighlightRow")) {
$('#parentDiv div.childClass').removeClass("HighlightRow");
highlightDiv.addClass('HighlightRow');
}
In the selector #parentDiv div.childClass, can you guarantee that div will be a direct descendent of #parentDiv? In which case:
.closest('#parentDiv>div.childClass');
and
$('#parentDiv>div.childClass')
You already have parentDiv. I'm guessing this is a DOM object, so you may be able to do the following:
$(parentDiv).children("div.childClass")
Just hide the DIV that is currently highlighted:
$('#parentDiv div.HighlightRow').removeClass("HighlightRow");

My guess is this is the most unoptimized line:
$('#parentDiv div.childClass').removeClass("HighlightRow");
You should profile it to make sure (create a date object outside the call and output the getTime() value before and after each call).
Here you are asking jQuery to iterate over all DOM elements that match that selector and remove the class. If there are 1000 rows, jQuery will need to interogate each one to see if it needs to remove a class. Ugh. Here it is with that lookup removed:
// namespace scoped cache
var Hash_$_Cache = {
$parentDiv : $('#parentDiv'),
$tgt_row : $([]) // empty jq object to start
};
// find the closest (hlisting) home listing to the middle of
// the scrollwindow and highlight
//
var highlightChildDiv = function (parentDiv){
var
scrollElemPos = parentDiv.offset(),
$tgt_row
;
$tgt_row = $(document.elementFromPoint(
scrollElemPos.left + parentDiv.width() / 2,
scrollElemPos.top + parentDiv.height() / 2)
).closest('#parentDiv div.childClass')
;
// bail if row is already highlighted
if ($tgt_row.hasClass('HighlightRow')){ return; }
Hash_$_Cache.$tgt_row.removeClass('HighlightRow');
$tgt_row.addClass('HighlightRow');
// save highlighted row for later
Hash_$_Cache.$tgt_row = $tgt_row; // store new row in cache
};
Hope that helps!

I prefer to use the following methodology:
https://gist.github.com/3841424#file-domcache-js
Or, you may replace the DOM object with a method in this implementation:
var myNS = {
myEventHandler: function(event){
this.DOM.$el.doSomething();
},
cacheDOM: function(){
return {
$el: $("#matrix")
};
},
initialize: function(){
this.DOM = this.cacheDOM();
}
};

Related

How do I update the value of a custom attribute (enum-of-strings) in SFCC via code?

Requirement: I want to update the value of a custom attribute (name: badges) (type: enum-of-strings) for a Product via code. I want to set the value "bestSeller" as selected. How should I do that update because the code below is not working?
Screenshot of the Custom Attribute in Business Manager
Code snippet:
function updateBestSeller() {
var ProductMgr = require('dw/catalog/ProductMgr');
var Site = require('dw/system/Site');
var UUIDUtils = require('dw/util/UUIDUtils');
var CustomObjectMgr = require('dw/object/CustomObjectMgr');
var currentSite = Site.getCurrent();
var bestSellerOrderUnits = Object.hasOwnProperty.call(currentSite.preferences.custom, 'bestSellerOrderUnits') ? currentSite.getCustomPreferenceValue('bestSellerOrderUnits') : 0;
try {
Transaction.wrap(function () {
var count = 1;
var products = ProductMgr.queryAllSiteProducts();sni
var HashSet = require('dw/util/HashSet');
var badges = new HashSet();
if (products.count > 0) {
while (products.hasNext() && count < 5) {
var product = products.next();
var badges = [];
badges.push('bestSeller');
if (Object.hasOwnProperty.call(product.custom, 'badges')) {
product.custom.badges = badges
}
count++;
Logger.debug('{0}',product.ID);
}
}
products.close();
});
} catch (ex) {
Logger.error(ex.toString());
return new Status(Status.ERROR, 'ERROR', 'UPDATE failed');
}
return new Status(Status.OK, 'OK', 'UPDATE successful');
}
I think what is probably happening here is that your attempt to validate that a product has a badges key in the product.custom attribute collection is failing. This prevents the update from occurring.
I suggest removing the condition around the following line:
product.custom.badges = badges;
If that line were to not execute, then the update to the database would never occur.
The way custom attributes work is that they will never exist until a value is set for that attribute for a given persistent object. (eg: Product) So checking to see if it exists via something like: 'badges' in product.custom (which is the recommended way) will often be false even when the custom attribute definition exists because many products have never had a badge value set. Once an object has had a value set to a particular custom attribute even if it is now set to null then it will exist.
Additionally, there are some other issues with the code that may be causing problems. One example is defining the badges variable twice within the same scope. Another example is sni that is placed at the end of the line where you define the products variable. This is likely causing an error in your code. Finally, it is a good practice to avoid the use of the queryAllSiteProducts method if you can. An alternative may be to use the ProductSearchModel instead; This may not always meet your need, but it is a good idea to rule it out before resorting to queryAllSiteProducts for performance reasons.
Something else to consider is that if badges currently has any selected values, you'll be overwriting those values with the code you have today. Consider setting badges initially to [] then check to see if there is a value for that product by doing:
if ('badges' in product.custom && !empty(product.custom.badges) {
badges = product.custom.badges;
}

I'm trying to use jquery to create a div containing columns but I can't get my array to format correctly

I have an array that contains dates. and for some reason I can't get it to show on my screen I've been debugging for a few days now and I've tracked it down to a single line, but the line has worked before and I can't figure out what the issue might be.
The array looks like this:
var selectItems =
[ "05-26-2017", "06-02-2017", "06-09-2017",
"06-16-2017", "06-23-2017", "06-30-2017", "07-07-2017", "07-14-2017",
"07-21-2017", "07-28-2017"...];
It's passed as an argument from another function, but that's how it's showing in console.log().
I might be going about this the wrong way, maybe even a lot further around then I need to but this is what I've come up with:
1. function setTHead(selectItems) {
2 var formatString;
3. for (var x = 0; x < 12; x++) {
4. formatString = selectItems[x].replace(/[^0-9/-]/g, "").toString();
5. console.log(selectItems);
6. $('#datTab').append("<div id='col" + x + "' class='column'>'" + formatString + "'</div>");
7. }
8. }
the array up top is what's showing from the console.log 5 lines down.
the sixth line is what is seeming to give me issues. Nothing is put on the page at all.
I'm getting a console error saying:
jQuery.Deferred exception: selectItems is undefined setTHead#http://localhost/mySite/script.js:136:9
startUp2#http://localhost/mySite/script.js:146:5
#http://localhost/mySite/table.php:19:9
mightThrow#http://localhost/mySite/lib/jquery.js:3586:52
resolve/</process<#http://localhost/mySite/lib/jquery.js:3654:49
setTimeout handler*resolve/<#http://localhost/mySite/lib/jquery.js:3692:37
fire#http://localhost/mySite/lib/jquery.js:3320:30
fireWith#http://localhost/mySite/lib/jquery.js:3450:29
fire#http://localhost/mySite/lib/jquery.js:3458:21
fire#http://localhost/mySite/lib/jquery.js:3320:30
fireWith#http://localhost/mySite/lib/jquery.js:3450:29
ready#http://localhost/mySite/lib/jquery.js:3923:13
completed#http://localhost/mySite/lib/jquery.js:3933:9
EventListener.handleEvent*#http://localhost/mySite/lib/jquery.js:3949:9
#http://localhost/mySite/lib/jquery.js:39:9
#http://localhost/mySite/lib/jquery.js:17:3
undefined
followed by:
TypeError: selectItems is undefined
and thats pointing to line 6.
if anyone has any advice I would be very much appreciative. Thank you in advance.
EDIT: A little more code:
function startTblView(defSel) {
if (defSel === true) {
setCookie('defSel', true, 7);
} else{
setCookie('defSel', false, 7);
}
saveSelected();
window.open('table.php', '_self');
defSel = getCookie('defSel');
if (defSel) {
selectItems = getDefDates();
}else {
selectItems = reGetSelected();
}
setTHead(selectItems);
}
defSel, is a boolean passed from my last page stating whether I'm doing a default view or a custom view, the custom view is passed from saveSelected();
saveSelected is a function for just saving the selected global value as a cookie so I can pull it out on the next page.
getDefDates pulls the default values for the array
reGetSelected, gets the selected array from the cookie.
I apologize for wonky naming conventions. I'm the only one working on this site and I'm just making sure the names don't overlap.
You can do this :
HTML code
<div id="datTab"></div>
JS code
var selectItems =
[ "05-26-2017", "06-02-2017", "06-09-2017",
"06-16-2017", "06-23-2017", "06-30-2017", "07-07-2017", "07-14-2017",
"07-21-2017", "07-28-2017"];
function setTHead(selectItems) {
var formatString;
$.each( selectItems, function( index, value ){
formatString = value.replace(/[^0-9/-]/g, "").toString();
$('#datTab').append("<div id='col" + index + "' class='column'>'" + value + "'</div>");
});
};
You can use $.each, its better than 'for' with javascript.
The .each() method is designed to make DOM looping constructs concise
and less error-prone. When called it iterates over the DOM elements
that are part of the jQuery object. Each time the callback runs, it is
passed the current loop iteration, beginning from 0. More importantly,
the callback is fired in the context of the current DOM element, so
the keyword this refers to the element.
I did a JsFiddle
Here.

Overwriting a key, if it exists, in a key/value array

I'm allowing users to highlight rows in a table by clicking them.
When they click them, HighToggle() is called.
HighToggle() checks to to see if the bgcolor of the td matches the highlighter color and either highlights or un-highlights.
function HighToggle(cObj,vid,vColor,hColor) {
if ((vColor == hColor)) {
$(cObj).css('background-color','#FFFFFF'); // un-highlight
vx.push({'id' : vid, 'status' : 'off'});
} else {
$(cObj).css('background-color','#' + Highlighter.Hex); // highlight
vx.push({'id' : vid, 'status' : 'on'});
}
}
It is important that I store these in an array, because I have recordset navigation that deletes the rows (from view) and loads in new rows via JSON. Otherwise, when I wanted to submit the data to the server, I could just loop through table rows and build the array pre-submit.
So, that works fine, except that it adds duplicate id's in the case of each time something is highlighted or un-highlighted, so it might look like:
1: on, 7: on, 7: off, 2: on, 4: on, 1: off, 3: on, 1: on, 2: off.
Which is just a mess.
Following the same click process that results in that array above, what I'd like to end up with is..
1: on, 7: off, 2: off, 4: on, 3: on
Rows never interacted with (5, 6, 8, 9, 10 if the result set only had 10 rows), need not have an entry in the array at all.
I'm looking for a fast way to search my array and override rows if they exist.
I have thought of one solution that I haven't tried to implement yet because I feel like there might be something better.
I could make a separate array, let's say it's called vindexes. Vindexes could use the ID passed as the vid variable like so..
vindexes[vid] = vx.length;
HighToggle could check to see if vindexes[vid] exists, and if so, use that id to override rather than vx.push.
It would work (wouldn't it?) but is it reinventing the wheel? Is there a faster way? This function will be used a lot on each page.
I've found the grep function, since I'm using jQuery, but I don't think grep is applicable to me here since I want to know the index of where the ID was found.
Thank you for any advice.
Edit: Here's what I devised myself, though I think that the answer is a more elegant and sensible solution.
function HighToggle(cObj,vid,vColor,hColor) {
vid = Number(vid);
if (!(vid in vi)) {
inIndex = vx.length;
} else {
inIndex = vi[vid];
}
if ((vColor == hColor)) {
$(cObj).css('background-color','#FFFFFF');
vi[vid] = inIndex;
vx[inIndex] = {'id' : vid, 'status' : 'off'};
} else {
vi[vid] = inIndex;
$(cObj).css('background-color','#' + Highlighter.Hex);
vx[inIndex] = {'id' : vid, 'status' : 'on'};
}
}
My solution, and the answer are both effective for my use, but perhaps someone else will run into this situation and benefit from the code I've provided.
You could use an object:
var vx = {};
function HighToggle(cObj,vid,vColor,hColor) {
if ((vColor == hColor)) {
$(cObj).css('background-color','#FFFFFF'); // un-highlight
vx[vid] = "off";
} else {
$(cObj).css('background-color','#' + Highlighter.Hex); // highlight
vx[vid] = "on";
}
}
Then if you need an array later (for submitting to server, etc):
var vxArray = [];
for (var key in vx) {
vxArray.push({id: key, status: vx[key]});
}

How to set goog.ui.Autocomplete minimum input to 0

I would like the autocomplete to show the entire list when the input box gets focused (no input is given). Would also like the auto complete to match substrings without having to fiddle with private variables.
At the moment the code is:
autocomplete = goog.ui.ac.createSimpleAutoComplete(
gsa.Game.gameData.teams, team2, false);
matcher=autocomplete.getMatcher();
matcher.useSimilar_=true
autocomplete.setMatcher(matcher);
Similar matches work but have to set a private variable for that (no getter or setter available).
The other one I have not been able to find out; how to show all data when no input is given (like a smart select input). So when the textbox receives focus it'll show all data since there is no filter text given. These are basic things that one would like to configure but can't find it in the API documentation.
You need to create descendants of goog.ui.ac.AutoComplete, goog.ui.ac.ArrayMatcher and goog.ui.ac.InputHandler. Also you will directly create the instance of auto complete object instead of calling goog.ui.ac.createSimpleAutoComplete.
In goog.ui.ac.AutoComplete descendant you assign custom input handler and matcher.
goog.provide('my.ui.ac.AutoComplete');
goog.require('goog.ui.ac.Renderer');
goog.require('my.ui.ac.ArrayMatcher');
goog.require('my.ui.ac.InputHandler');
my.ui.ac.AutoComplete = function(data, input, opt_multi, opt_useSimilar) {
var renderer = new goog.ui.ac.Renderer();
var matcher = new my.ui.ac.ArrayMatcher(data, !opt_useSimilar);
var inputhandler = new my.ui.ac.InputHandler(null, null, !!opt_multi, 300);
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
inputhandler.attachAutoComplete(this);
inputhandler.attachInputs(input);
};
goog.inherits(my.ui.ac.AutoComplete, goog.ui.ac.AutoComplete);
In goog.ui.ac.ArrayMatcher descendant you need to override getPrefixMatches() method, since the default behaviour discards empty strings. So if there is an empty string, we just return the first x rows from the data.
goog.provide('my.ui.ac.ArrayMatcher');
goog.require('goog.ui.ac.ArrayMatcher');
my.ui.ac.ArrayMatcher = function(rows, opt_noSimilar) {
goog.ui.ac.ArrayMatcher.call(this, rows, opt_noSimilar);
};
goog.inherits(my.ui.ac.ArrayMatcher, goog.ui.ac.ArrayMatcher);
my.ui.ac.ArrayMatcher.prototype.getPrefixMatches = function(token, maxMatches) {
if (token == '')
{
// for empty search string, return first maxMatches rows
return this.rows_.slice(0, maxMatches);
}
else
{
return goog.base(this, 'getPrefixMatches', token, maxMatches);
}
};
In goog.ui.ac.InputHandler descendant you need to override processFocus() method, and force to show the autocomplete popup. This can be done by calling update() method with first parameter set to true.
goog.provide('my.ui.ac.InputHandler');
goog.require('goog.ui.ac.InputHandler');
my.ui.ac.InputHandler = function(opt_separators, opt_literals, opt_multi, opt_throttleTime) {
goog.ui.ac.InputHandler.call(this, opt_separators, opt_literals, opt_multi, opt_throttleTime);
};
goog.inherits(my.ui.ac.InputHandler, goog.ui.ac.InputHandler);
my.ui.ac.InputHandler.prototype.processFocus = function(target) {
goog.base(this, 'processFocus', target);
// force the autocomplete popup to show
this.update(true);
};

Duplicating script and incrementing id names to dynamically add input content

I am currently working on a website to easily modify css properties and animate using jquery. The page will then output the code for the user to copy and paste into their work.
I am wanting to dynamically add input fields so users can enter their own parameters in "keyframes" for the animation.
I was wondering if there is a way to have a button to create the new elements and the jquery to have it operate. Currently I have tried setting a variables value to increment with the button then have a "for" loop to create the new keyframe elements.
Is there a way to append the previous keyframes with new keyframes
this is a sample of the things I need to set when creating the DOM nodes for the input fields, but the main issue I am having is incrementing the value of "i" and duplicating the code only once.
//////////TOP TEXT INPUT///////////
var newTop = document.createElement('input')
newTop.type = "text"
newTop.id ="animation_" + i + "_top" // i = 1
newTop.size="10"
newTop.value="100"
newTop.name="animation_" + i +"_top" // i = 1
newTop.textContent = "top";
var refTopSibling = document.getElementById('frame_1')
var refTopParent = refTopSibling.parentNode
refTopParent.appendChild(newTop)
//////////TOP TEXT INPUT///////////
var newLeft = document.createElement('input')
newLeft.type = "text"
newLeft.id ="animation_" + i + "_left" // i = 2
newLeft.size="10"
newLeft.value="100"
newLeft.name="animation_" + i +"_left" // i = 2
newLeft.textContent = "left";
var refTopSibling = document.getElementById("animation_" + i + "_top")
var refTopParent = refTopSibling.parentNode
refTopParent.appendChild(newLeft)
Regards,
Andrew
EDIT--------------------------
Thanks for the replies. Unfotunately it's a little large for JS fiddle. I'm only a noob so this may not be the most elegant way to do it, but I used an if statement to increment the variables current value on each press... unfortunately it's not dynamic but I have enough options to allow 11 keyframes of animation that can be dynamically added to the DOM.
function addFrame(){
var n;
if (document.getElementById('frame_1')){n=2};
if (document.getElementById('frame_2')){n=3};
if (document.getElementById('frame_3')){n=4};
if (document.getElementById('frame_4')){n=5};
if (document.getElementById('frame_5')){n=6};
if (document.getElementById('frame_6')){n=7};
if (document.getElementById('frame_7')){n=8};
if (document.getElementById('frame_8')){n=9};
if (document.getElementById('frame_9')){n=10};
if (document.getElementById('frame_10')){n=11};
//rest of the code here
}
Now to figure out a way to draw svg dynamically and animate it with user inputs ;-)
This isn't necessarily the best implementation but I believe it meets your requirements.
function SampleObject(position, i)
{
this.posString = "new" + position;
$(this[this.posString] = document.createElement("input"))
.css({ type: "text",
id: "animation_" + i + "_" + position,
size: "10" ,
value: "100" ,
name: "animation_" + i +"_" + position,
textContent: position
});
$("#frame_1").parent().append(el);
}
//Public methods
SampleObject.prototype.getInput = function()
{
return this[this.posString];
}
//Static members
SampleObject.set = [];
//Static methods
SampleObject.new = function(position);
{
this.set.push(new SampleObject(position, this.set.length + 1));
}
//Quick test
SampleObject.new("top");
jQuery rewrite aside this does three actions, albeit the last might be surplus to requirement.
1) Rather than write a new function for every position the constructor of the object takes a position string (any string would be fine but for your objective position seems apt). The string is appended to "new" to form the "new" that is used to create an object member named in accordance with the current multi-function approach in the post that stores the new input object - personally I think the member naming is redundant but I don't know the project. The object has a get method for returning the input form.
2) Using the static "Sample.new" method (which takes a position parameter) a new SampleObject which has an index value of 1 + (static member) SampleObject.set, the set variable is used to store the new objects as they are created. This might not be necessary but I imagine you'll want to interact with them later or attach event.
As for the "frame_" if statements
$("[id^=frame_]").length() + 1
Assuming that frames are always created iteratively this will return the appropriate value for n.

Categories

Resources