FranckFreiburger / vue-pdf

vue.js pdf viewer
MIT License
2.23k stars 522 forks source link

Refreshing PDF when src is a dynamic prop and should display multiple pages #239

Open Igal-Kleiner opened 4 years ago

Igal-Kleiner commented 4 years ago

I created the following component, PdfViewer, to which I pass a fileName as a prop. When the fileName is passed, it should look for the URL in the store getter and refresh the pdf component with the new pdf.

<template>
  <div class="fill-height pdf-container">
    <template v-if="true">
      <pdf
        v-for="p in numberOfPages"
        :key="`${fileName}-${p}`"
        :src="src"
        :page="p"
      />
    </template>
  </div>
</template>
<script>
import { mapGetters } from 'vuex'
import pdf from 'vue-pdf'

export default {
  props: {
    fileName: {
      type: String,
      required: true
    }
  },
  components: {
    pdf
  },
  data() {
    return {
      src: null,
      page: 1,
      numberOfPages: 0
    }
  },
  computed: {
    ...mapGetters({
      getAttachments: 'questions/getAttachments'
    })
  },
  methods: {
    init() {
      this.src = null
      this.numberOfPages = 0
      if (this.fileName) {
        let url = this.getAttachments[this.fileName]
        let loadingTask = pdf.createLoadingTask(url)
        loadingTask.promise.then(pdf => {
          this.src = pdf.loadingTask
          this.numberOfPages = pdf.numPages
        })
      }
    },
  },
  watch: {
    fileName() {
      this.$nextTick(() => {
        this.init()
      })

    }
  },
  created() {
    this.init()
  },
}
</script>

The first file is loaded OK, but when passing another fileName - the component doesn't refresh and I don't see the new file.

I discovered, that when the fileName changes and the init() method runs again after watcher, for some reason it skips the loadingTask.promise.then part. However, if I change back to the first fileName, that was already displayed, then it's being displayed again and the above code runs normally.

I tried many possible solutions: create the component in the loop for every element in my getter object and then render it only if it matches the passed fileName, tried this.src.destroyed() both in beforeDestroy() life cycle and in the fileName watcher, tried to use this.$forceUpdate() after init() and many more... Nothing seems to be working.

Is it a bug? Or is there a possible solution for that?

Igal-Kleiner commented 4 years ago

Well, apparently there's some issue with vue-pdf library. Eventually I solved it by setting timeout when assigning fileName prop and re-rendering the component:

<PdfViewer v-if="selectedFileName" :fileName="selectedFileName" />
onFileNameSelected(fileName) {
  this.selectedFileName = null
  setTimeout(() => {
    this.selectedFileName = fileName
  }, 0)
}

And then in the PdfViewer component it's just:

created() {
  this.src = pdf.createLoadingTask(this.getAttachments[this.fileName])
},
mounted() {
  this.src.promise.then(pdf => {
    this.numberOfPages = pdf.numPages
  })
}

That did the trick for me, though feels kinda hacky. I think the library should support dynamic URL changes out of the box.

ux-engineer commented 4 years ago

A short example using Composition API:

<template>
  <section>
    <p v-if="loading">
      Loading...
    </p>
    <p v-else-if="error">
      Error msg
    </p>
    <pdf
      v-for="p in numPages"
      :key="p"
      else
      :page="p"
      :src="src"
    />
  </section>
</template>
const loading = ref(true);
const error = ref(false);
const data: Ref<ArrayBuffer | undefined> = ref();
const numPages = ref(0);
const src = ref();

onMounted(async () => {
  // Process download only if path includes our backend server base url
  if (props.path.includes(API_BASE_URL)) {
    const downloadUrl = props.path;
    // Get blob from server
    const blob = await reqBlob(downloadUrl);
    // Convert binary blob to array buffer
    data.value = await new Response(blob).arrayBuffer();
    // Create file loading task
    src.value = await pdf.createLoadingTask({ data: data.value }).promise;
    // Set pages num
    numPages.value = src.value.numPages;
    // Unset loading state
    loading.value = false;
  } else {
    error.value = true;
  }
});

return {
  loading,
  error,
  data,
  src,
  numPages,
};