How to listen for a page $emit in a nuxt layout? - vue.js

Nuxt 2.15.6; I want to switch the layout of my menu component by dynamically switching menu components in my root layout.
default.vue
<template>
<component :is="navLayout"></component>
<Nuxt :navLayout="navLayout = $event" />
</template>
data() {
return {
navLayout: "default"
};
},
In the "child" components of , my pages eg. login.vue (/login) I $emit an event;
...
import nav2 from "#/layouts/nav2";
...
created() {
this.$emit("navLayout", nav2);
},
Now it seems to be the <Nuxt> component is not able to catch the event. I also tried calling a <Nuxt #navLayout="test()" /> method.
How can I avoid this.$root.$emit(...); in my login.vue and
this.$root.$on("navLayout", navLayout => {
this.navLayout = navLayout;
});
in default.vue?

EDIT: This answer works fine as it looks like you cannot do it right now: https://github.com/nuxt/nuxt.js/issues/8122#issuecomment-709443008
In the child component
<button #click="$nuxt.$emit('eventName', 'nice payload')">nice</button>
On the default layout
<script>
export default {
created() {
this.$nuxt.$on('eventName', ($event) => this.test($event))
},
methods: {
test(e) {
console.log('test ok >>', e)
},
},
}
</script>
Putting a listener on Nuxt itself does not work.
<Nuxt #navLayout="navLayout = $event" :navLayout="navLayout" />
I can see the event go and the listener plugged to <nuxt></nuxt> but it does not trigger any method with the listener...
PS: works for <nuxt-child></nuxt-child> at least.

Maybe I don't understand your question correctly, but it seems to me that you are trying to do yourself what layouts are meant to do. This would mean that you would create a layout with the menu component for login, a default layout etc. like this:
login.vue
<template>
<LoginMenu>
<Nuxt/>
</template>
default.vue
<template>
<DefaultMenu>
<Nuxt/>
</template>
On your page you would do:
export default {
layout: login
}
And then that would load the layout with the login menu component. On all other pages it would load the default menu.
More info here: https://nuxtjs.org/examples/layouts/

Related

Paginated async Component doesn't trigger setup() on route change

I have a paginated component. The async setup() method is pulling data from an API to populate the page. It works fine when the route is directly loaded, but when I change the route to a different page slug (eg. clicking a router-link), the component is not reloaded and setup is not executed again to fetch the new data.
I guess I somehow want to force reloading the component?
This is my MainApp component it has the router view and fallback.
<router-view v-slot="{ Component }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
loading...
</template>
</Suspense>
</router-view>
The router looks kinda like that. You see the page component takes a page_slug:
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "",
component: MainApp,
children: [
{
name: "page",
path: "page/:page_slug",
component: Page,
props: true,
},
// [...]
]
}
And this is how my Page component looks like. It uses the page_slug to load data from an API which is then used in the template:
<template>
<div> {{ pageData }} </div>
</template>
export default defineComponent({
name: "Page",
props: {
page_slug: {
type: String,
required: true,
},
},
async setup(props) {
const pageData = await store.dispatch("getPageData", {
page_slug: props.page_slug
});
return { pageData }
}
}
When I directly open the route, the fallback "loading..." is nicely shown until the data is returned and the component is rendered.
But when I do a route change to another page, then async setup() is not executed again. In that case the url in the browser updates, but the data just remains the same.
How can I solve this case? Do I have to force reload the component somehow? Or have an entirely different architecture to the data loading?
The answer is simple, when trying to create Vue 3 Single File Components (SFCs) in Composition API way as shown below:
<template>
<!-- Your HTML code-->
</template>
<script>
export default {
name: 'ComponentName',
async setup():{
// Your code
}
};
</script>
<style>
/*Your Style Code*/
</style>
<script>, will only executes once when the component is first imported. So, when the data have changed by other component, the component above will not updated or in other words not re-created.
To make your component re-created whenever it about to mount, you have to use <script setup> which will make sure the code inside will execute every time an instance of the component is created, but you need to re-write your script code with few changes in comparison when using setup() method, and also you are able to use both of scripts like this:
<script>
// normal <script>, executed in module scope (only once)
runSideEffectOnce()
// declare additional options
export default {
name: "ComponentName",
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// executed in setup() scope (for each instance)
</script>
Read this documentation carefully to have full idea.

Why does a keydown event get broadcasted to a newly rendered component?

I want to conditionally render a component based on the user's keypress.
Once the new component is rendered, an input field should get focused.
For some reason, Vue broadcasts the keypress I use to render the component to the new component! The result is that the key I pressed to render the component gets displayed in the input field!
How is that even possible? The keypress triggers the mounting of the new component and the focusing of the input only happens after it is mounted.
Minimum working example:
// App.vue
<template>
<div>
<p>App</p>
<Hello v-if="view == 'hello'" />
</div>
</template>
<script>
import Hello from "./components/Hello.vue";
export default {
components: {
Hello
},
data() {
return {
view: null
};
},
methods: {
changeView() {
this.view = "hello";
}
},
created() {
document.addEventListener("keydown", this.changeView);
}
};
</script>
// Hello.vue
<template>
<div>
<p>Hello</p>
<input ref="input" type="text" />
</div>
</template>
<script>
export default {
mounted() {
this.$refs.input.focus();
}
};
</script>
This is because the events are flowing like this:
keydown renders the new component -> focus moved to new component -> keyup fires in new component (where is focus now) and character stays there.
Change the trigger to keyup and it should work.

How to dynamically mount vue component with props

Scenario / context
I have an overview component which contains a table and an add button. The add button opens a modal component. When i fill in some text fields in the modal and click the save button, a callback (given as prop) is called so the parent component (the overview) is updated. The save button also triggers the model toggle function so the model closes.
So far works everything like expected but when i want to add a second entry, the modal is "pre-filled" with the data of the recently added item.
Its clear to me that this happens because the model component keeps mounted in the background (so its just hidden). I could solve this by "reset" the modals data when the toggle function is triggered but i think there should be a better way.
I have a similar issue when i want to fetch data in a modal. Currently i call the fetch function in the mounted hook of the modal. So in this case the fetch happens when the parent component mounts the modal. This does not make sense as it should only (and each time) fetch when the modal is opened.
I think the nicest way to solve this is to mount the modal component dynamically when i click the "add" (open modal) button but i can't find how i can achieve this. This also avoids that a lot of components are mounted in the background which are possibly not used.
Screenshot
Example code
Overview:
<template>
<div>
// mount of my modal component
<example-modal
:toggleConstant = modalToggleUuid
:submitHandler = submitHandler />
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
someList: [],
}
},
mounted() {
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Modal:
<template>
<div>
<input v-model="item.type">
<input v-model="item.name">
<input v-model="item.location">
</div>
</template>
<script>
export default {
data() {
return {
modalToggleUuid: someUuid,
item: {},
}
},
mounted() {
// in some cases i fetch something here. The data should be fetched each time the modal is opened
},
methods: {
showModal: function() {
EventBus.$emit(this.modalToggleUuid);
},
submitHandler: function(item) {
this.someList.push(item);
}
}
}
</script>
Question
What is the best practive to deal with the above described scenario?
Should i mount the modal component dynamically?
Do i mount the component correctly and should i reset the content all the time?
You are on the right way and in order to achieve what you want, you can approach this issue with v-if solution like this - then mounted() hook will run every time when you toggle modal and it also will not be present in DOM when you are not using it.
<template>
<div>
// mount of my modal component
<example-modal
v-if="isShowModal"
:toggleConstant="modalToggleUuid"
:submitHandler="submitHandler"
/>
// The overview component HTML is here
</div>
</template>
<script>
export default {
data() {
return {
isShowModal: false,
modalToggleUuid: someUuid,
someList: []
};
},
mounted() {},
methods: {
showModal: function() {
this.isShowModal = true;
},
submitHandler: function(item) {
this.someList.push(item);
this.isShowModal = false;
}
}
};
</script>

Component Declaration And Communication

I have added a component declaration to the default main.js file which is generated during the Webpack project creation process as
import Modal from '#/components/Modal'
Vue.component('modal', Modal)
And in the App.vue, I have
<modal v-show="showModal"></modal>
<button id="show-modal" v-on:click="showModal = true">Click to have a modal</button>
And they work fine. Now, I need to setup a "props down, events up" communication channel between the parent and a child. To do so, I need to add a property, called 'isActive', the Modal component so that the root component can send a message to the child component, that is
<modal isActive="showModal"></modal>
<button id="show-modal" v-on:click="showModal = true">Click to have a modal</button>
I guess the component declaration should be something like:
Vue.component('modal', {
props: ['isActive'],
Modal
})
It doesn't work, however, due to
Failed to mount component: template or render function not defined.
I have tried different variants without a luck.
My second question is that how a child event changes its parent data. For example, in the child component
<button class="modal-close is-large" v-on:click="closeModal"></button>
the closeModal event is handled in the following javacript code in the child component.
export default {
method: {
closeModal: function(event) {
...
}
}
}
How can I set its parent data showModal to false?
Update:
The code segment of Modal:
<template>
<div class="signin">
<div class="modal" v-bind:class="{ 'is-active': isActive }">
...
</div>
<button class="modal-close is-large" v-on:click="isActive = false"></button>
</div>
</div>
</template>
<script>
import axios from 'axios'
import _ from 'lodash'
import Notification from '#/components/Notification'
import { required, email } from 'vuelidate/lib/validators'
export default {
name: 'signin',
components: {
Notification
},
data: () => ({
isActive: true,
email: '',
...
}),
...
}
</script>
Bulma is used for styling. And the isActive is defined in the Modal. I think it needs to be changed to achieve "props down".
As it looks, your file /components/Modal contains a full definition of a component: the template, and the script parts for it. So you can just bind the component to the tag-name you want to use in your markup:
import Modal from '#/components/Modal'
Vue.component('modal', Modal)
This is basically what you had in the beginning. To pass properties to this component, add the props-line directly to your component, that is into /components/Modal:
...
export default {
name: 'signin',
components: {
Notification
},
props: ['isActive'],
data: () => ({
...
As for the second question, how to communicate back to the parent, have a look at Vue's Custom Events. Basically, your Modal component could issue a "close"-event like this:
methods: {
closeModal: function(event) {
this.$emit('modalClose')
}
}
and when you use the component, you could listen to it like this:
<modal v-bind:isActive="showModal" v-on:modalClose="showModal = false"></modal>
Note that you should use v-bind for providing the value to isActive. If you don't use v-bind, the value is just passed once when the component is created. This means, the component would never see a change to this prop when it is changed by the parent. By using v-bind, changes by the parent to this attribute are pushed down to the child-component, so the Modal actually sees the updated value and can react to it.

How to set keyup on whole page in Vue.js

Is it possible to set a v-on:keyup.enter on the whole page, not only for an input element in javascript framework Vue.js ?
Perhaps a better way to do this is with a Vue component. This would allow you to control when you listen to events by including or not including the component. Then you could attach event listeners to Nuxt using the no-ssr component.
Here is how you create the component:
<template>
<div></div>
</template>
<script>
export default {
created() {
const component = this;
this.handler = function (e) {
component.$emit('keyup', e);
}
window.addEventListener('keyup', this.handler);
},
beforeDestroy() {
window.removeEventListener('keyup', this.handler);
}
}
</script>
<style lang="stylus" scoped>
div {
display: none;
}
</style>
Then on the page you want to use that component you'd add this HTML:
<keyboard-events v-on:keyup="keyboardEvent"></keyboard-events>
And then you'll have to add your event handler method:
methods: {
keyboardEvent (e) {
if (e.which === 13) {
// run your code
}
}
}
Short answer is yes, but how depends on your context. If you are using vue-router as I am on a current project, you would want to add to the outer-most element you want that applied to. In my case I'm using the actual app.vue entry point's initial div element.
There is one catch that I believe is a hard requirement, the element has to be within the potentially focusable elements. The way I'm dealing with that is setting a -1 tabindex and just declaring my super-hotkeys (mostly for debug purposes right now) on the parent element in my app.
<template>
<div
id="app-main"
tabindex="-1"
#keyup.enter="doSomething"
>
<everything-else></everything-else>
</div>
</template>
EDIT:
As a side note, I also added a touch of additional configuration to my vue-router to make sure the right element is focused when I transition pages. This allows the pageup/pagedown scrolling to already be in the right section based on the content area being the only scrollable section. You'd also have to add the tabindex="-1" to the app-content element as well.
router.afterEach(function (transition) {
document.getElementById('app-content').focus();
});
and the basis of my app-content component:
<template>
<div id="app-content" tabindex="-1">
<router-view
id="app-view"
transition="app-view__transition"
transition-mode="out-in"
></router-view>
</div>
</template>
I created a small npm module that takes care of global keypress events in Vue, hope it makes someone's life easier:
https://github.com/lupas/vue-keypress
My simplest approach:
Add into your root Vue component (or any other component):
new Vue({
//...
created() {
window.addEventListener('keypress', this.onKeyPress);
},
beforeDestroy() {
window.removeEventListener('keypress', this.onKeyPress);
},
methods: {
onKeyPress(e) {
console.log('KEYPRESS EVENT', e)
//... your code
}
}
//...
})
In Vue 3 composition API, you can do it with a composable:
import { onMounted, onUnmounted } from "vue";
export function useKeyupEvent(handler) {
onMounted(() => document.addEventListener("keyup", handler));
onUnmounted(() => document.removeEventListener("keyup", handler));
}
and then in your component setup:
useKeyupEvent( event => console.log(event))