Including objects from external .js files - javascript

I have been searching for many hours over several days for this answer and though there are many topics on how to include files in a project (also here at Stack Overflow), I have not yet found THE solution to my problem.
I'm working on a project where I want to include one single object at a time, from many different files (I do not want to include the files themselves, only their content). All the object in all the files have the same name, only the content is different.
It is important that I do not get a SCRIPT tag in the head section of the page as all the content from the files will have the same names. None of the files will have functions anyways, only one single object, that will need to be loaded one at the time and then discarded when the next element is loaded.
The objects will hold the data that will be shown on the page and they will be called from the menu by an 'onclick' event.
function setMenu() // The menu is being build.
{
var html = '';
html += '<table border="0">';
for (var i = 0; i<menu.pages.length; i++)
{
html += '<tr class="menuPunkt"><td width="5"></td><td onclick="pageName(this)">'+ menu.pages[i] +'</td><td width="5"></td></tr>';
}
// menu is a global object containing elements such as an array with
// all the pages that needs to be shown and styling for the menu.
html += '</table>';
document.getElementById("menu").innerHTML = html;
style.setMenu(); // The menu is being positioned and styled.
}
Now, when I click on a menu item the pageName function is triggered and I'm sending the HTML element to the function as well, it is here that I want the content from my external file to be loaded into a local variable and used to display content on the page.
The answer I want is "How to load the external obj into the function where I need it?" (It may be an external file, but only in the term of not being included in the head section of the project). I'm still loading the the file from my own local library.
function pageName(elm) // The element that I clicked is elm.
{
var page = info.innerHTML; // I need only the innerHTML from the element.
var file = 'sites/' + page + '.js'; // The file to be loaded is created.
var obj = ?? // Here I somehow want the object from the external file to be loaded.
// Before doing stuff the the obj.
style.content();
}
The content from the external file could look like this:
// The src for the external page: 'sites/page.js'
var obj = new Object()
{
obj.innerHTML = 'Text to be shown';
obj.style = 'Not important for problem at hand';
obj.otherStuff = ' --||-- ';
}
Any help will be appreciated,
Molle

Using the following function, you can download the external js file in an ajax way and execute the contents of the file. Please note, however, that the external file will be evaluated in the global scope, and the use of the eval is NOT recommended. The function was adopted from this question.
function strapJS(jsUrl) {
var jsReq = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
if (jsReq === null) {
console.log("Error: XMLHttpRequest could not be initiated.");
}
jsReq.onload = function () {
try {
eval(jsReq.responseText);
} catch (e) {
console.log("Error: The script file contains errors." + e);
}
};
try {
jsReq.open("GET", jsUrl, true);
jsReq.send(null);
} catch (e) {
console.log("Error: Cannot retrieving data." + e);
}
}
JSFiddle here
Edit: 1
After some refactoring, I came up with this:
function StrapJs(scriptStr, jsObjName) {
var self = this;
self.ScriptStr = scriptStr;
self.ReturnedVal = null;
function _init() {
eval(self.ScriptStr);
self.ReturnedVal = eval(jsObjName);
}
_init();
}
You can then get the script string any way you want and just instantiate a new StrapJs object with the script string and name of the object to return inside the script string. The ReturnedVal property of the StrapJs object will then contain the object you are after.
Example usage:
var extJS = "var obj = " +
"{ " +
" innerHTML : 'Text to be shown', " +
" style : 'Not important for problem at hand', " +
" otherStuff : ' --||-- ' " +
"}; ";
var extJS2 = "var obj = " +
"{ " +
" innerHTML : 'Text to be shown 2', " +
" style : 'Not important for problem at hand 2', " +
" otherStuff : ' --||-- 2' " +
"}; ";
var strapJS = new StrapJs(extJS, 'obj');
var strapJS2 = new StrapJs(extJS2, 'obj');
console.log(strapJS.ReturnedVal.innerHTML);
console.log(strapJS2.ReturnedVal.innerHTML);
See it in action on this fiddle

Related

Alternate for eval() to execute auto-generated JS code from the server

var val = 3;
var code = "var a = 5; if (a >= val) { console.log(a + ' >= ' + val); a; } else { console.log(a + ' < 3 ' + val); val; }";
console.log(eval(code));
This is the scenario where an alternative to eval() is required.
The Server can send any kind of JS code which could be run on a particular block.
Do not use eval(code) or new Function(code) as both are basically the same thing and should be blocked by CSP.
Just return your content from the server as content-type: text/javascript then get it into your page with a <script> block or import.
On the server you would have something like (pseudo code, as I don't know what tech stack you're on):
[Route("serverActionReturningCode")]
public string ActionReturningCode()
{
// return the content as JS
Response.Headers.Add("content-type", "text/javascript");
// build the response object as JS
return "window.latestResult = {" +
"a: '" + a + "', " +
"b: '" + b + "', " +
"generatedCode: function() { ... }" +
"};";
}
Then in your page:
<script src="serverActionReturningCode"></script>
<script>
// Now the script above has run and set window.latestResult
console.log('a', window.latestResult.a);
console.log('b', window.latestResult.b);
console.log('function output', window.latestResult.generatedCode());
</script>
This will let you dynamically generate JS functions on the server.
However, if you can avoid the functions and just need to pass values it is a lot simpler to use JSON instead.
It seems to be like there is no way other than to live with eval or change the entire design of the application. Even if we look for any other alternatives, it's going to be the change in the name and syntax. But the security issues are going to be the same. Its the design of the application that JS CodeGen tool in the server will generate JS code snippets and send it via JSON in certain fields which has to be picked and executed in the front-end. But in this design, we can assure one thing that the JS code is generated only at the design time of the user and not at the runtime.
Thanks for your help.
You can do it like this. Using Eval() is not recommended.
function looseJsonParse(obj){
return Function('"use strict";return (' + obj + ')')();
}
console.log(looseJsonParse(
"{a:(4-1), b:function(){}, c:new Date()}"
))
Refer this MDN article https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
to dig more into it.

D365 Error when calling function from another JS

I am having an issue calling JS function from another JS file.
Main JS where the function is defined.
var Common = Common || {};
Common.BaseAction = Common.BaseAction || {};
Common.BaseAction.SetNotification = function (message, level, uniqueId)
{
Xrm.Page.ui.setFormNotification(message, level, uniqueId);
}
Common.BaseAction.clearNotification = function (uniqueId) {
Xrm.Page.ui.clearFormNotification(uniqueId);
}
JS from where I am calling the function
var apItem = apItem || {};
apItem.BaseForm = apItem.BaseForm || {};
apItem.BaseForm.SetName = function ()
{
var bookName = Xrm.Page.getAttribute("ap_bookid").getValue()[0].name;
var condition = Xrm.Page.getAttribute("ap_condition").getText();
if (bookName !== null && condition !== null) {
Xrm.Page.getAttribute("ap_name").setValue(bookName + " - " + condition);
}
}
apItem.BaseForm.CountOverDueBy = function() {
var rentedTill = Xrm.Page.getAttribute("ap_rented_till").getValue();
var nowD = Date.now();
if (rentedTill !== null) {
var overdueBy = parseInt((Date.now() - rentedTill) / 86400000);
if (overdueBy > 0) {
Xrm.Page.getAttribute("ap_overdue_by").setValue(overdueBy);
Common.BaseAction.SetNotification("Book is Overdue by " + overdueBy
+ " Days.", "WARNING", "OverDueWarning");
}
else {
Xrm.Page.getAttribute("ap_overdue_by").setValue(null);
Common.BaseAction.clearNotification("OverDueWarning");
}
}
}
In the entity's form, I have added both above files with common.js being at the top and from the event handler I am calling function apItem.BaseForm.CountOverDueBy
Save + Published and Ctrl + F5 gives following error
ReferenceError: Common is not defined
at Object.apItem.BaseForm.CountOverDueBy (https://<domain>/%7B636651014350000438%7D/WebResources/ap_ItemFormBase.js?ver=2091450722:24:13)
at eval (eval at RunHandlerInternal (https://<domain>/form/ClientApiWrapper.aspx?ver=2091450722:153:1), <anonymous>:1:17)
at RunHandlerInternal (https://<domain>/form/ClientApiWrapper.aspx?ver=2091450722:159:1)
at RunHandlers (https://<domain>/form/ClientApiWrapper.aspx?ver=2091450722:118:1)
at OnScriptTagLoaded (https://<domain>/form/ClientApiWrapper.aspx?ver=2091450722:233:1)
at https://<domain>/form/ClientApiWrapper.aspx?ver=2091450722:202:1
I have tried everything but nothing seems to be working.
The way you register the JS files in form, starting from common.js on top & then ap_ItemFormBase.js should work. But product team made few performance improvements around script files like lazy script loading/parallel script loading. This is little tricky & modern scripting is messy between different clients like UUI & web.
Like explained in blog post, if you set the pre-requisite js as dependency, it will load before you consume it in dependent files.
Open the ap_ItemFormBase.js web resource from solution (not from Form Properties), go to Dependencies tab & add the common.js. This will make sure file is ready before reference is used.

Programmatically referencing a javascript file at runtime

I am having a problem programatically referencing a javascript file from
a javascript file. Normally I would just reference it in an html page but can't in this case as the directory location of the file is only available at runtime. I have checked out several posts on this topic but as of yet have been unable to find anything that works. Below are code snippets from 4 files:
index.html assigns an ID to the header tag and references the first of two javascript files called scriptFile.js.
scriptFile.js, is a global data file for all new users.
A second file, also called scriptFile.js, is a user specific data file created for each user at runtime. The second javascript file contains data specific to a single user, is located in a different directory than index.html scriptFile.js, but otherwise is structurally identical to the first scriptFile.js. In both cases the files consist of a function returning an array object.
The last file is index.js. Its purpose is to programmatically reference the second scriptFile.js file identified above.
When index.js runs, the value for scripts.length = 2 which is what it is supposed to be. scripts[0].getAttribute('id') and
scripts[0].getAttribute('src') both output correctly. However, while scripts[1].getAttribute('id') outputs correctly as
id = "scriptOneID", the scripts[1].getAttribute('src') outputs as null which is obviously incorrect. A second problem is
that the data for the scriptOneID.enabled property outputs as true, when it should be outputing as false. This tells me
that the object = loadData() statement is referencing data from the index.html scriptFile.js and not the
index.js userNumber/scriptFile.js. This has been verified in testing.
Here are the code snippets. Suggestions or ideas on how to correct this are appreciated. Thanks...
// 1. index.html page
<head id="headID">
<script id="scriptZeroID" type="text/javascript" src="scriptFile.js">
</script>
// 2. scriptFile.js
function loadData()
{
var object = [{ property: 0, enabled: true }];
return object;
};
// 3. userNumber/scriptFile.js
function loadData()
{
var object = [{ property: 0, enabled: false }];
return object;
};
// 4. index.js page
//global declarations and assignments
var object = new Array;
object = loadData(); // assign the data in loadData function to an object array
// local declarations and assignments
var head = document.getElementById("headID");
var script = document.createElement("script");
var userNumber = sessionStorage.getItem("userNumber");
var scriptPath = userNumber + "/scriptFile.js";
script.id = "scriptOneID";
script.type = "text/javascript";
script.scr = scriptPath;
head.appendChild( script );
var scripts = document.getElementsByTagName("script");
console.log("script: " + scripts.length); // output = 2
for (var i = 0; i < scripts.length; i++)
{
console.log("output: " + scripts[i].getAttribute('id') + " / " + scripts[i].getAttribute('src'));
// output for scripts[0]: id = "scriptZeroID", scr = "scriptFile.js"
// output for scripts[1]: id = "scriptOneID", scr = null
};
object = loadData(); // assign the data in loadData function to an object array
console.log("print test value: " + object[1].enabled ); // output = true // this is incorrect, it should = false
Your issue is a script load time problem. In other words it's an asynchronous problem.
Javascript these days is fast. Really fast. Your script gets to your last call to "loadData" long before the script you injected into the head gets loaded. Therefore you are accessing the original version of "loadData" twice.
Just to clarify my comment on the "notification". Add a function like this (as an example) to you index.js file:
function populateObj() {
object = loadData();
}
Then at the bottom of your injected script add:
populateObj();
That's all there is too it. When the injected script finishes loading and is executed your "object" global variable will have the correct data in it.
JQuery has a function to load scripts at any time after page load.
Looks like this:
$.getScript("yourscripttoload.js", function(){
console.log("Script loaded and executed.");
});
Maybe look into that to see if it will work in your situation.
This is a complete poc solution on how to the programatically reference a javascript file from within a javascript file using a callback function that does not require jQuery. It is based on the premise that the same script file will be run during the page load and then again from a different location, with different data, during runtime . The following solution replaces index.js file in the above example.
// global declarations & assignments
var head = document.getElementById("headID");
var script = document.createElement("script");
var userNumber = sessionStorage.getItem("userNumber");
var scriptPath = userNumber + "/scriptFile.js";
// call back function
function loadScript(scriptPath, callback)
{
script.id = "scriptOneID";
script.type = "text/javascript";
script.src = scriptPath;
script.async = false;
script.defer = false;
// bind the load event to the callback function
script.onreadystatechange = callback;
script.onload = callback;
// append the script to the page header
head.appendChild(script);
};
// place the code that you want to run inside an anonymous function
var anonymousFunction = function()
{
var scripts = document.getElementsByTagName("script");
console.log("script: " + scripts.length); // output = 2
for (var i = 0; i < scripts.length; i++)
{
console.log("output: " + scripts[i].getAttribute('id') + " / " +
scripts[ i].getAttribute('src'));
// output for scripts[0]: id = "scriptZeroID", scr = "scriptFile.js"
// output for scripts[1]: id = "scriptOneID", scr = "1234567890/scriptFile.js" where userNumber = "1234567890"
};
object = loadData(); // assign the data in loadData function to an object array
console.log("print test value: " + object[1].enabled ); // output = false this is correct
};
// execute the loadScript function
loadScript( scriptPath, anonymousFunction );

Remove the map function

I have the following javascript promise, I'm looping through a list of documents, upload them one by one to Dropbox (API call), get back a shared link for each one of the documents, save them in an array and then generate an email with these links.
docs = self.checkedDocs();
body = "Please click on the link(s) below to view your document(s): ";
$.when.apply($, docs.map(function (doc) {
return self.service.getDropboxLink(doc).then(function (dropboxLink) {
return lineBreak + doc.documentDescription() + ": " + dropboxLink;
});
})).done(function () {
var attachment = [].join.call(arguments, '');
formatEmail(attachment, body);
});
What I'm trying to do is the exact same thing but for only one document, I understand that I don't need the map anymore but I'm not sure how to do it.
Could you help?
The $.when construct is used precisely because you want to wait for several promises. With only one document, most of the complexity goes away:
self.service.getDropboxLink(doc).then(function(dropboxLink) {
var attachment = doc.documentDescription() + ": " + dropboxLink;
openEmail("Please click on the link below to view your document: ", attachment);
});

Copy selected List-Items to an other list with ECMA Script

i need a script that copies all my selected list items to an other (custom) list. I found nice solution for documents:
var context = SP.ClientContext.get_current();
var web = context.get_web();
context.load(web);
var _destinationlib = web.get_lists().getByTitle('DestinationLibrary');
context.load(_destinationlib);
var notifyId;
var currentlibid = SP.ListOperation.Selection.getSelectedList();
var currentLib = web.get_lists().getById(currentlibid);
var selectedItems = SP.ListOperation.Selection.getSelectedItems(context);
var count = CountDictionary(selectedItems);
for(var i in selectedItems)
{
alert('Now copying ' + i);
var currentItem = currentLib.getItemById(selectedItems[i].id);
context.load(currentItem);
var File = currentItem.get_file();
context.load(File);
//Excecuting executeQueryAsync to get the loaded values
context.executeQueryAsync
(
function (sender, args) {
if(File != null) {
var _destinationlibUrl = web.get_serverRelativeUrl() + _destinationlib.get_title() + '/' + File.get_name();
File.copyTo(_destinationlibUrl, true);
notifyId = SP.UI.Notify.addNotification('Moving fileā€¦' + File.get_serverRelativeUrl() + 'to' + _destinationlibUrl, true);
//Excecuting executeQueryAsync to copy the file
context.executeQueryAsync(
function (sender, args) {
SP.UI.Notify.removeNotification(notifyId);
SP.UI.Notify.addNotification('File copied successfully', false);
},
function (sender, args) {
SP.UI.Notify.addNotification('Error copying file', false);
SP.UI.Notify.removeNotification(notifyId);
showError(args.get_message());
});
}
},
function (sender, args) {
alert('Error occured' + args.get_message());
}
);
}
I dont know what i have to change to get it working for normal list items. I tried to exchange
var File = currentItem.get_file();
context.load(File);
with
var title = currentItem.get_Title();
context.load(title);
var number = currentItem.get_item('number');
context.load(number);
but it dosnt work. It would be great if somebody can give me a hint what i have to do.
many thx
Fabulus
It looks like that you took code above from here.
Try to be attentively. This code copies selected files (not list items!) to another document library!
For your needs better try to code your own solution. See SharePoint JavaScript Class Library for details. You can have two possible architectures:
Make all work from JavaScript. And the first your step will be addItem method of SP.List.
Make processing of selection on client in JavaScript and call your custom server-side component (may be an application page) for items copying (creating copies in new list of already existed items from initial list.). See this for example.
Also be careful with context.load. It's recommended to write all next code in context.executeQueryAsync. Use Firebug in FF and developer tools in Chrome for debugging your code and to find what is wrong.

Categories