Skip to content

Vue Integration

Torrin integrates seamlessly with Vue 3 applications using the Composition API. This guide shows you how to build upload components with progress tracking, pause/resume, and error handling.

Installation

bash
npm install @torrin-kit/client
bash
yarn add @torrin-kit/client
bash
pnpm add @torrin-kit/client
bash
bun add @torrin-kit/client

Basic Example

A simple file uploader with progress tracking using Composition API:

vue
<script setup lang="ts">
import { ref } from 'vue';
import { createTorrinClient } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

const progress = ref(0);
const status = ref('idle');

const handleFileChange = async (event: Event) => {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (!file) return;

  const upload = torrin.createUpload({ file });

  upload.on('progress', (p) => {
    progress.value = p.percentage;
  });

  upload.on('status', (s) => {
    status.value = s;
  });

  try {
    const result = await upload.start();
    console.log('Upload complete:', result.location);
  } catch (error) {
    console.error('Upload failed:', error);
  }
};
</script>

<template>
  <div class="space-y-4">
    <input 
      type="file" 
      @change="handleFileChange"
      class="block w-full text-sm"
    />
    
    <div v-if="status !== 'idle'" class="space-y-2">
      <div class="w-full bg-gray-200 rounded-full h-2.5">
        <div 
          class="bg-green-600 h-2.5 rounded-full transition-all" 
          :style="{ width: `${progress}%` }"
        />
      </div>
      <div class="flex justify-between text-sm">
        <span>{{ status }}</span>
        <span>{{ progress.toFixed(1) }}%</span>
      </div>
    </div>
  </div>
</template>

Advanced Example

A complete uploader with pause, resume, and cancel functionality:

vue
<script setup lang="ts">
import { ref, shallowRef } from 'vue';
import { createTorrinClient, type TorrinUpload } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

const progress = ref(0);
const status = ref<string>('idle');
const error = ref<string | null>(null);
const upload = shallowRef<TorrinUpload | null>(null);

const handleFileChange = async (event: Event) => {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  if (!file) return;

  error.value = null;
  progress.value = 0;

  const newUpload = torrin.createUpload({ 
    file,
    metadata: { 
      userId: 'user123',
      timestamp: new Date().toISOString() 
    }
  });

  upload.value = newUpload;

  newUpload.on('progress', (p) => {
    progress.value = p.percentage;
    console.log(`${p.chunksCompleted}/${p.totalChunks} chunks uploaded`);
  });

  newUpload.on('status', (s) => {
    status.value = s;
  });

  newUpload.on('error', (err) => {
    error.value = err.message;
  });

  try {
    const result = await newUpload.start();
    console.log('Upload complete:', result);
    upload.value = null;
  } catch (err: any) {
    error.value = err.message;
  }
};

const handlePause = () => {
  upload.value?.pause();
};

const handleResume = () => {
  upload.value?.resume();
};

const handleCancel = async () => {
  await upload.value?.cancel();
  upload.value = null;
  progress.value = 0;
  status.value = 'idle';
};
</script>

<template>
  <div class="max-w-md p-6 bg-white rounded-lg shadow">
    <h2 class="text-xl font-bold mb-4">Upload File</h2>
    
    <input 
      type="file" 
      @change="handleFileChange"
      :disabled="status === 'uploading'"
      class="block w-full mb-4"
    />

    <div v-if="error" class="mb-4 p-3 bg-red-50 border border-red-200 rounded">
      <p class="text-sm text-red-600">{{ error }}</p>
    </div>

    <div v-if="status !== 'idle'" class="space-y-4">
      <div>
        <div class="flex justify-between text-sm mb-1">
          <span class="font-medium">Status: {{ status }}</span>
          <span>{{ progress.toFixed(1) }}%</span>
        </div>
        <div class="w-full bg-gray-200 rounded-full h-3">
          <div 
            class="bg-green-600 h-3 rounded-full transition-all duration-300" 
            :style="{ width: `${progress}%` }"
          />
        </div>
      </div>

      <div class="flex gap-2">
        <button
          v-if="status === 'uploading'"
          @click="handlePause"
          class="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
        >
          Pause
        </button>
        
        <button
          v-if="status === 'paused'"
          @click="handleResume"
          class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
        >
          Resume
        </button>
        
        <button
          v-if="status === 'uploading' || status === 'paused'"
          @click="handleCancel"
          class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
        >
          Cancel
        </button>
      </div>
    </div>
  </div>
</template>

Composable

Create a reusable composable for upload management:

typescript
// composables/useTorrinUpload.ts
import { ref, shallowRef } from 'vue';
import { createTorrinClient, type TorrinUpload } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

export function useTorrinUpload() {
  const progress = ref(0);
  const status = ref<string>('idle');
  const error = ref<string | null>(null);
  const upload = shallowRef<TorrinUpload | null>(null);

  const startUpload = async (file: File, metadata?: Record<string, any>) => {
    error.value = null;
    progress.value = 0;

    const newUpload = torrin.createUpload({ file, metadata });
    upload.value = newUpload;

    newUpload.on('progress', (p) => {
      progress.value = p.percentage;
    });

    newUpload.on('status', (s) => {
      status.value = s;
    });

    newUpload.on('error', (err) => {
      error.value = err.message;
    });

    try {
      return await newUpload.start();
    } catch (err: any) {
      error.value = err.message;
      throw err;
    }
  };

  const pause = () => {
    upload.value?.pause();
  };

  const resume = () => {
    upload.value?.resume();
  };

  const cancel = async () => {
    await upload.value?.cancel();
    upload.value = null;
    progress.value = 0;
    status.value = 'idle';
  };

  return {
    progress,
    status,
    error,
    startUpload,
    pause,
    resume,
    cancel,
  };
}

Usage:

vue
<script setup lang="ts">
import { useTorrinUpload } from './composables/useTorrinUpload';

const { progress, status, error, startUpload, pause, resume, cancel } = useTorrinUpload();

const handleFile = async (e: Event) => {
  const input = e.target as HTMLInputElement;
  const file = input.files?.[0];
  if (file) {
    try {
      const result = await startUpload(file, { userId: '123' });
      console.log('Done:', result);
    } catch (err) {
      console.error('Failed:', err);
    }
  }
};
</script>

<template>
  <div>
    <input type="file" @change="handleFile" />
    <p>Progress: {{ progress }}%</p>
    <p>Status: {{ status }}</p>
    <p v-if="error">Error: {{ error }}</p>
    <button @click="pause">Pause</button>
    <button @click="resume">Resume</button>
    <button @click="cancel">Cancel</button>
  </div>
</template>

With Resume Support

Enable automatic resume after page refresh:

typescript
import { createTorrinClient, createLocalStorageResumeStore } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
  resumeStore: createLocalStorageResumeStore(), // Enable auto-resume
});

Now if the user refreshes the page and selects the same file, the upload will automatically resume from where it left off.

Options API

For Vue 2 or Options API users:

vue
<script lang="ts">
import { defineComponent } from 'vue';
import { createTorrinClient } from '@torrin-kit/client';

const torrin = createTorrinClient({
  endpoint: 'http://localhost:3000/api/uploads',
});

export default defineComponent({
  data() {
    return {
      progress: 0,
      status: 'idle',
      upload: null as any,
    };
  },
  methods: {
    async handleFileChange(event: Event) {
      const input = event.target as HTMLInputElement;
      const file = input.files?.[0];
      if (!file) return;

      this.upload = torrin.createUpload({ file });

      this.upload.on('progress', (p: any) => {
        this.progress = p.percentage;
      });

      this.upload.on('status', (s: string) => {
        this.status = s;
      });

      try {
        const result = await this.upload.start();
        console.log('Done:', result);
      } catch (error) {
        console.error(error);
      }
    },
    handlePause() {
      this.upload?.pause();
    },
    handleResume() {
      this.upload?.resume();
    },
    async handleCancel() {
      await this.upload?.cancel();
      this.progress = 0;
      this.status = 'idle';
    },
  },
});
</script>

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <div v-if="status !== 'idle'">
      <progress :value="progress" max="100"></progress>
      <p>{{ progress }}% - {{ status }}</p>
      <button @click="handlePause">Pause</button>
      <button @click="handleResume">Resume</button>
      <button @click="handleCancel">Cancel</button>
    </div>
  </div>
</template>

Next Steps