How to execute a function through postMessage - javascript

I have this code inside a iframe:
window.addEventListener('message', function(e){
if(e.data == 'test')
console.log(e);
}, false);
and this inside the parent document:
$('#the_iframe').get(0).contentWindow.postMessage('test', 'http://localhost/');
So the parent document sends a "test" message to the iframe and it works.
But how can I define a function in the parent document, and somehow send this function through postMessage to the iframe, which will execute the function locally?
The function does some changes to the document like this:
var func = function(){
$("#some_div").addClass('sss');
}
(#some_div exists in the iframe, not the parent document)

There's nothing that would prevent you from passing a stringified function as postmessage event data. Implementation is trivial, for any function declaration like
function doSomething(){
alert("hello world!");
}
You could encodeURI its string interpretation:
console.log(encodeURI(doSomething.toString()));
//function%20doSomething()%20%7B%0A%20%20%20%20alert(%22hello%20world!%22);%0A%7D
It can then be executed as part of a closure - something not overly imaginative like
eval('('+decodeURI(strCallback)+')();');
There's a fiddle'd proof of concept without the cross-frame architecture - I'll see if I can put together a postMessage version, but it would be non-trivial to host w/jsfiddle
Update
As promised, a full mockup that works (links below). With correct event.origin checks this would be sufficiently inpenetrable, but I know for the fact that our security team would never let eval into production like this :)
Given the option I'd suggest the functionality be normalized across the two pages so that only a parametric message would need to be passed (i.e. pass arguments not functions); however there are definitely a few scenarios where this is a preferred approach.
Parent code:
document.domain = "fiddle.jshell.net";//sync the domains
window.addEventListener("message", receiveMessage, false);//set up the listener
function receiveMessage(e) {
try {
//attempt to deserialize function and execute as closure
eval('(' + decodeURI(e.data) + ')();');
} catch(e) {}
}
Iframe code:
document.domain = "fiddle.jshell.net";//sync the domains
window.addEventListener("message", receiveMessage, false);//set up the listener
function receiveMessage(e) {
//"reply" with a serialized function
e.source.postMessage(serializeFunction(doSomething), "http://fiddle.jshell.net");
}
function serializeFunction(f) {
return encodeURI(f.toString());
}
function doSomething() {
alert("hello world!");
}
Prototype mockup: parent code and iframe code.

You can't really. Although the (draft) spec for postMessage talks about structured objects, e.g. nested objects and arrays, [...] JavaScript values (strings, numbers, Dates, etc) and [...] certain data objects such as File Blob, FileList, and ArrayBuffer objects most browsers only allow strings (including JSON, of course). Read more at MDN or dev.opera. Yet I'm quite sure that it won't be possible to send function objects, at least not as closures preserving their scope.
So you'll end in stringifying the function and eval() it in the iframe, if you really want to execute some code from the parent window. However, I can see no reason for any application to allow evaluation of arbitrary code (even if from registered domains); it would be better to build an message API which can receive (JSON-)string commands and invoke its own methods.

In this case I'd try different approach. Why? Bergi already explained why it won't work the way you want it.
You can define (and redefine your functions) in parent page:
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript">
var fun = function() { alert('I am a function!'); };
$(function() {
// use this function to change div background color
fun = function() {
// get desired div
var theDiv = $('#frame').contents().find('#some_div');
theDiv.css('background', '#ff0000');
};
// or override it, if you want to change div font color
fun = function() {
var theDiv = $('#frame').contents().find('#some_div');
theDiv.css('color', '#ff0000');
};
// in this example second (font color changing) function will be executed
});
</script>
</head>
<body>
<iframe id="frame" src="frame.htm"></iframe>
</body>
</html>
and call your function from within frame-page:
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript">
$(function() {
parent.fun();
});
</script>
</head>
<body>
<div id="some_div">I am a div (but inside frame)!</div>
</body>
</html>
It may be inconvenient, but it works.

Expanding upon the following question you can stringify a function, use postMessage to send the function body over, and then use eval to execute it.
Essentially what you are doing is marshalling the function so that it can be sent to the iframe and then unmarshalling it on the other end. To do so use the following code:
Iframe:
window.addEventListener("message", function (event) {
var data = JSON.parse(event.data);
var callback = window[data.callback];
var value = data.value;
if (data.type === "function")
value = eval(value);
var callback = window[data.callback];
if (typeof callback === "function")
callback(value);
}, false);
function callFunction(funct) {
funct();
}
Parent:
var iframe = $("#the_iframe").get(0).contentWindow;
postMessage(function () {
$("#some_div").addClass("sss");
}, "callFunction");
function postMessage(value, callback) {
var type = typeof value;
if (type === "function")
value = String(value);
iframe.postMessage({
type: type,
value: value,
callback: callback
}, "http://localhost");
}
Once you get the function body in your iframe and eval it you can use it like a normal function. You can assign it to a variable, pass it around, use call or apply on it, bind it, and so on.
Note however that the scope of the function is dynamic (i.e. any non-local variables in the function must be already defined in your iframe window).
In your case the jquery $ variable which you are using in your function is non-local. Hence the iframe must already have jquery loaded. It won't use the jquery $ variable from the parent window.

You can always send postMessage back to the iframe and let iframe handle the message. Of course you must count that it wont get executed before next command in iframe.

Related

Parameter's purpose

Im doing a project right now where certain elements of the page change depending on the mouse positioning and made some code basing myself off an explanation I saw in Mozilla. I understand most of it, but there is still one part that is bothering me, which is the function parameter. Throughout my learning of basic Javascript, I have never understood parameters despite the countless explanations. What does a parameter do? How does the page know its purpose? To give an example here is the code:
<!doctype html>
<html>
<head>
<title>Change</title>
</head>
<body>
<p id="hurb"></p>
<script>
document.addEventListener('mousemove', movee);
function movee(a) {
var eub = a.clientX;
document.getElementById("hurb").innerHTML = eub;
}
</script>
</body>
</html>
What does a do here? How does the page know what it does?
Can someone please explain this in a way that can be understandable for a beginner? All the other explanations in pages dont really help me out.
Thanks!
The parameter, as its name suggests, is something upon which usually the function's return value depends. Hence, your function can have a parameter that is not used throughout the function. Here is an example:
function foo() {
console.log("foo");
}
function bar(par1, par2, par3) {
console.log("bar");
}
foo(); // foo
bar(); // bar
bar(45,100); // bar
Note that however you call bar() it logs "bar", since the parameter is never used inside the function and hence never contributes to the return value of the function.
In your case, the function is an event handler function, i.e. is called when some event (mousemove in your case) is fired. When an event is fired, the browser passes an Event object (particularly a MouseEvent object in your case) to the event handler function. This event object is a typical JS object, with properties such as clientX, clientY, pageX, pageY, etc.... By defining your function with a parameter, that parameter is going to take the value of the Event object. To access the Event object's properties, you do not need to define a parameter for your function. Hence, this function:
function movee(a) {
var eub = a.clientX;
document.getElementById("hurb").innerHTML = eub;
}
and this one:
function movee() {
var a = event;
var eub = a.clientX;
document.getElementById("hurb").innerHTML = eub;
}
are typical of each other.
How does the page know its purpose?
The page doesn't. All the page does is that it executes your function. If it encounters the parameter, it uses its value. Otherwise, it just continues execution.

how to call a javascript function within a function

I have javascript file called screener.js
function ScreenerPage() {
function onScreenListChange() {
do stuff
};
}
from the index.html file I include the javascript file like this:
<script type="text/javascript" src="./js/screener.js"></script>
Then later in the head section of index.html I instantiate the screenerPage object like this:
<script type="text/javascript">
$(document).ready(function () {
screenerPage = new ScreenerPage();
}
</script>
Then down in the body section there is a select with onchange event that calls
<select id="screenList" onchange="screenerPage.onScreenListChange()">
but the browser shows error:
Uncaught TypeError: screenerPage.onScreenListChange is not a function
What am I doing wrong?
The way javascript works is it has objects and the way of which they are created matter!
Here is the way i've found that works for this kind of thing
screener.js
var ScreenerPage = function() {
this.onScreenListChange = function() {
//do stuff
console.log("test")
}
}
Later on
var a = new ScreenerPage();
a.onScreenListChange();
if you have any questions on how it works feel free to try to message me!
The reason it does not work is that you're having a scope issue.
The function ScreenerPage is defined in the global scope, meaning it is accessible anywhere. Now inside that function you define a local function called onScreenListChange.
You are able to do that, but that function only exists within the scope of the function you defined it in.
When I look at your function, I think you want to use classes. A class is a single name that can have multiple variables / methods to do a specific task.
class ScreenerPage {
constructor(text){
this.onScreenListChange(text) // Call this "method" on object creation.
}
onScreenListChange(text) {
console.log(text ? text : 'do stuff');
};
}
var a = new ScreenerPage('hi'); // now watch your console log.
a.onScreenListChange();

Add event listener of a method present in another js file

I am accessing few methods written in another js file. So i'm accessing them like this:
file1:
function minit() {
this.addval = function(val1, val2) {
return val1 + val2;
}
function autoexecute(d) {
//do something here//
//raise event//
}
};
file2:
var con = new minit();
var result = con.addval(2, 3);
/*
con.autoexecute(function(d) { //Wanna do something like this
alert(d);
});
*/
Above things are working as expected, getting result..
Now, Suppose autoexecute(d) method is invoking automatically after a time interval. How can i know if the method is executed ?
So that, I want to create an event(in file2) of autoexecute(d)(in file1).
UPDATE:
I hope this example will help you to understand the issue..
company.js //this is the main file which will be used as a reference in ui.html
function hello(personname) { //this method will invoke automatically after 1 minute..
}
ui.html
<script src="company.js"></script>
<script>
$(document).ready(function() {
function bye(personame) { //this method will be called automatically if hello method invoked.... personame is the argument passed from hello method//
alert("comany.js -> hello function is executed");
}
});
</script>
You can only do this if the functions have the same scope (global scope is the best case scenario). If the autoexecute function has local scope then you cannot to do it.
In essence, override the original function like this...
// keep a reference to the original function
var _autoexecute = autoexecute;
// override the original function with your new one
function autoexecute(d) {
alert("before autoexecute"); // fired before the original autoexecute
_autoexecute(d); // call the original autoexecute function
alert("after autoexecute"); // fired after the original autoexecute
}
Now, whenever autotexecute is called it will call your new function which can handle both before and after events, as well as calling the original function. Just remove the (horrible) alerts and replace with event handlers as required.
To my knowledge, and someone should correct me if I am wrong, there is no way (at least without some library) to detect a function being fired in javascript. Function executions do not fire an event that other functions can 'handle' in that that sense.
In your example you wanted a function to automatically fire after the other function has fired, all you need to do is call the function you want to fire at the end of the one that was "fired" in the first place. Confusing but hope this helps.
function handler(){
alert("main function was fired!");
}
function main(){
//Code of main goes here and then at the end just add:
handler();
}
Now when your "main" has finished its work it will call upon the handler function.
Regardless of where you define the handler function, which can be a different file or same file, so long as it is reachable from within the main's scope, it will be fired at the end of it. It can even be declared after main has been declared, so long as it is declared before main is fired.

Changing execution context from parent to child frame

I'm not even sure if this is possible, but is there anyway to set an execution context beyond setting the value for "this"?
The case I am primarily referring to is executing code from one frame in the context of another frame, so that when I access global objects (ex: window, document...) from a function that was defined in frame1, It will be executed in the frame2 environment.
If this is not possible, what are some workarounds? Please don't say "just define the function in the child frame", I'm dealing with a larger application framework, and it would be both pointless and memory inefficient if I had to load up two instances of the entire framework.
EDIT: Here is some code which should demonstrate what I am trying to do. When run, it should show an alert that, if a solution is found, displays "iframe.html" at the end of the location string.
<script>
function run() {
go.call(window.iframe);
}
function go() {
alert(window.location);
}
</script>
<iframe src="iframe.html" name="iframe" onload="run()">
Thanks.
This makes use of the deprecated with statement as well as eval, but it may be the only way to accomplish what you want.
function run() {
with (window.iframe) {
eval("(" + go.toString() + ")()");
}
}
function go() {
alert(window.location);
}
If you can "frame" your "integration" code in a closure that aliases window to window.iframe, you could achieve what you want:
(function(window) {
// your integration code...
// the whole code you want to frame...
alert(window.location);
})(window.iframe);
But, you have to "frame" all the code you want to interact with.
Also, you can expose "integration" functions for you to call from "outside" by passing other "context" objects in:
var context = {};
(function(window, context) {
// your integration code...
context.f = function() { ... };
// the whole code you want to frame...
alert(window.location);
})(window.iframe, context);
context.f();
Use Function.apply and Function.call.
function foo(x, y, z) {
console.log(this === someFrame); // true
}
foo.call(someFrame, 1, 2, 3);
EDIT
Based on your code sample and comments below, the answer is no. Scripts cannot change the global scope.
Include the code in both frames. Then, call the function in the other frame. Let's say there's a top level global function called doWork().
You field a button press in frame2 and you want to execute a function in frame1 (where frame1 is the window object representing frame1). Assuming they are in the same domain and pass the same-origin tests, you can simply reach into frame1 and call doWork() in that frame with:
frame1.doWork();
This works because all top level javascript global variables and functions are properties of the window object for that browser window/frame. Each window/frame has it's own set of top level properties. If you are within the same domain and can get the appropriate window object, you can just call javascript in that window. The javascript must be present in that window. You can't execute javascript from your window in the content of another window, but if you put the code in both windows, you can execute it in either window by starting with the right window object.
How you get the right window object for a particular frame depends upon how your frames are structured.
Again assuming these frames are in the same domain and pass the usual same-origin tests, it would also be possible write a javascript function in frame2 that operates on frame1 contents. For example, you could have this code in frame2:
frame1.document.getElementById("foo").value = "";
So, you couldwrite a function in frame2 that would operate on frame1. You cannot, however, just execute a function from frame2 in the content of frame1 without writing it to know about operating on a different frame.
For example, you could write a function like this that takes the desired window to operate on:
function clearValue(id, win) {
win.document.getElementById(id).value = "";
}

Creating a JavaScript callback in an iframe

UPDATE: The back-end service was powered by an ASP.Net AJAX Web Service proxy.
1) The main page has two global objects, one for the back end connections (Svc) and another for handling the DOM (Main). It also dynamically loads documents into an iframe.
2) These iframes need to access services provided by Svc, and also supply a callback function.
3) The problem - passing a function created in the iframe to the parent frame, it's treated as an object not a function and cannot be invoked.
Can anyone provide a better solution than what I've got currently in the iframe:
var Main = parent.Main,
Svc = parent.Svc;
Svc.method(data, Main.createCallback(
function(response) {}
));
and in the parent frame:
Main.createCallback = function(func) {
return function() {
func.apply(func, arguments);
}
}
if you override the iFrame's function from the main, the main scope will then be used.
The inverse problem can be seen here, in your case, you just override the frame's function itself i.e:
document.getElementById('yourFrameID').contentWindow.targetFunctionInFrame = targetFunctionInMain;
Bonus: if you can modify the iFrame's code, I would suggest to:
In the frame:
make a placeholder function callbackParent() {}
add a call to this function into your iframe code, so that you just have to override the callbackParent from your main.
In the main:
make the function which should be invoked function doStuff() {}
override the function as described above document.getElementById('yourFrameID').contentWindow.callBackParent = doStuff;
I use iframes to modularize my app too.They are a kind of includes embedding all CSS, HTML and JS for a module.
My first attempts were by returning a function too, but then I found it quite hard for sharing scopes.
Now I make directly a reference to the main parent object in the iframe.
eg:
var Svc = parent.Svc, JSON = parent.JSON, $ = parent.$;
Svc.module1 = {
method1:function(arg){
...
},
...
}
The global var JSON and jQuery references are here to have them available inside the methods.
My guest is that Svc.method is making some checks to see if the callback has some criteria before calling it. This criteria might be that the callback function must created by the same framework (here it's ASP.Net). You have to find what that criteria is. if "Main.createCallback" works, it's because it's meeting that criteria.
Sorry but your all wrong... add this....
const event = new CustomEvent('MSGSent', { detail: "fff variable" });
Call it like this....use a global variable for detail... like an array []
window.dispatchEvent(event);
Now after the iframe loads add this code and you get an Object back in the main page....
iframe.onload = function() {
try {
iframe.contentWindow.addEventListener('MSGSent',function(e){
alert(e.detail);
});
} catch (error) {
}
};
The problem is ASP.Net AJAX Web Service proxies, which don't appear to support calling the web service from an iframe with an inline callback function.

Categories

Resources