Chrome: Communication Between Tabs

bob.ts - Aug 26 '19 - - Dev Community

When putting a recent talk together about Asynchronous JavaScript, I was looking to build a browser controlled presentation that allowed the controlling browser tab to control the presentation tab. In particular, I was looking at managing three things:

  1. Slide Position
  2. Slide Font Size
  3. Slide Actions

For the third one, Slide Actions, I was looking to trigger display of some code (preferably in Developer Tools > Console) as well as potentially running the code.

As a long-time front-end developer, I know that browser tabs are sandboxed, but have seen this type of functionality over time ... but remembering where was daunting. I also wanted to do the research and not look into (felt like cheating) some of the presentation tools (such as reveal.js) that have this functionality.

What I came across was BroadcastChannel and it is supported in Firefox and Chrome per caniuse.com. Since I can't imaging trying to give a presentation using IE or Edge, I considered this great information.

Setup Channels

Use of this functionality wound up being pretty simple ... this code initiated the process in the index.html JavaScript code (_functionality.js) ...

const pnChannel = new BroadcastChannel('le-slides-position');
const fsChannel = new BroadcastChannel('le-slides-font-size');
const anChannel = new BroadcastChannel('le-slides-actions');
Enter fullscreen mode Exit fullscreen mode

In the _navigation.js, _font-sizing.js, and _code-examples files, there are matching declarations ...

// _navigation.js
const channel = new BroadcastChannel('le-slides-position');

// _font-sizing.js
const channel = new BroadcastChannel('le-slides-font-size');

// _code-examples.js
const channel = new BroadcastChannel('le-slides-actions');
Enter fullscreen mode Exit fullscreen mode

NOTE: Each of these lines is in a separate file, hence the use of const channel on each line.

Channel Communication

Here, we'll just examine sending data from the controlling index.html, _functionality,js code ...

const actions = {
  init: (force = false) => {
    if (!initFired || force) {
      fsChannel.postMessage('init');
      pnChannel.postMessage('init');
      anChannel.postMessage('init');
      initFired = true;
    }
  },

  up: () => {
    if (!upButton.hasClass('disabled')) {
      fsChannel.postMessage('trigger-up');              
    }
  },
  reset: () => {
    fsChannel.postMessage('trigger-reset');         
  },
  down: () => {
    if (!downButton.hasClass('disabled')) {
      fsChannel.postMessage('trigger-down');                
    }
  },

  previous: () => {
    if (!previousButton.hasClass('disabled')) {
      pnChannel.postMessage('trigger-previous');                
    }
  },
  next: () => {
    if (!nextButton.hasClass('disabled')) {
      pnChannel.postMessage('trigger-next');
    }
  },

  triggerAction: (action) => {
    anChannel.postMessage(action);
  }
};
Enter fullscreen mode Exit fullscreen mode

Position Channel

Now, looking at the pnChannel (position channel) ... we can see that the .onmessage funcitonality expects a state. The state sent can include data, in this case what the current index is ... also, additional data is sent, such as previous and next disable states and these buttons can be adjusted appropriately.

pnChannel.onmessage = (states) => {
  cardIndex = states.data.currentIndex;
  updateContent();

  if (states.data.previousDisabled) {
    previousButton.addClass('disabled');
  } else {
    previousButton.removeClass('disabled');
  }

  if (states.data.nextDisabled) {
    nextButton.addClass('disabled');
  } else {
    nextButton.removeClass('disabled');
  }
};
Enter fullscreen mode Exit fullscreen mode

In the _navigation.js file, there it recieves a triggerAction whose data is actually used to execute some functionality ...

channel.onmessage = (triggerAction) => {
  actions[triggerAction.data]();
};

const actions = {
  init: () => {
    nextButton.hide();
    previousButton.hide();
  },

  'trigger-previous': () => {
    slideStateMachine.next('previous');
  },
  'trigger-next': () => {
    slideStateMachine.next('next');
  },

  'report-states': (index) => {
    channel.postMessage({
      currentIndex: index,
      previousDisabled: previousButton.hasClass('disabled'),
      nextDisabled: nextButton.hasClass('disabled')
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

With this code, it should become clear that sending a message is simply a matter of utilizing the .postMessage functionality of a channel.

Font Sizing Channel

Looking at the fsChannel we can see the .onmessage expects a state again, allowing for the button states to be assigned ...

fsChannel.onmessage = (states) => {
  if(states.data.upDisabled) {
    upButton.addClass('disabled');
  } else {
    upButton.removeClass('disabled');
  }

  if(states.data.downDisabled) {
    downButton.addClass('disabled');
  } else {
    downButton.removeClass('disabled');
  }     
};
Enter fullscreen mode Exit fullscreen mode

This is connected to the **_font-sizing.js* code, which again triggers various actions ...

channel.onmessage = (triggerAction) => {
  actions[triggerAction.data]();
};

const actions = {
  init: () => {
    upButton.hide();
    downButton.hide();
    resetButton.hide();
  },

  'trigger-up': () => {
    fontStateMachine.next('up');
  },
  'trigger-reset': () => {
    fontStateMachine.next('reset');      
  },
  'trigger-down': () => {
   fontStateMachine.next('down');
  },

  'report-states': () => {
    channel.postMessage({
      upDisabled: upButton.hasClass('disabled'),
      downDisabled: downButton.hasClass('disabled')
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Action Channel

Looking at the anChannel we can see that here, the response state data is simply sent to console.log ...

anChannel.onmessage = (states) => {
  console.log('action reply:', states.data);
};
Enter fullscreen mode Exit fullscreen mode

The associated code in the _code-examples.js file is a bit more complicated ...

channel.onmessage = (states) => {
  const cardAction = cardActions[states.data];
  if (states.data === 'init') {
    cardAction();
  } else {
    if (cardAction.showDisplay) {
      console.log(cardAction.display);      
    }
    cardAction.fn();        
  }
};
Enter fullscreen mode Exit fullscreen mode

In this case, I will admit that I "cheated" a bit for a specific purpose ... I used some JSON data ...

"fn": "triggerImage('queues.png', false)"
Enter fullscreen mode Exit fullscreen mode

... and within the _code-examples.js init functionality, I rebuild them as executable functions. Thus, I was able to use a JSON file to control the elements on each screen, as well as what could be "executed" on the presentation screen ...

const name = card.options[j].name;
const optionFn = new Function(card.options[j].fn);
cardActions[name] = {
  fn: optionFn,
  showDisplay: card.options[j].showFn,
  display: card.options[j].fn
};
Enter fullscreen mode Exit fullscreen mode

Conclusions

I learned a lot of exciting things with the project and the code is available on my GitHub account. I'd rather not give it away directly, so I am not going to link to it here.

The content in my article JavaScript Enjoys Your Tears is what I use to present Single-Threaded and Asynchronous JavaScript?.

This was an interesting project and at some point, I can see myself working this into a presentation, in and of itself.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .