How can I modify the XMLHttpRequest responsetext received by another function? - javascript

I am trying to modify the responseText received by a function that I cannot modify. This function creates a XMLHttpRequest that I can attach to, but I have been unable to "wrap" the responseText in a way that allows me to modify the content before the original function receives it.
Here's the full original function:
function Mj(a, b, c, d, e) {
function k() {
4 == (m && 'readyState' in m ? m.readyState : 0) && b && ff(b) (m)
}
var m = new XMLHttpRequest;
'onloadend' in m ? m.addEventListener('loadend', k, !1) : m.onreadystatechange = k;
c = ('GET').toUpperCase();
d = d || '';
m.open(c, a, !0);
m.send(d);
return m
}
function ff(a) {
return a && window ? function () {
try {
return a.apply(this, arguments)
} catch(b) {
throw jf(b),
b;
}
} : a
}
I have also tried to manipulate the reiceiving function k(); in an attempt to reach my goal, but since it doesn't depend on any data passing to the function (for example k(a.responseText);) I had no success.
Is there any way that I can achieve this? I do not wish to use js libraries (such as jQuery);
EDIT: I understand that I cannot change .responseText directly since it is read-only, but I am trying to find a way to change the content between the response and receiving function.
EDIT2: Added below one of the methods I have tried to intercept and change .responseText which has been addapted from here: Monkey patch XMLHTTPRequest.onreadystatechange
(function (open) {
XMLHttpRequest.prototype.open = function (method, url, async, user, pass) {
if(/results/.test(url)) {
console.log(this.onreadystatechange);
this.addEventListener("readystatechange", function () {
console.log('readystate: ' + this.readyState);
if(this.responseText !== '') {
this.responseText = this.responseText.split('&')[0];
}
}, false);
}
open.call(this, method, url, async, user, pass);
};
})(XMLHttpRequest.prototype.open);
EDIT3: I forgot to include that the functions Mj and ff are not globally available, they are both contained inside an anonymous function (function(){functions are here})();
EDIT4: I have changed the accepted answer because AmmarCSE's does not have any of the problems and complexity linked to jfriend00's answer.
The best answer explained in short is as follows:
Listen to whichever request you want to modify (make sure your listener will intercept it before the original function destination does, otherwise there is no point in modifying it after the response has already been used).
Save the original response (if you want to modify it) in a temporary variable
Change the property you want to modify to "writable: true", it will erase whichever value it had. In my case I use
Object.defineProperty(event, 'responseText', {
writable: true
});
Where event is the object returned by listening to the load or readystatechange event of the xhr request
Now you can set anything you want for your response, if all you wanted was to modify the original response then you can use that data from your temporary variable and then save the modifications in the response.

Edit: See the second code option below (it is tested and works). The first one has some limitations.
Since you can't modify any of those functions, it appears you have to go after the XMLHttpRequest prototype. Here's one idea (untested, but you can see the direction):
(function() {
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
var oldReady;
if (async) {
oldReady = this.onreadystatechange;
// override onReadyStateChange
this.onreadystatechange = function() {
if (this.readyState == 4) {
// this.responseText is the ajax result
// create a dummay ajax object so we can modify responseText
var self = this;
var dummy = {};
["statusText", "status", "readyState", "responseType"].forEach(function(item) {
dummy[item] = self[item];
});
dummy.responseText = '{"msg": "Hello"}';
return oldReady.call(dummy);
} else {
// call original onreadystatechange handler
return oldReady.apply(this, arguments);
}
}
}
// call original open method
return open.apply(this, arguments);
}
})();
This does a monkey patch for the XMLHttpRequest open() method and then when that is called for an async request, it does a monkey patch for the onReadyStateChange handler since that should already be set. That patched function then gets to see the responseText before the original onReadyStateChange handler is called so it can assign a different value to it.
And, finally because .responseText is ready-only, this substitutes a dummy XMLHttpResponse object before calling the onreadystatechange handler. This would not work in all cases, but will work if the onreadystatechange handler uses this.responseText to get the response.
And, here's an attempt that redefines the XMLHttpRequest object to be our own proxy object. Because it's our own proxy object, we can set the responseText property to whatever we want. For all other properties except onreadystatechange, this object just forwards the get, set or function call to the real XMLHttpRequest object.
(function() {
// create XMLHttpRequest proxy object
var oldXMLHttpRequest = XMLHttpRequest;
// define constructor for my proxy object
XMLHttpRequest = function() {
var actual = new oldXMLHttpRequest();
var self = this;
this.onreadystatechange = null;
// this is the actual handler on the real XMLHttpRequest object
actual.onreadystatechange = function() {
if (this.readyState == 4) {
// actual.responseText is the ajax result
// add your own code here to read the real ajax result
// from actual.responseText and then put whatever result you want
// the caller to see in self.responseText
// this next line of code is a dummy line to be replaced
self.responseText = '{"msg": "Hello"}';
}
if (self.onreadystatechange) {
return self.onreadystatechange();
}
};
// add all proxy getters
["status", "statusText", "responseType", "response",
"readyState", "responseXML", "upload"].forEach(function(item) {
Object.defineProperty(self, item, {
get: function() {return actual[item];}
});
});
// add all proxy getters/setters
["ontimeout, timeout", "withCredentials", "onload", "onerror", "onprogress"].forEach(function(item) {
Object.defineProperty(self, item, {
get: function() {return actual[item];},
set: function(val) {actual[item] = val;}
});
});
// add all pure proxy pass-through methods
["addEventListener", "send", "open", "abort", "getAllResponseHeaders",
"getResponseHeader", "overrideMimeType", "setRequestHeader"].forEach(function(item) {
Object.defineProperty(self, item, {
value: function() {return actual[item].apply(actual, arguments);}
});
});
}
})();
Working demo: http://jsfiddle.net/jfriend00/jws6g691/
I tried it in the latest versions of IE, Firefox and Chrome and it worked with a simple ajax request.
Note: I have not looked into all the advanced ways that Ajax (like binary data, uploads, etc...) can be used to see that this proxy is thorough enough to make all those work (I would guess it might not be yet without some further work, but it is working for basic requests so it looks like the concept is capable).
Other attempts that failed:
Tried to derive from the XMLHttpRequest object and then replace the constructor with my own, but that didn't work because the real XMLHttpRequest function won't let you call it as a function to initialize my derived object.
Tried just overriding the onreadystatechange handler and changing .responseText, but that field is read-only so you can't change it.
Tried creating a dummy object that is sent as the this object when calling onreadystatechange, but a lot of code doesn't reference this, but rather has the actual object saved in a local variable in a closure - thus defeating the dummy object.

One very simple workaround is to change the property descriptor for responseText itself
Object.defineProperty(wrapped, 'responseText', {
writable: true
});
So, you can extend XMLHttpRequest like
(function(proxied) {
XMLHttpRequest = function() {
//cannot use apply directly since we want a 'new' version
var wrapped = new(Function.prototype.bind.apply(proxied, arguments));
Object.defineProperty(wrapped, 'responseText', {
writable: true
});
return wrapped;
};
})(XMLHttpRequest);
Demo

I needed to intercept and modify a request response so I came up with a little bit of code. I also found that some websites like to use response as well as the responseText which is why my code modifies both.
The Code
var open_prototype = XMLHttpRequest.prototype.open,
intercept_response = function(urlpattern, callback) {
XMLHttpRequest.prototype.open = function() {
arguments['1'].match(urlpattern) && this.addEventListener('readystatechange', function(event) {
if ( this.readyState === 4 ) {
var response = callback(event.target.responseText);
Object.defineProperty(this, 'response', {writable: true});
Object.defineProperty(this, 'responseText', {writable: true});
this.response = this.responseText = response;
}
});
return open_prototype.apply(this, arguments);
};
};
the first param of the intercept_response function is a regular expression to match the request url and the second param is the function to be used on the response to modify it.
Example Of Usage
intercept_response(/fruit\.json/i, function(response) {
var new_response = response.replace('banana', 'apple');
return new_response;
});

By request I include below an example snippet showing how to modify the response of a XMLHttpRequest before the original function can receive it.
// In this example the sample response should be
// {"data_sample":"data has not been modified"}
// and we will change it into
// {"data_sample":"woops! All data has gone!"}
/*---BEGIN HACK---------------------------------------------------------------*/
// here we will modify the response
function modifyResponse(response) {
var original_response, modified_response;
if (this.readyState === 4) {
// we need to store the original response before any modifications
// because the next step will erase everything it had
original_response = response.target.responseText;
// here we "kill" the response property of this request
// and we set it to writable
Object.defineProperty(this, "responseText", {writable: true});
// now we can make our modifications and save them in our new property
modified_response = JSON.parse(original_response);
modified_response.data_sample = "woops! All data has gone!";
this.responseText = JSON.stringify(modified_response);
}
}
// here we listen to all requests being opened
function openBypass(original_function) {
return function(method, url, async) {
// here we listen to the same request the "original" code made
// before it can listen to it, this guarantees that
// any response it receives will pass through our modifier
// function before reaching the "original" code
this.addEventListener("readystatechange", modifyResponse);
// here we return everything original_function might
// return so nothing breaks
return original_function.apply(this, arguments);
};
}
// here we override the default .open method so that
// we can listen and modify the request before the original function get its
XMLHttpRequest.prototype.open = openBypass(XMLHttpRequest.prototype.open);
// to see the original response just remove/comment the line above
/*---END HACK-----------------------------------------------------------------*/
// here we have the "original" code receiving the responses
// that we want to modify
function logResponse(response) {
if (this.readyState === 4) {
document.write(response.target.responseText);
}
}
// here is a common request
var _request = new XMLHttpRequest();
_request.open("GET", "https://gist.githubusercontent.com/anonymous/c655b533b340791c5d49f67c373f53d2/raw/cb6159a19dca9b55a6c97d3a35a32979ee298085/data.json", true);
_request.addEventListener("readystatechange", logResponse);
_request.send();

You can wrap the getter for responseText in the prototype with a new function and make the changes to the output there.
Here is a simple example that appends the html comment <!-- TEST --> to the response text:
(function(http){
var get = Object.getOwnPropertyDescriptor(
http.prototype,
'responseText'
).get;
Object.defineProperty(
http.prototype,
"responseText",
{
get: function(){ return get.apply( this, arguments ) + "<!-- TEST -->"; }
}
);
})(self.XMLHttpRequest);
The above function will change the response text for all requests.
If you want to make the change to just one request then do not use the function above but just define the getter on the individual request instead:
var req = new XMLHttpRequest();
var get = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
'responseText'
).get;
Object.defineProperty(
req,
"responseText", {
get: function() {
return get.apply(this, arguments) + "<!-- TEST -->";
}
}
);
var url = '/';
req.open('GET', url);
req.addEventListener(
"load",
function(){
console.log(req.responseText);
}
);
req.send();

I ran into the same problem when I was making a Chrome extension to allow cross origin API calls. This worked in Chrome. (Update: It doesn't work in the newest Chrome version).
delete _this.responseText;
_this.responseText = "Anything you want";
The snippet runs inside a monkeypatched XMLHttpRequest.prototype.send who is redirecting the requests to the extensions background script and replace all the properties on response. Like this:
// Delete removes the read only restriction
delete _this.response;
_this.response = event.data.response.xhr.response;
delete _this.responseText;
_this.responseText = event.data.response.xhr.responseText;
delete _this.status;
_this.status = event.data.response.xhr.status;
delete _this.statusText;
_this.statusText = event.data.response.xhr.statusText;
delete _this.readyState;
_this.readyState = event.data.response.xhr.readyState;
That didn't work in Firefox, but I found a solution that worked:
var test = new XMLHttpRequest();
Object.defineProperty(test, 'responseText', {
configurable: true,
writable: true,
});
test.responseText = "Hey";
That doesn't work in Chrome, but this work in both Chrome and Firefox:
var test = new XMLHttpRequest();
var aValue;
Object.defineProperty(test, 'responseText', {
get: function() { return aValue; },
set: function(newValue) { aValue = newValue; },
enumerable: true,
configurable: true
});
test.responseText = "Hey";
The last was copy past from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
None of the solutions works in Safari. I tried to make a new XMLHttpRequest with writable properties, but it was not allowed to call open or send from it. I also tried this solution: https://stackoverflow.com/a/28513219/3717718. Unfortunately it produced the same error in Safari:
TypeError: Attempting to configurable attribute of unconfigurable property.

First-class function variables are wonderful things! function f() {a; b; c; } is exactly the same thing as var f = function () {a; b; c; } This means you can redefine functions as needed. You want to wrap the function Mj to return a modified object? No problem. The fact that the responseText field is read-only is a pain, but if that's the only field you need...
var Mj_backup = Mj; // Keep a copy of it, unless you want to re-implement it (which you could do)
Mj = function (a, b, c, d, e) { // To wrap the old Mj function, we need its args
var retval = Mj_backup(a,b,c,d,e); // Call the original function, and store its ret value
var retmod; // This is the value you'll actually return. Not a true XHR, just mimics one
retmod.responseText = retval.responseText; // Repeat for any other required properties
return retmod;
}
Now, when your page code calls Mj(), it will invoke your wrapper instead (which will still call the original Mj internally, of course).

I search very a lot and made one solution to solve the problem
const open_prototype = XMLHttpRequest.prototype.open,
intercept_response = function (urlpattern, callback) {
XMLHttpRequest.prototype.open = function () {
arguments['1'].includes(urlpattern) && this.addEventListener('readystatechange', function (event) {
if (this.readyState === 4) {
var response = callback(event.target.responseText);
Object.defineProperty(this, 'response', {writable: true});
Object.defineProperty(this, 'responseText', {writable: true});
this.response = this.responseText = response;
}
});
return open_prototype.apply(this, arguments);
};
};
and you can use the function like this
intercept_response('SOME_PART_OF_YOUR_API', (response) => {
const new_response = response.replace('apple', 'orange');
return new_response;
})
and now all apples replaced with oranges 😊

Related

Create an instance of a jQuery plugin

I have several pages which I wish to allow the the user to inline edit many fields and update the server DB. To implement this, my intent is to create a jQuery plugin which I can do the typical passing of the configuration options and uses ajax to save the results.
(function($){
var methods = {
init : function (options) {return this.each(function () {/* ... */});},
method1 : function () {return this.each(function () {/* ... */});},
method2 : function () {return this.each(function () {/* ... */});}
};
$.fn.myEditPlugin= function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); //Line 10
} else if (typeof method === 'object' || ! method) {
return methods.init.apply(this, arguments); //Line 12
} else {
$.error('Method ' + method + ' does not exist on jQuery.myEditPlugin');
}
};
}(jQuery)
);
For each individual page, there are several options which are common to all (i.e. the url endpoint, the record's primary key, etc) and I would rather not duplicate each when applying the plugin.
Originally, I was just going to define a function on each page which takes some input and applies the common options to each.
function wrapEdit(e,options) {
options.url='/page1/etc';
options.pk=document.getElementById('pk').value;
return $(e).myEditPlugin(options);
}
wrapEdit('.someclass',{foo:123});
It doesn't seem all that professional to me, so in my obsessive quest, thought I would make a class which I could pass the common options to and it would apply the plugin.
class WrapEdit(options)
{
constructor(options) {
this.options = options;
}
this.applyIndividualOptions=function(e, options) {
return $(e).myEditPlugin(Object.assign({}, this->options, options));
}
}
var wrapEdit=new WrapEdit({url: '/page1/etc', pk: document.getElementById('pk').value});
wrapEdit.applyIndividualOptions('.someclass',{foo:123});
Better, but not very jQueryish as I will be passing the select element instead of directly applying the plugin to elements typical of jQuery.
Is it possible to create an instance of a jQuery plugin which keeps previously defined data? Maybe something like the following:
$.myEditPlugin({url: '/page1/etc', pk: document.getElementById('pk').value});
$('.someclass').myEditPlugin({foo:123}); //Will also pass previously defined url and pk to myEditPlugin
Or maybe best to create a custom jQuery plugin per page which just adds the extra options and initiates the real plugin...
$.fn.myEditPluginInstance = function(options) {
return this.myEditPlugin(Object.assign({url: '/page1/etc', pk: document.getElementById('pk').value}, options));
};
Creating a function to be called against a jquery collection
The basic idea is to define a new property (function) in jQuery.fn, before any call to your plugin is made (In other words, any code related to the application is executed). You can use an "Immediately Invoked Function Expressions" (a.k.a. IIFEs) to fence your plugin API in. Then you have to loop over the collection and execute any code your plugin needs to apply on the collection items.
Basic skeleton:
(function ($) {
// Enclosed scope (IIFE)
// You can define private API/variables in here
// …
// Once your plugin API is ready, you have to apply the magic to each item
// in the collection in some ways. You must add a property to jQuery.fn object.
$.fn.myAwesomePlugin = function(Opt) {
var defaultConfig = {option1: 'someValue' /*, …*/};
// Eval supplied Opt object (Validate, reject, etc.)
// If all goes well, eventually merge the object with defaults.
$.extend(defaultConfig, Opt);
// Apply the magic against each item in the jQuery collection
// (Your plugin may not need to use "each" function though)
// Return the jQuery collection anyway to keep chaining possible.
// Once again, this is not required, your plugin may return something else depending on the options passed earlier for instance.
return this.each(function(el, idx) {
// Your plugin magic applied to collection items…
});
}
})(jQuery);
You should be able to call your plugin $('someSelector').myAwesomePlugin(); right after the declaration.
Simple implementation example:
(function ($) {
let required = {url: null, pk: null}
// Function to be executed upon first call to the plugin
, populateCommons = () => {
let ep = $('#someNode').data('endpoint')
, pk = document.querySelector('#pk')
;
// Basic tests to alert in case the page
// doesn't comply with the plugin requirements
if( typeof ep !== 'string' || !/^\/[a-z]+/.test(ep) || !pk) {
throw ` "myEditPlugin" init phase error:
Detected endpoint: '${ep}'
Is PK value found: ${!!pk}
`;
}
[required.url, required.pk] = [ep, +pk.value];
};
$.fn.myEditPlugin = function(Opt) {
let allOpts;
// First call will trigger the retrival of common data
// that should be available as static data somewhere every page source.
!required.url && populateCommons();
allOpts = $.extend({}, Opt, required);
return this.each(function(el, idx) {
// Your logic here, request
console.log("Payload is", allOpts);
});
}
})(jQuery);
function debounce(fn, time) {
debounce.timer && (clearTimeout(debounce.timer));
debounce.timer = setTimeout(() => (fn(), debounce.timer = null), time);
}
$('[type="text"]').keydown(function(e){
debounce(() => this.value && $(this).myEditPlugin({foo:this.value, bar: 'Contextual value'}), 2000);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="pk" type="hidden" value="5">
<div id="someNode" data-endpoint="/api/endpoint">
Editing the below input will trigger the plug-in code
</div>
<input type="text" title="Edit me"/>
Related documentation here

Proxy object cannot be added to DOM (traps doesn't trigger either)

I am trying to make a Proxy object of Image to trap properties but even with an empty handler I get an error message.
TypeError: Argument 1 of Node.appendChild does not implement interface Node.
The proxy object is suppose to act as the target object so this baffles me a little. As far as I understand you should be able to do this with DOM nodes as well (?).
Also: I cannot start loading the image and have the onload handler triggered when setting the src property.
How should I use the Proxy so I can "take over" for example the "src" property and otherwise have it act like a regular image object?
My code
'use strict';
//--- normal image use ---
var imgNormal = new Image();
imgNormal.onload = function(){
console.log('Normal loaded OK');
document.body.appendChild(imgNormal);
};
imgNormal.src = 'https://i.imgur.com/zn7O7QWb.jpg';
//--- proxy image ---
var imgProxy = new Proxy(Image, { // I also tried with 'new Image()' and HTMLImageElement
set: function(a,b,c,d){
console.log('set '+b);
return Reflect.set(a,b,c,d);
}
});
imgProxy.onload = function(){
console.log('Proxy loaded OK');
document.body.appendChild(imgProxy);
};
imgProxy.src = 'https://i.imgur.com/zn7O7QWb.jpg';
document.body.appendChild(imgProxy); // double-up to demo error
Update: Thanks to #Harangue! using "new" (bah..) certainly made the proxy object come to life but now I am unable to trap the setting of properties. It seem to ignore the trap completely - example:
var proxy = new Proxy(Image, {
set: function(a,b,c,d){
console.log('set '+b); // doesn't show
return Reflect.set(a,b,c,d);
}
});
var imgProxy = new proxy();
imgProxy.onload = function(){
console.log('Proxy loaded OK');
document.body.appendChild(imgProxy);
};
imgProxy.src = 'https://i.imgur.com/zn7O7QWb.jpg';
How can I trap the property setting using a valid proxy?
Update 2 On the other hand - using new with the new proxy only seem to use the original constructor. All examples I can find does not use new:
var myProxy = new Proxy(.., ..); // should suffer
Using then on top of that new myProxy() only seem to use the original constructor which is not what I want as it ignores the traps.
var proxy = new Proxy(Image, {}); //should be sufficent??
var proxy2 = new proxy();
console.log(proxy2); //-> says Image (not proxy..)
The traps seem to work in my first attempts but the proxy doesn't behave as expected. This is so confusing, and so new. Happy for any input how both of these can be solved (traps and behavior).
Never underestimate the importance of the new keyword. ;)
//--- proxy image ---
var imgProxy = new Proxy(Image, { // I also tried with 'new Image()'
set: function(a,b,c,d){
console.log('set '+b);
return Reflect.set(a,b,c,d);
}
});
imgProxy.src = 'https://i.imgur.com/zn7O7QWb.jpg';
document.body.appendChild(new imgProxy); // double-up to demo error
With the proxy you effectively extend the Image object. But sending the Image constructor itself, rather than the DOM Node returned by it, would indeed be missing the needed appendChild.
As an alternative to a proxy you can also overwrite the property on the object itself and therefore control it's behaviour:
function findDescriptor(obj, prop){
if(obj != null){
return Object.hasOwnProperty.call(obj, prop)?
Object.getOwnPropertyDescriptor(obj, prop):
findDescriptor(Object.getPrototypeOf(obj), prop);
}
}
var img = new Image();
var {get, set} = findDescriptor(img, "src");
Object.defineProperty(img, "src", {
configurable: true,
enumerable: true,
//get: get, //keep behaviour
get(){ //overwrite getter
var v = get.call(this); //call the original getter
console.log("get src:", v, this);
return v;
},
//same for setter
set(v){
console.log("set src:", v, this);
//modify value before applying it to the default setter
v = v.toLowerCase();
set.call(this, v);
}
});
img.src = "FileWithUppercaseLetters.jpg"; //setter
img.src; //trigger getter
And since this property is defined on the Image.prototype*, you can simply extend this class and modify the behaviour on the prototype of the inherited Class
*at least in FF, have to check the other browsers

Variable content outside of a method

I'm working on getting jsonp information back from a page and I want to run various functions on that information. The information comes back fine but I can't seem to find a way to make it accessible outside of the function. I know it's something to do with closure and function scope but I can't figure out how to make it work, any ideas?
I can achieve what I'm trying to do in the rest of the script by making multiple calls to the json file but I assume it's better to just query the json once and pop it into a variable and try work off that? I'm relatively new to this set up so any suggestions appreciated.
Effectively from the code below I want to be able to get the allMatches variable accessible outside of the getData method after it runs.
Thanks for your time, all help greatly appreciated.
var AppInfo = {
getData : function(){
var responseJsonVar;
var callbackName, script, newInfo, mydata,allMatches;
// Get a random name for the callback
callbackName = "checkGames" + new Date().getTime() + Math.floor(Math.random() * 10000);
// create the jsonP script call on the page
script = document.createElement('script');
script.src = "http://www.hookhockey.com/index.php/temp-gillian/?callback=" + callbackName;
document.documentElement.appendChild(script);
// call the json
window[callbackName] = function(data) {
responseJsonVar = data; // this is the info back from the json file
//the filtered data source from json
var allMatches = responseJsonVar["matches"];
console.dir('allMatches inside the function: ' + allMatches); //this comes back fine
// Remove our callback ('delete' with 'window properties fails on some versions of IE, so we fall back to setting the property to 'undefined' if that happens)
try {
delete window[callbackName];
}
catch (e) {
window[callbackName] = undefined;
}
//I've tried putting a return value (return allMatches) in here and then calling window[callbackName]() outside of the function but I get undefined for matches
}; // end window[callbackName] function
//this is what I think I should be doing to get the info out on its own
console.dir('allMatches OUTSIDE the function: ' + allMatches); //this doesn't come back 'allMatches is not defined'
} //end getdata method
} //end AppInfo
AppInfo.getData();
You could just create a property on your AppInfo object called allMatches and set that property when the data comes back from the jsonp call:
var AppInfo = {
allMatches: null, // NEW PROPERTY TO HOLD RETURNED DATA
confirmDataAvailableOutsideFunction: function () { // NEW FUNCTION TO VERIFY DATA AVAILABLE OUTSIDE getData()
console.dir('AppInfo.allMatches OUTSIDE the function AFTER jsonp call executes: ' + AppInfo.allMatches); //this doesn't come back 'allMatches is not defined'
},
getData: function () {
var responseJsonVar;
var callbackName, script, newInfo, mydata, allMatches;
// Get a random name for the callback
callbackName = "checkGames" + new Date().getTime() + Math.floor(Math.random() * 10000);
// create the jsonP script call on the page
script = document.createElement('script');
script.src = "http://www.hookhockey.com/index.php/temp-gillian/?callback=" + callbackName;
document.documentElement.appendChild(script);
// call the json
window[callbackName] = function (data) {
responseJsonVar = data; // this is the info back from the json file
//the filtered data source from json
AppInfo.allMatches = responseJsonVar["matches"]; // store data in allMatches property
console.dir('allMatches inside the function: ' + AppInfo.allMatches); //this comes back fine
AppInfo.confirmDataAvailableOutsideFunction(); // call test method to verify allMatches property is set
// Remove our callback ('delete' with 'window properties fails on some versions of IE, so we fall back to setting the property to 'undefined' if that happens)
try {
delete window[callbackName];
}
catch (e) {
window[callbackName] = undefined;
}
//I've tried putting a return value (return allMatches) in here and then calling window[callbackName]() outside of the function but I get undefined for matches
}; // end window[callbackName] function
//this is what I think I should be doing to get the info out on its own
console.dir('AppInfo.allMatches OUTSIDE the function BEFORE jsonp call executes: ' + AppInfo.allMatches); //this doesn't come back 'allMatches is not defined'
} //end getdata method
}; //end AppInfo
AppInfo.getData();
Note that I modified the text of your second console.dir to indicate that it's running before the jsonp call returns, and therefore the allMatches property is still null at that point.
That's why, after implementing #peter-b's suggestion to use window.allMatches instead of local variable allMatches, window.allMatches OUTSIDE the function was undefined--you were checking it before it was set.
#peter-b's solution would work fine, provided that you didn't try to access window.allMatches before it was set. So if you want the data stored in a global variable, you can use his method; if you'd rather have it stored on your AppInfo object, you can use mine.
Alternatively, you can wrap everything in an immediate function that has allMatches as a local variable:
(function () {
var allMatches = null;
var AppInfo = {
getData: function (dataReadyCallback) {
/* ... */
allMatches = responseJsonVar["matches"];
dataReadyCallback();
/* ... */
}
};
AppInfo.getData(allMatchesReady);
function allMatchesReady() {
console.dir('allMatches OUTSIDE the function AFTER jsonp call executes: ' + allMatches);
}
}());
One easy way to get what you want is to assign allMatches as a property of the window, making it accessible everywhere. Replace
var allMatches = responseJsonVar["matches"];
with
window.allMatches = responseJsonVar["matches"];
and then access it later using window.allMatches.

node.js automatic logging with metadata

I want to create a Node.js logged decorator (function filter) that works like this (pseudocode warning):
logged = function(f) {
return function() {
log(f.file, f.line, f.class, f.name, arguments)
return f.call(this, arguments)
}
}
Is there a way of accesing the information above? I'm going to be doing heavy logging, so throwing a fake Exception and reading the trace is probably not viable (or is it?).
Note: by f.class I mean the name of the function that holds the prototype. Also, I'm using coffee-script, in case it matters
You don't have to throw the exception, var stack = new Error().stack is enough. From there it is possible to parse out the file, line number, and the class name. BUT the problem is, that it only tracks the function calls, so now to get the proper information for the decorated function Error should be initialized some where within the function or the class. Doing this in decorator, you can get only the information about the file/linenumber where the decoration of the function was occurred, and not where the function was declared.
function User(){}
User.prototype.foo = function(){};
User.stack_ = function(){
return new Error().stack
};
// ...
function wrapp(Ctor, name){
var stack = Ctor.stack_();
var info = parseStack(stack);
var orig_ = Ctor.prototype[name];
Ctor.prototype[name] = function(){
console.log('Info:', info);
return orig_.apply(this, arguments);
};
}
function parseStack(stack){
// implement better stacktrace parser
return stack.split('\n')[1];
}
// ...
wrapp(User, 'foo');
// ...
var user = new User;
user.foo();
// and you get smth. like
// Info: at Function.User.stack_ (filename:line:column)
I think this is maximum you can get, and you must always define this stack_ function.

OO Javascript and this keyword. Object Literals

I'm having issues with Javascript properties and "this" keyword. Forgive me here for asking my third and final JS OOP question. OOP in Javascript has been a headache for me today.
I'm trying to set the property 'source' but the error console is saying it's undefined in parseSource method.
After a little research I believe this.source is is referring to window.source? The code is a boilerplate from Mozilla. When creating extensions init is called by FireFox when the plugin is initialized.
What's the best way to go about setting the properties when creating objects using literal notation?
var myExtension = {
source: null,
init: function() {
// The event can be DOMContentLoaded, pageshow, pagehide, load or unload.
if(gBrowser) {
gBrowser.addEventListener("DOMContentLoaded", this.onPageLoad, false);
}
},
onPageLoad: function(aEvent) {
doc = aEvent.originalTarget; // doc is document that triggered the event
win = doc.defaultView; // win is the window for the doc
// Skip frames and iFrames
if (win.frameElement) return;
this.source = win.document.getElementById('facebook').innerHTML;
myExtension.parseSource();
},
parseSource: function() {
if(this.source == null) {
// So something
} else {
// Do something else
}
}
}
window.addEventListener("load", function() { myExtension.init(); }, false);
When you pass a callback function to gBrowser.addEventListener like this:
gBrowser.addEventListener("DOMContentLoaded", this.onPageLoad, false);
you are passing a reference to the function which is essentially "detached" from the this object where it is defined. So, you need to do something like the following in order to correctly maintain what this references:
init: function() {
var self = this;
if(gBrowser) {
gBrowser.addEventListener("DOMContentLoaded", function () {
self.onPageLoad();
}, false);
}
},
In newer browsers (you did say this is a FF extension), you can use Function.bind to the same effect:
init: function() {
if(gBrowser) {
gBrowser.addEventListener("DOMContentLoaded", this.onPageLoad.bind(this), false);
}
},
Once that's cleared up, you can change the onPageLoad function to:
onPageLoad: function(aEvent) {
doc = aEvent.originalTarget; // doc is document that triggered the event
win = doc.defaultView; // win is the window for the doc
// Skip frames and iFrames
if (win.frameElement) return;
this.source = win.document.getElementById('facebook').innerHTML;
this.parseSource();
},
Edit
A stripped-down demo: http://jsfiddle.net/mattball/bDe6N/
The problem is that methods in Javacript forget about their this if you pass them as a parameter. They only work if you pass them looking like a method
//this doesn't work in JS
f = obj.method
f()
//wtf man! You have to call it looking like a method
obj.method()
In your case this happens because you pass this.onPageLoad as a parameter. Function parameters act like the variable from the last example.
The workaround is to use a wrapper function in order to preserve the method-call appearance
addEventListener( ..., function(){ return this.onPageLoad(); }, ...)
except that this is not lexicaly scoped and the inner function gets a wrong copy as well. After another quick fix we obtain
var that = this;
addEventListener(..., function(){ that.onPageLoad(); }, ...);
This should do the job now.

Categories