2

I'm building a page that will have various 'sections' that the user will pass through, each with their own logic. For instance:

  1. Loading section: loading blinker. When finished, continue to:
  2. Splash section: an introduction with some UI elements that need to be interacted with to continue. (let's just say there's a 'slide to unlock')
  3. Video premiere: a custom player using the Youtube embed Javascript API.

Since some of these sections have a lot of specific logic, I'm separating these out into components. Almost all of this logic is internal to the component, but occasionally, I'd like to call a function within component A from component B. See the latter lines of main.js and Splash.js

main.js

import $ from 'jquery';
import Loading from './components/Loading.js';
import Splash from './components/Splash.js';
import Premiere from './components/Premiere.js';

$(() => {
    Loading.init();
    Splash.init();
    Premiere.init();

    Splash.onUnlock = Premiere.playVideo;
});

Loading.js:

const Loading = {
    init() {
        // watches for events, controls loading UI, etc.
    },
    // ... other functions for this section
}
export default Loading;

Splash.js

const Splash = {
    init() {
        // watches for events, controls unlocking UX, etc.
    },
    // ... other functions for this section
    unlock() {
        // called when UX is completed
        this.onUnlock();
    }
}
export default Splash;

Premiere.js

const Premiere = {
    init() {
        this.video = $('#premiereVideo');
        // watches for events, binds API & player controls, etc.
    },
    // ... other functions for this section
    playVideo() {
        this.video.play()
    },
}
export default Premiere;

Now, I'd expect that when this.onUnlock() is called within Splash, Premiere.playVideo() would be triggered. But, I get an error: video is not defined - because it's looking for Splash.video, not Premiere.video.

From what I understand, assigning an object or its property to a variable creates a reference to that property, not a duplicate instance. It seems I'm not understanding this correctly.

Changing this.video.play() to Premiere.video.play() works, but I feel like I'm still missing the point.

What's up?

(Possibly related sub-question: would I benefit from defining these components as classes, even if they're only going to be used once?)

2
  • Yes they would benefit from being classes, makes it more readable. Secondly it could be due to the this not being what you think it is, try Splash.onUnlock = Premiere.playVideo.bind(Premiere); Commented Jul 7, 2016 at 21:16
  • 1
    As long as you're using ES6, look into promises: www.html5rocks.com/en/tutorials/es6/promises/ Then main can be just be Loading.init().then(Splash.init).then(Premiere.init); Commented Jul 7, 2016 at 21:39

1 Answer 1

4

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().

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.