0

I'm designing a 3D visualisation applet in Javascript. When used, a Canvas will be created and placed inside an HTML element on a webpage, specified by the developer who is implementing the applet. Various graphical elements will be drawn onto the canvas.

One of the requirements is that the applet should support user-defined callbacks.

For example, when an end-user clicks on the canvas, a function provided by the developer should be executed.

My problem is how to allow the developer to set these callbacks.

The obvious and simplest way for me is simply to make them member of the class, setting this.action_left_click = null in the constructor, and then if the user sets it to anything other than null it gets called when an event fires. This is pretty straightforward and quick but also has no safeties.

Would you suggest doing otherwise, for example using getters and setters or a .setLeftClickAction method? Or is there some other pattern that you would consider most fitting for the situation?

1
  • this post is rather hard to read (wall of text). Would you mind editing it into a better shape?
    – gnat
    Nov 25 '20 at 16:06
2

I am assuming based on your description that you are thinking of initializing the application something like this.

<html>
  <body>
    <div id="mount"></div>
    <script type="text/javascript">
      const app = new Visualization(document.getElementById("mount"));
    </script>
  </body>
</html>

or this

<html>
  <body>
    <div id="mount"></div>
    <script type="text/javascript">
      const app = new Visualization().start(document.getElementById("mount"));
    </script>
  </body>
</html>

So I imagine your code looks something like this.

class Visualization {
    constructor(root) {
        this.start(root);
    }

    start(root) {
        // magic
    }
}

So if you want to take callbacks at the start, first you should edit either your constructor or start method to take an object as a parameter.

<html>
  <body>
    <div id="mount"></div>
    <script type="text/javascript">
      const app = new Visualization({
        root: document.getElementById("mount")
      });
    </script>
  </body>
</html>
class Visualization {
    constructor({ root }) {
        this.start(root);
    }

    start(root) {
        // magic
    }
}

This gives you a perfect place to handle optional arguments, since if a key is undefined or null in the object you can just pick a default value.

class Visualization {
    constructor({ root, onLeftClick, onRightClick, onMiddleClick }) {
        const onLeftClickCb = onLeftClick ?? (() => {});
        const onRightClickCb = onRightClick ?? (() => {});
        const onMiddleClickCb = onMiddleClick ?? (() => {});

        // set fields, do whatever
        this.start(root);
    }

    start(root) {
        // magic
    }
}

And that's basically all there is to it. If you are on older browsers you can use || instead of ??, but either way usage now looks like

<html>
  <body>
    <div id="mount"></div>
    <script type="text/javascript">
      const app = new Visualization({
        root: document.getElementById("mount"),
        onLeftClick: (pos) => { console.log(pos); }
      });
    </script>
  </body>
</html>

And wherever you need to, just assume the function is defined and call it. If none was passed you will just hit the No Op implementation

As a corollary, you shouldn't need to use getter and setter methods in JavaScript in the same way you would in Java. You can just have public properties and if needed write methods to change their behavior via the get and set keywords.

1
  • Thanks for the advice, but this isn't precisely my use case. You're right about the way the initialisation works, but my point is I want the user to be able to change these callbacks dynamically, not just set them at the start and leave them as such. I can also add the initialisation at construction but it's not the essential bit. About set and get, I've been using them more like in Python, with a hidden _variable and then a public variable with get and set.
    – Okarin
    Nov 26 '20 at 11:48
2

Using custom events would be the way to go. Dispatch the event on the <canvas> element. You can pass along custom data as well, which could include an instance of the "applet".

Since the <canvas> element is the only way outsiders interact with your code, you are free to hide the instances of the applet in an Immediately Invoked Function Expression so you do not clutter up the global namespace.

// 'this' being an instance of your applet
let event = new CustomEvent('action:leftClick', { detail: this });

this.canvas.dispatchEvent(event);

Then it is just a simple:

let canvas = document.querySelector('canvas');

canvas.addEventListener('action:leftClick', event => {
    event.target // the canvas element

    event.detail // an instance of your applet
});

To handle the event. It gets an event object where the target property is the canvas element, and event.detail is the applet.

3
  • This is useful as a way of handling it internally, but I'm aiming at providing a higher level interface to the user. There's a bunch of other code that goes into e.g. raycasting the click to find out which object is being clicked on that I need to wrap the user defined callback.
    – Okarin
    Nov 27 '20 at 11:21
  • @Okarin: you actually have two separate questions that require two separate answers. For callbacks, I prefer events. For other things, go with configuration. I would ask a different question for the other things. I answered your question about how to handle events. Nov 27 '20 at 16:52
  • My question was one of design. When I asked "how" to allow the user to set them I didn't mean that I didn't know of possible ways, since I suggest some right after that, but what would be considered the best.
    – Okarin
    Nov 28 '20 at 10:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.