Search code examples
javascriptmoduleencapsulationprotectedaccessor

Is it ok to inject additional data into a module's scope? Or: What are the possibilities of managing/exposing module scoped state from/to the outside?


I am designing Javascript modules using the Revealing Module Pattern. In my html I have onmouseover="Test.OnLoad()". I need a callback after that is completed. I set the callback with Test.Callback. Is there a better way to do this? I basically need to write a lot of JavaScript modules and want to keep the functionality encapsulated. Thanks!

const Test = (function () {
  const items = [];
  let callBack;

  function handleOnLoad() {
    items.push({ name: "Test" });
    callBack(items);
  }
  function setCallBack(cb) {
    callBack = cb;
  }

  return{
    OnLoad: handleOnLoad,
    CallBack: setCallBack
  };
}());

Test.CallBack(function(items) {
  console.log(`Item 0: ${ items[0].name }, items.length: ${ items.length }`);
});
body { margin: 0; }
.test { display: inline-block; }
.test:hover { cursor: pointer; }
.as-console-wrapper { min-height: 88%!important; }
<div class="test" onmouseover="Test.OnLoad()">mouseover test</div>


Solution

  • The OP's approach is one way of achieving the goal of a customizable callback with items access while keeping items entirely encapsulated within its module scope.

    Another approach could be a getter which exposes an (maybe even immutable) items copy into public. Any callback related implementation (if it was just for the sake o the items encapsulation) then was not necessary.

    In order to not accidentally alter/mutate encapsulated data, it mostly already is good enough to not pass the encapsulated reference but a shallow copy of such data. Sometimes, and if appropriate/available, one might even pass a full clone.

    Of cause one has to change the mouseover handling too ...

    const Test = (function () {
      const items = [];
    
      function handleOnLoad() {
        items.push({ name: `Test_${ items.length + 1 }` });
      }
      function getItems() {
        return (typeof structuredClone === 'function')
          && structuredClone(items) // a full `items` clone.
          || [...items];  // not the `items` reference ...
                          // but just a shallow copy of it.
      }
    
      return{
        OnLoad: handleOnLoad,
        getItems,
      };
    }());
    
    function logLastItemName(items) {
      console.log(`last item name: ${ items.at(-1).name }, items.length: ${ items.length }`);
    }
    
    function init() {
      document
        .querySelector('.test.event-listener')
        .addEventListener('mouseover', (/*evt*/) => {
          Test.OnLoad();
          logLastItemName(Test.getItems());  
        });
    }
    init();
    body { margin: 0; }
    .test { display: inline-block; margin-right: 20px; }
    .test:hover { cursor: pointer; }
    .as-console-wrapper { min-height: 88%!important; }
    <div
      class="test inline-script"
      onmouseover="Test.OnLoad(); logLastItemName(Test.getItems());"
    >inline-script mouseover test</div>
    
    <div class="test event-listener">event-listener mouseover test</div>

    Letting modules feature exactly a single public function which creates custom callbacks is yet another way to go.

    Maybe it's even the to be favored solution since one neither needs to inject any data into the module nor does one have to implement a getter function for a controlled items access.

    The creator function takes a single argument, the custom function which later gets passed the items reference, and returns a function which is wrapped around the module's and the custom handler part.

    Another advantage is the possibility to create as many customized but module or items related handlers as one is in need for.

    const Test = (function () {
      const items = [];
    
      function writeItem() {
        items.push({ name: `Test_${ items.length + 1 }` });
      }
    
      function createLoadHandler(customCallback) {
        return function handleLoad (/*evt*/) {
    
          writeItem();
          customCallback(items);
        }
      }
    
      return{
        createLoadHandler
      };
    }());
    
    function logLastItemName(items) {
      console.log(`last item name: ${ items.at(-1).name }, items.length: ${ items.length }`);
    }
    const customLoadHandler = Test.createLoadHandler(logLastItemName);
    
    function init() {
      document
        .querySelector('.test.event-listener')
        .addEventListener('mouseover', customLoadHandler);
    }
    init();
    body { margin: 0; }
    .test { display: inline-block; margin-right: 20px; }
    .test:hover { cursor: pointer; }
    .as-console-wrapper { min-height: 88%!important; }
    <div
      class="test inline-script"
      onmouseover="customLoadHandler();"
    >inline-script mouseover test</div>
    
    <div class="test event-listener">event-listener mouseover test</div>