I am trying to populate a table of people with their name and a profile picture. The name is sourced from a Firestore database, and the picture uses the name to find a related picture in a Firebase Storage bucket.
I have watched hours of videos and have scoured nearly a hundred articles at this point, so my example code has pieces from each as I've been trying every combination and getting mixed but unsuccessful results.
In this current state which returns the least amount of errors, I am able to successfully populate the table with the names, however in that same component it is not able to pull the profile picture. The value used for the profile picture is updated, but it is updated from the placeholder value to undefined.
GamePlanner.vue
<template>
<div>
<Field />
<Bench />
<!-- <Suspense>
<template #default> -->
<PlanSummary />
<!-- </template>
<template #fallback>
<div class="loading">Loading...</div>
</template>
</Suspense> -->
</div>
</template>
PlanSummary.vue
<template>
<div class="summaryContainer">
<div class="inningTableContainer">
<table class="inningTable">
<tbody>
<!-- <Suspense> -->
<!-- <template #default> -->
<PlayerRow v-for="player in players.value" :key="player.id" :player="(player as Player)" />
<!-- </template> -->
<!-- <template #fallback>
<tr data-playerid="0">
<img class="playerImage" src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50" />
Loading players...
<span class="playerNumber">00</span>
</tr>
</template> -->
<!-- </Suspense> -->
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onErrorCaptured, ref } from "vue";
import { useFirestore, useCollection } from "vuefire";
import { collection } from "firebase/firestore";
import { Player, Inning } from "#/definitions/GamePlanner";
import PlayerRow from "./PlayerRow.vue";
const db = useFirestore();
const gamePlanID = "O278vlB9Xx39vkZvIsdP";
// const players = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
// const players = ref(useCollection(collection(db, `/gameplans/${gamePlanID}/participants`)));
// Unhandled error during execution of scheduler flush
// Uncaught (in promise) DOMException: Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.
const players = ref();
players.value = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
// Seeminly infinite loop with "onServerPrefetch is called when there is no active component instance to be associated with."
// Renders 5 (??) undefined players
// One error shown: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'substring')
// const players = computed(() => useCollection(collection(db, `/gameplans/${gamePlanID}/participants`)));
// onErrorCaptured((error, vm, info) => {
// console.log("Error loading Summary component: ", error, "vm: ", vm, "info: ", info);
// throw error;
// });
</script>
PlayerRow.vue
<template>
<tr :key="player2.id" :data-playerid="player2.id">
<td>
<img class="playerImage" :src="playerPictureURL" />
{{ player2.nickname || player2.firstName + " " + player2.lastName }}
<span class="playerNumber">{{ player2.playerNumber }}</span>
</td>
</tr>
</template>
<script lang="ts" setup>
import { ref, PropType, computed, onMounted, watch } from "vue";
import { useFirebaseStorage, useStorageFileUrl } from "vuefire";
import { ref as storageRef } from 'firebase/storage';
import { Player, Inning } from "#/definitions/GamePlanner";
const fs = useFirebaseStorage();
const props = defineProps({
'player': { type: Object as PropType<Player>, required: true },
// 'innings': Array<Inning>
});
const player2 = ref(props.player);
// const innings = computed(() => props.innings);
// const playerPictureURL = computed(() => {
// const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
// const playerPictureResource = storageRef(fs, `playerPictures/${playerPictureFilename}`);
// return useStorageFileUrl(playerPictureResource).url.value as string;
// });
// const playerPictureURL = ref(() => {
// const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
// const playerPictureResource = storageRef(fs, `playerPictures/${playerPictureFilename}`);
// return useStorageFileUrl(playerPictureResource).url.value as string;
// });
const playerPictureURL = ref("https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50");
async function getPlayerPictureURL() {
console.log("PlayerRow.ts getPlayerPictureURL");
const playerPictureFilename = `${player2.value.firstName.substring(0,1)}${player2.value.lastName}.png`.toLowerCase();
const playerPictureResource = await storageRef(fs, `playerPictures/${playerPictureFilename}`);
playerPictureURL.value = await useStorageFileUrl(playerPictureResource).url.value as string;
}
onMounted(() => {
console.log("PlayerRow.ts onMounted");
getPlayerPictureURL();
});
watch(playerPictureURL, (newVal, oldVal) => {
console.log("PlayerRow.ts watch playerPictureURL");
console.log("newVal: " + newVal);
console.log("oldVal: " + oldVal);
});
</script>
I was under the impression that <Suspense> would need to wrap the <PlayerRow> component since I am using the storageRef and useStorageUrl methods, but it seems to introduce more issues. Based on the vuefire documentation and inspecting the definitions int he code itself, it does not appear that they are asynchronous, however trying to to immediately invoke them does not produce an immediate/actual result.
Relevant Package Versions
{
"vue": "^3.2.45"
"firebase": "^9.15.0",
"typescript": "^4.9.3",
"vite": "^4.0.0",
"vue-router": "^4.1.6",
"vue-tsc": "^1.0.11",
"vuefire": "3.0.0-beta.6"
}
According to this documentation we are supposed to use useCollection with including collection as a argument as follows:
const todos = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`))
And I see you are using it the correct way but instead of assigning to a ref you can assign it directly to players variable. As you are trying to use this reactive object as the value of a ref object, which is not supported.
You can solve your issue with changing this line:
const players = ref();
players.value = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
To :
const players = useCollection(collection(db, `/gameplans/${gamePlanID}/participants`));
For more information about this topic you can go through the following docs
Related
I am trying to pass a value into a child component. The child component will then preform the save operation. The parent doesn't need to know anything about it. I am able to pass in the object but not save its updated form.
Parent
<template>
<div v-show="isOpened">
<EditModal #toggle="closeModal" #update:todo="submitUpdate($event)"
:updatedText="editText" :todo="modalPost" />
</div>
</template>
<script setup lang="ts">
import Post from "../components/Post.vue";
import { api } from "../lib/api";
import { ref } from "vue";
import { onMounted } from "vue-demi";
import EditModal from "../components/EditModal.vue";
const postArr = ref('');
const message = ref('');
let isOpened = ref(false);
let modalPost = ref('');
let editText = ref('');
function closeModal() {
isOpened.value = false
}
function openModal(value: string) {
isOpened.value = true
modalPost.value = value
}
// call posts so the table loads updated item
function submitUpdate(value: any) {
console.log("called update in parent " + value)
editText.value = value
posts()
}
</script>
Child EditModal
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" :value="props.todo.post"></textarea>
</div>
<!-- Modal footer -->
<div>
<button data-modal-toggle="defaultModal" type="button"
#click="update(props.todo.blogId, props.todo.post)">Save</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { api } from "../lib/api";
import { reactive, ref } from "vue";
const props = defineProps({
todo: String,
updatedText: String,
})
const emit = defineEmits(
['toggle','update:todo']
);
function setIsOpened(value: boolean) {
emit('toggle', value);
}
function update(id: string, value: string) {
console.log('the value ' + value)
try {
api.updateBlog(id, value).then( res => {
emit('update:todo', value)
emit('toggle', false);
})
} catch (e) {
console.log('Error while updating post: '+ e)
}
}
</script>
I know the props are read only, therefore I tried to copy it I can only have one model.
I do not see the reason I should $emit to the parent and pass something to another variable to pass back to the child.
I am trying to pass in text to a modal component where it can edit the text and the child component saves it.
Advice?
First, your component should be named EditTodo no EditModal because you are not editing modal. All Edit components should rewrite props to new local variables like ref or reactive, so you can work on them, they won't be read only any more.
Child EditTodo.vue
<template>
<div>
<div>
<textarea id="updateTextArea" rows="10" v-model="data.todo.title"></textarea>
</div>
<div>
<button data-modal-toggle="defaultModal" type="button" #click="update()">Save</button>
</div>
</div>
</template>
<script setup lang="ts">
import { api } from "../lib/api";
import { reactive } from "vue";
const props = defineProps<{ id: number, todo: { title: string} }>()
const emit = defineEmits(['toggle']);
const data = reactive({ ...props })
// I assuming api.updateBlog will update data in database
// so job here should be done just need toogle false modal
// Your array with todos might be not updated but here is
// no place to do that. Well i dont know how your API works.
// Database i use will automaticly update arrays i updated objects
function update() {
try {
api.updateBlog(data.id, data.todo ).then(res => {
emit('toggle', false);
})
}
}
</script>
Parent
<template>
<div>
<BaseModal v-if="todo" :show="showModal">
<EditTodo :id="todo.id" :todo="todo.todo" #toggle="(value) => showModal = value"></EditTodo>
</BaseModal>
</div>
</template>
<script setup lang="ts">
const showModal = ref(false)
const todo = reactive({ id: 5, todo: { title: "Todo number 5"} })
</script>
I separated a modal object with edit form, so you can create more forms and use same modal. And here is a simple, not fully functional modal.
<template>
<div class="..."><slot></slot></div>
</template>
<script setup>
defineProps(['show'])
defineEmits(['toogle'])
</script>
You might want to close modal when user click somewhere outside of modal.
I was learning about Async Components in Vue. Unfortunately in that documentation Vue did not show any example of using Async Components in the <template> part of a Vue SFC. So after searching on the web and reading some articles like this one and also this one, I tried to use this code to my Vue component:
<!-- AsyncCompo.vue -->
<template>
<h1>this is async component</h1>
<button #click="show = true">login show</button>
<div v-if="show">
<LoginPopup></LoginPopup>
</div>
</template>
<script>
import { defineAsyncComponent, ref } from 'vue';
import ErrorCompo from "#/components/ErrorCompo.vue";
const LoginPopup = defineAsyncComponent({
loader: () => import('#/components/LoginPopup.vue'),
/* -------------------------- */
/* the part for error handling */
/* -------------------------- */
errorComponent: ErrorCompo,
timeout: 10
}
)
export default {
components: {
LoginPopup,
},
setup() {
const show = ref(false);
return {
show,
}
}, // end of setup
}
</script>
And here is the code of my Error component:
<!-- ErrorCompo.vue -->
<template>
<h5>error component</h5>
</template>
Also here is the code of my Route that uses this component:
<!-- test.vue -->
<template>
<h1>this is test view</h1>
<AsyncCompo></AsyncCompo>
</template>
<script>
import AsyncCompo from '../components/AsyncCompo.vue'
export default {
components: {
AsyncCompo
}
}
</script>
And finally the code of my actual Async component called LoginPopup.vue that must be rendered after clicking the button:
<!-- LoginPopup.vue -->
<template>
<div v-if="show1">
<h2>this is LoginPopup component</h2>
<p>{{retArticle}}</p>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const getArticleInfo = async () => {
// wait 3 seconds to mimic API call
await new Promise(resolve => setTimeout(resolve, 3000));
const article = "my article"
return article
}
const show1 = ref(false);
const retArticle = ref(null);
onMounted(
async () => {
retArticle.value = await getArticleInfo();
show1.value = true;
}
);
return {
retArticle,
show1
}
}
}
</script>
When I comment the part below from AsyncCompo.vue everything works correctly and my component loads after 3s when I clicks the button:
errorComponent: ErrorCompo,
timeout: 10
But I want to test the error situation that Vue says in my component. I am not sure that my code implementation is absolutely true, but with code above when I use the errorComponent, I receive this warning and error in my console:
I also know that we could handle these situations with <Suspense> component, but because my goal is learning Async Components, I don't want to use them here. Could anyone please help me that how I can see and test my "error component" in the page? is my code wrong or I must do something intentionally to make an error? I don't know but some articles said that with decreasing timeout option I could see error component, but for me it gives that error.
I have a function which gives me list of objects. After update it renders correctly for a second than the list disappears. Any idea why this is happening ?
<script setup lang="ts">
import { getList, Version } from "#/services/firebaseService";
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
const platformRef = ref(
useRouter().currentRoute.value.query.platform?.toString()
);
const abiRef = ref(useRouter().currentRoute.value.query.abi?.toString());
const list = ref([] as Version[]);
watch([platformRef, abiRef], async ([platform, abi]) => {
if (platform) {
list.value = await getList(platform, abi);
console.log(list.value);
}
});
</script>
<template>
<section>
<div class="form-selector">
<div class="form-block">
<o-radio v-model="platformRef" name="platform" native-value="android"
>Android</o-radio
>
<!-- rest of the form to set values for platform and abi -->
</div>
</div>
<div class="card" v-for="item in list" :key="item.name">
{{ item.name }}
</div>
</section>
</template>
and console output looks correctly (shows proxy to the array)
Proxy {}
Object[[Target]]:
Array(1)0:
{name: 'v0.2.0-beta.apk', link: 'https://firebasestorage.googleapis.com/v0/b/...'}
[[Prototype]]: Objectlength: 1
[[Prototype]]: Array(0)
UPDATE when chenged the function inside watch to this:
const result = await getList(platform, abi);
console.log(result);
console.log(result.length)
I get correct array but length 0...
I think that problem is in this string:
list.value = await getList(platform, abi);
You try to set the value field of list, but arrays do not have this field;
Instead, you should use this:
list = await getList(platform, abi);
I'm trying to implement vue-phone-input by wrapping it with a Quasar q-field.
It's mostly working. The input works fine and it shows validation errors underneath the input.
The problem is that I can submit the form even if there is a validation error.
How do I prevent this from happening?
Normally when using a q-form with a q-input and q-btn it will automatically stop this from happening.
So why doesn't it work here with q-field and vue-tel-input?
<template>
<q-form #submit="handlePhoneSubmit">
<q-field
v-if="isEditingPhone"
autocomplete="tel"
label="Phone"
stack-label
:error="isPhoneError"
error-message="Please enter a valid phone number."
outlined
hide-bottom-space
>
<vue-tel-input
v-model="phoneInput"
#validate="isPhoneError = !isPhoneError"
></vue-tel-input>
</q-field>
<q-btn
color="primary"
text-color="white"
no-caps
unelevated
style="max-height: 56px"
type="submit"
label="Save"
#submit="isEditingPhone = false"
/>
</q-form>
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue';
import { VueTelInput } from 'vue-tel-input';
import 'vue-tel-input/dist/vue-tel-input.css';
const phone: Ref<string | null> = ref('9999 999 999');
const isEditingPhone = ref(true);
const isPhoneError = ref(false);
const phoneInput: Ref<string | null> = ref(null);
const handlePhoneSubmit = () => {
phone.value = phoneInput.value;
console.log('Form Saved');
};
</script>
First, you should use the :rules system from Quasar instead of :error and #validate
<q-field :rules="[checkPhone]"
function checkphone(value: string) {
return // validate the value here
}
Then, if the submit doesn't suffice, you may need to set a ref on your <q-form, then call its validate() method.
Here how to do it (I removed parts of the code to highlight what's required).
<template>
<q-form ref="qform" #submit="handlePhoneSubmit">
//..
</q-form>
</template>
<script setup lang="ts">
import { QForm } from "quasar";
import { ref } from "vue";
//..
const qform = ref<QForm|null>(null);
async function handlePhoneSubmit() {
if (await qform.value?.validate()) {
phone.value = phoneInput.value;
}
}
I am using Vue-3 and Vite in my project. in one of my view pages called Articles.vue I used suspense component to show loading message until the data was prepared. Here is the code of Articles.vue:
<template>
<div class="container">
<div class="row">
<div>
Articles menu here
</div>
</div>
<!-- showing articles preview -->
<div id="parentCard" class="row">
<div v-if="error">
{{ error }}
</div>
<div v-else>
<suspense>
<template #default>
<section v-for="item in articleArr" :key="item.id" class="col-md-4">
<ArticlePrev :articleInfo = "item"></ArticlePrev>
</section>
</template>
<template #fallback>
<div>Loading...</div>
</template>
</suspense>
</div>
</div> <!-- end of .row div -->
</div>
</template>
<script>
import DataRelated from '../composables/DataRelated.js'
import ArticlePrev from "../components/ArticlePrev.vue";
import { onErrorCaptured, ref } from "vue";
/* start export part */
export default {
components: {
ArticlePrev
},
setup (props) {
const error = ref(null);
onErrorCaptured(e => {
error.value = e
});
const {
articleArr
} = DataRelated("src/assets/jsonData/articlesInfo.json");
return {
articleArr,
error
}
}
} // end of export
</script>
<style scoped src="../assets/css/viewStyles/article.css"></style>
As you could see I used a composable js file called DataRelated.js in my page that is responsible for getting data (here from a json file). This is the code of that composable:
/* this is a javascript file that we could use in any vue component with the help of vue composition API */
import { ref } from 'vue'
export default function wholeFunc(urlData) {
const articleArr = ref([]);
const address = urlData;
const getData = async (address) => {
const resp = await fetch(frontHost + address);
const data = await resp.json();
articleArr.value = data;
}
setTimeout(() => {
getData(address);
}, 2000);
return {
articleArr
}
} // end of export default
Because I am working on local-host, I used JavaScript setTimeout() method to delay the request to see that the loading message is shown or not. But unfortunately I think that the suspense component does not understand the logic of my code, because the data is shown after 2000ms and no message is shown until that time. Could anyone please help me that what is wrong in my code that does not work with suspense component?
It's a good practice to expose a promise so it could be chained. It's essential here, otherwise you'd need to re-create a promise by watching on articleArr state.
Don't use setTimeout outside the promise, if you need to make it longer, delay the promise itself.
It could be:
const getData = async (address) => {
await new Promise(resolve => setTimeout(resolve, 2000);
const resp = await fetch(frontHost + address);
const data = await resp.json();
articleArr.value = data;
}
const promise = getData(urlData)
return {
articleArr,
promise
}
Then:
async setup (props) {
...
const { articleArr, promise } = DataRelated(...);
await promise
...
If DataRelated is supposed to be used exclusively with suspense like that, it won't benefit from being a composable, a more straightforward way would be is to expose getData instead and make it return a promise of the result.