Search code examples
angulartableau-api

Trouble passing filter (viz-filter) value from Angular application to embedded web-component Tableau report


I'm having trouble embedding and configuring a Tableau report inside of an Angular 15 app. What I want to do is just pass an initial filter value, and then show the report. Here is a code sample of a report running properly in regular old HTML/JS:

<!DOCTYPE html>
<html lang="en">
<head>
    <script type="module" src="https://public.tableau.com/javascripts/api/tableau.embedding.3.latest.js"></script>
</head>
<body>

<tableau-viz id="tableauViz" src="https://public.tableau.com/views/DigitalMarketingWebTraffic/Cockpit?:language=en-US&:display_count=n&:origin=viz_share_link">
    <viz-filter field="ga:channelGrouping" value="Social" />
</tableau-viz>

</body>
</html>

This works fine. The report loads, filtered to the proper channelGrouping. However, if I bring this into an Angular context, I cannot get the dynamic value I want to pass to viz-filter to bind. This has gotta be something silly, but I can't figure it out.

In my Angular application, I'm including this in my index.html page (note that it's different from the referenced version in the above example):

<script type="module"
  src="https://embedding.tableauusercontent.com/tableau.embedding.3.1.0.min.js"></script>

...and then this is the Angular component:

import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';

@Component({
  selector: 'app-tableau-embed',
  standalone: true,
  imports: [CommonModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<tableau-viz
  id="tableau1"
  src="https://public.tableau.com/views/DigitalMarketingWebTraffic/Cockpit?:language=en-US&:display_count=n&:origin=viz_share_link">
</tableau-viz>`,
  styleUrls: [],
})
export class TableauEmbedComponent {}

This works fine.

But as soon as I introduce viz-filter, I start to run into weird problems. For example, this works:

import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';

@Component({
  selector: 'app-tableau-embed',
  standalone: true,
  imports: [CommonModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<tableau-viz
  id="tableau1"
  src="https://public.tableau.com/views/DigitalMarketingWebTraffic/Cockpit?:language=en-US&:display_count=n&:origin=viz_share_link">
  <viz-filter field="ga:channelGrouping" value="Direct" />
</tableau-viz>`,
  styleUrls: [],
})
export class TableauEmbedComponent {}

However, if I try to bind the value attribute to an Angular class variable, like this:

import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';

@Component({
  selector: 'app-tableau-embed',
  standalone: true,
  imports: [CommonModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<tableau-viz
  id="tableau1"
  src="https://public.tableau.com/views/DigitalMarketingWebTraffic/Cockpit?:language=en-US&:display_count=n&:origin=viz_share_link">
  <viz-filter field="ga:channelGrouping" [value]="channelGrouping" />
</tableau-viz>`,
  styleUrls: [],
})
export class TableauEmbedComponent {
  channelGrouping = 'Direct';
}

...it doesn't work. In the DOM inspector, what I see is literally this:

enter image description here

The entire value attribute just doesn't show up. I've tried this:

  <viz-filter field="user_id" [attr.value]="channelGrouping" />

...which seems like it does the right thing, in that the DOM inspector shows the value attribute, with the proper value passed along:

enter image description here

...but Tableau doesn't appear to like that because the Tableau report ignores that value, which makes me think it's not really getting passed to Tableau, for whatever reason. (Again, if I hard-code that value in viz-filter, then Tableau behaves properly.)

I've also tried this:

<viz-filter field="ga:channelGrouping" value="{{ channelGrouping }}" />

and:

<viz-filter field="ga:channelGrouping" attr.value="{{ channelGrouping }}" />

...but I get the same issue where I just see <viz-filter field="ga:channelGrouping" />.

And for the sake of completion, if I do this:

<tableau-viz
        id="tableauViz"
        src="https://public.tableau.com/views/DigitalMarketingWebTraffic/Cockpit?:language=en-US&:display_count=n&:origin=viz_share_link">
        <viz-filter field="ga:channelGrouping" [value]="channelGrouping" />
        <div [id]="channelGrouping"></div>
      </tableau-viz>

I see this:

enter image description here

...where the class variable channelGrouping is clearly bound to the id attribute of the div tag which is a sibling to the viz-filter tag.

Here's the start of a Stackblitz which has all the code that should be required for this to run--but of course for some reason this code is not actually working in Stackblitz.

https://stackblitz.com/edit/angular-uch2bk?file=src%2Ftableau-embed.ts


Solution

  • For anybody who stumbles across this, I've figured out one approach that works. I can't say with 100% certainty that this is the "right" way to do this, though it probably is.

    The issue is that Angular treats the value attribute on viz-filter differently than the src and id attributes on tableau-viz. In short, you can't use data binding on value in viz-filter, and so need to use Renderer2 to grab that component's native element, and then call setAttribute against that. Here's the complete code of the Angular component I'm using to embed a Tableau dashboard:

    import { CommonModule } from '@angular/common';
    import {
      AfterViewInit,
      CUSTOM_ELEMENTS_SCHEMA,
      Component,
      ElementRef,
      HostListener,
      Input,
      OnInit,
      Renderer2,
      SimpleChanges,
      ViewChild,
      inject,
    } from '@angular/core';
    
    @Component({
      selector: 'app-tableau-embed',
      standalone: true,
      imports: [CommonModule],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      template: `<div #wrapper>
        <tableau-viz
          [id]="vizIndex"
          [src]="url"
          width="{{ screenWidth }}"
          hide-tabs
          toolbar="hidden">
          <viz-filter #vizFilterUserId field="user_id"></viz-filter>
        </tableau-viz>
      </div>`,
      styleUrls: [],
    })
    export class TableauEmbedComponent implements AfterViewInit, OnInit {
      private renderer = inject(Renderer2);
    
      @Input() dashboardIndex = 0;
      @Input() toolbar = 'hidden';
      @Input() url = '';
      @Input() user_id = '';
      @ViewChild('wrapper') wrapperElement?: ElementRef<HTMLElement>;
      @ViewChild('vizFilterUserId', { static: false }) vizFilterUserId?: ElementRef;
      @HostListener('window:resize', ['$event'])
      onWindowResize() {
        this.calculateDashboardSize();
      }
    
      initialized = false;
      screenWidth: number = 0;
      vizIndex = `Tableau-Viz-${this.dashboardIndex}`;
    
      calculateDashboardSize = () => {
        const bufferSize = 25;
        this.screenWidth =
          this.wrapperElement?.nativeElement.offsetWidth || 0 - bufferSize;
      };
    
      ngOnInit(): void {
        this.calculateDashboardSize();
      }
    
      ngAfterViewInit(): void {
        if (this.vizFilterUserId) {
          this.renderer.setAttribute(
            this.vizFilterUserId.nativeElement,
            'value',
            this.user_id
          );
        }
      }
    }