Error in mounted hook: "RangeError: Maximum call stack size exceeded" VUEJS - vue.js

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

Related

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>

Vue.js 3 - How can I pass data between Vue components and let both views also update?

I tried the following.
Please note the commented line in parent.vue that doesn't even commit the new state for me.
However maybe someone can guide me to a better solution for a global state shared by multiple components?
main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createStore } from 'vuex'
const app = createApp(App);
export const store = createStore({
state: {
textProp: 'test',
count: 1
},
mutations: {
setState(state, newState) {
console.log('setState');
state = newState;
}
},
getters: {
getAll: (state) => () => {
return state;
}
}
});
app.use(store);
app.mount('#app')
parent.vue
<template>
<div class="parent">
<div class="seperator" v-bind:key="item" v-for="item in items">
<child></child>
</div>
<button #click="toonAlert()">{{ btnText }}</button>
<button #click="veranderChild()">Verander child</button>
</div>
</template>
<script>
import child from "./child.vue";
import {store} from '../main';
export default {
name: "parent",
components: {
child,
},
store,
data: function () {
return {
items: [
{
id: 1,
valueText: "",
valueNumber: 0,
},
{
id: 2,
valueText: "",
valueNumber: 0,
},
{
id: 3,
valueText: "",
valueNumber: 0,
},
],
};
},
props: {
btnText: String,
},
methods: {
toonAlert() {
alert(JSON.stringify(this.$store.getters.getAll()));
},
veranderChild() {
console.log('child aan het veranderen (parentEvent)');
this.$store.commit('setState', { // This is especially not working.
textProp: 'gezet via de parent',
count: 99
})
this.$store.commit({type: 'setState'}, {
'textProp': 'gezet via de parent',
'count': 99
});
},
},
};
</script>
<style>
.seperator {
margin-bottom: 20px;
}
.parent {
/* background: lightblue; */
}
</style>
child.vue
<template>
<div class="child">
<div class="inputDiv">
text
<input #change="update" v-model="deText" type="text" name="deText" />
</div>
<div class="inputDiv">
nummer
<input v-model="hetNummer" type="number" name="hetNummer" />
</div>
<button #click="toonState">Toon huidige state</button>
</div>
</template>
<script>
import {store} from '../main';
export default {
name: "child",
store,
data: function() {
return {
'hetNummer': 0
}
},
methods: {
update(e) {
let newState = this.$store.state;
newState.textProp = e.target.value;
// this.$store.commit('setState', newState);
},
toonState()
{
console.log( this.$store.getters.getAll());
}
},
computed: {
deText: function() {
return '';
// return this.$store.getters.getAll().textProp;
}
}
};
</script>
<style>
.inputDiv {
float: right;
margin-bottom: 10px;
}
.child {
max-width: 300px;
height: 30px;
margin-bottom: 20px;
/* background: yellow; */
margin: 10px;
}
</style>
You have a misconception about JavaScript unrelated to Vue/Vuex. This doesn't do what you expect:
state = newState;
Solution (TL;DR)
setState(state, newState) {
Object.assign(state, newState);
}
Instead of setting the state variable, merge the new properties in.
Explanation
Object variables in JavaScript are references. That's why if you have multiple variables referring to the same object, and you change a property on one, they all mutate. They're all just referring to the same object in memory, they're not clones.
The state variable above starts as a reference to Vuex's state object, which you know. Therefore, when you change properties of it, you mutate Vuex's state properties too. That's all good.
But when you change the whole variable-- not just a property-- to something else, it does not mutate the original referred object (i.e. Vuex's state). It just breaks the reference link and creates a new one to the newState object. So Vuex state doesn't change at all. Here's a simpler demo.
Opinion
Avoid this pattern and create an object property on state instead. Then you can just do state.obj = newState.
You should use a spread operator ... to mutate your state as follows :
state = { ...state, ...newState };
LIVE EXAMPLE
but I recommend to make your store more organized in semantic way, each property in your state should have its own setter and action, the getters are the equivalent of computed properties in options API they could be based on multiple state properties.
export const store = createStore({
state: {
count: 1
},
mutations: {
SET_COUNT(state, _count) {
console.log("setState");
state.count=_count
},
INC_COUNT(state) {
state.count++
}
},
getters: {
doubleCount: (state) => () => {
return state.count*2;
}
}
});
**Note : ** no need to import the store from main.js in each child because it's available using this.$store in options api, but if you're working with composition api you could use useStore as follows :
import {useStore} from 'vuex'
setup(){
const store=useStore()// store instead of `$store`
}

Nuxt.js infinite-loading triggered immediately after page load

I'm using Nuxt.js with Infinite loading in order to serve more list articles as the users scrolls down the page. I've placed the infinite-loading plugin at the bottom of my list of articles (which lists, from the very beginning, at least 10 articles, so we have to scroll down a lot before reaching the end of the initial list).
The problem is that as soon as I open the page (without scrolling the page), the infiniteScroll method is triggered immediately and more articles are loaded in the list (I'm debugging printing in the console "I've been called").
I don't understand why this happens.
<template>
<main class="mdl-layout__content mdl-color--grey-50">
<subheader :feedwebsites="feedwebsites" />
<div class="mdl-grid demo-content">
<transition-group name="fade" tag="div" appear>
<feedarticle
v-for="feedArticle in feedArticles"
:key="feedArticle.id"
:feedarticle="feedArticle"
#delete-article="updateArticle"
#read-article="updateArticle"
#write-article="updateArticle"
#read-later-article="updateArticle"
></feedarticle>
</transition-group>
</div>
<infinite-loading spinner="circles" #infinite="infiniteScroll">
<div slot="no-more"></div>
<div slot="no-results"></div
></infinite-loading>
</main>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import subheader from '~/components/subheader.vue'
import feedarticle from '~/components/feedarticle.vue'
export default {
components: {
subheader,
feedarticle,
},
props: {
feedwebsites: {
type: Array,
default() {
return []
},
},
},
computed: {
...mapState({
feedArticles: (state) => state.feedreader.feedarticles,
}),
...mapGetters({
getInfiniteEnd: 'feedreader/getInfiniteEnd',
}),
},
methods: {
async updateArticle(id, status) {
try {
const payload = { id, status }
await this.$store.dispatch('feedreader/updateFeedArticle', payload)
} catch (e) {
window.console.log('Problem with uploading post')
}
},
infiniteScroll($state) {
window.console.log('I've been called')
setTimeout(() => {
this.$store.dispatch('feedreader/increasePagination')
try {
this.$store.dispatch('feedreader/fetchFeedArticles')
if (this.getInfiniteEnd === false) $state.loaded()
else $state.complete()
} catch (e) {
window.console.log('Error ' + e)
}
}, 500)
},
},
}
</script>
<style scoped>
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease-out;
}
</style>
Put <infinite-loading> in <client-only> tag :
<client-only>
<infinite-loading></infinite-loading>
</client-only>

How to clean localeStorage only after closing the browser (VueJs)?

The question was: "How to clean localeStorage only after closing the browser (not closing the browser tab and reloading the page). And this must be implemented in the Vue component.
Found the following solution (Clear localStorage on tab/browser close but not on refresh):
window.onbeforeunload = function (e) {
window.onunload = function () {
window.localStorage.isMySessionActive = "false";
}
return undefined;
};
window.onload = function () {
window.localStorage.isMySessionActive = "true";
};
But I do not understand how to properly and where to call these listeners.
You can simply register your listeners in your main.js file or in your App.vue file
main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
new Vue({
beforeMount() {
window.addEventListener("load", this.onLoad);
window.addEventListener("beforeunload", this.onUnload);
},
beforeDestroy() {
window.removeEventListener("load", this.onLoad);
window.removeEventListener("beforeunload", this.onUnload);
},
methods: {
onLoad(event) {
window.localStorage.setItem("isMySessionActive", true);
},
onUnload(event) {
window.localStorage.setItem("isMySessionActive", false);
}
},
render: (h) => h(App)
}).$mount("#app");
App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" width="25%">
<HelloWorld msg="Hello World!"/>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: {
HelloWorld
},
beforeMount() {
window.addEventListener("load", this.onLoad);
window.addEventListener("beforeunload", this.onUnload);
},
beforeDestroy() {
window.removeEventListener("load", this.onLoad);
window.removeEventListener("beforeunload", this.onUnload);
},
methods: {
onLoad(event) {
window.localStorage.setItem("isMySessionActive", true);
},
onUnload(event) {
window.localStorage.setItem("isMySessionActive", false);
}
}
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>

Error message when trying to pass data from component to page in Nuxt

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>