Running multiple Javascript scripts only when certain HTML elements exist? - javascript

Here's the situation. I'm building a site which uses lots of scripts looking somewhat like this:
function getRandomArrayIndex(source_array) {
return Math.floor(Math.random() * source_array.length);
}
function getRandomArrayEntry(source_array) {
var random_index = getRandomArrayIndex(source_array);
return source_array[random_index];
}
function getRandomBlah() {
var blahs = [
["A"],
["B"],
["C"],
["D"],
["E"],
["F"],
["G"],
["H"],
["I"],
["L"],
["M"],
["N"],
["O"],
["P"],
["R"],
["S"],
["T"],
["V"],
["W"],
["Y"],
]; var random_blah = getRandomArrayEntry(blahs);
return random_blah;
}
function displayBlah(blah) {
const TEXT_ROW = 0;
const LINK_ROW = 1;
var blah_text = blah[TEXT_ROW];
var blah_link = blah[LINK_ROW]; if (blah_link != null) {
document.getElementById("blah").innerHTML = '' + blah_text + '';
} else {
document.getElementById("blah").innerHTML = blah_text;
}
}
function generateRandomBlah(){
var random_blah = getRandomBlah();
displayBlah(random_blah);
}
And this will, when called with <body onload="generateRandomBlah()">, insert one of the letters at random into <span id="blah"></span>.
So there's about 15 of these scripts, each with their own functions named slightly differently for different uses - generateRandomBlah2, etc, with a corresponding different place in the HTML for each script to do its work.
Because I'm not a very good coder, the way the whole thing works is that in the 'body onload' tag, there's about 15 different 'generateRandomBlah()' functions just within this one tag. The nature of the site means that on any one page, I will only need 2 or 3 of these scripts at once, but I require the ability to call any of them on any page. As you can see, my current tactic is to just call them all at once, and if the corresponding doesn't exist for a script, it'll just ignore that fact and move onto the next one.
Except that it doesn't ignore the fact that there's no corresponding <span>.
As soon as one isn't present, the rest of the scripts break and don't actually do what they're supposed to do. Looking at the code in Chrome's 'inspect code' shows an error at the first script which happens to break: "Uncaught TypeError: Cannot set property 'innerHTML' of null". I see a couple of potential solutions, but I might be completely off:
1) Add some code into each script which tells it that, if there's no <span id> on the page to insert its code, it ends gracefully and moves onto the next one - gradually (obviously in less than a second speed-wise) going through the scripts and only running them if actually exists. (As you can see, the problem is that a script will get 'snagged' on the fact that there's no place to insert its code and doesn't just end gracefully if that happens.
2) Get rid of the 'onload' stuff and just make each script self-containing, calling its own function. I don't know if this would fix the problem, though.
Anyway, some help would be much appreciated, as I'm stumped.

As you said in your first solution, testing for null before trying to "do" anything is probably your best bet short of completely recoding.
function getRandomArrayIndex(source_array) {
if(source_array === null) return; // similar lines in each function should fix everything
return Math.floor(Math.random() * source_array.length);
}
That said, a much better approach than your current solution of making a bunch of very similar functions and running them all is to create a single function that can take parameters indicating what it should be doing. If they really are quite similar, it shouldn't be difficult and will result in a lot fewer lines of code. It would also be nice if you can find a way for your page to not call all of the functions every time, though with fixing the null pointers that's a much smaller issue.

Not sure if I'm missing something here, but since even the error message complains about the value being null the obvious solution is to check for null:
var element = document.getElementById("blah");
if( element !== null ) {
element.innerHTML = '' + blah_text + '';
}
If you need to do that often you can make a function for it:
function updateIfExists( id, content ) {
var element = document.getElementById( id );
if( element !== null ) {
element.innerHTML = content;
}
}
updateIfExists( 'blah', '' + blah_text + '' );

Related

Overwriting existing object properties

I am having trouble with two small blocks of code and understanding why one works and one doesn't.
I have an array of 'tasks'.
When a user updates a task, I need to update the AuditLog, to show the update.
I have two blocks of code, first one works, second one doesn't.
1st block - when I view the updated task, it shows the updated log.
2nd block - when I view the updated task, the log is not updated.
I suspect that it is something to do with the way I am referencing, assigning things.
This block correctly updates the "task" and viewing it shows the updated "audit log".
//update task with server response 'a'
this.updateTask(task).subscribe(a => {
let updatedTask = initializeTask(a);
task.auditLog = updatedTask.auditLog;
});
Then I thought I could simplify the code a bit...
but when using this block, when I view the task, it does not show the updated log.
(I should mention that printing 'intializeTask(a)' works correctly and prints the updated log values, so it's not to do with that).
//update task with server response 'a'
this.updateTask(task).subscribe(a => {
task = initializeTask(a);
});
I feel like I may be missing something fundamental in the way JavaScript references/assigns variables etc. Perhaps someone can shed some light on it.
Thanks in advance!
PS: initializeTask simply ensures that nothing is left undefined and that dates are formatted correctly:
export function initializeTask(t): Task {
t.subject = t.subject || '';
t.startTime = initDate(t.startTime);
t.state = t.state || 'Published';
t.createdBy = t.createdBy || '';
if (t.auditLog) {
t.auditLog.forEach(d => {
d.createdOn = initDate(d.createdOn);
});
}
return t;
}

Merge small js-files into one bigger

I want to tidy up the js-code used on my php-website to increase the loading speed. For the moment i include in every website the required js-file.
My plan is to merge all js-files into one big one. Not every page uses every js-code, so i started something but don't know if this makes any sense.
I have already read the article One JS File for Multiple Pages but the method of Paul Irish is way to complicated for me (for the moment) as a beginner.
This is my approach:
I create the file core.js and call it on every website like..
<script src="js/core.js"></script>
In core.js i first get the name of the corresponding page.
var path = window.location.pathname;
var page = path.split("/").pop();
var page_name = page.slice(0, -4);
Then i check which site requires which js-script (pseudo-code).
if (page_name == 'xyz'){
execute this code which is only used on this site
}
if (page_name == 'abc' || 'xyz' || 'def'){
execute another code which is used on multiple sites
}
if (page_name == 'ghi' || 'jkl' || 'mno' || 'xyz'){
include jquery for multiple sites
}
...
...
This means a lot of work for me, because i have a lot of js, so i wanted to ask first if this is a good solution to tidy up.
By the way: The js code i place on my website doesn't change often.
Thank you
Misch
A solution for your problem could be something like:
if(selector) {
//run code
}
This runs the code inside the block only if a particular selector exists. This way you don't have to go through all the trouble of getting the name of the page, splitting and slicing the string etc (this is also prone to errors).
So let's say you want to add some innerHTML on some node it will look something like this:
function bar (text) {
alert(text)
}
if(document.getElementById('#foo')) {
bar('#foo exists!')
}
This way bar is only called when a node with id #foo exists.
Split your javascript into sensible groups. You may have an admin section to your site, so have admin.js.
It's also worth noting that most browsers will only download the javascript file once and then cache it. You said that your code does not change very often so you may find that putting it all in one file doesn't actually have that much of an affect.
Lets say you have pages like page1, page2, page3 etc.
Then your core.js will include all the codes of all the pages and then just initialize the code which you want to use
var page1= (function () {
var Init = function (){
//write the codes used by page 1
};
return {
Initialize: function () {
Init();
}
};
})();
var page2= (function () {
var Init = function (){
//write the codes used by page 2
};
return {
Initialize: function () {
Init();
}
};
})();
var page3= (function () {......});
var page = path.split("/").pop();
var path = window.location.pathname;
var page = path.split("/").pop();
var page_name = page.slice(0, -4);
if (page_name == 'pg1'){
page1.Initialize();
}
if (page_name == 'pg2' || 'pg3'){
page2.Initialize();
page3.Initialize();
}
if (page_name == 'pg4' ){
page4.Initialize();
}
To be honest, this is just going to slow down your performance. If your users stay a long time on the website, then one single file reduces a bit of clutter in your code. But, if the user is visiting only a few pages, single file is just extra burden on bandwidth. There is a possibility, most of your users might not even need more than half of your js.
Plus, those extra conditions aren't really helping anyone. So, I would say, don't use single file option.

Google Scripts/Basic JavaScript - Issue fixed by debugger

I'm working on a Google Scripts add on for Google Sheets, but I'm trying to get the script working before I actually set it up on the sheet. The code below works fine if I set a breakpoint somewhere in the extractNumbers function. If I just execute the code without breakpoints, I get an error:
TypeError: Cannot call method "replace" of undefined. (line 36, file "")
Here's the code:
var myVar = phoneCheck("a1","a2","o1","o2");
Logger.log(myVar);
function phoneCheck(newCell,newHome,oldCell,oldHome) {
Logger.clear();
var newCell = extractNumbers(newCell);
var oldCell = extractNumbers(oldCell);
var newHome = extractNumbers(newHome);
var oldHome = extractNumbers(oldHome);
if (newCell === oldCell) {
return newCell;
exit;
} else if (newCell === oldHome && newHome === oldCell) {
return oldCell;
exit;
}
if (newCell === '' && oldCell !== '' ) {
return oldCell;
exit;
}
if (newCell !== oldCell && newCell !== oldHome) {
return newCell;
exit;
}
return "No value found";
exit;
}
function extractNumbers(input) {
Logger.log(input);
var str = input;
return str.replace( /\D+/g, '');
}
Now I realize my if/then logic is more than a bit inelegant, but for my purposes, quick and dirty is fine. I just need it to run.
ALSO, I have read of other novice JavaScript programmers having similar issues related to the sequence of code execution. If someone would like to link to a concise source aimed at a non-advanced audience, that would be great too. Thanks!
EDIT: I put my code into a new fiddle and it works fine, but it continues to fail in Google Scripts editor unless running in debug mode with a breakpoint. The problem seems to be that the function parameters aren't available to the function unless there is a breakpoint. Anyone have access to Google Scripts that can try my updated code from https://jsfiddle.net/hrzqg64L/ ?
None of the suggestions got to the root of your problem - and neither did your answer, although you've avoided the problem by putting an enclosure around everything.
There's no AJAX, no asynchronous behavior - it's simpler than that. "Shadowing of parameters" is likewise a red herring. Bad coding practice, yes - but not a factor here.
If someone would like to link to a concise source aimed at a non-advanced audience, that would be great too.
Sorry - no such thing. I can explain what's going on, but can't guarantee it will be accessible to novices.
The exception
Let's just clarify what causes the exception, or thrown error, that you've observed.
As written, extractNumbers() will throw an exception if it has a null parameter (or any non-string parameter) passed to it. If you choose to extractNumbers() then hit "run", you'll get:
TypeError: Cannot call method "replace" of undefined. (line 36, file "")
That is telling you that on line 36, which is return str.replace( /\D+/g, '');, the variable str contains an object that is undefined (...and has no replace() method).
For bullet-proof code, you would check your parameter(s) to ensure they are valid, and handle them appropriately. Sometimes that would be with a valid default, and other times you might return an error or throw an exception that is more explicit about the parameter problems.
Running code in Google's debugger
The only way to run code in Google's Debugger is to select a function, then choose "run" or "debug". Assuming you posted all your code, you had just two functions to choose from:
phoneCheck()
extractNumbers()
Whenever Google Apps Script runs any part of a script, the entire script is loaded and scanned to find all symbols & check syntax. The scope of all symbols is noted as well, and so are any dependencies between functions and global symbols (symbols outside of any closure, or block of code).
That takes some time. To speed things up when asked to execute a specific function, the global symbols are only evaluated if they are a dependency for the requested function or the functions it may call. There is another condition that will trigger evaluation of global symbols, and that is if there is a possibility that the debugger may need to stop and display values.
When this happens, any code that is outside a closure (outside a function, for example) will be executed. This is what you observed when you set breakpoints.
Why did it work when breakpoints were set?
As explained, just having a breakpoint set triggers evaluation of global symbols.
You start this script with a few lines of code that are not in any closure:
var myVar = phoneCheck("a1","a2","o1","o2");
Logger.log(myVar);
It is that code which makes the only proper invocation of phoneCheck() with parameters. Because myVar is evaluated, phoneCheck() gets called with parameters, and in turn calls extractNumbers() with a defined parameter.
Unfortunately, because of the way the debugger works, you cannot choose to run that code yourself. You need to rely on these side-effect behaviors.
How to fix this?
Simple. Don't rely on global code to invoke functions under test. Instead, write an explicit test function, and call that.
function test_phoneCheck() {
var myVar = phoneCheck("a1","a2","o1","o2");
Logger.log(myVar);
}
Finally found the issue, but I don't fully understand it.
This question got me thinking about scope and how it might be different in the Google Script environment. I figured a simple workaround would be to enclose the entire script in its own void function, and it worked! Also, I simplified the script quite a bit with an array:
function init () {
var numberArray = ["a3", "a2", "o3", "o10"];
var myVar = phoneCheck(numberArray);
Logger.log(myVar);
function phoneCheck(myArray) {
var phoneString = '';
Logger.clear();
var arrayLength = myArray.length;
for (i = 0; i < arrayLength; i++) {
phoneString += myArray[i].replace(/\D+/g, '');
}
return phoneString;
}
}
Also, I realize the functionality of this script is different than the original, but I was really just trying to solve this problem. Now that I have, I can finish the script properly.
Thanks for all the suggestions, everyone! I learned a lot of good things, even though they turned out not to be the answer.

'Date' is undefined in IE9 in javascript loaded by FacePile

I'm currently getting an error within Facebook's FacePile code, and I'm baffled by the cause.
facepile.php loads a script which, among other things, has these lines (when pretty-printed):
...
o = document.createElement('script');
o.src = l[n];
o.async = true;
o.onload = h;
o.onreadystatechange = function() {
if (o.readyState in c) {
h();
o.onreadystatechange = null;
}
};
d++;
a.appendChild(o);
...
(a == document.body, d++ is irrelevant here)
This code loads a script with src = http://static.ak.fbcdn.net/rsrc.php/v1/yW/r/pmR8u_Z_9_0.js or something equally cryptic (the filename changes occasionally).
In that script, there are these lines at the very top (also when pretty-printed):
/*1331654128,176820664*/
if (window.CavalryLogger) {
CavalryLogger.start_js(["\/8f24"]);
}
window.__DEV__ = window.__DEV__ || 0;
if (!window.skipDomainLower && document.domain.toLowerCase().match(/(^|\.)facebook\..*/))
document.domain = window.location.hostname.replace(/^.*(facebook\..*)$/i, '$1');
function bagofholding() {
}
function bagof(a) {
return function() {
return a;
};
}
if (!Date.now)
Date.now = function now() {
return new Date().getTime();
};
if (!Array.isArray)
Array.isArray = function(a) {
return Object.prototype.toString.call(a) == '[object Array]';
};
...
And I'm getting an error which says "SCRIPT5009: 'Date' is undefined", right at the if (!Date.now) portion. Debugging near that point reveals that Date, Array, Object, Function, etc are all undefined.
Er... how? window exists, as does document (though document.body is null) and a handful of others, but plenty of pre-defined objects aren't. Earlier versions of IE don't seem to have this problem, nor do any other browsers, but multiple machines running IE9 (including a clean VM) all have the same issue.
I doubt I can do anything about it, but I'm very curious how this is happening / what the underlying problem is. Does anyone know, or can they point me to something that might help?
-- edit:
Prior to posting this question, I had found this site: http://www.guypo.com/technical/ies-premature-execution-problem/
While it seemed (and still does) like it might be the source of the problem, I can't replicate it under any smaller circumstances. All combinations I've tried still have Date, etc defined ; which isn't too surprising, as otherwise I'm sure others would be seeing many more problems with IE.
If you step through with a javascript debugger at the first point any JS gets run. At the same time add a watch for Date/Array etc. and note when it goes to null. Might be slow and laborious but I can't see why it wouldn't work.
You may want to try adding the script in a document.ready function. In other words, insure that the FB script is processed only after the DOM is ready. But, based on the link you give to Guy's Pod (great article, by the way), it seems you're right in the assertion that IE is downloading and executing the script pre-maturely (hence my suggestion to add a wrapper so that it only executes after the DOM ready event). IE9 is probably sandboxing the executing script (outside the document/window scope).

Auto-load/include for JavaScript

I have file called common.js and it's included in each page of my site using <script />.
It will grow fast as my sites functionality will grow (I hope; I imagine). :)
Lets example I have a jQuery event:
$('#that').click(function() {
one_of_many_functions($(this));
}
For the moment, I have that one_of_many_functions() in common.js.
Is it somehow possible that JavaScript automatically loads file one_of_many_functions.js when such function is called, but it doesn't exist? Like auto-loader. :)
The second option I see is to do something like:
$('#that').click(function() {
include('one_of_many_functions');
one_of_many_functions($(this));
}
That not so automatically, but still - includes wanted file.
Is any of this possible? Thanks in an advice! :)
It is not possible to directly auto-load external javascripts on demand. It is, however, possible to implement a dynamic inclusion mechanism similar to the second route you mentioned.
There are some challenges though. When you "include" a new external script, you aren't going to be able to immediately use the included functionality, you'll have to wait until the script loads. This means that you'll have to fragment your code somewhat, which means that you'll have to make some decisions about what should just be included in the core vs. what can be included on demand.
You'll need to set up a central object that keeps track of which assets are already loaded. Here's a quick mockup of that:
var assets = {
assets: {},
include: function (asset_name, callback) {
if (typeof callback != 'function')
callback = function () { return false; };
if (typeof this.assets[asset_name] != 'undefined' )
return callback();
var html_doc = document.getElementsByTagName('head')[0];
var st = document.createElement('script');
st.setAttribute('language', 'javascript');
st.setAttribute('type', 'text/javascript');
st.setAttribute('src', asset_name);
st.onload = function () { assets._script_loaded(asset_name, callback); };
html_doc.appendChild(st);
},
_script_loaded: function (asset_name, callback) {
this.assets[asset_name] = true;
callback();
}
};
assets.inlude('myfile.js', function () {
/* do stuff that depends on myfile.js */
});
Sure it's possible -- but this can become painful to manage. In order to implement something like this, you're going to have to maintain an index of functions and their corresponding source file. As your project grows, this can be troublesome for a few reasons -- the 2 that stick out in my mind are:
A) You have the added responsibility of maintaining your index object/lookup mechanism so that your scripts know where to look when the function you're calling cannot be found.
B) This is one more thing that can go wrong when debugging your growing project.
I'm sure that someone else will mention this by the time I'm finished writing this, but your time would probably be better spent figuring out how to combine all of your code into a single .js file. The benefits to doing so are well-documented.
I have created something close to that a year ago. In fact, I have found this thread by search if that is something new on the field. You can see what I have created here: https://github.com/thiagomata/CanvasBox/blob/master/src/main/New.js
My project are, almost 100% OOP. So, I used this fact to focus my solution. I create this "Class" with the name "New" what is used to, first load and after instance the objects.
Here a example of someone using it:
var objSquare = New.Square(); // Square is loaded and after that instance is created
objSquare.x = objBox.width / 2;
objSquare.y = objBox.height / 2;
var objSomeExample = New.Stuff("some parameters can be sent too");
In this version I am not using some json with all js file position. The mapping is hardcore as you can see here:
New.prototype.arrMap = {
CanvasBox: "" + window.MAIN_PATH + "CanvasBox",
CanvasBoxBehavior: "" + window.MAIN_PATH + "CanvasBoxBehavior",
CanvasBoxButton: "" + window.MAIN_PATH + "CanvasBoxButton",
// (...)
};
But make this more automatic, using gulp or grunt is something what I am thinking to do, and it is not that hard.
This solution was created to be used into the project. So, the code may need some changes to be able to be used into any project. But may be a start.
Hope this helps.
As I said before, this still is a working progress. But I have created a more independent module what use gulp to keep it updated.
All the magic que be found in this links:
https://github.com/thiagomata/CanvasBox/blob/master/src/coffee/main/Instance.coffee
https://github.com/thiagomata/CanvasBox/blob/master/src/node/scripts.js
https://github.com/thiagomata/CanvasBox/blob/master/gulpfile.js
A special look should be in this lines of the Instance.coffee
###
# Create an instance of the object passing the argument
###
instaceObject = (->
ClassElement = (args) ->
window[args["0"]].apply this, args["1"]
->
ClassElement:: = (window[arguments["0"]])::
objElement = new ClassElement(arguments)
return objElement
)()
This lines allows me to initialize a instance of some object after load its file. As is used in the create method:
create:()->
#load()
return instaceObject(#packageName, arguments)

Categories

Resources