Cannot read property 'firstChild' of null [solved]
Chayemor opened this issue · 28 comments
I don't know if this is wcjs-player issue or NW.js issue. It seems that when module is called on new Window, somehow the window
objects points to parent window.
Environment
- NW.js v0.12.3 (32 bits)
- Windows 8
- Webchimera.js (player) VLC Build (2.2.1)
Problem
New window is opened from parent window. Webchimera module is loaded but it crashes because its window object points to parent Window.
PARENT
<div id="chimera_container"></div>
...
var gui; // global variable for parent object
var wjs; // global variable for parent object
....
gui = require('nw.gui');
wjs = wjs = require('wcjs-player');
....
video_emit = gui.Window.open('video_emit.html',{
"window" : {
"focus": true,
"toolbar": true,
"frame": true,
"width" : 640,
"height" : 640
},
"dependencies": {
"wcjs-player": "^0.5.6"
},
});
video_emit Window
<div id="vid"></div>
....
var wjs; // global variable for new window object
wjs = require('wcjs-player'); // window.require(...) doesn't work either
video_container = new wjs("#vid"); <---- CRASHES **** !!!
In node_modules\wcjs-player\index.js line 82 I added
console.log(window.document);
PARENT CONSOLE OUTPUT
#document
...
<div id="chimera_container"></div>
video_emit CONSOLE OUTPUT
#document
...
<div id="chimera_container"></div>
Somehow when the function in wcjs-player module executes, the window object points to the parent Window always instead of window.video_emit.window . Am I loading the module wrong in the child window?
I know that adding new-instance: true to the gui.Window.open
of the parent would fix this, but I need parent and new window to communicate via the global variable usable only if they are on the same render.
Hey Chayemor,
This isn't a wcjs-player
issue, I think this is a node.js
issue (not sure if I can call it a nw.js
or electron
issue per se, even if these platforms are literally the only ones that can create multiple windows). The reason it happens is quite easy to explain, and it's related to all modules, not just wcjs-player
.
If you require()
a module, it only really gets required once, every time you require it after that, it doesn't matter if you change the window or in what conditions you require it, it will always just return the reference to this module, not reload the module, so if you required it in your first window, wcjs-player
gets initialized on that particular window, and as your getting only the reference to it, it will still be directly linked to that first window.
wcjs-player
has a partial safe-guard for this, as it removes the module from the cache on page change, but it can't identify new windows in order to clear the cache for them also. The way to do this correctly is by clearing the require
cache manually before re-requiring it in the new window, to do this you'd need to do:
// clear wcjs-player from require cache when page changes
var clearModules = [
"wcjs-player",
"jquery" // https://github.com/jaruba/wcjs-player/issues/38
];
for (var i in clearModules) {
if (global.require.cache) {
for (module in global.require.cache) {
if (global.require.cache.hasOwnProperty(module) && module.indexOf(clearModules[i]) > -1) delete global.require.cache[module];
}
} else if (require.cache) {
for (module in require.cache) {
if (require.cache.hasOwnProperty(module) && module.indexOf(clearModules[i]) > -1) delete require.cache[module];
}
}
}
Mmm, seems to still be doing it. After clearing cache with your code (console.log
of global.require.cache
is an empty Object after clear), and then doing
require("wcjs-player");
The same exception occurs because the window.document
points to parent document, so it will never find the context
when I try to do new () .
Thanks for the help either way.
try console.log(window.document)
before require('wcjs-player')
Prints out proper window.document
.
that was fast...
Had tried it already XD.
let's try to hack it, put everything in a setTimeout(...,1000)
or maybe in the window.load
event..
I've had this issue before though, with totally different js files in new windows in nw.js
, i was absolutely sure that this require cache was the main reason for it, many have fixed their issues with wcjs-player
in new windows by clearing their require caches.
I'm pretty sure I found some hack for it a long time ago, I just can't figure out what it was... for the sake of insanity, let's do something crazy.. copy node_modules/wcjs-player
to node_moduels/wcjs-player2
and do require('wcjs-player2')
in the new window. :))
This is insane. Same problem. I even installed require-new module. (link)
var req_new = require('require-new');
wjs = req_new('wcjs-playerdiff');
I went ahead and did NOT require module on my first window. Still happens. How is that even possible?
But if I change the app's package.json file and set video_emit as first window to open up, then it works. But if I open a new window, same problem.
Don't be amazed, I've even seen errors/console.logs leak to the main window from the .html
pages of newly opened windows. Strange thing is, only the first ones leaked, then the rest went to the child window as they should..
I don't know what else to do, clearing cache is not workign and I really need to be able to use wcjs-player in opened window. :S
Uhm... I'm testing this in the simplest of ways, if you turn on the toolbar of nw.js, you'll see that it has 2 refresh buttons, one on the right, another on the left.
If you refresh the new window with the left refresh button, it just gives the same error again, but if you refresh it with the right button, the player starts in the new window.
A programatic refresh (location.reload()
) results in reproducing the action of the left refresh button. I think the right refresh button is a node.js level refresh, I wonder how we can reproduce that action..
Ok, I hacked it. :)
Example Usage:
var new_win = require('nw.gui').Window.open('index.html');
new_win.on('document-start', function() {
new_win.reload(3);
});
Looking through other tickets related to this error, it seems that only NW.js
has this issue. There have been users that switched to Electron
to fix it.
Now the player loads, but the global variable is brand new, not sharing the one it had with parent window. All the functions the parent window *loaded * on global variable are gone.
( from your proof of concept)
** index.html **
var wjs = require("wcjs-player");
var player = new wjs("#player").addPlayer({ autoplay: true });
global.HI = "yes global is here";
player.addPlaylist("http://archive.org/download/CartoonClassics/Krazy_Kat_-_Keeping_Up_With_Krazy.mp4");
new_window.html
console.log(global.HI);
var new_win = require('nw.gui').Window.open('index.html');
new_win.on('document-start', function() {
new_win.reload(3);
});
The new_window.html output is undefined.
Try using localStorage
or process['env']
as a a replacement for whatever your using global
for.
Or if you have more complex needs you could connect the two windows with websockets to pass data between them.
Appreciate all your help! I'll be trying Electron, if it doesn't work well I'll stick with NW.js and use localStorage. Thanks.
Hi! I have the same problem and it's also connected to this issue
#34
That's all because of separate Javascript contexts introduced after NW v13 . By default nw app runs in the 'separate context mode' where we have Node context and Browser context and all required modules (like wcjs-player) are running in Node context which has it's own window object. That's why there are no styles and no DOM elements we're trying to get from wcjs-player module.
@Chayemor for me worked mixed context mode (in package.json add chromium-args: "--mixed-context") so any modules required with require() are running in the same context.
@jaruba since I have some other problems in mixed-mode and prefer not to use it if possible, I'd like to ask whether it will be possible to somehow wrap wcjs-player module and pass a proper window object (for example when we create a new player)?
Thanks
@KiresMA I remember attempting that with a different user at one point (to pass the window
object on player creation) and it didn't work for him for some reason..
We can try it again though, if you'd like.. it's not too complicated as you may imagine.. just replace all instances of window.
with myWindow.
, then do:
var myWindow = window
var eventHooked = false
at the top of the script file, then delete these lines https://github.com/jaruba/wcjs-player/blob/master/index.js#L48-L69
and on this line https://github.com/jaruba/wcjs-player/blob/master/index.js#L222
add something like:
if (wcpSettings && wcpSettings.window) myWindow = wcpSettings.window
// deinitializate when page changed
if (!eventHooked) {
eventHooked = true
myWindow.addEventListener('beforeunload', function(e) {
// stop all players
for (var wjsIndex in players) if (players.hasOwnProperty(wjsIndex) && players[wjsIndex].vlc) players[wjsIndex].vlc.stop();
// clear wcjs-player from require cache when page changes
var clearModules = [
"wcjs-player",
"jquery" // https://github.com/jaruba/wcjs-player/issues/38
];
for (var i in clearModules) {
if (global.require.cache) {
for (module in global.require.cache) {
if (global.require.cache.hasOwnProperty(module) && module.indexOf(clearModules[i]) > -1) delete global.require.cache[module];
}
} else if (require.cache) {
for (module in require.cache) {
if (require.cache.hasOwnProperty(module) && module.indexOf(clearModules[i]) > -1) delete require.cache[module];
}
}
}
});
}
that should be everything.. then you can just do:
var player = new wjs("#player").addPlayer({
window: window,
autoplay: true,
wcjs: require('wcjs-prebuilt')
// OR
// wcjs: require('webchimera.js')
// OR
// wcjs: require([path-to-Webchimera.js.node-file])
});
If you test this and it works for you, I'll gladly accept a Pull Request for it, as it's a very common issue with NW.js
That different user probably was me :)
I tried it one more time and window object is used before addPlayer function.
Here https://github.com/jaruba/wcjs-player/blob/master/index.js#L41-L45
and here https://github.com/jaruba/wcjs-player/blob/master/index.js#L81
So when we create a new instance of wjs we still have non proper 'window'
that's why it didn't work for me last time and now either.
By the way, thank you for your quick answers! You are really quick in responses! :)
https://github.com/jaruba/wcjs-player/blob/master/index.js#L41-L45 - This can also be moved in the if (!eventHooked) { ... }
case.. but the other one.. uhmm.. that's tricky..
I think we thought about this the wrong way in the first place.. because the way I made wcjs-player
was to replicate the developer experience of jQuery. So it makes sense that we should use jQuery like logic here too.. As with jQuery, window
should be passed to wjs()
, not .addPlayer()
, as that's the main hook to the window and primarily used to attach to document elements..
So instead of doing:
wjs.prototype.addPlayer = function(wcpSettings) {
if (wcpSettings && wcpSettings.window) myWindow = wcpSettings.window
...
What we should actually do is:
function wjs(context, newWindow) {
if (newWindow) myWindow = newWindow
...
Then use it like:
var player = new wjs("#player", window).addPlayer({
autoplay: true,
wcjs: require('wcjs-prebuilt')
// OR
// wcjs: require('webchimera.js')
// OR
// wcjs: require([path-to-Webchimera.js.node-file])
});
And it should work! :)
@KiresMA Good catch on https://github.com/jaruba/wcjs-player/blob/master/index.js#L81 , it all makes sense now..
I think this could even be taken one step further and we could localize each window
to individual players, thus fixing issues such as multiple open windows with different players, etc. once and for all
Here's my final train of thought..
https://github.com/jaruba/wcjs-player/blob/master/index.js#L38-L69
Should be changed to:
function killPlayers() {
// stop all players
for (var wjsIndex in players) if (players.hasOwnProperty(wjsIndex) && players[wjsIndex].vlc) players[wjsIndex].vlc.stop();
// clear wcjs-player from require cache when page changes
var clearModules = [
"wcjs-player",
"jquery" // https://github.com/jaruba/wcjs-player/issues/38
];
for (var i in clearModules) {
if (global.require.cache) {
for (module in global.require.cache) {
if (global.require.cache.hasOwnProperty(module) && module.indexOf(clearModules[i]) > -1) delete global.require.cache[module];
}
} else if (require.cache) {
for (module in require.cache) {
if (require.cache.hasOwnProperty(module) && module.indexOf(clearModules[i]) > -1) delete require.cache[module];
}
}
}
}
function attachKiller(window) {
// deinitializate when page changed
window.removeEventListener('beforeunload', killPlayers)
window.addEventListener('beforeunload', killPlayers)
// inject css
if (!$("link[href='"+relbase+"/css/general.css']", window.document).length) {
$('<link href="'+relbase+'/css/general.css" rel="stylesheet">', window.document).appendTo("head");
window.document.styleSheets[0].addRule('.wcp-menu-items::-webkit-scrollbar','width: 44px !important;');
window.document.styleSheets[0].addRule('.wcp-menu-items::-webkit-scrollbar-track','background-color: #696969 !important; border-right: 13px solid rgba(0, 0, 0, 0); border-left: 21px solid rgba(0, 0, 0, 0); background-clip: padding-box; -webkit-box-shadow: none !important;');
window.document.styleSheets[0].addRule('.wcp-menu-items::-webkit-scrollbar-thumb','background-color: #e5e5e5; border-right: 13px solid rgba(0, 0, 0, 0); border-left: 21px solid rgba(0, 0, 0, 0); background-clip: padding-box; -webkit-box-shadow: none !important;');
window.document.styleSheets[0].addRule('.wcp-menu-items::-webkit-scrollbar-thumb:hover','background-color: #e5e5e5 !important; border-right: 13px solid rgba(0, 0, 0, 0); border-left: 21px solid rgba(0, 0, 0, 0); background-clip: padding-box; -webkit-box-shadow: none !important;');
window.document.styleSheets[0].addRule('.wcp-menu-items::-webkit-scrollbar-thumb:active','background-color: #e5e5e5 !important; border-right: 13px solid rgba(0, 0, 0, 0); border-left: 21px solid rgba(0, 0, 0, 0); background-clip: padding-box; -webkit-box-shadow: none !important;');
}
}
Then we'd go here:
https://github.com/jaruba/wcjs-player/blob/master/index.js#L71
And change it to something like:
function wjs(context, newWindow) {
this.window = newWindow || window
attachKiller.call(this)
...
Then replace all occurrences of window.
with this.window.
(even in the new attachKiller()
function, but make sure to keep this.window = newWindow || window
intact when replacing)
I hope I made it all clear enough.. And if it works, we'd have localized window
s for each player :)
Same usage would apply:
var player = new wjs("#player", window).addPlayer({
autoplay: true,
wcjs: require('wcjs-prebuilt')
// OR
// wcjs: require('webchimera.js')
// OR
// wcjs: require([path-to-Webchimera.js.node-file])
});
I tested the last code I recommended with NW.js 0.17.1 32bit, and something very strange is happening..
I still get:
Uncaught TypeError: Cannot read property 'addRule' of undefined
from:
this.window.document.styleSheets[0].addRule('.wcp-menu-items::-webkit-scrollbar','width: 44px !important;');
but if i do:
this.window.console.log(this.window.document.styleSheets[0].addRule)
right above it, it logs:
function addRule() { [native code] }
correctly.. same goes for everything else in the module, it's like although I'm sending everything to the proper window
now, it still errors out for no reason.. so something is definitely wonky in NW.js these days.. i have no idea how i could fix this silliness..
Well, for me it's the same situation :(
i think this should be discussed with the NW.js guys directly, but they rarely if ever answer any of their issues (or at least that was the case in the past)
i switched to using Electron instead of NW.js in my projects just because of the fact that the Electron guys always answer their issues, and actually want to help fix them too