Error message when trying to pass data from component to page in Nuxt - vue.js

I've created a component in Nuxt to get data from a Firestore database and would like to show that data in a page I created.
When I embed the component in a page I keep getting the error:
Property or method "provinces" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Now, when I copy the code from the component in the page it works just fine, so I assume the problem is with passing the data between the component and the page.
Full code of the component in components/index.vue :
<template>
<section class="container">
<div class="index">
<div v-for="province in provinces" :key="province.id">
<div class="div_title">
<h2>{{ province.name_nl }}</h2>
</div>
</div>
</div>
</section>
</template>
<script>
// import { VueperSlides, VueperSlide } from 'vueperslides'
// import 'vueperslides/dist/vueperslides.css'
import firebase from 'firebase'
// import fireDb from '#/plugins/firebase.js'
export default {
name: 'Index',
components: {
// VueperSlides,
// VueperSlide
},
data: function() {
return {}
},
async asyncData() {
const moment = require('moment')
const date = moment(new Date()).format('YYYY-MM-DD')
const housesArray = []
const provincesArray = []
await firebase
.firestore()
.collection('provinces')
.orderBy('name_nl')
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
provincesArray.push(doc.data())
})
})
await firebase
.firestore()
.collection('houses')
.where('valid_until', '>', date)
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
housesArray.push(doc.data())
})
})
return {
provinces: provincesArray,
houses: housesArray
}
}
}
</script>
<style scoped>
div {
text-align: center;
}
h2 {
margin-top: 5vh;
margin-left: auto;
margin-right: auto;
width: 95vh;
}
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
p {
text-align: left;
}
li {
min-width: 100% !important;
margin-left: 0px;
text-align: left;
}
</style>
Page where I insert the component in pages/index.vue:
<template>
<v-layout column justify-center align-center>
<v-flex xs12 sm8 md6>
<div class="text-xs-center">
<logo />
<tabs />
<index />
</div>
</v-flex>
</v-layout>
</template>
<script>
import Logo from '~/components/Logo.vue'
import Tabs from '~/components/Tabs.vue'
import firebase from 'firebase'
import Index from '~/components/Index.vue'
export default {
components: {
Logo,
Tabs,
Index
},
data: function() {
return {}
}
}
</script>
I would expect the page to display the data that I retrieved when I import the component into the page but I keep getting the same error.
Should I be using the Nuxt store to transfer data between a component and a page or am I doing something else wrong?

The lifecycle hook asyncData is not know within Vue components. It's only known in Nuxt pages.
It's better to do the data request within your pages component and pass it as a property to your component:
pages/index.vue
<template>
<index :houses="houses" />
</template>
<script>
const delay = time => new Promise(resolve => setTimeout(resolve, time));
export default {
async asyncData() {
// fake data
await delay(500);
return {
houses: [...]
}
}
}
</script>
components/index.vue
<template>
<pre>{{ houses }}</pre>
</template>
<script>
export default {
props: {
houses: {
required: true,
type: Array
}
}
}
</script>

Related

VueJS - use sidebar component event to control navbar

I am building a web app using Vue3 + vuetify3 running on Vite. I have a v-navigation-drawer within my Sidebar.vue component (child) and I'm emitting mouse events to have my App.vue (parent) listen to it and allow another child components (in this case Topbar.vue) to use that to move the title to the right as the sidebar opens and closes. The problem I'm seeing is that I can log the events in App.vue from the Sidebar as true/false but when I check those values in Topbar.vue they're undefined. I have tried different approaches but can't come up with a solution. If somebody can give me some pointers on how to figure this out I would really appreciate it.
For the sake of space, I'm not including all the css rules and all the items in the sidebar, just the functional sections worth looking at but again, the sidebar is not the issue but the way the topbar is not getting the value from the App.vue.
Sidebar.vue
<template>
<v-navigation-drawer v-model="drawer" class="sidepanel" expand-on-hover rail>
<v-list class="sidebarlist" density="compact" nav>
<router-link to="/">
<v-list-item
class="sidebar-item"
:style="{
backgroundColor: selectedRoute === 'dashboard' ? '#9100e9' : '',
}"
#click="selectedRoute = 'dashboard'"
prepend-icon="mdi-home"
title="Dashboard"
value="dashboard"
></v-list-item>
</router-link>
</v-list>
</v-navigation-drawer>
</template>
<script lang="ts">
export default {
data() {
return {
drawer: true,
selectedRoute: "",
};
},
methods: {
onHover() {
this.$emit("mouseenter");
},
onLeave() {
this.$emit("mouseleave");
},
},
};
</script>
<style scoped>
.sidebar-opened {
opacity: 1;
transition: 0.3s ease-out;
}
.sidebar-item:hover {
background-color: #4b247a;
transition-duration: 0.4s;
}
/* and more */
</style>
App.vue
<template>
<v-app :theme="theme">
<TopbarComp :title="$route.meta.title" :sidebarHovered="sidebarHovered"/>
<SideBarComp #mouseenter="onHover" #mouseleave="onLeave"/>
<router-view />
</v-app>
</template>
<script lang="ts">
import { ref } from "vue";
import SideBarComp from "./components/Sidebar.vue";
export default {
components: {
SideBarComp,
},
methods: {
onHover() {
// Slide Topbar to the right
this.sidebarHovered = true;
console.log('Sidebar entered')
},
onLeave() {
// Slide Topbar back in place
this.sidebarHovered = false;
}
},
setup() {
const theme = ref("dark");
const sidebarHovered = ref(false);
return {
theme,
sidebarHovered
};
}
};
</script>
Topbar.vue
<template>
<v-toolbar fixed app color="#2D2D2D">
<v-toolbar-items>
<v-toolbar-title
class="text-uppercase pa-5"
:class="{ 'slide-right': topbarSlide || sidebarHovered }"
#mouseenter="onHover"
#mouseleave="onLeave"
>
<span class="font-weight-light" style="color: #9E9E9E;">{{ firstWord + ' '}} </span>
<span>{{ restOfWords }}</span>
</v-toolbar-title>
</v-toolbar-items>
</v-toolbar>
</template>
<script lang="ts">
export default {
props: ['title', 'sidebarHovered'],
data() {
return {
toptitle: this.title || '',
topbarSlide: false,
};
},
computed: {
firstWord() {
return this.toptitle.split(" ")[0];
},
restOfWords() {
return this.toptitle.split(" ").slice(1).join(" ");
},
},
methods: {
onHover() {
this.topbarSlide = true;
console.log('The value of the Sidebar is: ',this.sidebarHovered)
console.log('The value of the Topbar is: ', this.topbarSlide)
},
onLeave() {
this.topbarSlide = false;
},
},
};
</script>
<style>
.slide-right {
transform: translateX(20%) !important;
transition: transform 0.3s ease-out !important;
}
</style>

Vue 3 Composition API...how to replace getElementById

I wrote the following in Vue 3 Composition API.
If you look on the "onMounted" I'm attaching an event listener to the window to keep the status box to the bottom of the screen. I'm using absolute position and bottom 0.
I'm wondering if I can make this more "Vue" by replacing the getElementById? I tried refs but it's not working.
Any suggestions or should I leave well enough alone?
Thanks
<template>
<div>
<transition name="toast">
<div id="slide" class="slide-modal" v-if="statuses.length">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div v-for="status in statuses" :key="status.id" class="status-div">
<div>{{ status.text }}</div>
<div #click="closeStatus(status)"><span class="-x10-cross"></span></div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, onMounted } from "vue";
import { useStore } from "../model/DataStore";
export default defineComponent({
setup() {
const store = useStore();
const statuses = ref([]);
const statusMessage = computed(() => store.state.statusMessage);
function addStatus(newMessage) {
statuses.value.push({
id: statuses.value.length + 1,
text: newMessage
})
}
watch(statusMessage, (newValue: string, oldValue: string) => {
addStatus(statusMessage.value);
})
onMounted(() => {
window.addEventListener("scroll", function (e) {
let vertical_position = 0;
vertical_position = pageYOffset;
if(document.getElementById("slide")){
document.getElementById('slide').style.bottom = -(vertical_position) + 'px';
}
});
})
return {
store,
statuses,
addStatus
};
},
methods: {
clearAllNotifications() {
this.statuses = []
},
closeStatus(elm: any) {
const index = this.statuses.map((status) => status.id).indexOf(elm.id);
this.statuses.splice(index, 1);
}
}
})
</script>
and here's the slide modal style:
.slide-modal {
max-height: 200px;
width: 500px;
background-color: #f2f2f2;
color: #505050;
padding: 8px;
display: flex;
flex-direction: column;
gap: 8px;
overflow-x: hidden;
position: absolute;
bottom: 0;
}
The docs give a pretty simple example
<template>
<div ref="root">This is a root element</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div>This is a root element</div>
})
return {
root
}
}
}
</script>
use ref() when creating
pass to template in return function
use ref="..." in template
and in onMounted, access via ref.value
so for your code it would be...
<template>
<div>
<transition name="toast">
<div ref="slideRef" class="slide-modal" v-if="statuses.length">
<div class="clear-notifications" #click='clearAllNotifications()'>Clear all notifications</div>
<div v-for="status in statuses" :key="status.id" class="status-div">
<div>{{ status.text }}</div>
<div #click="closeStatus(status)"><span class="-x10-cross"></span></div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, onMounted } from "vue";
import { useStore } from "../model/DataStore";
export default defineComponent({
setup() {
const store = useStore();
const statuses = ref([]);
const slideRef = ref();
const statusMessage = computed(() => store.state.statusMessage);
function addStatus(newMessage) {
statuses.value.push({
id: statuses.value.length + 1,
text: newMessage
})
}
watch(statusMessage, (newValue: string, oldValue: string) => {
addStatus(statusMessage.value);
})
onMounted(() => {
window.addEventListener("scroll", function (e) {
let vertical_position = 0;
vertical_position = pageYOffset;
if(slideRef.value){
slideRef.value.style.bottom = -(vertical_position) + 'px';
}
});
})
return {
store,
statuses,
addStatus,
slideRef
};
},
})
</script>

Nuxt - Using router.push inside a component not changing pages correctly

index.vue -
<template>
<div>
<Header />
<div class="container">
<SearchForm />
</div>
</div>
</template>
<script>
const Cookie = process.client ? require('js-cookie') : undefined
export default {
data() {
return {
form: {
email: '',
password: ''
},
show: true
}
},
methods: {
logout() {
// Code will also be required to invalidate the JWT Cookie on external API
Cookie.remove('auth')
this.$store.commit('setAuth', {
auth: null,
user_type: null
})
}
}
}
</script>
<style>
.container {
/* margin: 0 auto; */
/* min-height: 100vh; */
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
</style>
jobs.vue -
<template>
<div>
<Header />
<SearchForm />
<b-container class="main_container">
<b-row>
<h1> Results for "{{q}}"</h1>
</b-row>
<b-row>
<ul id="array-rendering">
<li v-for="item in results" :key="item.job_id">
{{ item.job_title }}
{{ item.job_city }}
{{ item.job_state }}
{{ item.job_work_remote }}
</li>
</ul>
</b-row>
</b-container>
</div>
</template>
<script>
const Cookie = process.client ? require('js-cookie') : undefined
export default {
// middleware: 'notAuthenticated',
watchQuery: ['q'],
data() {
return {
q: null,
results: []
}
},
async fetch() {
this.q = this.$route.query.q
this.results = await this.$axios.$get('/api/job/search', {
params: {
keyword: this.q,
}
})
},
methods: {
}
}
</script>
<style>
.container {
align-items: center;
text-align: center;
}
</style>
SearchForm.vue component -
<template>
<div id='searchFormDiv'>
<b-form inline #submit.prevent="onSubmit">
<label class="sr-only" for="inline-form-input-name"> keyword</label>
<b-form-input v-model="form.keyword" id="inline-form-input-name" class="mb-2 mr-sm-2 mb-sm-0" placeholder="Job title or keyword" size="lg"></b-form-input>
<label class="sr-only" for="inline-form-input-username">location</label>
<b-input-group class="mb-2 mr-sm-2 mb-sm-0">
<b-form-input v-model="form.location" id="inline-form-input-username" size="lg" placeholder="City, state or zip"></b-form-input>
</b-input-group>
<b-button type="submit" variant="primary">Find Jobs</b-button>
</b-form>
</div>
</template>
<script>
import {
BIconSearch,
BIconGeoAlt
} from 'bootstrap-vue'
export default {
data() {
return {
form: {
keyword: '',
location: ''
}
}
},
created () {
this.form.keyword = this.$route.query.q
},
methods: {
onSubmit() {
this.$router.push({
path: 'jobs',
query: {
q: this.form.keyword
}
});
}
},
components: {
BIconSearch,
BIconGeoAlt
},
}
</script>
<style>
#searchFormDiv {
margin-top: 50px
}
</style>
The route for "http://localhost:3000/" returns the index.vue page.
In this vue page, I have a component with a search form. Once you complete these form and hit the seach button, it will re-direct to a results page
if this.form.keyword = "Data", the next URL will be "http://localhost:3000/jobs?q=Data" and it will be using the jobs.vue page.
The issue I'm running into is the CSS is not being loaded from the jobs.vue page. It's still coming from the index.vue page for some reason. If I refresh the page, then the CSS from jobs.vue is loading. I need the CSS to load from jobs.vue on the initial redirect. All of the query data is working as expected so thats a plus.
However, the following CSS is being applied from index.vue for some reason instead of the CSS from the jobs.vue page -
display: flex;
justify-content: center;
Does anyone know whats going on here? This app is SSR and not SPA.
You have to scope your css from the index.vue page to the other pages with the scoped directive (see docs https://vue-loader.vuejs.org/guide/scoped-css.html)
<style scoped>
/* local styles */
</style>
<style>
/* global styles */
</style>
You can add your global CSS in your layouts/default.vue file.
This solved the issue -
methods: {
onSubmit() {
window.location = 'http://localhost:3000/jobs?q=' + this.form.keyword;
}
},

Error in mounted hook: "RangeError: Maximum call stack size exceeded" VUEJS

I have following router.js in my vuejs app
import Vue from 'vue'
import Router from 'vue-router'
import {client_auth, annotator_auth} from './middleware/auth'
import {reroute_home} from '#/middleware/reroute'
import Editor from './components/editor/Editor.vue'
Vue.use(Router)
const router = new Router({
{
path: '/annotator/studio',
name: 'annotator_studio',
component: Editor,
props: (route) => ({
image_id: route.query.q
}),
meta: {
title: "Annotation Studio",
middleware: annotator_auth
}
}
]
})
function nextFactory(context, middleware, index) {
const subsequentMiddleware = middleware[index];
if (!subsequentMiddleware) return context.next;
return (...parameters) => {
context.next(...parameters);
const nextMiddleware = nextFactory(context, middleware, index + 1);
subsequentMiddleware({ ...context, next: nextMiddleware });
};
}
router.beforeEach((to, from, next) => {
if (to.meta.middleware) {
const middleware = Array.isArray(to.meta.middleware)
? to.meta.middleware
: [to.meta.middleware];
const context = {
from,
next,
router,
to,
};
const nextMiddleware = nextFactory(context, middleware, 1);
return middleware[0]({ ...context, next: nextMiddleware });
}
return next();
});
export default router;
and following is my Editor.uve
<template>
<div id="container">
<div id="view-item">
<app-view/>
</div>
</div>
</template>
<script>
import View from './view/View.vue'
export default {
components: {
'app-view': View,
}
}
</script>
<style scoped>
#container {
display: flex;
flex-flow: column;
height: 100%;
}
#stepper-item {
flex: 0 1 auto;
margin: 5px;
}
#view-item {
display: flex;
flex: 1 1 auto;
margin: 0px 5px 5px 5px;
}
</style>
In this Editor.uve i am importing a child view called View.vue. Following is my View.vue
<template lang="html">
<div
id="view"
class="elevation-2 pointers-please">
<app-osd-canvas/>
<app-annotation-canvas/>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import OSDCanvas from './OSDCanvas.vue'
import AnnotationCanvas from './AnnotationCanvas.vue'
export default {
components: {
'app-osd-canvas': OSDCanvas,
'app-annotation-canvas': AnnotationCanvas
},
computed: {
...mapState({
projectImageName: state => state.image.projectImageName
})
},
async mounted () {
await this.setProjectImageName('demo')
this.loadDemo()
},
methods: {
...mapActions({
setProjectImageName: 'image/setProjectImageName',
loadDemo: 'editor/loadDemo',
loadProject: 'editor/loadProject'
})
}
}
</script>
<style scoped>
#view {
position: relative;
display: flex;
flex: 1 1 auto;
}
</style>
In this View.vue again i am importing a child component called OSDCanvas.
My OSDCanvas.uve looks like following.
<template lang="html">
<div id="osd-canvas" />
</template>
<script>
import { mapActions } from 'vuex'
export default {
mounted () {
this.setupOsdCanvas('osd-canvas')
},
methods: {
...mapActions({
setupOsdCanvas: 'image/setupOsdCanvas'
})
}
}
</script>
<style>
#osd-canvas {
position: absolute;
height: 100%;
width: 100%;
}
</style>
I am facing following error when i hit /annotator/studio route
[Vue warn]: Error in mounted hook: "RangeError: Maximum call stack size exceeded" found in
---> <AppOsdCanvas> at src/components/editor/view/OSDCanvas.vue
<AppView> at src/components/editor/view/View.vue
<Editor> at src/components/editor/Editor.vue
<VContent>
<VApp>
<App> at src/App.vue
<Root>
I have tried some online solutions as well but the issue is still the same. Any help is appreciated.
UPDATE
following is image/setupOsdCanvas in actions.js in store
setupOsdCanvas: ({
commit
}, payload) => {
commit('setupOsdCanvas', payload)
},
and foloowing is setupOsdCanvas in mutations.js in store
setupOsdCanvas: (state, payload) => {
state.OSDviewer = new openseadragon.Viewer({
id: payload,
showNavigationControl: false,
showNavigator: true,
navigatorId: 'navigator',
maxZoomPixelRatio: 2
})
// Prevent rotation and 'flipping' of the image through the default keybaord
// shortcuts, R and F. (these were conflicting with other annotation tool
// shortcuts when the image was open)
state.OSDviewer.addHandler('canvas-key', e => {
if (e.originalEvent.code === 'KeyR' || e.originalEvent.code === 'KeyF') {
e.preventDefaultAction = true
}
})
}

HeaderComponent with custom functionality

I would like to know how can I implement a CustomHeader with a custom functionality (other than sort, for example).
Basically what I want to know is how to communicate my HeaderComponent with my component that holds the grid. E.g.:
<template>
<div style="height: 100%" class="table-chart" ref="root">
<div class="title" ref="title">{{ objectData.title }}</div>
<div class="ag-dashboard" style="height: 100%; width: 90%; margin: 0 auto">
<ag-grid-vue
:style="{ height: tableHeight }"
:gridOptions="gridOptions"
:columnDefs="columnDefs"
:rowData="rowData"
/>
</div>
</div>
</template>
<script>
export default {
components: {
'HeaderComponent': {
template: '<span>{{this.params.displayName}} <span #click="custom">CLICK</span></span>',
methods: {
custom() {
// emmit an event here or find a way to comunnicate with the function "customEvent" below
}
}
}
},
methods: {
customEvent() {
console.log('Event from header');
}
},
beforeMount() {
// ... setup Ag-Grid and the HeaderComponent in the columns' headerComponentFramework
}
}
</script>
Apreciate any help,
The cleaner way I found of doing this was through an EventBus:
import Vue from 'vue';
const EventBus = new Vue();
export default {
//...
'HeaderComponent': {
// ...
methods: {
custom() {
EventBus.$emit('customEvent');
}
}
// ...
mounted() {
EventBus.$on('customEvent', () => {
// Do whatever...
});
}
}