Javascript : Eval external script within defined namespace

806 Views Asked by At

Sorry for the essay, couldn't find a short way to explain this :(

Context

I am currently building a WebPart for Office 365 which is pretty much like a client side App that users can add to their page and configure through a UI.

The WebPart renders some content, and I would like users to be able to provide an external script that could run AFTER the rendering process. So far this is easy, I could simply do something like this in the WebPart code :

// Render the WebPart
this.render();

// Load external script
this.loadExternalScript(this.props.externalScriptUrl);

The issue is I would like the external script to act like a callback, that my WebPart could call in order to provide some context to it.

Solution 1

The first solution that I found, was to provide guidance for the users on how to create their external script using a specific namespace based on the file name, and a precise function so that way my WebPart could :

  • Render
  • Load the external script (Which would populate that specific namespace)
  • Retrieve the external script's namespace (based on the file name)
  • Call the external script callback using the namespace and provide the context

That works well and it looks like this :

MyExternalScript.js

MyNamespace.ExternalScripts.MyExternalScript = {

    onPostRender: function(wpContext) {
        console.log(wpContext);
    }
}

WebPart

// Render the WebPart
this.render();

// Load external script
this.loadExternalScript(this.props.externalScriptUrl);

// Calls the script callback if available
var scriptNamespace = MyNamespace.ExternalScripts.MyExternalScript;
var scriptCallback = scriptNamespace  ? scriptNamespace.onPostRender : null;

if(scriptCallback) {
    scriptCallback(this.wpContext);
}

That works fine but building the namespace based on the file's name is a pretty sketchy thing to ask users to do, and I would love to find something more straight forward than this hacky solution.

Solution 2

Another solution I thought about was to do the following :

  • Remove the funky namespace from external script and leave defined function signature
  • Load script content as a string
  • Let the WebPart dynamically create a unique namespace name for that particular script
  • Prepend the script content with the namespace
  • eval() the whole thing
  • Call the callback

That would look along these lines :

MyExternalScript.js

onPostRender: function(wpContext) {
    console.log(wpContext);
}

WebPart

// Render the WebPart
this.render();

// Load external script content
$scriptContent = this.readExternalScript(this.props.externalScriptUrl);

// Append unique namespace
$scriptContent = "MyNamespace.ExternalScripts.MyExternalScript = {" + $scriptContent + "}";

// Eval everything within that namespace
eval($scriptContent);

// Calls the script callback if available
var scriptNamespace = MyNamespace.ExternalScripts.MyExternalScript;
var scriptCallback = scriptNamespace  ? scriptNamespace.onPostRender : null;

if(scriptCallback) {
    scriptCallback(this.wpContext);
}

I did some quick testing and that looks like it's working, the fact that the WebPart dynamically generates the namespace is way better than asking the user to comply to a complicated namespace, however I am not sure if there is a better solution than using eval().

All I need at the end of the day is to find a way to make my WebPart "aware" of the callback it needs to call. I also have to make sure that the namespacing is WebPart-unique and script-unique as there could be 4 WebParts on the same page, loading different scripts, so I have to avoid namespace conflicts at all costs.

Anyone have a better idea?

Thanks!

2

There are 2 best solutions below

1
Daniel Jee On BEST ANSWER

I don't quite understand the context here but how about passing in the post render function as a callback?

var runExternalScript = Function('onPostRender', `
// your external script
  console.log('rendering...');
  console.log('rendering finished');
  if (onPostRender) onPostRender();
`)

function postCallback(){ console.log('finished!') }

runExternalScript(postCallback)
0
Union3008 On

So as explained in my comment, the main goal is to be able to execute an external script which has access to a "magic" variable "wpContext" provided by the WebPart.

The timing on which the external script is called/evaluated isn't really important since it's up to the WebPart to decide when he wants to call it.

Following your example, I think it would look more like this :

WebPart

// Render the WebPart
this.render();

// Load the external script
var runExternalScript = Function('wpContext', `
  // External script content
  console.log('External script is able to use the WebPart context!');
  console.log(wpContext);
`);

// Run the external script while providing the current WebPart's context
runExternalScript(this.wpcontext)

This is far more elegant than my eval() because I do not have any namespace to generate, and the external script doesn't even have to comply to a specific function signature in order for the WebPart to retrieve it, it can simply use the "wpContext" variable straight up in it's content.

So even if that looks like a better solution, am I missing another solution that wouldn't require the use of eval/Function or this is pretty much the way to go?

Thanks!