Skip to content

Dynamically changing PDF files

Vue PDF Viewer allows developers to change PDF files in a Vue application dynamically. There are 3 ways to load PDF files into the PDF Viewer:

  • URL (http/https)
  • Internal File Path
  • Blob (From API or File Upload)

This tutorial will guide you on how to switch between different PDF sources using radio buttons. We'll create the following components in the components directory:

  • AppPdfViewer.vue - A reusable PDF viewer component
  • PdfViewerUrl.vue - A component to display PDF from a URL
  • PdfViewerFile.vue - A component to display PDF from a file path
  • PdfViewerBlob.vue - A component to display PDF from blob data
  • RadioGroupPdfSource.vue - A component to switch between PDF sources

Create a reusable AppPdfViewer component

Refer to Making Vue PDF Viewer Reusable for more information.

After creating the AppPdfViewer component, then you can use it in your application by passing different PDF file sources to it. The AppPdfViewer component will display the PDF file based on the source type.

Display a default PDF file via URL

Here is an example of how you can load a PDF file from URL (http/https) with the AppPdfViewer component.

Let's create a PdfViewerUrl component to display a PDF file from URL (http/https).

vue
<script setup lang="ts">
import { ref } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
const pdfFile = ref<string>(DEFAULT_FILE);
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script setup>
import { ref } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
const pdfFile = ref(DEFAULT_FILE);
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script lang="ts">
import { defineComponent } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
export default defineComponent({
  data() {
    return {
      pdfFile: DEFAULT_FILE,
    };
  }
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script>
import AppPdfViewer from "./AppPdfViewer.vue";
const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
export default {
  data() {
    return {
      pdfFile: DEFAULT_FILE,
    };
  }
}
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>

default-pdf-file-via-url

Display a default PDF file via Internal File Path

Another method to load a PDF file is via an Internal File Path. With Vite, there are 2 ways to do this:

  1. Place the PDF files in the public directory if they do not need processing by Vite. Files in the public directory are served as-is.
  2. Use the src/assets directory for PDFs that need to be processed or optimized by Vite.

Here is an example of how you can load a PDF file from an Internal File Path from the public directory with the AppPdfViewer component.

Let's create a PdfViewerForm component to display a PDF file from an Internal File Path from the public directory.

vue
<script setup lang="ts">
import { ref } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const DEFAULT_FILE = "/Sample-PDF-Form.pdf"; // The file is in the public directory
const pdfFile = ref<string>(DEFAULT_FILE);
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script setup>
import { ref } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const DEFAULT_FILE = "/Sample-PDF-Form.pdf"; // The file is in the public directory
const pdfFile = ref(DEFAULT_FILE);
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script lang="ts">
import { defineComponent } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

export default defineComponent({
  data() {
    return {
      pdfSrc: "/Sample-PDF-Form.pdf",
    };
  }
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script setup>
import AppPdfViewer from "./AppPdfViewer.vue";

export default {
  data() {
    return {
      pdfFile: "/Sample-PDF-Form.pdf",
    }
  }
}
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>

default-pdf-file-via-internal-file-path

Display a default PDF file via Blob

This method is to load a PDF file by converting it into a Blob URL, which is useful when working with files from APIs or file uploads.

Let's create a PdfViewerBlob component to display a PDF file from Blob.

vue
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const pdfFile = ref<string>("");

onMounted(async () => {
  // Fetch PDF from an API (able to use Internal File Path or URL (http/https)
  const response = await fetch(
    "/Monterail - State_of_vue.js_2021_report_short_annotated.pdf"
  );
  const blob = await response.blob();
  pdfFile.value = URL.createObjectURL(blob);
});

onBeforeUnmount(() => {
  // Clean up the Blob URL to avoid memory leaks
  if (pdfFile.value) {
    URL.revokeObjectURL(pdfFile.value);
  }
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script setup>
import { ref } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";

const pdfFile = ref("");

onMounted(async () => {
  // Fetch PDF from an API (able to use Internal File Path or URL (http/https)
  const response = await fetch(
    "/Monterail - State_of_vue.js_2021_report_short_annotated.pdf"
  );
  const blob = await response.blob();
  pdfFile.value = URL.createObjectURL(blob);
});

onBeforeUnmount(() => {
  // Clean up the Blob URL to avoid memory leaks
  if (pdfFile.value) {
    URL.revokeObjectURL(pdfFile.value);
  }
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script lang="ts">
import { defineComponent } from "vue";
import AppPdfViewer from "./AppPdfViewer.vue";
export default defineComponent({
  data() {
    return {
      pdfFile: "" as string,
    };
  },
  async mounted() {
    // Fetch PDF from an API (able to use Internal File Path or URL (http/https)
    const response = await fetch(
      "/Monterail - State_of_vue.js_2021_report_short_annotated.pdf",
    );
    const blob = await response.blob();
    this.pdfFile = URL.createObjectURL(blob);
  },
  beforeUnmount() {
    // Clean up the Blob URL to avoid memory leaks
    if (this.pdfFile) {
      URL.revokeObjectURL(this.pdfFile);
    }
  },
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>
vue
<script>
import AppPdfViewer from "./AppPdfViewer.vue";

export default {
  data() {
    return {
      pdfFile: "",
    };
  },
  async mounted() {
    // Fetch PDF from an API (able to use Internal File Path or URL (http/https)
    const response = await fetch(
      "/Monterail - State_of_vue.js_2021_report_short_annotated.pdf"
    );
    const blob = await response.blob()
    this.pdfFile = URL.createObjectURL(blob)
  },
  beforeUnmount() {
    // Clean up the Blob URL to avoid memory leaks
    if (this.pdfFile) {
      URL.revokeObjectURL(this.pdfFile)
    }
  },
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" />
  </div>
</template>

onBeforeUnmount ensures the Blob URL is revoked to prevent memory leaks.

default-pdf-file-via-blob

Remark: For more details of the best practices of each method, you may refer to Handling Source of PDF File.

Create a radio buttons component to switch between PDF sources

Here is an example of how you can create a radio buttons component to switch between PDF sources. Let's create a RadioGroupPdfSource component to switch between PDF sources.

vue
<script setup lang="ts">
import { ref, reactive, watch } from "vue";

type SourceType = "general" | "form" | "annotation";
type SourceOption = {
  label: string;
  value: SourceType;
}
const emit = defineEmits<{
  (e: "update:source-type", value: SourceType): void
}>();
const currentSourceType = ref<SourceType>("general");
const fileOptions = reactive<SourceOption[]>([
  {
    label: "General (URL)",
    value: "general",
  },
  {
    label: "PDF Form (Internal File Path)",
    value: "form",
  },
  {
    label: "Annotated PDF (Blob)",
    value: "annotation",
  },
]);
// Emit the source type to the parent component
watch(currentSourceType, (newVal) => {
  emit("update:source-type", newVal);
});
</script>
<template>
  <div :style="{ padding: '12px' }">
    <p>Switch PDF File</p>
    <div v-for="option in fileOptions" :key="option.value">
      <input type="radio" :id="`src-type-${option.value}`" :value="option.value" v-model="currentSourceType" />
      <label :for="`src-type-${option.value}`"> {{ option.label }} </label>
    </div>
  </div>
</template>
vue
<script setup>
import { ref, reactive, watch } from "vue";

const currentSourceType = ref("general");
const fileOptions = reactive([
  {
    label: "General (URL)",
    value: "general",
  },
  {
    label: "PDF Form (Internal File Path)",
    value: "form",
  },
  {
    label: "Annotated PDF (Blob)",
    value: "annotation",
  },
]);
// Emit the source type to the parent component
watch(currentSourceType, (newVal) => {
  emit("update:source-type", newVal);
});
</script>
<template>
  <div :style="{ padding: '12px' }">
    <p>Switch PDF File</p>
    <div v-for="option in fileOptions" :key="option.value">
      <input type="radio" :id="`src-type-${option.value}`" :value="option.value" v-model="currentSourceType" />
      <label :for="`src-type-${option.value}`"> {{ option.label }} </label>
    </div>
  </div>
</template>
vue
<script lang="ts">
import { defineComponent } from "vue";

type SourceType = "general" | "form" | "annotation";
type SourceOption = {
  label: string;
  value: SourceType;
}
export default defineComponent({
  data() {
    return {
      currentSourceType: "general" as SourceType,
      fileOptions: [
        {
          label: "General (URL)",
          value: "general",
        },
        {
          label: "PDF Form (Internal File Path)",
          value: "form",
        },
        {
          label: "Annotated PDF (Blob)",
          value: "annotation",
        },
      ] as SourceOption[],
    };
  },
  watch: {
    currentSourceType(newVal) {
      this.$emit("update:source-type", newVal);
    }
  }
})
</script>
<template>
  <div :style="{ padding: '12px' }">
    <p>Switch PDF File</p>
    <div v-for="option in fileOptions" :key="option.value">
      <input type="radio" :id="`src-type-${option.value}`" :value="option.value" v-model="currentSourceType" />
      <label :for="`src-type-${option.value}`"> {{ option.label }} </label>
    </div>
  </div>
</template>
vue
<script>
export default {
  data() {
    return {
      currentSourceType: "general",
      fileOptions: [
        {
          label: "General (URL)",
          value: "general",
        },
        {
          label: "PDF Form (Internal File Path)",
          value: "form",
        },
        {
          label: "Annotated PDF (Blob)",
          value: "annotation",
        },
      ],
    }
  },
  watch: {
    currentSourceType(newVal) {
      this.$emit("update:source-type", newVal);
    }
  }
}
</script>
<template>
  <div :style="{ padding: '12px' }">
    <p>Switch PDF File</p>
    <div v-for="option in fileOptions" :key="option.value">
      <input type="radio" :id="`src-type-${option.value}`" :value="option.value" v-model="currentSourceType" />
      <label :for="`src-type-${option.value}`"> {{ option.label }} </label>
    </div>
  </div>
</template>

radio-buttons-component

Complete example

Now, we have five components ready to complete the example.

  • AppPdfViewer - The reusable component that displays a PDF file.
  • PdfViewerUrl - The component that displays AppPdfViewer with a PDF file from a URL.
  • PdfViewerForm - The component that displays AppPdfViewer with a PDF file from an Internal File Path.
  • PdfViewerBlob - The component that displays AppPdfViewer with a PDF file from a Blob.
  • RadioGroupPdfSource - The component that displays radio buttons to switch between different PDF sources.

Let's update the App.vue file which is the root component of Vue application. It will display the RadioGroupPdfSource component and conditionally render the appropriate PDF viewer component based on the selected source type.

Here is a complete example of how you can switch between different PDF sources using radio buttons.

vue
<script setup lang="ts">
import { ref } from "vue";
import PdfViewerUrl from "./components/PdfViewerUrl.vue";
import PdfViewerForm from "./components/PdfViewerForm.vue";
import PdfViewerBlob from "./components/PdfViewerBlob.vue";
import RadioGroupPdfSource from "./components/RadioGroupPdfSource.vue";

// The type of the source type
// Should be the same as the type in the `RadioGroupPdfSource` component
type SourceType = "general" | "form" | "annotation"; 

const currentSourceType = ref<SourceType>("general");
const handleSourceTypeChange = (sourceType: SourceType) => {
  currentSourceType.value = sourceType;
};
</script>
<template>
  <div>
    <RadioGroupPdfSource @update:source-type="handleSourceTypeChange" />
    <PdfViewerUrl v-if="currentSourceType === 'general'" />
    <PdfViewerForm v-else-if="currentSourceType === 'form'" />
    <PdfViewerBlob v-else-if="currentSourceType === 'annotation'" />
  </div>
</template>
vue
<script setup>
import { ref } from "vue";
import PdfViewerUrl from "./components/PdfViewerUrl.vue";
import PdfViewerForm from "./components/PdfViewerForm.vue";
import PdfViewerBlob from "./components/PdfViewerBlob.vue";
import RadioGroupPdfSource from "./components/RadioGroupPdfSource.vue";

const currentSourceType = ref("general");
const handleSourceTypeChange = (sourceType) => {
  currentSourceType.value = sourceType;
};
</script>
<template>
  <div>
    <RadioGroupPdfSource @update:source-type="handleSourceTypeChange" />
    <PdfViewerUrl v-if="currentSourceType === 'general'" />
    <PdfViewerForm v-else-if="currentSourceType === 'form'" />
    <PdfViewerBlob v-else-if="currentSourceType === 'annotation'" />
  </div>
</template>
vue
<script lang="ts">
import { defineComponent } from "vue";
import PdfViewerUrl from "./components/PdfViewerUrl.vue";
import PdfViewerForm from "./components/PdfViewerForm.vue";
import PdfViewerBlob from "./components/PdfViewerBlob.vue";
import RadioGroupPdfSource from "./components/RadioGroupPdfSource.vue";

// The type of the source type
// Should be the same as the type in the `RadioGroupPdfSource` component
type SourceType = "general" | "form" | "annotation"; 

export default defineComponent({
  data() {
    return {
      currentSourceType: "general" as SourceType,
    };
  },
  methods: {
    handleSourceTypeChange(sourceType: SourceType) {
      this.currentSourceType = sourceType;
    }
  }
});
</script>
<template>
  <div>
    <RadioGroupPdfSource @update:source-type="handleSourceTypeChange" />
    <PdfViewerUrl v-if="currentSourceType === 'general'" />
    <PdfViewerForm v-else-if="currentSourceType === 'form'" />
    <PdfViewerBlob v-else-if="currentSourceType === 'annotation'" />
  </div>
</template>
vue
<script>
import PdfViewerUrl from "./components/PdfViewerUrl.vue";
import PdfViewerForm from "./components/PdfViewerForm.vue";
import PdfViewerBlob from "./components/PdfViewerBlob.vue";
import RadioGroupPdfSource from "./components/RadioGroupPdfSource.vue";

export default {
  data() {
    return {
      currentSourceType: "general",
    };
  },
  methods: {
    handleSourceTypeChange(sourceType) {
      this.currentSourceType = sourceType;
    }
  }
}
</script>
<template>
  <div>
    <RadioGroupPdfSource @update:source-type="handleSourceTypeChange" />
    <PdfViewerUrl v-if="currentSourceType === 'general'" />
    <PdfViewerForm v-else-if="currentSourceType === 'form'" />
    <PdfViewerBlob v-else-if="currentSourceType === 'annotation'" />
  </div>
</template>

Bonus: Handling error when loading a PDF file

When loading a PDF file, there are some cases that the file might not be found or cannot be loaded. In that case, you can use the loadError event to handle the error.

Here is steps of how you can handle the loadError event to display an error message when the PDF file fails to load:

  1. First, emit the loadError event from the AppPdfViewer component
  2. Handle the loadError event in PdfViewerUrl, PdfViewerForm, and PdfViewerBlob component
  3. Display the error message to the user if there is an error

Here is an example of how you can handle the loadError event within the PdfViewerUrl component.

vue
<script setup lang="ts">
import { ref } from "vue";
import AppPdfViewer from "./components/AppPdfViewer.vue";

const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
const pdfFile = ref<string>(DEFAULT_FILE);
const handleLoadError = (error: any) => {
  console.error("Error loading PDF file:", error);
  alert("Error loading PDF file");
};
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" @loadError="handleLoadError" />
  </div>
</template>
vue
<script setup>
import { ref } from "vue";
import AppPdfViewer from "./components/AppPdfViewer.vue";

const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
const pdfFile = ref("");
const handleLoadError = (error) => {
  console.error("Error loading PDF file:", error);
  alert("Error loading PDF file");
};
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" @loadError="handleLoadError" />
  </div>
</template>
vue
<script lang="ts">
import { defineComponent } from "vue";
import AppPdfViewer from "./components/AppPdfViewer.vue";
const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
export default defineComponent({
  data() {
    return {
      pdfFile: DEFAULT_FILE,
    };
  },
  methods: {
    handleLoadError(error: any) {
      console.error("Error loading PDF file:", error);
      alert("Error loading PDF file");
    }
  }
});
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" @loadError="handleLoadError" />
  </div>
</template>
vue
<script>
import AppPdfViewer from "./components/AppPdfViewer.vue";

const DEFAULT_FILE = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf";
export default {
  data() {
    return {
      pdfFile: DEFAULT_FILE,
    };
  },
  methods: {
    handleLoadError(error: any) {
      console.error("Error loading PDF file:", error);
      alert("Error loading PDF file");
    }
  }
};
</script>
<template>
  <p>PDF File: {{ pdfFile }}</p>
  <div :style="{ width: '1028px', height: '700px' }">
    <AppPdfViewer :src="pdfFile" @loadError="handleLoadError" />
  </div>
</template>

Subsequently, you can apply this concept of handling the loadError event in both PdfViewerForm and PdfViewerBlob components to display an error message accordingly.