"Outsource" recurring code into a function with parameters (js) - javascript

so i use one js file to load multiple html and js files whenever they are needed. I have a working code for plenty modules. In the example below you can see the first two modules. All of them look exactly the same. Now i want to "outsource" recurring code into a function with parameters so that the code-amount overall gets minimized. Since i have never done something like this before i could need some help (i am learning js at the moment). I would realy appreciate some help.
//first module
if (moduleID === "placeone") {
var isLoaded = 0;
if (isLoaded) {
console.log("file already loaded");
returnValue = new PlaceOneModule(id, moduleInitialData);
}
$("#placeone").load("html/modules/PlaceOneModule.html", function (response, status, xhr) {
console.log("PlaceOneModule.html" + " " + status);
$.getScript("js/modules/PlaceOneModule.js").done(function () {
console.log("PlaceOneModule.js geladen");
isLoaded = 1;
returnValue = new PlaceOneModule(id, moduleInitialData);
}).fail(function () {
console.log("PlaceOneModule.js nicht geladen");
});
});
}
//second module
if (moduleID === "placetwo") {
var isLoaded = 0;
if (isLoaded) {
console.log("file already loaded");
returnValue = new PlaceTwoModule(id, moduleInitialData);
}
$("#placetwo").load("html/modules/PlaceTwoModule.html", function (response, status, xhr) {
console.log("PlaceTwoModule.html" + " " + status);
$.getScript("js/modules/PlaceTwoModule.js").done(function () {
console.log("PlaceTwoModule.js geladen");
isLoaded = 1;
returnValue = new PlaceTwoModule(id, moduleInitialData);
}).fail(function () {
console.log("PlaceTwoModule.js nicht geladen");
});
});
}

The question is rather complex to answer, as there are many things to account for.
var cache = {};
function module(name, target, done) {
if (!(name in cache)) {
return $(target).load('html/modules/' + name + '.html', function(response, status, xhr) {
console.log(name + '.html ' + status);
$.getScript('js/modules/' + name + '.js')
.done(function() {
console.log(name + '.js geladen');
cache[name] = window[name];
done(null, cache[name]);
})
.fail(function() {
var message = name + '.js nicht geladen';
cache[name] = function() {
console.error(message);
};
done(message);
});
});
}
setTimeout(function() {
done(null, cache[name]);
}, 0);
}
I'll try to explain my train of thought behind this:
var cache = {} - you will need something to keep track of each individual module
function module(name, target, done) {
name would be the base name of your module, e.g. PlaceTwoModule, this was already used consistently across the html and js files and the js function name
target would be the selector where the html file should be loaded
as one of the actions you take requires async operation, the entire functionality needs to become async, I introduce a callback (done) argument
if (!(name in cache)) - if the module is not yet cached, it requires some fetching, so the load is triggered first thing
once the load completes, it will fire the $.getScript
if the $.getScript works out, the name will be assumed to be in window and a reference is stored in the cache variable, after that, the done callback is invoked (with the function as second argument).
if the $.getScript didn't work out, we add a function to the cache, which does nothing more than telling you it will not work, after that, the done callback is invoked (with an error as first argument).
if the name did exist in the cache, we will be calling the done callback right after we exit the module function
So, how to use this?
It now boils down to calling the module function
module('#placeone', 'PlaceOneModule', function(error, PlaceModule) {
if (error) throw new Error(error);
var instance = new PlaceModule(id, initial);
// ...
}
I have used the common function(error, value) {..} signature for the callback function, which always has the error as first argument (allowing for other arguments to be added and made optional).
There are some caveats/assumptions worth mentioning:
I have not provided a failsafe for preventing multiple loads of the same module (so it is the same as in your example) if earlier calls to module are still loading
no matter what target you invoke module with, it will only load 'once' (well, see the previous line ;-) )
I assume the loaded modules are in the global (window) scope in order to keep the example simple, keep in mind to not 'pollute the global scope'
This has become a rather elaborate answer, I hope I explained every step involved sufficiently.

Something like this possibly:
var modules = [];
modules.push({
js: 'PlaceOneModule',
id: 'placeone'
});
modules.push({
js: 'PlaceTwoModule',
id: 'placetwo'
});
var module = modules.filter(function(m) {
return m.id === moduleID;
});
if (module) {
var isLoaded = 0;
if (!!window[module.js]) {
console.log("file already loaded");
returnValue = window[module.js];
}
$("#" + module.id).load("html/modules/" + module.js + ".html", function(response, status, xhr) {
$.getScript("js/modules/" + module.js + ".js").done(function() {
returnValue = new window[module.js](id, moduleInitialData);
}).fail(function() {
console.log("PlaceOneModule.js nicht geladen");
});
});
}

Related

This code doesn't seem to fire in order?

My problem is that the code does not seem to be running in order, as seen below.
This code is for my discord.js bot that I am creating.
var Discord = require("discord.js");
var bot = new Discord.Client();
var yt = require("C:/Users/username/Documents/Coding/Discord/youtubetest.js");
var youtubetest = new yt();
var fs = require('fs');
var youtubedl = require('youtube-dl');
var prefix = "!";
var vidid;
var commands = {
play: {
name: "!play ",
fnc: "Gets a Youtube video matching given tags.",
process: function(msg, query) {
youtubetest.respond(query, msg);
var vidid = youtubetest.vidid;
console.log(typeof(vidid) + " + " + vidid);
console.log("3");
}
}
};
bot.on('ready', () => {
console.log('I am ready!');
});
bot.on("message", msg => {
if(!msg.content.startsWith(prefix) || msg.author.bot || (msg.author.id === bot.user.id)) return;
var cmdraw = msg.content.split(" ")[0].substring(1).toLowerCase();
var query = msg.content.split("!")[1];
var cmd = commands[cmdraw];
if (cmd) {
var res = cmd.process(msg, query, bot);
if (res) {
msg.channel.sendMessage(res);
}
} else {
let msgs = [];
msgs.push(msg.content + " is not a valid command.");
msgs.push(" ");
msgs.push("Available commands:");
msgs.push(" ");
msg.channel.sendMessage(msgs);
msg.channel.sendMessage(commands.help.process(msg));
}
});
bot.on('error', e => { console.error(e); });
bot.login("mytoken");
The youtubetest.js file:
var youtube_node = require('youtube-node');
var ConfigFile = require("C:/Users/username/Documents/Coding/Discord/json_config.json");
var mybot = require("C:/Users/username/Documents/Coding/Discord/mybot.js");
function myyt () {
this.youtube = new youtube_node();
this.youtube.setKey(ConfigFile.youtube_api_key);
this.vidid = "";
}
myyt.prototype.respond = function(query, msg) {
this.youtube.search(query, 1, function(error, result) {
if (error) {
msg.channel.sendMessage("There was an error finding requested video.");
} else {
vidid = 'http://www.youtube.com/watch?v=' + result.items[0].id.videoId;
myyt.vidid = vidid;
console.log("1");
}
});
console.log("2");
};
module.exports = myyt;
As the code shows, i have an object for the commands that the bot will be able to process, and I have a function to run said commands when a message is received.
Throughout the code you can see that I have put three console.logs with 1, 2 and 3 showing in which order I expect the parts of the code to run. When the code is run and a query is found the output is this:
I am ready!
string +
2
3
1
This shows that the code is running in the wrong order that I expect it to.
All help is very highly appreciated :)
*Update! Thank you all very much to understand why it isn't working. I found a solution where in the main file at vidid = youtubetest.respond(query, msg) when it does that the variable is not assigned until the function is done so it goes onto the rest of my code without the variable. To fix I simply put an if statement checking if the variable if undefined and waiting until it is defined.*
Like is mentioned before, a lot of stuff in javascript runs in async, hence the callback handlers. The reason it runs in async, is to avoid the rest of your code being "blocked" by remote calls. To avoid ending up in callback hell, most of us Javascript developers are moving more and more over to Promises. So your code could then look more like this:
myyt.prototype.respond = function(query, msg) {
return new Promise(function(resolve, reject) {
this.youtube.search(query, 1, function(error, result) {
if (error) {
reject("There was an error finding requested video."); // passed down to the ".catch" statement below
} else {
vidid = 'http://www.youtube.com/watch?v=' + result.items[0].id.videoId;
myyt.vidid = vidid;
console.log("1");
resolve(2); // Resolve marks the promises as successfully completed, and passes along to the ".then" method
}
});
}).then(function(two) {
// video is now the same as myyt.vidid as above.
console.log(two);
}).catch(function(err) {
// err contains the error object from above
msg.channel.sendMessage(err);
})
};
This would naturally require a change in anything that uses this process, but creating your own prototypes seems.. odd.
This promise returns the vidid, so you'd then set vidid = youtubetest.response(query, msg);, and whenever that function gets called, you do:
vidid.then(function(id) {
// id is now the vidid.
});
Javascript runs async by design, and trying to hack your way around that leads you to dark places fast. As far as I can tell, you're also targetting nodeJS, which means that once you start running something synchronously, you'll kill off performance for other users, as everyone has to wait for that sync call to finish.
Some suggested reading:
http://callbackhell.com/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://stackoverflow.com/a/11233849/3646975
I'd also suggest looking up ES6 syntax, as it shortens your code and makes life a hellofalot easier (native promises were only introduced in ES6, which NodeJS 4 and above supports (more or less))
In javascript, please remember that any callback function you pass to some other function is called asynchronously. I.e. the calls to callback function may not happen "in order". "In order" in this case means the order they appear on the source file.
The callback function is simply called on certain event:
When there is data to be processed
on error
in your case for example when the youtube search results are ready,
'ready' event is received or 'message' is received.
etc.

Using promise instead of events Call back

I'm having the following code, this all code is implemented in specified function myFunc, I need that the all function will finish (myFunc) i.e. when the file was extracted successfully/or not to return some status (success/ error).
var myFunc = () => {
var DecompressZip = require('decompress-zip');
var unzipper = new DecompressZip(filename)
unzipper.on('error', function (err) {
console.log('Caught an error');
});
unzipper.on('extract', function (log) {
console.log('Finished extracting');
});
unzipper.on('progress', function (fileIndex, fileCount) {
console.log('Extracted file ' + (fileIndex + 1) + ' of ' + fileCount);
});
unzipper.extract({
path: 'some/path',
filter: function (file) {
return file.type !== "SymbolicLink";
}
});
};
Since this open source is working with event this is a problem (to get return status...) my intention is to change it to promise by promisify or
like following:
var myFunc = () => {
return new Promise(function(resolve, reject) {
var DecompressZip = require('decompress-zip');
var unzipper = new DecompressZip(filename)
unzipper.on('error', function (err) {
console.log('Caught an error');
reject();
});
unzipper.on('extract', function (log) {
console.log('Finished extracting');
resolve();
});
unzipper.on('progress', function (fileIndex, fileCount) {
console.log('Extracted file ' + (fileIndex + 1) + ' of ' + fileCount);
});
unzipper.extract({
path: 'some/path',
filter: function (file) {
return file.type !== "SymbolicLink";
}
});
};
My questions are:
Since I'm not too expert in JS Does it make sense to convert the events to promise?
There is other
good solution which I can use for the use-case?
This is the OP
https://github.com/bower/decompress-zip
1) Yes it makes perfect sense to convert such events to a Promise. Currently, different libraries have different ways of expressing events (events, messages, callback functions, then error callback function, exceptions, error codes, etc...). Promises will soon unify all this now they're a Javascript standard. So it a good practice to put a Promise layer around the libraries you're using and use only Promises in your code instead of the old mess.
2) Your solution looks good to me.
Converting events to promises only makes sense when you are absolutely positive that the "end" event will only fire once.
So in this case, yes, what you are suggesting for the implementation should work (assuming I understand your code correctly).

Parse Cloud - delete property (pointer)

My data model consist of 3 objects, two of them (the children) are linked to the parent using a pointer.
MyModel is the parent that has 2 properties: colors and goal. Both are pointers to other Objects.
When i delete the parent I want the children to be deleted as well, the caveat is that the pointer might be nil, so I'd need to check if there is something there before attempting deletion.
[I'm new to Javascript so maybe that's also part of the problem]
Parse.Cloud.beforeDelete("MyModel", function(request) {
if request.has(request.object.colors) {
color = request.object.colors;
Parse.Object.destroyAll(color, {
success: function() {},
error: function(error) {
console.error("Error deleting related color " + error.code + ": " + error.message);
}
});
}
if request.has(request.object.goal) {
goal = request.object.goal;
Parse.Object.destroyAll(goal, {
success: function() {},
error: function(error) {
console.error("Error deleting related goal " + error.code + ": " + error.message);
}
});
}
});
Lets break this into smaller functions and correct a couple problems in the OP code along the way. It's very helpful to reduce things to smaller, promise-returning functions keep the code modular and the concurrency manageable.
EDIT
Generally, it's preferred to use pointers to relate objects. Here's a general purpose function to delete an object related via pointer:
function deleteRelatedPointer(myModel, pointerName) {
var pointer = myModel.get(pointerName);
if (pointer) {
return pointer.fetch().then(function(relatedObject) {
return relatedObject.destroy();
});
} else {
return null;
}
}
Some authors relate objects via a string column containing the id of the related object. Here's the equivalent function to delete an object related by id:
function deleteRelatedId(myModel, columnName, relatedClass) {
var relatedId = myModel.get(columnName);
if (relatedId) {
var query = new Parse.Query(relatedClass);
return query.get(relatedId).then(function(relatedObject) {
return relatedObject.destroy();
});
} else {
return null;
}
}
Now, the beforeDelete method is easy to write and understand. (Assuming the relationships via pointers):
Parse.Cloud.beforeDelete("MyModel", function(request, response) {
var myModel = request.object;
deleteRelatedPointer(myModel, "colors").then(function() {
return deleteRelatedPointer(myModel , "goal");
}).then(function() {
response.success();
}, function(error) {
response.error(error);
});
}
The other important thing to notice is that the before hook takes a response object and is required to invoke success / error on that object after the related tasks are complete.
All of this hinges on promises, which are necessary and immensely useful. Read about parse's implementation of them here.

Why does my jQuery when().then() function trigger before the ajax request in when is done?

I need to set an async callback, because a function fetches content from a remote location. I'm doing this:
$.when( priv[box.view.renderWith](content, box.view.gadget_id) ).then(function(late) {
console.log("done");
console.log(late)
console.log($(content))
$(content).append(late).enhanceWithin();
});
with my when function triggering a single Ajax request. In it's callback I'm returning an element to append to $(content).
My problem is, the then function fires immediately and long before my ajax callback is run and returns something.
Question:
Is it not possible to use when() with a function that makes an ajax-request? Do I have to make the ajax request directly in when()? Or why is then() triggered right away? How could I workaround this?
Thanks!
EDIT:
My current version of the snippet:
$.when( priv[box.view.renderWith](content, box.view.gadget_id) ).then(function(fragment) {
// DOM manip...
console.log("NOW WE ARE DONE WITH WHEN");
console.log(fragment)
$(content).append(fragment).enhanceWithin();
});
And the function, I'm calling (without content generation part):
priv.constructListbox = function (element, internal) {
var no_data_body,
no_data_cell,
portable,
gadget_id = element.getAttribute("data-gadget-id") || internal,
settings = priv.gadget_properties[gadget_id],
portal_type = settings.portal_type_title,
// wrapper
$parent = $(element.parentNode);
if (settings !== undefined) {
// ASYNC > this will trigger an Ajax request
portable = priv.erp5.allDocs({
"query": "type: \"" + settings.datasource + "\"",
"limit": [0, (settings.configuration.pagination.items_per_page_select[0] || 30)],
"wildcard_character": "%",
"include_docs": true
}).always(function (answer) {
.... stuff ...
// finish
// return to calling function
if (internal) {
console.log("foo");
console.log("no we only give back a fragment");
return fragment_container;
}
$parent.empty().append( fragment_container ).enhanceWithin();
});
// if internal call, return the promise object
if (internal) {
console.log("foo internal, promise");
return portable;
}
} else {
// error handler
}
};
When I console portable inside my then callback, I get the promise object, so now the function is returning the promise vs an element. However when resolved, I was hoping I would get my fragment_container when I'm not ... getting anything :-(
Hopefully clear enough.
Best advice I ever heard is to treat Async programming like normal functions and then add the promises at the end.
I'm having diffculty seeing where you are setting fragment_container, but here goes..
priv.constructListbox = function (element, internal) {
var dfd = new $.Deferred();
...
if (settings !== undefined) {
portable = priv.erp5.allDocs({
"query": "type: \"" + settings.datasource + "\"",
"limit": [0, (settings.configuration.pagination.items_per_page_select[0] || 30)],
"wildcard_character": "%",
"include_docs": true
}).always(function (answer) {
.... stuff ...
// finish
// return to calling function
if (internal) {
console.log("foo");
console.log("no we only give back a fragment");
dfd.resolve({message:"You did it!", element: fragment_container });
}
$parent.empty().append( fragment_container ).enhanceWithin();
});
} else {
dfd.reject({result:"Nope - no data came out"});
// error handler
}
return dfd.promise();
};
then it's easy to see what you've returned:
$.when( priv[box.view.renderWith](content, box.view.gadget_id) ).then(function(fragment) {
console.log("NOW WE ARE DONE WITH WHEN");
console.log(fragment);
},
function(fragment) {
console.log("It failed");
console.log(fragment);
});

Async Template loading

I'm trying to asynchronously load 25 html templates
Here's my code:
var loopingLoadTemplate = function(index){
var name = names[index];
$.get('templates/' + name + '.html', function (data) {
that.templates[name] = data;
tplCount++;
console.log(tplCount + " " + names.length);
if (tplCount === names.length){
callback();
}
});
};
for (i = 0; i < names.length; i++){
loopingLoadTemplate(i);
}
tplCount is a counter that I keep so I know when it's safe to fire the callback
the problem is that, there are 25 templates to load, and when I checked under network tab in Chrome console, I see all 25 templates getting loaded properly, but console.log tells me the tplCount stops at 21, which I have no idea why. Is it because the for loop is firing so fast that some callbacks of the $ functions did not fire?
How do I safely asynchronously load all these templates?
So I also tried an synchronously fallback using recursive calls, but it mysteriously stops after loading some templates and gives no warning sign
var loadTemplate = function (index) {
var name = names[index];
console.log("loading " + names[index]);
$.get('templates/' + name + '.html', function (data) {
that.templates[name] = data;
index++;
if (index < names.length) {
loadTemplate(index);
console.log("trying another load");
} else {
callback();
console.log("trying callback");
}
});
};
loadTemplate(0);
OK, just figured out why its failing
so, indeed all the templates are loaded properly, but, if a html template has no content, the data in the callback function becomes undefined, instead of being empty as what I had expected
when the data is undefined, it fails the line:
that.templates[name] = data;
without any warnings or errors and the rest of the code in the callback are not excuted
since all the templates do get loaded, checking the network tab would give all status success result

Categories

Resources