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
On this page
Overview
Getting Started
Examples