Asynchronously modify value of component in Quasar - vue.js

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

Related

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

Async loading child component doesn't trigger v-if

Hi everyone and sorry for the title, I'm not really sure of how to describe my problem. If you have a better title feel free to edit !
A little bit of context
I'm working on a little personal project to help me learn headless & micro-services. So I have an API made with Node.js & Express that works pretty well. I then have my front project which is a simple one-page vue app that use vuex store.
On my single page I have several components and I want to add on each of them a possibility that when you're logged in as an Administrator you can click on every component to edit them.
I made it works well on static elements :
For example, here the plus button is shown as expected.
However, just bellow this one I have some components, that are loaded once the data are received. And in those components, I also have those buttons, but they're not shown. However, there's no data in this one except the title but that part is working very well, already tested and in production. It's just the "admin buttons" part that is not working as I expect it to be :
Sometimes when I edit some codes and the webpack watcher deal with my changes I have the result that appears :
And that's what I expect once the data are loaded.
There is something that I don't understand here and so I can't deal with the problem. Maybe a watch is missing or something ?
So and the code ?
First of all, we have a mixin for "Auth" that isn't implemented yet so for now it's just this :
Auth.js
export default {
computed: {
IsAdmin() {
return true;
}
},
}
Then we have a first component :
LCSkills.js
<template>
<div class="skills-container">
<h2 v-if="skills">{{ $t('skills') }}</h2>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addModal" />
<LCModal ref="addModal"></LCModal>
<div class="skills" v-if="skills">
<LCSkillCategory
v-for="category in skills"
:key="category"
:category="category"
/>
</div>
</div>
</template>
<script>
import LCSkillCategory from './LCSkillCategory.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkills',
components: {
LCSkillCategory,
LCAdmin,
LCModal,
},
computed: mapState({
skills: (state) => state.career.skills,
}),
mixins: [Auth],
};
</script>
<style scoped>
...
</style>
This component load each skills category with the LCSkillCategory component when the data is present in the store.
LCSkillCategory.js
<template>
<div class="skillsCategory">
<h2 v-if="category">{{ name }}</h2>
<LCAdmin
v-if="IsAdmin && category"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
<div v-if="category">
<LCSkill
v-for="skill in category.skills"
:key="skill"
:skill="skill"
/>
</div>
<LCAdmin v-if="IsAdmin" :addModal="$refs.addSkillModal" />
<LCModal ref="addSkillModal"></LCModal>
</div>
</template>
<script>
import LCSkill from './LCSkill.vue';
import { mapState } from 'vuex';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
import Auth from '../../mixins/Auth';
export default {
name: 'LCSkillCategory',
components: { LCSkill, LCAdmin, LCModal },
props: ['category'],
mixins: [Auth],
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.category,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
And so each category load a LCSkill component for each skill of this category.
<template>
<div class="skill-item">
<img :src="img(skill.icon.hash, 30, 30)" />
<p>{{ name }}</p>
<LCAdmin
v-if="IsAdmin"
:editModal="$refs.editModal"
:deleteModal="$refs.deleteModal"
/>
<LCModal ref="editModal"></LCModal>
<LCModal ref="deleteModal"></LCModal>
</div>
</template>
<script>
import LCImageRendering from '../../mixins/LCImageRendering';
import { mapState } from 'vuex';
import Auth from '../../mixins/Auth';
import LCAdmin from '../LCAdmin.vue';
import LCModal from '../LCModal.vue';
export default {
name: 'LCSkill',
mixins: [LCImageRendering, Auth],
props: ['skill'],
components: { LCAdmin, LCModal },
computed: mapState({
name: function() {
return this.$store.getters['locale/getLocalizedValue']({
src: this.skill,
attribute: 'name',
});
},
}),
};
</script>
<style scoped>
...
</style>
Then, the component with the button that is added everywhere :
LCAdmin.js
<template>
<div class="lc-admin">
<button v-if="addModal" #click="addModal.openModal()">
<i class="fas fa-plus"></i>
</button>
<button v-if="editModal" #click="editModal.openModal()">
<i class="fas fa-edit"></i>
</button>
<button v-if="deleteModal" #click="deleteModal.openModal()">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
<script>
export default {
name: 'LCAdmin',
props: ['addModal', 'editModal', 'deleteModal'],
};
</script>
Again and I'm sorry it's not that I haven't look for a solution by myself, it's just that I don't know what to lookup for... And I'm also sorry for the very long post...
By the way, if you have some advice about how it is done and how I can improve it, feel free, Really. That how I can learn to do better !
EDIT :: ADDED The Store Code
Store Career Module
import { getCareer, getSkills } from '../../services/CareerService';
const state = () => {
// eslint-disable-next-line no-unused-labels
careerPath: [];
// eslint-disable-next-line no-unused-labels
skills: [];
};
const actions = {
async getCareerPath ({commit}) {
getCareer().then(response => {
commit('setCareerPath', response);
}).catch(err => console.log(err));
},
async getSkills ({commit}) {
getSkills().then(response => {
commit('setSkills', response);
}).catch(err => console.log(err));
}
};
const mutations = {
async setCareerPath(state, careerPath) {
state.careerPath = careerPath;
},
async setSkills(state, skills) {
state.skills = skills;
}
}
export default {
namespaced: true,
state,
actions,
mutations
}
Career Service
export async function getCareer() {
const response = await fetch('/api/career');
return await response.json();
}
export async function getSkills() {
const response = await fetch('/api/career/skill');
return await response.json();
}
Then App.vue, created() :
created() {
this.$store.dispatch('config/getConfigurations');
this.$store.dispatch('certs/getCerts');
this.$store.dispatch('career/getSkills');
this.$store.dispatch('projects/getProjects');
},
Clues
It seems that if I remove the v-if on the buttons of the LCAdmin, the button are shown as expected except that they all show even when I don't want them to. (If no modal are associated)
Which give me this result :
Problem is that refs are not reactive
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
See simple demo below...
const vm = new Vue({
el: "#app",
components: {
MyComponent: {
props: ['modalRef'],
template: `
<div>
Hi!
<button v-if="modalRef">Click!</button>
</div>`
}
},
data() {
return {
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component :modal-ref="$refs.modal"></my-component>
<div ref="modal">I'm modal placeholder</div>
</div>
The solution is to not pass $ref as prop at all. Pass simple true/false (which button to display). And on click event, $emit the event to the parent and pass the name of the ref as string...

Hey! Do you know how to dynamically change the layout in Nuxt without store?

There is my default layout.
I want to change visibility on my header, when my modal is open, but I don't know how to change it dynamically. Any ideas?
// default.vue
<template>
<div class="container">
<header class="default-header">
<router-link class="logo" to="/"></router-link>
<div class="button-group">
<router-link to="/login" class="btn">Log in</router-link>
<router-link to="/register" class="btn">Sign up</router-link>
</div>
</header>
<nuxt />
</div>
</template>
//index.vue
<template>
<div>
<div #click="openModal">Open Modal</div>
<modal-popup v-model="showModal"></modal-popup>
</div>
</template>
<script>
import ModalPopup from "~/components/ModalPopup";
export default {
name: "Login",
components: {
ModalPopup
},
data() {
return {
showModal: false;
}
},
methods() {
openModal() {
this.showModal = true;
},
closeModal() {
this.showModal = false;
},
}
};
</script>
To do it without a store you can create an EventBus to trigger events which you can listen to. An eventbus provides communication between vue components.
You can create an eventbus js file.
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
Then in your components you can import it
import EventBus from '/eventbus'
In the index.vue component in your openModal() function you can change it to trigger the open modal event like this
openModal() {
EventBus.$emit('modal-opened', true);
this.showModal = true;
}
then in your default.vue component you can add a listener in the mounted function
mounted() {
EventBus.$on(‘modal-open’, function (payLoad) {
// Change header visibility here
});
}
Extra Note
If you don't want to import the Eventbus all the time you can import Eventbus in your app.js and just before your new Vue() you can add bus to the vue properties so your file will look something like this
import Vue from 'vue'
import EventBus from '/eventbus'
Object.defineProperties(
Vue.prototype,
{
$bus: {
get() => { return EventBus; }
}
}
)
new Vue({})
Then you can access the bus in your components like this
this.$bus.$emit('modal-open', true);
and
this.$bus.$on('modal-open', function(payload) {
})
Hope that helps

Why async component doesn't change when the route update?

I'm using async components in my code on CodeSandbox (seen below). When I click on goto product-2 after goto product-1, nothing happens. I expected the <product-item> component to change based on the clicked component, but only the URL changes. I even have a beforeRouteUpdate hook function. How do I fix this?
// main.js
import Vue from "vue";
import App from "./App.vue";
import VueRouter from 'vue-router';
import ProductPage from './product-page.vue';
Vue.use(VueRouter);
Vue.config.productionTip = false;
const routes = [
{ path: '/:productId', component: ProductPage },
]
const router = new VueRouter({
routes // short for `routes: routes`
})
new Vue({
router,
render: h => h(App)
}).$mount("#app");
// App.vue
<template>
<div id="app">
<router-link to="/product-1">goto product1</router-link>
<br>
<br>
<router-link to="/product-2">goto product2</router-link>
<div>Router view:</div>
<router-view :key="$route.params.productId"></router-view>
</div>
</template>
// product-page.vue
<template>
<div>
<product-item></product-item>
</div>
</template>
<script>
export default {
name: "product-page",
components: {
ProductItem: () => ({
component: import("./product-item.vue"),
loading: { template: "<div>loading....</div>" }
})
}
};
</script>
<template>
<div>
product item: {{product}}
</div>
</template>
<script>
export default {
name: "ProductItem",
mounted: function () {
this.product = this.$route.params.productId;
},
beforeRouteUpdate: function(to, from, next) {
this.product = to.params.productId;
next();
},
data: function () {
return {
product: null
}
},
};
</script>
The problem is the route path (i.e., /:productId) does not actually change between the links for /product-1 and /product-2 (even though the parameter values do), so router-view does not re-render.
The workaround is to key the router-view based on the productId parameter:
<router-view :key="$route.params.productId" />

vuex module mode in nuxtjs

I'm trying to implement a todo list using modules mode in the vuex store in nuxtjs but get the error this.$store.todo is undefined and cant find much about this relating to nuxt
Can anyone assist please I have
store index.js
export const state = () => ({
})
export const mutations = {
}
store todo.js
export const state = () => ({
todos: [],
})
export const mutations = {
mutations ...
}
export const actions = {
actions ...
}
export const getters = {
getters ...
}
index.vue page
<template>
<div>
<h2>Todos:</h2>
<p> Count: {{ doneTodosCount }} </p>
<ul v-if="todos.length > 0">
<li v-for="(todo, i) in todos" :key="i">
...
</li>
</ul>
<p v-else>Done!</p>
<div class="add-todo">
<input type="text" v-model="newTodoText">
<button #click="add">Add todo</button>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions, mapGetters } from 'vuex'
export default {
name: 'app',
data () {
return {
newTodoText: ""
}
},
created () {
this.$store.todo.dispatch('loadData')
},
computed: {
...mapState(['todos', ]),
...mapGetters([ 'doneTodosCount', 'doneTodos'])
},
methods: {
toggle (todo) {
this.$store.todo.dispatch('toggleTodo', todo)
},
}
}
</script>
From what i read I thought this should work but doesn't
I should add it all works fine if i don't use modules mode and just have a single index.js setup
Many Thanks
You need to call it differently
this.$store.dispatch('todo/toggleTodo', todo)
Also better to call it in fetch method, not created