What would you say if I tell you that you can have all great features of Tableau wrapped in custom customer portal without even exiting the Tableau ecosystem? You can have nice looking, interactive customer portal in the Tableau Viz itself! Setup custom navigation menu (drop-downs, accordions – you name it) and you are ready to go! You know what? You can have it in Tableau Online and Public as well!
[sc name=”Customer UI embed”]

Document structure

Good thing is that when publishing Dashboard to Tableau Server / Online /Public Tableau is creating a .html page with divs and iframes reflecting dashboard structure. Bad thing is that not all divs have their unique IDs so referencing them directly is bit of a problem. Having consistent webpage structure it’s quite easy to alter it using JavaScript after it’s rendered.

Each object you place on your customer portal / Tableau dashboard creates separate iframe in Tableau Viz Page. Naming of those iframes is consistent and therefore they can be accessed easily with JavaScript.

Numbering is based on the order in which you placed objects on dashboard not how they are actually displayed top to bottom. However it’s you can find proper name just by searching by the name of web connector used in the frame.

Cross-Origin Issues

Problem starts when you would like to have separate Object/Iframe in your customer portal for navigation and other one to display data in. Each of those iframes will have data fetched from ‘outside’ of the Viz (despite being on the same server) so it will most likely cause Cross-Origin issues. To mitigate that problem, the easiest way is to pass variables from your navigation through messages. Be careful though, messages include all ‘stuff’ that is happening in the browser, so it might contain data passed from third party pages or apps. To increase security, it’s recommended to always check the message origin..

There’s one more thing to consider. Even if it’s obvious that published customer portal page displayed on the server should already have all the .js files implemented, all iframes included, cannot access it. Therefore, body iframe (the one displaying the viz) must include Tableau JS api reference

<script src="http://yourserver/javascripts/api/tableau-2.min.js"></script>

Setting up Customer Portal

Let’s assume simple 2 objects customer portal  – navigation and body of the page. I would like various data to show in the body section after selecting an item from drop-down in navigation.

From the numbering it’s clear that body web object was added as first to the Tableau Dashboard. Now the tricky part – as web objects are placed as separate iframes within master iframe we have to reference them from the very top of the document when accessing through JavaScript:

frame = window.top.parent.document.getElementsByClassName("tb-viz ng-isolate-scope")[0].querySelector("iframe").contentWindow.document.getElementsByName("frame_1")[0];

As you can see it’s hard to get the right frame due to lack of IDs.

Sending the message

Ok, so how should I pass variables from navigation to body frame then? It’s not that hard!

In the navigation iframe:

<-- simple button -->
<a href="#" class="button-link" id="button1" viz="dashboardName/FirstSheetName">Show me the Viz!</a>

<script>
$(".button-link").click(function(){
	clicku($(this).attr("viz"));
	return false;
});

function clicku(variable)
{
	var frame;
	frame = window.top.parent.document.getElementsByClassName("tb-viz ng-isolate-scope")[0].querySelector("iframe").contentWindow.document.getElementsByName("frame_1")[0];
	frame.contentWindow.postMessage("VizChange_"+variable, '*'); 
}
</script>

in the body iframe:

window.addEventListener('message', function(event) { 

  if (~event.origin.indexOf('http://yourservername')) { 
    if (event.data != variable && ~event.data.search("VizChange_")){
      variable = event.data.substring(10,event.data.length);
      initViz(variable);
    }
  } else { 
  console.log("origin not ok");
  return false;
  } 

}); 

function initViz(variable) {

  target_viz = variable;
  var containerDiv = document.getElementById("vizContainer2"),
  concat_url = "http://yourservername/views/"+target_viz;
  url=concat_url;

  options = {            
    hideTabs: false,
    hideToolbar: true
  };

    if (viz ==undefined){
      viz = new tableau.Viz(containerDiv, url); 
    } else {
      viz.dispose();
      viz = new tableau.Viz(containerDiv, url); 
    }
}

 

What’s going on here? In the navigation iframe there’s a simple button with viz parameter (so you can add multiple buttons with different vizs) and script that catches clicks. If it was clicked then viz parameter is fetched and passed to function which sends message to body frame. Note that frame variable reference to frame_1 which is the body iframe.

Body frame has event listener which grabs sent message, checks if the origin and prefix is ok (there are a lot of messages going back and forth so we have to separate the noise) and maps viz variable to something that is used later in the tableau InitViz. One important thing here: note that there’s viz.dispose() function. Tableau API will prevent another instance of Tableau Viz from displaying in customer portal if there’s already viz present in the div.

Simplify the design

As nice and clean it is to have separate web object in Tableau for navigation and body, it comes with troubles when using dropdowns. Unfortunately frame displayed at the top is always displayed underneath the next iframe (no, z-index, doesn’t work here).. This results in nasty clipping or a lot of wasted space on the dashboard.

Yes, you could simply make the navigation part bigger but this will waste a lot of space. Easier way is  to place navigation and body in one document and use only singe webobject.

With simplified approach you can drop messaging system and simply reference functions in body directly.

Simplified body.html is available  body_meowbi.html

Feel free to comment below!