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>
Related
I have a component named ProductArea which displays products loaded from the Prismic API. The products loaded are dependant on a category which is selected by the user in a sidebar.
I'm using Vuex and struggling to come up with a flow that avoids a situation where category is not yet available in my store (category is also loaded from Prismic).
Here is what the parent of ProductArea looks like:
<template>
<div>
<NavBar />
<!-- <Header /> -->
<main>
<div v-if="!$fetchState.pending" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex-1 min-w-0 bg-white xl:flex">
<Sidebar :navigation="navigation" />
<ProductArea />
</div>
</div>
</main>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import NavBar from '#/components/NavBar.vue'
import Sidebar from '#/components/Sidebar.vue'
import Header from '#/components/Header.vue'
import CategoryHeader from '#/components/CategoryHeader.vue'
import ProductGrid from '#/components/ProductGrid.vue'
import { mapActions } from 'vuex'
import { mapGetters } from 'vuex'
export default {
name: 'App',
components: {
Sidebar,
NavBar,
Header,
CategoryHeader
},
data() {
return {
navigation: null
}
},
async fetch() {
const component = this
await this.fetchCategories()
.then(function(navigationResult) {
const navigation = component.$store.getters.navigation
component.navigation = navigation
})
},
fetchOnServer: true,
methods: {
...mapActions({ fetchCategories: 'fetchCategories', fetchProducts: 'fetchProducts' })
}
}
</script>
I assumed having v-if="!$fetchState.pending" would prevent ProductArea from being created until category has been loaded into the store, however this doesn't seem to be the case.
Here is ProductArea:
<template>
<div class="bg-white lg:min-w-0 lg:flex-1">
<CategoryHeader :category="this.category" :products="this.products" />
<div class="sm:p-6">
<ProductGrid :category="this.category.primary.category" :products="this.products" />
</div>
</div>
</template>
<script lang="ts">
import { mapActions } from 'vuex'
import { mapGetters } from 'vuex'
import Locale from '#/types/locale'
export default {
name: 'ProductArea',
data() {
return {
category: this.$store.getters.category,
products: Array
}
},
async fetch() {
const component = this
await this.fetchProducts(this.category)
.then(function(productsResult) {
const products = component.$store.getters.products
component.products = products
console.log(products)
})
},
fetchOnServer: true,
methods: {
...mapActions({ fetchProducts: 'fetchProducts' })
}
}
</script>
Here's the error I'm receiving:
Error in fetch(): TypeError: Cannot read property 'products' of undefined
This error is referring to the undefined category within the fetchProducts called via fetch on the ProductsArea component.
Can anyone point me in the right direction? What would be the optimal flow here to prevent category being accessed before it is available?
You could set a default category. If you don't want to do that, bring the Vuex category into the parent and only show <ProductArea> when it's defined:
Parent
<ProductArea v-if="category" />
computed: {
...mapGetters(['category'])
}
This is necessary because your v-if on $fetchState.pending only tests whether all the categories are loaded, but for the child component you also need to test that a category has been selected.
In fact, you can simplify all your code by mapping the getters instead of storing getters in variables, which is not a good practice. Those variables wouldn't be updated reactively when the getter changes. Instead, completely remove the data options from both components:
Parent
async fetch() {
await this.fetchCategories();
}
computed: {
...mapGetters(['category', 'navigation'])
}
Child
async fetch() {
await this.fetchProducts();
}
computed: {
...mapGetters(['category', 'products'])
}
Other improvements:
You can shorten the mapActions calls a bit:
Parent: ...mapActions(['fetchCategories'])
Child: ...mapActions(['fetchProducts'])
I need to render a different layout for the same route for a specific URI with different components depending on the user being on mobile or in desktop.
I would like to avoid having route path checks in the PageCommon(layout component) to keep it clean.
The app has a main component taking care of the layout, it has different router-views where we load the different components for each page URI. This would be a normal route for that.
{
path: '',
component: PageCommon,
children: [
{
path: '',
name: 'Home',
components: {
default: Home,
header: Header,
'main-menu': MainMenu,
'page-content': PageContent,
footer: Footer,
'content-footer': ContentFooter
}
},
I can't change the route components property once the component is loaded so I tried to make a wrapper and pass the components dynamically.
{
path: 'my-view',
name: 'My_View',
component: () => import('#/components/MyView/ViewWrapper')
},
In /components/MyView/ViewWrapper'
<page-common v-if="isMobile">
<my-mobile-view is="default"></my-mobile-view>
<main-menu is="main-menu"></main-menu>
</page-common>
<page-common v-else>
<my-desktop-view is="default"></my-desktop-view>
<header is="header"></header>
<main-menu is="main-menu"></main-menu>
<footer is="footer"></footer>
</page-common>
</template>
I would expect that the components passed inside page-common block would be substituted on the appropriate , but is not how it works, and Vue just loads page-common component with empty router-views.
Is there any approach for this?
Note that I already tried using :is property for loading different components, but the problem then is on how to tell the parent to use this or that component for this page. This is the code for that:
<template>
<component :is="myView"></component>
</template>
<script>
import DesktopView from "#/components/MyView/DesktopView";
import MobileView from "#/components/MyView/MobileView";
export default {
name: 'MyView',
components: {
DesktopView,
MobileView,
},
data(){
return {
myView: null,
isMobile: this.detectMobile()
}
},
methods : {
getViewComponent() {
return this.isMobile ? 'mobile-view' : 'desktop-view';
}
},
created() {
this.myView = this.getViewComponent();
}
}
</script>
I could use this approach for each of the PageCommon router views, creating a component for each that does the above, but it looks like a very bad solution.
A computed method is all you need.
You should have this top level Logic in App.vue and the <router-view> should be placed in both DesktopView and MobileView.
// App.vue
<template>
<component :is="myView"></component>
</template>
<script>
import DesktopView from "#/components/MyView/DesktopView";
import MobileView from "#/components/MyView/MobileView";
export default {
name: 'MyView',
components: {
DesktopView,
MobileView,
},
computed: {
myView() {
return this.detectMobile() ? 'mobile-view' : 'desktop-view';
}
}
}
</script>
You may also want to consider code splitting by setting up Dynamic Components for those layouts since Mobile will load Desktop View because it is compiled into final build, register them globally as dynamic imports instead if importing them in MyView and then delete components also after doing the following instead, this way only the one that is needed will be downloaded saving mobile users their bandwidth:
// main.js
import LoadingDesktopComponent from '#/components/LoadingDesktopComponent '
Vue.componenet('desktop-view', () => ({
component: import('#/components/MyView/DesktopView'),
loading: LoadingDesktopComponent // Displayed while loading the Desktop View
})
// LoadingDesktopComponent .vue
<template>
<div>
Optimizing for Desktop, Please wait.
</div>
</template>
<script>
export default {
name: 'loading-component'
}
</script>
Routing logic will only be processed when <router-view> is available,this means you can delay the presentation of Vue Router, for example you can have :is show a splash screen like a loading screen on any URI before displaying a component in :is that contains <router-view>, only than at that point will the URI be processed to display the relevant content.
I have a few components, javascript, and elements that needs to be ran in a certain order.
1st - opensheetmusicdisplay.min.js which I have in my index.html file. This isn't an issue.
2nd - <div id="xml">
3rd - xml-loader.js which depends on both the "xml" div and opensheetmusicdisplay.min,js
This is the index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<script rel="preload" src="<%= BASE_URL %>js/osmd/opensheetmusicdisplay.min.js"></script>
</head>
<body>
<div id="xml2">words go here</div>
<div id="app"></div>
</body>
</html>
And this is the JavaScript part I'm attempting to test:
window.onload = function() {
alert("xx == ", document.getElementById("xml2"));
}
alert("xx2 == ", document.getElementById("xml2"));
alert(JSON.stringify(opensheetmusicdisplay, null, 1));
When I run this, they both instances of "xml2" show blanks. The opensheetmusicdisplay does show data, which means it is reading from the source in the head section in index.html
It was pointed out to me in the comments that alert only take one argument. That's a mistake that I'm going to let sit for the moment. The error in the console is TypeError: document.getElementById(...) is null.
Now, this is the main.js. There are a lot of comments because of my various ideas:
// vue imports and config
import Vue from 'vue'
import App from '#/App'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
Vue.config.productionTip = false
// page imports
import Notation from '#/components/Notation'
import HomePage from '#/components/HomePage'
// component imports and registration
import { FoundationCSS } from '#/../node_modules/foundation-sites/dist/css/foundation.min.css'
Vue.component('foundation-css', FoundationCSS)
import SideNav from '#/components/SideNav'
Vue.component('side-nav', SideNav);
// import * as Osmd from '#/../public/js/osmd/opensheetmusicdisplay.min.js'
// Vue.component('osmd-js', Osmd)
// import { OsmdJs } from '#/components/Osmd'
import * as XmlJs from '#/../public/js/osmd/xml-loader.js'
Vue.component('xml-js', XmlJs)
// import XLoad from '#/components/XmlLoader'
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/',
components: {
maininfo: HomePage
}
},
{ path: '/chromatic-scales/c-chromatic-scale',
components: {
maininfo: Notation// ,
// xmlloader: XLoad
}
}
]
})
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
I registered XmlJs as global because this is the only way out of 100 things that actually works. I then embed it in Notation.vue like so:
<template>
<div>
<div id="xml">
{{ notation.data }}
</div>
<xml-js />
</div>
</template>
<script>
import axios from 'axios'
export default ({
data () {
return {
notation: null,
}
},
mounted () {
axios
.get('http://localhost:3000/chromatic-scales/c-chromatic-scale')
.then(result => (this.notation = result))
}})
</script>
<style scoped></style>
The last file is the meat and potatoes of what I'm trying to do. The xml-loader.js slurps the data from <div id="xml"> and does whatever magic the program does in order to render the output I want. The issue is that there doesn't seem to be anyway to wait for the stuff in {{ notation.data }}.
I am new to using vuejs and front-end javascript frameworks in general. I do recognize the code is probably not optimal at this time.
There is race condition where DOM element is not available at the time when it's accessed. The solution is to not access DOM elements created by Vue outside of it. DOM element is ready for use only after asynchronous request:
<template>
<div>
<div ref="xml" id="xml">
{{ notation.data }}
</div>
<xml-js />
</div>
</template>
<script>
import axios from 'axios'
export default ({
data () {
return {
notation: null,
}
},
async mounted () {
const result = await axios
.get('http://localhost:3000/chromatic-scales/c-chromatic-scale')
this.notation = result;
this.$nextTick(); // wait for re-render
renderXml(this.$ref.xml); // pass DOM element to third-party renderer
}})
You can import xml-loader.js into the Notation.vue as a function. Then you can simply do something like this:
mounted () {
axios.get(PATH).then(result => {
this.notation = result
let xmlResult = loadXML(result)
doSomethingWithResult(xmlResult)
}
},
methods: {
doSomethingWithResult (result) {
// do something
}
}
I have two components, the first one is for uploading a file and the second one to Show a file. Inside my Upload Component I would like to call the Preview Component and add a Parameter so that a method inside the Preview Component uses a value which is created inside the Upload Component.
So far I have done this:
UploadComponent.vue
<template>
…
<button #click="upload"></button>
<preview-component :url="this.location"></preview-component>
</template >
<script>
import PreviewComponent from '#/js/components/PreviewComponent';
export default {
components: {
'preview-component': PreviewComponent
},
props: ['url'],
data () {
return {
// ...
location: ''
}
},
methods: {
upload() {
// ... upload stuff then update the global var location
this.location = response.data.location;
},
}
}
</script>
This is my Preview Component:
<template>
<div id="body">
///...
</div>
</template>
<script>
export default {
props: ['url'],
methods: {
loadPdf (url) {
//
},
}
}
</script>
So far I am getting the error that url is not defined, so it actually does not sent the url from the UploadCOmponent to the PreviewComponent, how do I manage to sent it?
You got a ninja this in your UploadComponent's template.
It should be <preview-component :url="location"></preview-component>
I have a strange issue with my 404/index.vue which after $ nuxt generate generates my 404.html file.
I am using a Btn component in my vue file which works on index.vue file but not on my 404/index.vue.
It seems that it does not pass the onclick parameter to the component.
I think the following makes it more understandable:
404/index.vue
<script>
import { mapGetters } from 'vuex'
import Heading from '~/components/Heading/Heading.vue'
import Btn from '~/components/Btn/Btn.vue'
export default {
components: {
Heading,
Btn
},
computed: {
...mapGetters(['defaultLang', 'currentLang']),
...mapGetters('data', ['getPageBySlug']),
page () {
return this.getPageBySlug('404')
}
},
data: () => ({
externalRoute: false
}),
beforeRouteEnter (to, from, next) {
// check before route entering if we came from external or already from our app
next(vm => {
if (from.path === '/') {
vm.externalRoute = true
} else {
vm.externalRoute = false
}
})
},
methods: {
getBackPath () {
return `/${this.currentLang !== this.defaultLang ? this.currentLang : ''}`
},
goBack () {
// if we came externally we go to our root, else we go one route back in history
if (this.externalRoute) {
this.$router.push('/')
} else {
this.$router.go(-1)
}
}
}
}
</script>
<template>
<div class="Page">
<Heading :level="1">{{ page.title }}</Heading>
<Btn :click="goBack">{{ $t('error.link') }}</Btn>
</div>
</template>
As you can see the goBack function should be passed to the Btn component.
After generation of the static pages the button is there (with the correct language error.link) but it would not do anything on click.
My button component looks like this: (I simplified it a bit to reduce to the important stuff here:
<script>
export default {
name: 'Btn',
props: {
click: {
type: Function,
default: () => {},
required: false
}
}
}
</script>
<style src='./Btn.scss' lang='scss'></style>
<template>
<component
class="Btn"
v-on:click="click"
<slot />
</component>
</template>
I am using the same button component in my index.vue, which is rendered on the starting page. The Btn works nicely there (also after $ yarn generate and I cannot figure out what the difference would be. Somehow the Btn does not receive the onclick function on the 404 template.
If I add /404 to the router and I run the app locally, the Btn works.
It's really just isolated in the case when I generate the static pages that the Btn does not work on the 404 vue template anymore.
Any idea what the reason could be for this?