please.js is a Request/Response based wrapper around the PostMessage API that makes use of jQuery Promises. Ever faced trouble communicating between two frames on different domains? Trouble not, just say please!
Here's a quick example to load an iframe window's location.
var frameWindow = $('iframe').get(0).contentWindow;
please(frameWindow).call('window.location.reload');
Here's another one that fetches you the child document's height:
var frameWindow = $('iframe').get(0).contentWindow;
please(frameWindow).get('document.height').then(function (height) {
console.log('child document\'s height:', height);
});
Note: If you are using the above code on parent's document.onready function, it needs to be wrapped in frame.onload event.
$(document).ready(function () {
var $frame = $('iframe');
$frame.load(function () {
var frameWindow = $frame.get(0).contentWindow;
please(frameWindow).get('document.height').then(function (height) {
console.log('child document\'s height:', height);
});
});
})
- Development version Uncompressed with Comments 15kb
- Production version Minified 5kb
please.js is based on top of jQuery and the jQuery Promise API. jQuery version 1.6 or above is preferred. To make the communication between two windows on different domains work, both of them must be injected with the same version of jQuery and please.js.
Hint: `undefined` values become `null` values in cross-frame communication. This is because JSON serialization does not support `undefined` (e.g. `JSON.stringify([undefined, 'x'])` becomes `[null, 'x']`).
Getting started with please.js is easy:
- Initialize please. In both the windows (frames), add the below code:
please.init(window);
Before initialization however, you must make sure that both the current window and the target window have please.js loaded and initialized.
- Set the default
targetWindow
(recipient). Especially useful if the communication is needed only between two frames.
please.defaults({
// reference to the window to send messages to
targetWindow: otherWindow,
// what the target window's origin must be for the communication to facilitate
targetOrigin: otherWindowOrigin,
// conditionally restrict communication
sourceOrigin: function (messageEvent) {
return (/^https?://example.com/.test(messageEvent.origin));
}
});
- To send a message to the
targetWindow
set usingdefaults
, just call the methods on theplease
global object.
please.call('window.location.reload');
please.get('window.location.href').then(function (href) {
// use href variable here
});
- If you need to send a message to a
targetWindow
other than the one set usingdefaults
, you could use please as a function and providetargetWindow
andtargetOrigin
as parameters
// navigate the parent window away to some other url.
// works only if the parent window is on *.example.com
please(parent, '*.example.com').set('window.location.href', 'http://www.google.com');
defaults please.defaults( objectHash )
Sets the default targetWindow
to send message to, the targetOrigin
of that window, and a test for conditions under which communication should be sourceOrigin.
please.defaults({
// reference to the window to send messages to
targetWindow: $('iframe').get(0).contentWindow,
// what the target window's origin must be for the communication to facilitate
targetOrigin: '*.example.com',
// conditionally restrict communication
sourceOrigin: function (messageEvent) {
return (/^https?://example.com/.test(messageEvent.origin));
}
});
call please.call( functionName, [args...] )
Invokes a function functionName
in the other window. Pass any arguments needed after the functionName
. Returns a jQuery promise object. After the function is invoked in the other frame, the promise is resolved to the return value of the function. For example:
// in the parent frame, have this code:
function sayHello () {
return 'Hello from parent frame.';
}
// and in the child frame:
please(parent).call('sayHello').then(function (helloString) {
console.log('Parent said:', helloString);
})
You could also call a method on another object. Just pass the object name in the functionName
as well like object.someMethod
. For example:
// in the parent frame:
var SomeObject = {
someMethod: function () {
return 'someMethod says hello!';
}
};
// and in the child frame:
please(parent).call('SomeObject.someMethod').then(function (helloString) {
console.log('SomeObject.someMethod said:', helloString);
});
If you try to call a function that isn't defined, an error is thrown in the other frame and the error object is passed on to the source window. You can catch the error in the failCallback
of the then
method.
please(parent).call('someUndefinedFunction').then(function (retval) { // success callback
// this will never execute
console.log('response recieved from the parent frame.');
}, function (error) { // failure callback
console.log('error occured: ', error.stack);
});
The failCallback
will also be called even if the function executed in the other frame throws an error.
// in the parent frame:
function errorProneFunction () {
throw new Error('Something went wrong!');
}
// in the child frame:
please(parent).call('errorProneFunction').then(function (retval) { // success callback
// this will never execute
console.log('function returned with value:', retval);
}, function (error) {
console.log('error:', error.message); // Something went wrong!
});
set please.set( propertyName, propertyValue )
Sets a global variable or a property on an object in the other window. Returns a jQuery promise object. After the property is set, the promise object is resolved.
var someIframeWindow = $('iframe').get(0).contentWindow
please(someIframeWindow).set('someVariable', 'foo').then(function () {
// execute some code
please(someIframeWindow).get('someVariable').then(function (someVariable) {
console.log('someVariable:', someVariable);
});
});
Note: jQuery 1.8+ supports chaining then callbacks by returning promise objects. For 1.8 and above versions, the above code can be simplified like:
var someIframeWindow = $('iframe').get(0).contentWindow
please(someIframeWindow).set('someVariable', 'foo').then(function () {
return please(someIframeWindow).get('someVariable')
}).then(function (someVariable) {
console.log('someVariable:', someVariable);
});;
However, since all postmessage requests are sent in a linear order, and responses are received in the same order, a race condition would be impossible in this case. So calling both requests synchronously would also work:
var someIframeWindow = $('iframe').get(0).contentWindow
please(someIframeWindow).set('someVariable', 'foo');
please(someIframeWindow).get('someVariable').then(function (someVariable) {
console.log('someVariable:', someVariable);
});;
get please.get( propertyName )
Gets a value of a global variable or a property on an object in the other window. Returns a jQuery promise object that resolves with the value of the property requested.
please(parent).get('document.height', function (documentHeight) {
console.log('parent document\'s height:', documentHeight);
});
If the variable or property is not defined, it returns undefined
.
please(parent).get('someUndefinedProperty').then(function (propertyValue) {
console.log('value of the property: ', propertyValue); // undefined
});
If the property is requested on an object that is undefined, an error is thrown in the other frame and the promise object fails to resolve. You can catch the error in the failCallback
of the then
method, just like the please.call
method above.
var otherFrame = $('iframe').get(0).contentWindow;
please(otherWindow).get('undef.blahBlah', function (propertyValue) { // success callback
// this never gets executed
console.log('value of the property: ', propertyValue);
}, function (error) { // faliure callback
console.log('an error occured')
});
General changes on the list:
- Remove all instances of
$.extend
and add a custom implementation and use that instead. - Add a polyfill for Object.keys.
- Use something else instead of $.globalEval.
- We can't really write our own Promise/A+ implementation, and unfortunately must depend on jQuery for that. But it would be good to have an API that is generic enough such that people can use this library with the Promise/A+ of their choice. The assumption would be that the custom implementation conforms to the Promise/A+ spec: http://promises-aplus.github.io/promises-spec/. (Basically removing the dependency on jQuery).
Testing: The tests written so far are mostly functional / end-to-end tests with asynchronous behaviour. I've exposed _please
under the please
namespace so that each individual function can also be tested synchronously as a unit.
Future Roadmap: These items are good to have to enhance the library.
please.$
is basically a mock jQuery object and does not really need the presence of jQuery in the calling window. It is like a test double that always returns a promise that resolves / rejects based on the return value of the corresponding function call. It would be good to have a generic enough API that can mock any object in the other frame in the calling frame, and use that API to mock jQuery.- Add certain logic such that
please.js
need not be injected and initialized in both the frames. Only a couple of lines would be needed in the other frame to kickstart the communication. - Add support for communication in Chrome Extensions.
The MIT License
Copyright (c) 2013 Wingify Software Pvt. Ltd.