So to answer your question why do you get video is not defined is because you are trying to access this which has changed contexts.
Premiere.playVideo.bind(Premiere)
Where the bind will make sure that when playVideo is called, it is called in the context of premiere rather than the context of splash. This means the context of premiere has this.video.
The code I used to verify:
const Splash = {
init() {
// watches for events, controls unlocking UX, etc.
},
// ... other functions for this section
unlock() {
// called when UX is completed
this.onUnlock();
}
}
const Premiere = {
init() {
this.video = {
play() {
console.log("playing");
}
};
// watches for events, binds API & player controls, etc.
},
// ... other functions for this section
playVideo() {
console.log(this);
this.video.play()
},
}
Premiere.init();
Splash.onUnlock = Premiere.playVideo.bind(Premiere);
console.log(Splash);
Splash.unlock();
However, this particular "architecture" is a bit smelly to me. You could use the chain of responsibility pattern. This is where the current object knows what to call next after it has done it's work.
class DoWork {
constructor(nextWorkItem) {
this.nextWorkItem = nextWorkItem;
}
doWork() {
}
}
class LoadingComponentHandler extends DoWork {
constructor(nextWorkItem) {
super(nextWorkItem);
}
doWork() {
// do what you need here
console.log("Doing loading work")
// at the end call
this.nextWorkItem.doWork();
}
}
class SplashComponentHandler extends DoWork {
constructor(nextWorkItem) {
super(nextWorkItem);
}
doWork() {
// do what you need here
console.log("Doing Splash Work")
// at the end call
this.nextWorkItem.doWork();
}
}
class PremiereComponentHandler extends DoWork {
constructor(nextWorkItem) {
super(nextWorkItem);
}
doWork() {
// do what you need here
console.log("Doing Premiere Work")
// at the end call
this.nextWorkItem.doWork();
}
}
class FinishComponentHandler extends DoWork {
constructor() {
super(null);
}
doWork() {
console.log("End of the line now");
}
}
var finish = new FinishComponentHandler();
var premiere = new PremiereComponentHandler(finish);
var splash = new SplashComponentHandler(premiere);
var loading = new LoadingComponentHandler(splash);
loading.doWork();
The FinishComponent is part of the Null Object Pattern, where its implementation does a noop (no operation). This effectively ends the line of responsibility. Of course you don't need a FinishComponent, you can just not call the this.nextWorkItem.doWork() and the chain will end there. I have it there because it is easier to see where the chain stops.
You can see from the last four lines that the chain of responsibility is easy to see:
var finish = new FinishComponentHandler();
var premiere = new PremiereComponentHandler(finish);
var splash = new SplashComponentHandler(premiere);
var loading = new LoadingComponentHandler(splash);
The loading component will call doWork on the splash object which will in turn call doWork on the premiere object, so on, so fourth.
This pattern relies on the inheritence of DoWork which is the kind of interface for the handler.
This probably isn't the best implementation, but you can see how you don't have to worry about the last thing that was called, or how to specially call the next. You just pass in the object you wish to come next into the constructor and make sure you call at the end of your operations.
I noticed you had
// watches for events, controls unlocking UX, etc.
The doWork() functions can execute the bindings, delegating them to the proper components that deal with this. So like SplashComponentHandler can delegate off to SplashComponent. It's good practise to keep these separations of concerns.
How this addresses your issue
Splash.onUnlock = Premiere.playVideo.bind(Premiere);
Firstly, Splash.onUnlock has no implementation until you give it one. Secondly, the fact you're having to bind a context to your function because it is getting executed under a different context doesn't sound good.
So you can imagine in SplashComponentHandler.doWork():
doWork() {
var component = new SplashComponent();
component.initialise(); // when this is finished we want to execute the premiere
this.nextWorkItem.doWork();
}
And in PremiereComponentHandler.doWork():
doWork() {
var component = new PremiereComponent();
component.bind(); // makes sure there is a this.video.
component.playVideo();
}
See now that SplashComponentHandler now has no knowledge of the next handler, it just knows that when it has finished its job, it needs to call the next handler.
There is no this binding, because doWork() has been executed in the context of PremiereComponentHandler or what ever handler was passed to SplashComponentHandler.
Furthermore
Technically speaking, you're not limited to executing one handler after another. You can create a handler that executes many other handlers. Each handler that gets executed will have knowledge of the next one until you stop calling into.
Another question is, what happens if once premiere is done doing its work, how can splash do something else afterwards?. Simple, so working from the previous scenario of decoupling, this is SplashComponentHandler.doWork():
doWork() {
var component = new SplashComponent();
component.initialise(); // when this is finished we want to execute the premiere
this.nextWorkItem.doWork();
// so when we get to this execution step
// the next work item (PremiereComponentHandler)
// has finished executing. So now you can do something after that.
component.destroy(); // cleanup after the component
fetch('api/profile') // i don't know, what ever you want.
.then(profileContent => {
component.splashProfile = profileContent.toJson();;
});
}
On that last note of using a Promise you can make the whole doWork() async using promises. Just return the this.nextWorkItem.doWork() and then the initialisation steps look like this:
var finish = new FinishComponentHandler();
var premiere = new PremiereComponentHandler(finish);
var splash = new SplashComponentHandler(premiere);
var loading = new LoadingComponentHandler(splash);
loading
.doWork()
.then(() => {
// here we have finished do work asynchronously.
})
.catch(() => {
// there was an error executing a handler.
});
The trick to making it all use Promises is to make sure that you always return a promise from doWork().
thisnot being what you think it is, trySplash.onUnlock = Premiere.playVideo.bind(Premiere);