update MainLayout.vue element in Quasar dynamically - vue.js

I'm using Quasar Framework in which I'm trying to update an element on the MainLayout.vue layout component based on whether the user is logged in or not. I'm unable to get the login icon/button to change once I login to the site via a Login.vue component. I tried using provide/inject in the boot file to check if the user is logged in but it only updates after I refresh the whole page.
How do I get the login/logout q-btn to update dynamically based on the user login event from the Login.vue component?
Please let me know if I need to provide additional information.
MainLayout.vue
<template>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-toolbar-title>
<q-btn flat #click="$router.push('/')">
Homepage
</q-btn>
</q-toolbar-title>
<q-space />
<q-btn v-if="!userLoggedIn" icon="login" #click="router.push('/login')" />
<q-btn v-if="userLoggedIn" icon="logout" #click="logout" />
</q-toolbar>
</q-header>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup>
import { inject, ref } from "vue";
const userId = inject("userId", false);
const userLoggedIn = ref(userId);
</script>
Login.vue (only showing login related code)
import { inject, ref } from "vue";
const updateUserId = inject("updateLoginStatus");
const email = ref();
const password = ref();
const signInExistingUser = () => {
signInWithEmailAndPassword(auth, email.value, password.value)
.then(() => {
updateUserId(auth.currentUser.uid);
router.push("/");
})
.catch((error) => {
$q.notify({
// deal with error
});
});
};
boot.js
export default boot(async ({ app, router }) => {
const user = await getCurrentUser();
let userLoggedIn = ref(false);
const uid = ref(false);
if (user) {
userLoggedIn.value = true;
uid.value = user.uid;
}
const updateLoginStatus = (updatedUserId) => {
uid.value = updatedUserId;
userLoggedIn.value = !userLoggedIn.value;
console.log(userLoggedIn.value);
return userLoggedIn.value;
};
app.provide("updateLoginStatus", updateLoginStatus);
app.provide("userId", uid.value);
});

Related

Asynchronously modify value of component in Quasar

I am trying to modify the alias field when a promise is resolved. When I try to await the promise, Quasar errors out with:
[Vue warn]: Component <MainLayout>: setup function returned a promise, but no <Suspense>
boundary was found in the parent component tree. A component with async setup() must be
nested in a <Suspense> in order to be rendered.
I tried wrapping everything in the <Suspense> tag including the individual spot I'm awaiting that data, but I still get this error.
I'm trying to promisify a GUN DB event that resolves a user's alias by pubkey.
<template>
<Suspense>
<q-layout view="lHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
#click="toggleLeftDrawer"
/>
<q-toolbar-title> Quasar App </q-toolbar-title>
<div>{{ alias }}</div>
</q-toolbar>
</q-header>
<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<q-list>
<q-item-label header> Essential Links </q-item-label>
<EssentialLink
v-for="link in essentialLinks"
:key="link.title"
v-bind="link"
/>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</Suspense>
</template>
<script>
import { defineComponent, ref } from "vue";
import EssentialLink from "components/EssentialLink.vue";
import { GUN } from "boot/gun";
const gun = Gun();
const user = gun.user().recall({ sessionStorage: true });
const linksList = [
{
title: "Docs",
caption: "quasar.dev",
icon: "school",
link: "https://quasar.dev",
},
];
export default defineComponent({
name: "MainLayout",
components: {
EssentialLink,
},
async setup() {
const leftDrawerOpen = ref(false);
let alias = "Getting alias";
const pubkey = JSON.parse(sessionStorage.getItem("pair")).pub;
alias = new Promise((resolve, reject) => {
gun.user(pubkey).once((data) => resolve(data.alias));
});
return {
alias,
essentialLinks: linksList,
leftDrawerOpen,
toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
},
};
},
});
</script>
What is the proper way to await this data and update it in Quasar?
You could move the async operation to the mounted hook. Also, because you want alias to reactively update on the UI you should wrap it in a ref() when initializing. I've provided simplified code below showing how it can be done:
<template>
<div>{{ alias }}</div>
</template>
<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const alias = ref("Getting alias");
return {
alias,
};
},
async mounted() {
this.alias = await new Promise((resolve, reject) => {
gun.user(pubkey).once((data) => resolve(data.alias));
});
},
});
</script>
example codesandbox

How to prevent Quasar q-form submit when q-field has validation error?

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;
}
}

How to prevent double invoking of the hook created?

So I have a problem with my vue3 project. The gist: I need to support different layouts for some use cases: authorization, user profile's layout, group's layout, etc. I've got the opportunity by this way:
Create a component AppLayout.vue for managing layouts
<template>
<component :is="layout">
<slot />
</component>
</template>
<script>
import AppLayoutDefault from "#/layouts/EmptyLayout";
import { shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
export default {
name: "AppLayout",
setup() {
const layout = shallowRef(AppLayoutDefault);
const route = useRoute();
watch(
() => route.meta,
async (meta) => {
try {
const component =
await require(`#/layouts/${meta.layout}.vue`);
layout.value = component?.default || AppLayoutDefault;
} catch (e) {
layout.value = AppLayoutDefault;
}
}
);
return { layout };
},
};
</script>
So my App.vue started to look so
<template>
<AppLayout>
<router-view />
</AppLayout>
</template>
To render a specific layout, I've added to router's index.js special tag meta
{
path: '/login',
name: 'LoginPage',
component: () => import('../views/auth/LoginPage.vue')
},
{
path: '/me',
name: 'MePage',
component: () => import('../views/user/MePage.vue'),
meta: {
layout: 'ProfileLayout'
},
},
Now I can create some layouts. For example, I've made ProfileLayout.vue with some nested components: Header and Footer. I use slots to render dynamic page content.
<template>
<div>
<div class="container">
<Header />
<slot />
<Footer />
</div>
</div>
</template>
So, when I type the URL http://example.com/profile, I see the content of Profile page based on ProfileLayout. And here the problem is: Profile page invokes hooks twice.
I put console.log() into created() hook and I see the following
That's problem because I have some requests inside of hooks, and they execute twice too. I'm a newbie in vuejs and I don't understand deeply how vue renders components. I suggest that someting inside of the code invokes re-rendering and Profile Page creates again. How to prevent it?
Your profile page loaded twice because it's literally... have to load twice.
This is the render flow, not accurate but for you to get the idea:
Your layout.value=AppDefaultLayout. The dynamic component <component :is="layout"> will render it first since meta.layout is undefined on initial. ProfilePage was also rendered at this point.
meta.layout now had value & watcher made the change to layout.value => <component :is="layout"> re-render 2nd times, also for ProfilePage
So to resolve this problem I simply remove the default value, the dynamic component is no longer need to render default layout. If it has no value so it should not render anything.
<keep-alive>
<component :is="layout">
<slot />
</component>
</keep-alive>
import { markRaw, shallowRef, watch } from "vue";
import { useRoute } from "vue-router";
export default {
name: "AppLayout",
setup() {
console.debug("Loaded DynamicLayout");
const layout = shallowRef()
const route = useRoute()
const getLayout = async (lyt) => {
const c = await import(`#/components/${lyt}.vue`);
return c.default;
};
watch(
() => route.meta,
async (meta) => {
console.log('...', meta.layout);
try {
layout.value = markRaw(await getLayout(meta.layout));
} catch (e) {
console.warn('%c Use AppLayoutDefault instead.\n', 'color: darkturquoise', e);
layout.value = markRaw(await getLayout('EmptyLayout'));
}
}
);
return { layout }
},
};

How to use vue 3 suspense component with a composable correctly?

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.

Making API call using Axios with the value from input, Vue.js 3

I am making an app using this API. The point I'm stuck with is calling the API. If I give the name of the country, the data of that country comes.
Like, res.data.Turkey.All
I want to get the value with input and bring the data of the country whose name is entered.
I am getting value with searchedCountry. But I can't use this value. My API call does not happen with the value I get. I'm getting Undefined feedback from Console.
Is there a way to make a call with the data received from the input?
<template>
<div>
<input
type="search"
v-model="searchedCountry"
placeholder="Search country"
/>
</div>
</template>
<script>
import axios from 'axios';
import { ref, onMounted} from 'vue';
export default {
setup() {
let data = ref([]);
const search = ref();
let searchedCountry = ref('');
onMounted(() => {
axios.get('https://covid-api.mmediagroup.fr/v1/cases').then((res) => {
data.value = res.data.Turkey.All;
});
});
return {
data,
search,
searchedCountry,
};
},
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
I'm work with Vue.js 3
There are a few things wrong with your code:
Your axios call is only called once, when the component mounts (side note here, if you really want to do something like that, you can do it directly within the setup method)
You don't pass the value from searchedCountry to the axios API
Use const for refs
I'd use a watch on the searchedCountry; something like this (I don't know the API contract):
<template>
<div>
<input
type="search"
v-model="searchedCountry"
placeholder="Search country"
/>
</div>
</template>
<script>
import axios from 'axios';
import { ref, watch } from 'vue';
export default {
setup() {
const searchedCountry = ref('');
const data = ref([]);
watch(
() => searchedCountry,
(country) => axios.get(`https://covid-api.mmediagroup.fr/v1/cases/${country}`).then((res) => data.value = res.data.Turkey.All);
);
return {
data,
searchedCountry,
};
},
};
</script>