Vue Integration

Learn how to integrate the Qlik TypeScript SDK with Vue.js applications using the Composition API, composables, and reactive patterns.

Vue Integration Overview

Vue.js provides excellent reactivity and composability features that work seamlessly with the Qlik SDK. This guide covers Vue 3 Composition API patterns, custom composables, and reactive data management.

Composables
Custom composables for Qlik SDK functionality
Components
Reusable Vue components for Qlik data
State Management
Pinia integration for global state
Reactivity
Vue's reactive system with Qlik data

Getting Started

Installation & Setup

bash
# Create Vue 3 project
npm create vue@latest qlik-vue-app
cd qlik-vue-app

# Install dependencies
npm install
npm install qlik

# Install additional dependencies for state management
npm install pinia
npm install @vueuse/core  # Useful Vue utilities

# Install development dependencies
npm install --save-dev @types/node

Vue Composables

useQlik Composable

Core composable for managing Qlik SDK instance and authentication

typescript
// composables/useQlik.ts
import { ref, computed, onMounted, onUnmounted } from 'vue'
import Qlik from 'qlik'
import type { Ref } from 'vue'

interface QlikConfig {
  host: string
  webIntegrationId: string
  autoAuthenticate?: boolean
}

export function useQlik(config: QlikConfig) {
  const qlik = ref<Qlik | null>(null)
  const isConnected = ref(false)
  const isAuthenticating = ref(false)
  const error = ref<string | null>(null)
  const user = ref<any | null>(null)

  // Computed properties
  const isReady = computed(() => qlik.value && isConnected.value)
  const hasError = computed(() => error.value !== null)

  // Initialize Qlik instance
  const initialize = async () => {
    try {
      qlik.value = new Qlik({
        host: config.host,
        webIntegrationId: config.webIntegrationId
      })

      if (config.autoAuthenticate) {
        await authenticate()
      }

      console.log('✅ Qlik SDK initialized')
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to initialize Qlik'
      console.error('❌ Qlik initialization failed:', err)
    }
  }

  // Authenticate with Qlik
  const authenticate = async () => {
    if (!qlik.value || isAuthenticating.value) return

    try {
      isAuthenticating.value = true
      error.value = null

      await qlik.value.authenticateToQlik()
      isConnected.value = true

      // Get user info
      try {
        user.value = await qlik.value.getUserInfo()
      } catch (userErr) {
        console.warn('Could not fetch user info:', userErr)
      }

      console.log('✅ Authentication successful')
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Authentication failed'
      isConnected.value = false
      console.error('❌ Authentication failed:', err)
    } finally {
      isAuthenticating.value = false
    }
  }

  // Logout
  const logout = async () => {
    if (!qlik.value) return

    try {
      await qlik.value.logout()
      isConnected.value = false
      user.value = null
      console.log('✅ Logged out successfully')
    } catch (err) {
      console.error('❌ Logout failed:', err)
    }
  }

  // Check authentication status
  const checkAuth = async () => {
    if (!qlik.value) return false

    try {
      const authenticated = await qlik.value.isAuthenticated()
      isConnected.value = authenticated
      return authenticated
    } catch (err) {
      console.error('Auth check failed:', err)
      isConnected.value = false
      return false
    }
  }

  // Clear error
  const clearError = () => {
    error.value = null
  }

  // Lifecycle hooks
  onMounted(() => {
    initialize()
  })

  onUnmounted(() => {
    // Cleanup if needed
  })

  return {
    // State
    qlik: readonly(qlik),
    isConnected: readonly(isConnected),
    isAuthenticating: readonly(isAuthenticating),
    error: readonly(error),
    user: readonly(user),

    // Computed
    isReady,
    hasError,

    // Methods
    authenticate,
    logout,
    checkAuth,
    clearError
  }
}

// Usage in component
export default {
  setup() {
    const {
      qlik,
      isConnected,
      isAuthenticating,
      error,
      user,
      isReady,
      authenticate,
      logout,
      clearError
    } = useQlik({
      host: 'your-tenant.us.qlikcloud.com',
      webIntegrationId: 'your-web-integration-id',
      autoAuthenticate: true
    })

    return {
      qlik,
      isConnected,
      isAuthenticating,
      error,
      user,
      isReady,
      authenticate,
      logout,
      clearError
    }
  }
}

Vue Components

Reusable Qlik Components

Pre-built Vue components for common Qlik visualizations

vue
<!-- components/QlikChart.vue -->
<template>
  <div class="qlik-chart" :class="{ 'is-loading': isLoading }">
    <div v-if="title" class="chart-header">
      <h3 class="chart-title">{{ title }}</h3>
      <div class="chart-actions">
        <button @click="refresh" :disabled="isLoading" class="btn-refresh">
          <RefreshIcon :class="{ 'animate-spin': isLoading }" />
        </button>
      </div>
    </div>

    <div class="chart-content">
      <!-- Loading state -->
      <div v-if="isLoading" class="chart-loading">
        <div class="loading-spinner"></div>
        <p>Loading chart data...</p>
      </div>

      <!-- Error state -->
      <div v-else-if="error" class="chart-error">
        <AlertIcon />
        <p>{{ error }}</p>
        <button @click="refresh" class="btn-retry">Try Again</button>
      </div>

      <!-- Chart content -->
      <div v-else-if="hasData" class="chart-visualization" ref="chartRef">
        <!-- Chart will be rendered here -->
      </div>

      <!-- Empty state -->
      <div v-else class="chart-empty">
        <p>No data available</p>
      </div>
    </div>

    <!-- Chart metadata -->
    <div v-if="showMetadata && lastUpdated" class="chart-metadata">
      <small>Last updated: {{ formatDate(lastUpdated) }}</small>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { useQlikData } from '@/composables/useQlikData'
import { RefreshIcon, AlertIcon } from 'lucide-vue-next'

interface Props {
  app: any
  definition: any
  title?: string
  height?: number
  showMetadata?: boolean
  autoRefresh?: boolean
  refreshInterval?: number
}

const props = withDefaults(defineProps<Props>(), {
  height: 400,
  showMetadata: true,
  autoRefresh: false,
  refreshInterval: 30000
})

const chartRef = ref<HTMLElement>()

// Use the data composable
const {
  data,
  headers,
  isLoading,
  error,
  hasData,
  lastUpdated,
  refresh
} = useQlikData({
  app: computed(() => props.app),
  definition: props.definition,
  autoRefresh: props.autoRefresh,
  refreshInterval: props.refreshInterval
})

// Chart rendering logic
const renderChart = () => {
  if (!chartRef.value || !hasData.value) return

  // Implement your chart rendering logic here
  // This could use D3.js, Chart.js, or any other visualization library
  console.log('Rendering chart with data:', data.value)
}

// Format date utility
const formatDate = (date: Date) => {
  return new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit'
  }).format(date)
}

// Watch for data changes
watch(data, () => {
  renderChart()
}, { deep: true })

// Render chart when component mounts
onMounted(() => {
  renderChart()
})
</script>

<style scoped>
.qlik-chart {
  border: 1px solid #e2e8f0;
  border-radius: 0.5rem;
  overflow: hidden;
}

.chart-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  border-bottom: 1px solid #e2e8f0;
  background-color: #f8fafc;
}

.chart-title {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: #1e293b;
}

.chart-actions {
  display: flex;
  gap: 0.5rem;
}

.btn-refresh {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  background-color: white;
  color: #6b7280;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-refresh:hover {
  background-color: #f3f4f6;
  border-color: #9ca3af;
}

.btn-refresh:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.chart-content {
  position: relative;
  min-height: 400px;
}

.chart-loading,
.chart-error,
.chart-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 400px;
  color: #6b7280;
}

.loading-spinner {
  width: 2rem;
  height: 2rem;
  border: 2px solid #f3f4f6;
  border-top: 2px solid #3b82f6;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.chart-error {
  color: #dc2626;
}

.btn-retry {
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  border: 1px solid #dc2626;
  border-radius: 0.375rem;
  background-color: white;
  color: #dc2626;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-retry:hover {
  background-color: #dc2626;
  color: white;
}

.chart-metadata {
  padding: 0.5rem 1rem;
  border-top: 1px solid #e2e8f0;
  background-color: #f8fafc;
  color: #6b7280;
  font-size: 0.875rem;
}

.animate-spin {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
</style>

State Management with Pinia

Qlik Store with Pinia

Centralized state management for Qlik data and authentication

typescript
// stores/qlik.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import Qlik from 'qlik'

export const useQlikStore = defineStore('qlik', () => {
  // State
  const qlik = ref<Qlik | null>(null)
  const isAuthenticated = ref(false)
  const isConnecting = ref(false)
  const user = ref<any | null>(null)
  const error = ref<string | null>(null)
  const apps = ref<any[]>([])
  const currentApp = ref<any | null>(null)

  // Getters
  const isReady = computed(() => qlik.value !== null && isAuthenticated.value)
  const hasApps = computed(() => apps.value.length > 0)
  const currentAppName = computed(() => currentApp.value?.name || 'No app selected')

  // Actions
  const initialize = async (config: { host: string; webIntegrationId: string }) => {
    try {
      isConnecting.value = true
      error.value = null

      qlik.value = new Qlik(config)
      await qlik.value.authenticateToQlik()
      
      isAuthenticated.value = true
      
      // Get user info
      try {
        user.value = await qlik.value.getUserInfo()
      } catch (err) {
        console.warn('Could not fetch user info:', err)
      }

      // Load apps
      await loadApps()

      console.log('✅ Qlik store initialized')
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Initialization failed'
      console.error('❌ Qlik store initialization failed:', err)
    } finally {
      isConnecting.value = false
    }
  }

  const loadApps = async () => {
    if (!qlik.value || !isAuthenticated.value) return

    try {
      const appList = await qlik.value.getAppList()
      apps.value = appList.map((app: any) => ({
        id: app.id,
        name: app.name,
        description: app.description,
        published: app.published,
        owner: app.owner
      }))

      console.log(`✅ Loaded ${apps.value.length} apps`)
    } catch (err) {
      console.error('❌ Failed to load apps:', err)
    }
  }

  const selectApp = async (appId: string) => {
    if (!qlik.value || !isAuthenticated.value) return

    try {
      const app = await qlik.value.getApp(appId)
      currentApp.value = {
        id: appId,
        instance: app,
        name: apps.value.find(a => a.id === appId)?.name || 'Unknown App'
      }

      console.log(`✅ Selected app: ${currentApp.value.name}`)
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to select app'
      console.error(`❌ Failed to select app ${appId}:`, err)
    }
  }

  const logout = async () => {
    try {
      if (qlik.value) {
        await qlik.value.logout()
      }
      
      // Reset state
      qlik.value = null
      isAuthenticated.value = false
      user.value = null
      apps.value = []
      currentApp.value = null
      error.value = null

      console.log('✅ Logged out successfully')
    } catch (err) {
      console.error('❌ Logout failed:', err)
    }
  }

  const clearError = () => {
    error.value = null
  }

  const refreshApps = async () => {
    await loadApps()
  }

  // Return store interface
  return {
    // State
    qlik: readonly(qlik),
    isAuthenticated: readonly(isAuthenticated),
    isConnecting: readonly(isConnecting),
    user: readonly(user),
    error: readonly(error),
    apps: readonly(apps),
    currentApp: readonly(currentApp),

    // Getters
    isReady,
    hasApps,
    currentAppName,

    // Actions
    initialize,
    loadApps,
    selectApp,
    logout,
    clearError,
    refreshApps
  }
})

// Usage in component
export default {
  setup() {
    const qlikStore = useQlikStore()

    onMounted(() => {
      qlikStore.initialize({
        host: 'your-tenant.us.qlikcloud.com',
        webIntegrationId: 'your-web-integration-id'
      })
    })

    return {
      ...qlikStore
    }
  }
}

Complete Vue Application

Full Dashboard Example

Complete Vue.js dashboard using all the patterns above

vue
<!-- App.vue -->
<template>
  <div id="app" class="app">
    <!-- Navigation -->
    <nav class="navbar">
      <div class="nav-brand">
        <h1>Qlik Analytics Dashboard</h1>
      </div>
      <div class="nav-actions">
        <div v-if="isAuthenticated" class="user-info">
          <span>Welcome, {{ user?.name || 'User' }}</span>
          <button @click="logout" class="btn-logout">Logout</button>
        </div>
        <div v-else-if="isConnecting" class="connecting">
          <span>Connecting...</span>
        </div>
        <div v-else-if="error" class="error">
          <span>{{ error }}</span>
          <button @click="clearError" class="btn-clear">Clear</button>
        </div>
      </div>
    </nav>

    <!-- Main content -->
    <main class="main-content">
      <!-- Loading state -->
      <div v-if="isConnecting" class="loading-screen">
        <div class="loading-spinner large"></div>
        <p>Initializing Qlik connection...</p>
      </div>

      <!-- Error state -->
      <div v-else-if="error" class="error-screen">
        <h2>Connection Error</h2>
        <p>{{ error }}</p>
        <button @click="retryConnection" class="btn-retry">Retry Connection</button>
      </div>

      <!-- Authentication required -->
      <div v-else-if="!isAuthenticated" class="auth-screen">
        <h2>Authentication Required</h2>
        <p>Please authenticate to access your Qlik analytics.</p>
      </div>

      <!-- Dashboard -->
      <div v-else class="dashboard">
        <!-- App selector -->
        <div class="app-selector">
          <label for="app-select">Select Application:</label>
          <select 
            id="app-select" 
            v-model="selectedAppId" 
            @change="handleAppChange"
            class="app-select"
          >
            <option value="">Choose an app...</option>
            <option 
              v-for="app in apps" 
              :key="app.id" 
              :value="app.id"
            >
              {{ app.name }}
            </option>
          </select>
        </div>

        <!-- Charts grid -->
        <div v-if="currentApp" class="charts-grid">
          <QlikChart
            :app="currentApp.instance"
            :definition="salesChartDef"
            title="Sales by Product"
            :auto-refresh="true"
            :refresh-interval="30000"
          />
          
          <QlikChart
            :app="currentApp.instance"
            :definition="revenueChartDef"
            title="Revenue Trend"
            :auto-refresh="true"
          />
          
          <QlikDataTable
            :app="currentApp.instance"
            :definition="customerTableDef"
            title="Top Customers"
            :page-size="10"
          />
        </div>

        <!-- Empty state -->
        <div v-else class="empty-state">
          <p>Please select an application to view analytics.</p>
        </div>
      </div>
    </main>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useQlikStore } from './stores/qlik'
import QlikChart from './components/QlikChart.vue'
import QlikDataTable from './components/QlikDataTable.vue'

// Store
const qlikStore = useQlikStore()
const {
  isAuthenticated,
  isConnecting,
  user,
  error,
  apps,
  currentApp,
  isReady,
  initialize,
  selectApp,
  logout,
  clearError
} = qlikStore

// Local state
const selectedAppId = ref('')

// Chart definitions
const salesChartDef = {
  qDimensions: [
    { qDef: { qFieldDefs: ['Product'] } }
  ],
  qMeasures: [
    { qDef: { qDef: 'Sum(Sales)', qLabel: 'Total Sales' } }
  ],
  qInitialDataFetch: [
    { qLeft: 0, qTop: 0, qWidth: 2, qHeight: 100 }
  ]
}

const revenueChartDef = {
  qDimensions: [
    { qDef: { qFieldDefs: ['Month'] } }
  ],
  qMeasures: [
    { qDef: { qDef: 'Sum(Revenue)', qLabel: 'Monthly Revenue' } }
  ],
  qInitialDataFetch: [
    { qLeft: 0, qTop: 0, qWidth: 2, qHeight: 12 }
  ]
}

const customerTableDef = {
  qDimensions: [
    { qDef: { qFieldDefs: ['Customer'] } }
  ],
  qMeasures: [
    { qDef: { qDef: 'Sum(Sales)', qLabel: 'Total Sales' } },
    { qDef: { qDef: 'Count(OrderID)', qLabel: 'Orders' } }
  ],
  qInitialDataFetch: [
    { qLeft: 0, qTop: 0, qWidth: 3, qHeight: 10 }
  ]
}

// Methods
const handleAppChange = () => {
  if (selectedAppId.value) {
    selectApp(selectedAppId.value)
  }
}

const retryConnection = () => {
  initialize({
    host: import.meta.env.VITE_QLIK_HOST,
    webIntegrationId: import.meta.env.VITE_QLIK_WEB_INTEGRATION_ID
  })
}

// Initialize on mount
onMounted(() => {
  retryConnection()
})

// Watch for current app changes
watch(currentApp, (newApp) => {
  if (newApp) {
    selectedAppId.value = newApp.id
  }
})
</script>

<style scoped>
.app {
  min-height: 100vh;
  background-color: #f8fafc;
}

.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  background-color: white;
  border-bottom: 1px solid #e2e8f0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.nav-brand h1 {
  margin: 0;
  color: #1e293b;
  font-size: 1.5rem;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.btn-logout,
.btn-clear,
.btn-retry {
  padding: 0.5rem 1rem;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  background-color: white;
  color: #374151;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-logout:hover,
.btn-clear:hover,
.btn-retry:hover {
  background-color: #f3f4f6;
}

.main-content {
  padding: 2rem;
}

.loading-screen,
.error-screen,
.auth-screen {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 400px;
  text-align: center;
}

.loading-spinner.large {
  width: 3rem;
  height: 3rem;
  border: 3px solid #f3f4f6;
  border-top: 3px solid #3b82f6;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.dashboard {
  max-width: 1200px;
  margin: 0 auto;
}

.app-selector {
  margin-bottom: 2rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}

.app-select {
  padding: 0.5rem 1rem;
  border: 1px solid #d1d5db;
  border-radius: 0.375rem;
  background-color: white;
  min-width: 200px;
}

.charts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
  gap: 2rem;
}

.empty-state {
  text-align: center;
  padding: 4rem;
  color: #6b7280;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
</style>

🎯 Vue Integration Best Practices

Composables: Use composables for reusable Qlik functionality
Reactivity: Leverage Vue's reactive system for automatic UI updates
State Management: Use Pinia for centralized Qlik state management
Error Handling: Implement comprehensive error states in components
Performance: Use readonly refs and computed properties for optimization
Cleanup: Properly cleanup resources in onUnmounted hooks
TypeScript: Use TypeScript for better development experience
Testing: Write tests for composables and components