Q-Uploader with Composition API
-
Maybe I can work with those Events under the Quploader Api. They seem to be offering
@uploaded
and@uploading
… Also they can takefiles
-
Do you have any idea about what this refers to in the qv1{QUploaderBase} from “quasar”
this
refers to the Vue component itself.Thanks for uncovering the mystery
Qv2 docs will have the Components Api on top of the page so you can’t miss it
-
So, ok. I came up with a solution with factory function but it spits out an error.
This one is a free project on firebase.import firebase from "firebase/app"; import "firebase/storage"; const firebaseConfig = { apiKey: "AIzaSyCp5sEmtSaDOjIdm5E88Wy41cCqFOoYd2o", authDomain: "someproject-43c3a.firebaseapp.com", projectId: "someproject-43c3a", storageBucket: "someproject-43c3a.appspot.com", messagingSenderId: "445668329191", appId: "1:445668329191:web:9bebd22c71e3ca8c05c442", }; // init firebase firebase.initializeApp(firebaseConfig); // init services const projectStorage = firebase.storage(); export { projectStorage};
And this is the useStorage composable
import { projectStorage } from "../boot/firebase"; import { ref } from "vue"; const useStorage = () => { const error = ref(null); const downloadURL = ref(null); const filePath = ref(null); const uploadFile = async (file) => { filePath.value = `covers/${file.name}`; const storageRef = projectStorage.ref(filePath.value); try { const res = await storageRef.put(file); downloadURL.value = await res.ref.getDownloadURL(); } catch (err) { console.log(err.message); error.value = err; } }; return { uploadFile, downloadURL, error }; }; export default useStorage;
And this is where it’s placed in the the Quploader as a factory function.
<template> <q-page class="flex flex-center"> <q-uploade multiple :factory="factory" @factory-failed="onFactoryFail" ></q-uploader> </q-page> </template> <script> import { ref } from "vue"; import useStorage from "src/composables/useStorage"; import { projectStorage } from "src/boot/firebase"; export default { name: "PageIndex", setup(props) { const { uploadFile, downloadURL, } = useStorage(); //Events async function factory(files) { // console.log("files[0].name:", files[0].name); await uploadFile(files[0]); console.log("downloadURL.value:", downloadURL.value); } function onFactoryFail(err, files) { console.log("factoryFail error:", err); console.log("factoryFail files:", files); } return { factory, onFactoryFail }; }, }; </script>
It’s giving me the downloadURL.value, so it’s successful in uploading. @factory-failed is giving me the following errors about url:
factoryFail error: TypeError: Cannot read property 'url' of undefined at getProp (xhr-uploader-plugin.js?e40a:131) at performUpload (xhr-uploader-plugin.js?e40a:136) at eval (xhr-uploader-plugin.js?e40a:116)
factoryFail files: [File]
(Eventhough the bucket has that file uploaded in it)Eventually this is the screen that the user ends up with:
A similar error has been recorded in this topic but didn’t receive a solution.
-
I made a discovery today. I will be working on it over the next days. It’s the uploader-core which exposes all the functions and references. So I can begin working with the createUploaderComponent Hurray.
Edit: Ok ok this thing goes deeper than I’ve expected. There’s this xhr-uploader-plugin which exposes how things are running. I’ve finally found how the heck the queuedFiles can be called in the createUploaderComponent:
helpers.queuedFiles.value
. So I’ll keep running my expedition from there. -
I have converted github/birchb 's wonderful q-uploader firebase implementation into the composition api using this files above. And it’s uploading, updating the progess and removing the file on demand. It has 2 minor issues with emitting the ‘uploaded’ event and calculating the blue uploadSize label on top of the component when the item is removed. But I’m sure they’re solvable problems. So the code is…
import { createUploaderComponent } from "quasar"; import { computed, ref } from "vue"; import firebase from "firebase/app"; // export a Vue component export default createUploaderComponent({ // defining the QUploader plugin here name: "MyUploader", // your component's name props: { pathPrefix: { type: String, default: "", }, }, emits: ["uploaded", "failed", "removed"], injectPlugin({ props, emit, helpers }) { // can call any other composables here // as this function will run in the component's setup() const storage = ref(firebase.storage().ref()); const activeTasks = ref([]); // [ REQUIRED! ] // We're working on uploading files const isUploading = computed(() => { // return <Boolean> }); // [ optional ] // Shows overlay on top of the // uploader signaling it's waiting // on something (blocks all controls) const isBusy = computed(() => { // return <Boolean> }); // [ REQUIRED! ] // Abort and clean up any process // that is in progress function abort() { // ... } // [ REQUIRED! ] // Start the uploading process function upload() { if (props.disable || !helpers.queuedFiles.value.length) { return; } helpers.queuedFiles.value.forEach(async (file) => { await __uploadSingleFile(file); }); } function __uploadSingleFile(file) { let pathPrefix = props.pathPrefix || ""; // const fileRef = storage.value.child(pathPrefix + file.name) const fileRef = storage.value.child(`${pathPrefix}/${file.name}`); helpers.updateFileStatus(file, "uploading", 0); const uploadTask = fileRef.put(file); activeTasks.value.push(uploadTask); // Listen for state changes, errors, and completion of the upload. uploadTask.on( firebase.storage.TaskEvent.STATE_CHANGED, // or 'state_changed' (snapshot) => { // Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded if (file.__status !== "failed") { const loaded = Math.min( snapshot.totalBytes, snapshot.bytesTransferred ); helpers.uploadedSize.value += loaded - file.__uploaded; helpers.updateFileStatus(file, "uploading", loaded); } }, (error) => { // A full list of error codes is available at // https://firebase.google.com/docs/storage/web/handle-errors helpers.queuedFiles.value.push(file); helpers.updateFileStatus(file, "failed"); emit("failed", { file, error }); helpers.uploadedSize.value -= file.__uploaded; activeTasks.value = activeTasks.value.filter((t) => t !== uploadTask); }, () => { // Upload completed successfully, now we can get the download URL uploadTask.snapshot.ref .getDownloadURL() .then((downloadURL) => { const fullPath = uploadTask.snapshot.ref.fullPath; const fileName = uploadTask.snapshot.ref.name; helpers.uploadedFiles.value.push(file); helpers.updateFileStatus(file, "uploaded"); let uploadTime = _.round(new Date().getTime() / 1000); console.log("TCL: __uploadSingleFile -> uploadTime", uploadTime); let [fileSize, fileType] = [file.size, file.type]; console.log( downloadURL, fileName, fileSize, fileType, fullPath, uploadTime ); emit("uploaded", { downloadURL, fileName, fileSize, fileType, fullPath, uploadTime, }); helpers.uploadedSize.value += file.size - file.__uploaded; // helpers.uploadedSize.value = 0; }) .catch((error) => { emit("failed", { file, error }); }); activeTasks.value = activeTasks.value.filter((t) => t !== uploadTask); } ); } function removeFile(file) { if (props.disable) { return; } if (file.__status === "uploaded") { helpers.uploadedFiles.value = helpers.uploadedFiles.value.filter( (f) => f.name !== file.name ); // As of Quasar v2 beta7 uploadSize cannot be taken out of helpers/state. So removeFile won't be able to change blue header the label on top. helpers.uploadSize.value -= file.__uploaded; } else if (file.__status === "uploading") { file.__abort(); } else { helper.uploadSize.value -= file.size; } helpers.files.value = helpers.files.value.filter((f) => { if (f.name !== file.name) { return true; } f._img !== void 0 && window.URL.revokeObjectURL(f._img.src); return false; }); helpers.queuedFiles.value = helpers.queuedFiles.value.filter( (f) => f.name !== file.name ); emit("removed", [file]); } return { isUploading, isBusy, abort, upload, }; }, });
It gives the following warningBut it’s a fully functional Firebase Uploader for Quasar v2 now.[Vue warn]: Component emitted event "uploaded" but it is neither declared in the emits option nor as an "onUploaded" prop.
I don’t know why.Edit: emits: [“uploaded”, “failed”, “removed”],
-
@pazarbasifatih said in Q-Uploader with Composition API:
emits: [
// …your custom events name list
],it’s a warning that you should register your custom events in the component events options array. https://v3.vuejs.org/guide/component-custom-events.html#defining-custom-events
-
@metalsadman Great! Thank you for point that out.
Now let’s me see how I can fix the uploadSizeLabel issue.
Uploading (A feature to block parallel uploading would be great)
Uploaded
Remove File (See thatremoveFile()
does not recalculate the uploadSizeLabel because I can’t reach uploadSize from helpers. It should be configured via a PR in uploader-core.js)
And this problem leads to the following, when all files are removed.
And if you add a file, it keeps adding to the old uploadSize.
2.8 MB + 400 KB = 3.2 MB // true but this is not the behavior you’d expect to see.
When done with uploading, the label is a complete(!) mess
-
That’s the whole point with the label. It shows how much you’ve uploaded. There’s also the max-files and max-total-size props. If we remove from the total size (as you proposed) then those props are essentially useless since they can be bypassed easily.
But the larger picture is that you also have access to the “reset()” method, which resets the components to its initial state. You could do that after the successful upload.
-
@rstoenescu Great! I tried running
reset()
method in the MyUploader.js but I am probably doing something wrong because, it’s giving meReferenceError: reset is not defined at eval
error.So I tried creating my own
function reset()
inside the MyUploader.js but it has the infamous uploadSize.value in the original example, which is not inside const state, so I can’t use the helpers object for reaching it.function reset () { if (props.disable === false) { state.abort() state.uploadedSize.value = 0 uploadSize.value = 0 revokeImgURLs() state.files.value = [] state.queuedFiles.value = [] state.uploadedFiles.value = [] } }
I’m sure there must be a proper way of calling that function but I lack the knowledge.
Sorry to bother for this, but I’ve got kind of obsessed with it now. Wouldn’t putting uploadSize inside the
const state = {uploadSize: ref(0)}
in the uploader-core be a little bit more flexible? -
@pazarbasifatih said in Q-Uploader with Composition API:
ReferenceError: reset is not defined at eval
Add a
ref
property to the component.<q-uploader ref="myRefToQUploader" ..... />
then:
this.$refs[myRefToQUploader].reset()
See:
https://v3.vuejs.org/guide/component-template-refs.html -
@dobbel said in Q-Uploader with Composition API:
this.$refs[myRefToQUploader].reset()
Thank you, but
this
doesnt make sense in composition api. -
Anyway, I tried the putting scope.reset() in the queuedFiles list template, (as it seems like only place to access the prescribed reset(). function is through scope in the documentation)
It resets the whole thing… So I needed use the event @finish event but it didn’t fit any place I’ve inserted so far. I probably don’t understand this component at all. Why is it so hard? I am doing guess work here.
This is the Codepen I’ve tried doing it. -
@pazarbasifatih said in Q-Uploader with Composition API:
@dobbel said in Q-Uploader with Composition API:
this.$refs[myRefToQUploader].reset()
Thank you, but
this
doesnt make sense in composition api.convert it to composition api, assuming that you know how to use
refs
insidesetup
method since you are speaking about compositiion api. ie.someRef.value.reset()
. all methods in the api are accessible thru vue refs. -
@metalsadman Actually, I don’t assume myself knowing anything. I look at the documentation and try to find my way. My eyes simply couldn’t pick up anything about the fact that refs could be referred by xyz.value, on neither vue3 documentation nor the quasar’s. I’m a novice with some JS knowledge. Bare with me, please.
And thank you. Of course I didn’t know that. Now I’ll try that
-
@pazarbasifatih P.S. in composition api “this” is similar to context which can be accessed as second parameter in setup function
setup(props, context)
Or can be destructed if only refs is needed
setup(props, { refs })