Vue <script setup> Top level await causing template not to render - javascript

I'm using the new syntax in Vue 3 and I really like the idea of it, but once I tried to use a top level await I started to run in some problems.
This is my code:
<template>
<div class="inventory">
<a class="btn btn-primary">Test button</a>
<table class="table">
<thead>
<tr>Name</tr>
<tr>Description</tr>
</thead>
<tbody>
<tr v-for="(item, key) in inventory" :key="key">
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory'
const inventory: GetInventoryResponse = await apiGetInventory()
</script>
As you can see it's not that complicated, the apiGetInventory is just an axios call so I won't bother going into that.
The problem is, that if I have this top level await, my template doesn't render anymore, it's just a blank page in my browser. If I remove the two lines of code, it works fine.
Also the promise seems to revolve just fine, if I place a
console.log(inventory) underneath it I get an array with objects all fine and dandy.
Anyone have a clue what's going wrong here?

Top-level await must be used in combination with Suspense (which is experimental).
You should be able to just do it in onBeforeMount. Not as elegant; but a solid solution. Something like this:
<script setup lang="ts">
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory';
import {ref, onBeforeMount} from 'vue';
const inventory = ref<GetInventoryResponse>()
onBeforeMount( async () => {
inventory.value = await apiGetInventory()
})
</script>

Using onBeforeMount is good, but there are a couple of other options.
#skirtle suggested in Vue Discord chat to do the initialization inside an async lambda or function (possibly as an IIFE):
<script setup lang="ts">
let inventory: GetInventoryResponse
const loadData = async () => inventory = apiGetInventory()
loadData()
</script>
#wenfang-du suggested in How can I use async/await in the Vue 3.0 setup() function using Typescript to use promise chaining:
<script setup lang="ts">
let inventory: GetInventoryResponse
apiGetInventory().then(d: GetInventoryResponse => inventory = d)
</script>
The benefit of doing so is that the code is run before the beforeMount lifecycle hook.
You additionally need to take care of error handling as appropriate in both cases.

if you need for specific template(routes).
You can use router beforeResolve:
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory'
let inventory = false
router.beforeResolve(async to => {
// Skip if loaded or for specific vue file
if (inventory || to.meta?.layout === 404 || to.meta?.layout === 'blank') {
return
}
inventory = await apiGetInventory()
})

The Vue3 documentation says
Top-level await can be used inside script setup. The resulting code will be compiled as async setup()
In addition, the awaited expression will be automatically compiled in a format that preserves the current component instance context after the await.
For example:
<script setup lang="ts">
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory'
const inventory = ref(await apiGetInventory())
</script>
This only works if you use the Suspense compenent in the parent component, for instance:
<Suspense>
<RouterView />
<template #fallback>
Loading...
</template>
</Suspense>

Related

Vue - How to use a dynamic ref with composition API

I am using the composition API in Vue 3 and want to define a ref to a component.
Usually that would be done by adding ref="name" to the template and then defining a ref with the same name: const name = ref(null).
The problem is I want to use dynamic ref names.
Any idea how to solve this?
In the options API I could do something like this:
:ref="name${x}" and then access it in the code like this: this.$refs[name${x}].
But with the composition API I don't have access to $refs.
Is there a way to do this without using a function inside the template ref?
The :ref prop accepts a function (see documentation), which you can use to assign it to the variable you want:
<script setup>
const dynamicRef = ref(null);
</script>
<template>
<Count :ref="(el) => dynamicRef = el" />
</template>
Vue playground example
I guess the Refs inside v-for is a better match for what you are trying to achieve.
Requires Vue v3.2.25 or above
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
Vue SFC Playground

How to inject a mock Pinia store to a Vue component based on routing

I am using Vue together with Pinia stores and Vue-router.
I have a component Projects.vue that uses a project.store.ts to display some data on the screen when a user goes to someSite.com/projects:
<!-- Projects.vue -->
<template>
<div v-each="project in projects">
{{project.name}}
</div>
</template>
<script setup lang="ts">
import { useProjectStore } from "../stores/project.store";
const projectStore = useProjectStore();
const { projects } = storeToRefs(projectStore);
projectStore.getAll();
</script>
<!-- project.store.ts -->
export const useProjectStore = defineStore({
id: "project",
state: () =>
({
projects: [],
} as ProjectStoreState),
actions: {
async getAll() {
this.projects = await fetch.get(`${baseUrl}`);
}
},
});
What I want to do is to introduce Demo functionality, where if a user goes to someSite.com/demo/projects, it's served the same Projects.vue component, however with static data that I've defined in the code.
I was thinking to create a project.store.mock.ts, which follows the same interface just does
actions: {
async getAll() {
this.projects = [{name: "project1"}]
}
},
However, I am struggling to find a way to inject a different store based on the route. This might of course be a completely wrong way to approach it, but I would like to keep the same component, just present different data (data from different sources) based on routing

AsyncData not being called in nuxt

I want to set category data at asyncData() hook. But MainHeader Page Component never calls asyncData even if it is placed in a page. Can you explain why MainHeader Page Component does not call asyncData?
MainHeader is placed inside "com" folder which is placed on pages (/pages/com/MainHeader)
<template>
<div>
<header-nav :cateList="cateList"/>
</div>
</template>
<script>
import HeaderNav from '~/components/com/nav/HeaderNav.vue';
import CateApi from "~/util/api/category/cate-api";
export default {
components: {HeaderNav},
async asyncData(){
const cateList = await CateApi.getDispCateList();
return{
cateList,
}
},
data() {
return {
cateList: [],
}
},
}
</script>
default
(/layouts/default)
<template>
<div>
<main-header/>
<Nuxt/>
</div>
</template>
<script>
import MainHeader from "~/pages/com/MainHeader.vue"
export default {
components :{
MainHeader,
},
name: "defaultLayout"
}
</script>
You're probably reaching your page directly, something like /com/test-page I guess, there you will get first initial result on the server (you can check, you'll be getting a console.log in there), which is legit because this is how Nuxt works (server-side first then client-side).
Please follow the convention of naming your pages like my-cool-page and not myCoolPage and also keep in mind that asyncData works only inside of pages.
Your project is working perfectly fine, as an example, create the following file /pages/com/main-header.vue
<template>
<div>
<p> main header page</p>
<header-nav :cate-list="cateList" />
</div>
</template>
<script>
import HeaderNav from '~/components/com/nav/HeaderNav.vue';
export default {
components: { HeaderNav },
async asyncData() {
console.log("check your server if accessing this page directly, otherwise you'll see this one in your browser if client-side navigation")
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const cateList = await response.json()
return { cateList }
},
}
</script>

How to display async data in vue template

I'm interesting in the case of displaying in vue template data which loaded asynchroniously. In my particular situation I need to show title attribute of product object:
<td class="deals__cell deals__cell_title">{{ getProduct(deal.metal).title }}</td>
But the product isn't currently loaded so that the title isn't rendered at all. I found a working solution: if the products aren't loaded then recall getProduct function after the promise will be resolved:
getProduct (id) {
if (!this.rolledMetal.all.length) {
this.getRolledMetal()
.then(() => {
this.getProduct(id)
})
return {
title: ''
}
} else {
return this.getRolledMetalById(id)
}
}
However maybe you know more elegant solution because I think this one is a little bit sophisticated :)
I always use a loader or a spinner when data is loading!
<template>
<table>
<thead>
<tr>
<th>One</th>
<th>Two</th>
<th>Three</th>
</tr>
</thead>
<tbody>
<template v-if="loading">
<spinner></spinner> <!-- here use a loaded you prefer -->
</template>
<template v-else>
<tr v-for="row in rows">
<td>{{ row.name }}</td>
<td>{{ row.lastName }}</td>
</tr>
</template>
</tbody>
</table>
</template>
And the script:
<script>
import axios from 'axios'
export default {
data() {
return {
loading: false,
rows: []
}
},
created() {
this.getDataFromApi()
},
methods: {
getDataFromApi() {
this.loading = true
axios.get('/youApiUrl')
.then(response => {
this.loading = false
this.rows = response.data
})
.catch(error => {
this.loading = false
console.log(error)
})
}
}
}
</script>
There are a few good methods of handling async data in Vue.
Call a method that fetches the data in the created lifecycle hook that assigns it to a data property. This means that your component has a method for fetching the data and a data property for storing it.
Dispatch a Vuex action that fetches the data. The component has a computed property that gets the data from Vuex. This means that the function for fetching the data is in Vuex and your component has a computed property for accessing it.
In this case, it looks like your component needs to have a RolledMetal and based on that it retrieves a product. To solve this you can add methods that fetch both of them, and call them on the created lifecycle. The second method should be called in a then-block after the first one to ensure it works as expected.

Why do I continue to get the Tasks is not defined?

I am trying to follow the Meteor documentation here:
https://www.meteor.com/tutorials/blaze/collections
in adding a collection and being able to get an empty array back by doing this in the console:
Tasks.find().fetch()
but instead I get this:
Uncaught ReferenceError: Tasks is not defined
at <anonymous>:1:1
I am not sure where I have gone wrong since I am following their documentation, I believe the tree structure for the imports folder which I created according to the documentation is correct and the code I have implemented so far is also as suggested from their docs.
This is the client/main.js:
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import {Tasks} from '../imports/api/tasks';
import './main.html';
Template.hello.onCreated(function helloOnCreated() {
// counter starts at 0
this.counter = new ReactiveVar(0);
});
// templates can have helpers which are just functions and events and this
// particular event is a click event
Template.todos.helpers({
tasks() {
return Tasks.find({});
},
});
Template.todos.events({
});
This is the imports/api/tasks.js:
import {Mongo} from 'meteor/mongo';
export const Tasks = new Mongo.Collection('tasks');
This is the server/main.js:
import { Meteor } from 'meteor/meteor';
import {Tasks} from '../imports/api/tasks';
Meteor.startup(() => {
// code to run on server at startup
});
This is the client/main.html:
<head>
<title>tasklist</title>
</head>
<body>
<h1>Welcome to Meteor!</h1>
{{> todos}}
{{> info}}
</body>
<template name="todos">
</template>
<template name="info">
<h2>Learn Meteor!</h2>
<ul>
<li>Do the Tutorial</li>
<li>Follow the Guide</li>
<li>Read the Docs</li>
<li>Discussions</li>
</ul>
</template>
Tasks is defined, but only in the scope of where it is imported. Since it was never declared as global, you won't be able to access it within your browser's console.
If you want to see the tasks (the ones you are subscribing to) in the console, just update your helper function:
Template.todos.helpers({
tasks() {
let tasks = Tasks.find({});
console.log(tasks.fetch());
return tasks;
}
});
Or, you can check your database directly:
> meteor mongo
> db.tasks.find()

Categories