I need suggestions
-
I’m starting a profile admin menu, and need suggestions about which is the better way to set and update profile picture, I have no idea where I start, if I use file picker (with avatar, so user can see preview pic with right format (round)) or uploader. the better way to solve it.
I tried today with uploader, but had some issues to update, when user open his profile and I try to pick his pic uploaded before, I tried to do something like this:const file = { size: 0, type: '.jpg', __img: { src: this.form.userImagePath } } this.$refs.imgup.addFiles([file])
but I think this is not the better way to do
-
Edit: See this codesandbox for an example: https://codesandbox.io/s/loving-glade-7eigi
@zeppelinexpress What I do, is have a component called
user-avatar
that accepts animage
prop that’s an object. Inside that component, I check for eitherblob
orurl
key on the image object. In either case, it renders a simple image with the src value being either the url or blob. Here’s the code for that (render function used, name ituser-avatar.js
) (Edit: see next post for non-render function of the same, in which case, call ituser-avatar.vue
)export default { functional: true, props: { image: { type: Object, }, size: { type: String, default: '100px' }, }, render: function (createElement, context) { let imageEl if(context.props.image) { if(context.props.image.blob) { imageEl = createElement('img', { attrs: { draggable: false, src: context.props.image.blob, class: 'fit' }, style: { objectFit: 'cover', } }) } else { imageEl = createElement('img', { attrs: { draggable: false, src: context.props.image.url, }, }) } } else { imageEl = createElement('img', { attrs: { draggable: false, src: 'http://example.com/default-image.png' }, }) } return createElement('div', { on: context.data.on, style: { width: context.props.size, height: context.props.size, borderRadius: '50%', }, class: { ...(context.data.staticClass && { [context.data.staticClass]: true, }), 'overflow-hidden': true, 'non-selectable': true, }, }, [ imageEl ]) } }
Then, I have another component called
image-picker
that uses neither quasar’s file picker or uploader component. (regular vue component, name something likeimage-picker.vue
)<template> <div @dragenter.prevent="onDragEnter" @dragover.prevent="onDragOver" @dragleave.prevent="onDragLeave" @drop.prevent="onDrop" @click="$refs.input.click()" class="relative-position cursor-pointer file-picker q-pa-md" :class="{ dragging }" > <div>Click to select or drag and drop an image</div> <input ref="input" type="file" accept="image/*" @change="onInput" class="hidden" /> </div> </template> <script> export default { data() { return { dragging: false, } }, methods: { onDragEnter() { this.dragging = true }, onDragOver() { this.dragging = true }, onDragLeave() { this.dragging = false }, onDrop(e) { this.dragging = false this.$refs.input.files = e.dataTransfer.files this.update(e.dataTransfer.files[0]) }, onInput(e) { this.update(e.target.files[0]) }, update(file) { this.$emit('input', { file, }) }, onRejected(rejectedEntries) { this.$q.notify({ type: 'negative', message: `${rejectedEntries.length} file(s) did not pass validation constraints` }) } } } </script>
Then the parent component that holds the two above. This will show the current avatar from your API, or the new file if selected.
<template> <div> <user-avatar :image="avatar" /> <image-picker @input="onInput" /> </div> </template> <script> import ImagePicker from './image-picker' import UserAvatar from './user-avatar' export default { data() { return { newAvatar: null } }, computed: { currentAvatar() { return this.$store.state.path.to.avatar // should be object like { url: 'http://example.com/beets.png' } }, avatar() { return this.newAvatar || this.currentAvatar // this will be the selected file if exists, else our current avatar } }, methods: { onInput({ file }) { if(file instanceof File) { // Read the file, and convert to blob const reader = new FileReader() reader.onload = (e) => { this.newAvatar = { file, blob: e.target.result, } } reader.readAsDataURL(file) } }, }, components: { ImagePicker, UserAvatar, } } </script>
Finally, when you want to submit the form for the user to save the new avatar, do something like this:
const formData = new FormData() formData.append('somefield', 'somevalue') // append other fields if(this.newAvatar) { if(this.newAvatar.file instanceof File) { formData.append('avatar', this.newAvatar.file) } } // Submit FormData through axios
What this lets me do, is use the same
user-avatar
component throughout the app, and also let me use it to display either the current avatar or a blob of a file in the account update form.Note: all this code was stripped from various files, and I haven’t tested it, but it should hopefully help you.
-
Also, in case you’re not used to render functions, that very first component would be written something like this:
<template> <div class="overflow-hidden non-selectable" :style="{ width: size, height: size, 'border-radius': '50%' }" > <img v-if="image && image.blob" :src="image.blob" draggable="false" class="fit" style="object-fit: cover" /> <img v-else-if="image && image.url" :src="image.url" draggable="false" /> <img v-else src="http://example.com/default-image.png" /> </div> </template> <script> export default { props: { image: { type: Object, }, size: { type: String, default: '100px' }, }, } </script>
-
thanks @beets, gonna try right now
-
@zeppelinexpress Let us know if it works for you. You may have to adapt it a bit since I copy / pasted and reorganized a bit from my app, but the general idea works great. You can also kind of merge the
image-picker
and parent component so it pops up a file select when you click the avatar, which is a bit nicer ux wise. -
I got your general idea, I’m trying to do using your thought, working so far, thank you one more time @beets
-
@beets thanks for sharing this code.
I’m trying to use it in a profile.vue<template> <div> <user-avatar :image="avatar" /> <image-picker @input="onInput" /> </div> </template> <script> import ImagePicker from "../components/user/image-picker"; import UserAvatar from "../components/user/user-avatar"; export default { name: "Profile", components: { ImagePicker, UserAvatar }, data() { return { // my vars... Avatar: this.$store.state.server.user.avatar_fullpath, }, computed: { currentAvatar() { //return this.$store.state.path.to.avatar; // should be object like { url: 'http://example.com/beets.png' } return this.$store.state.server.user.avatar_fullpath; }, avatar() { return this.newAvatar || this.currentAvatar; // this will be the selected file if exists, else our current avatar } }, </script>
- when I click, a popup asks me for a file. I’ve put logs, and the file is selected, but after, nothing happens, the pic is not rendered.
It seems that :
if (file instanceof File) {} is not executed
even if file contains a file
- image-picker doesn’t call onRejected(rejectedEntries) {}
- how to have my default avatar instead of the default picture ?
imageEl = createElement("img", { attrs: { draggable: false, src: "http://example.com/default-image.png" } });
- when I click, a popup asks me for a file. I’ve put logs, and the file is selected, but after, nothing happens, the pic is not rendered.
-
@incremental said in I need suggestions:
It seems that :
if (file instanceof File) {} is not executed
Hm, in your
profile.vue
I don’t see theonInput
method. That would be where the if condition should live. Do you have it somewhere else in your code?image-picker doesn’t call onRejected(rejectedEntries) {}
Looking at the code I posted, I somehow never call that method. Maybe it’s also a bug in my code or I forgot to copy/paste some code. Since we aren’t using Quasar’s native file pickers, we would have to roll our own validation checks and call the onRejected ourselves. Something like:
update(file) { if(file.size > 1024 * 1024 * 5) { this.onRejected('Max file size is 5Mb') return } if(file.type != 'image/png' && file.type != 'image/jpeg' && file.type != 'image/gif') { this.onRejected('Not an image') return } this.$emit('input', { file, }) }, onRejected(message) { this.$q.notify({ type: 'negative', message: `File did not pass validation constraints, ${message}` }) }
how to have my default avatar instead of the default picture ?
Instead of
"http://example.com/default-image.png"
you could use"default-image.png"
and placedefault-image.png
in the public folder. That should work. In my case, the default image was stored on the server instead of on my local machine for testing, but either way works. -
@beets
thanks for onRejected(rejectedEntries) {}In my profile.vue, I have :
methods: { onInput(file) { console.log("Test.vue - onInput() : ", file); if (this.file instanceof File) { // Read the file, and convert to blob const reader = new FileReader(); reader.onload = e => { this.newAvatar = { file, blob: e.target.result }; }; reader.readAsDataURL(file); } },
but the if (this.file instanceof File) {} is always false.
I tried to suppress it but I get :
[Vue warn]: Error in v-on handler: “TypeError: FileReader.readAsDataURL: Argument 1 does not implement interface Blob.” -
@incremental said in I need suggestions:
console.log("Test.vue - onInput() : ", file);
What does this line output?
-
@beets it shows
-
@incremental I had an error in the code, the onInput should look like:
onInput({ file }) { if (file instanceof File) { // Read the file, and convert to blob const reader = new FileReader() reader.onload = (e) => { this.newAvatar = { file, blob: e.target.result } } reader.readAsDataURL(file) } }
I’ve made a codesandbox: https://codesandbox.io/s/loving-glade-7eigi
-
and for the default picture, my store has it in the form :
this.$store.state.server.user.avatar_fullpath
so I tried :imageEl = createElement("img", { attrs: { draggable: false, //src: "http://example.com/default-image.png" src: this.$store.state.server.user.avatar_fullpath } });
but It tells me the store is not defined…
… I use it in all my views ! -
@incremental That component is a functional one, so
this
doesn’t exactly work there. But, that should be a default image anyway, as in the user has never selected an image to begin with. -
@beets Ok I understand as I’m still learning JS.
Would it be possible to pass parameters to this component ?<user-avatar :image="avatar" default_image="image" :props.size="xx" />
-
@beets : of course with props:…
Thanks a lot for this fine code and your samples.
To conclude, did you find problems with <q-file> for this ?
-
@incremental Sorry for the delay, I assume you figured out passing a prop?
I didn’t really find any problem inherent to using a q-file, it was just unnecessary in my case. The
file-picker
component could be replaced with aq-file
component with some small modifications. -
@beets no problem.
No I haven’t tried the props yet, it was to understand the concepts…At this time, I am trying to use q-file, but I don’t find how to trigger when a user select or drop a file.
Ideally, I’d like to display it in user-avatar.vue inside my profile.vue then send it to axios when the user click ‘update profile’ -
@incremental
I’ve done it with a mix of q-file and beets user.avatar.vue<user-avatar :image="avatar" /> <q-file v-model="modelAvatar" style="max-width: 200px" clearable color="primary" bottom-slots accept=".jpg, image/*" max-file-size="102400" label="Ajoutez votre photo" counter @input="filePicked" @clear="fileCleared" @rejected="fileRejected" >
Thanks a lot
-
@beets Hello beets, sorry to annoy you again…
I’d like to load my default avatar from my server and allow modify it with your onInput().
onInput() works with a local file, but how could I load an image pointed by my profile avatar URL string ?
If I pass my URL string to “file”, it doesn’t works with :if (file instanceof File) { }
I saw a lot of code on the net, but I’m lost and don’t know what is the best and simpler…