_vm.$refs.menu.open is not a function - vue.js

I'm using vue-context to modify the default context menu, which I call from a component but when I try to interact with it I get an error, here's my code
<!-- Main -->
<p #contextmenu.prevent="$refs.menu.open">test</p>
<Context reference="menu" />
<script>
import Context from './map/context.vue'
export default { components: { Context } }
</script>
<!-- Component -->
<vue-context :ref="reference" :close-on-click="true" :close-on-scroll="true">
<li>
<a>
Do something
</a>
</li>
<li>
<a>
Do something else
</a>
</li>
</vue-context>
<script>
import VueContext from 'vue-context'
import 'vue-context/dist/css/vue-context.css'
export default {
props: ['reference'],
components: {
VueContext
}
}
</script>
When I right click on the page, I get the error _vm.$refs.menu.open is not a function

Because you wrapped VueContext inside a separate vue instance, the outer Vue instance (the one containing the #contextmenu call) does not have a $refs.menu. You can access the child's $refs by setting a ref on the child itself:
<p #contextmenu.prevent="$refs.wrapper.$refs.menu.open">test</p>
<Context ref="wrapper" reference="menu" />
See it working here.
I'd also argue you shouldn't pass the string 'menu' from parent but specify it directly inside <Context>'s template.
Dynamic props only make sense when you have some benefit from them changing value, which is clearly not the case here. You need that child ref to always be 'menu' so you can access its methods:
<p #contextmenu.prevent="$refs.wrapper.$refs.menu.open">test</p>
<Context ref="wrapper" />
Context.vue:
<template>
<vue-context ref="menu" :close-on-click="true" :close-on-scroll="true">
<li><a>Do something</a></li>
<li><a>Do something else</a></li>
</vue-context>
</template>
<script>
import VueContext from "vue-context";
import "vue-context/dist/css/vue-context.css";
export default { components: { VueContext } };
</script>
To summarize: $refs is a unified mechanism allowing you to access template elements, whether they're DOM elements or Vue instances.
Each component only contains its own $refs. To access the $refs of one of its children, you have to give the child a ref in parent scope and use $refs on that particular reference.
This actually makes a lot of sense in a scenario where you want multiple context menus with different contents for different items in your parent component (although you're probably better off simply passing down the menu items and their actions dynamically to a single context menu instance).

Related

VueJS 3: Access root HTML element in a slot

how do I reliably access the root HTML element in a slot? I tried slots.default()[0].el but its not consistent. I realized if the slot is a simple html, it is not null, which is great, but if it has some vue directives or components, it will be null. So how can I reliably get hold of the root HTML Element in a slot?
I found one possible solution: that is to have the slot content provider to explicitly set the element you want to reference to by providing a slot-prop method to invoke. And also since Vue 3 template supports multiple root elements, its not really reliable to assume that the slot will always have one root element.
Here is the example of the solution:
<template>
<slot :set-element="(el) => element = el"></slot>
</template>
<script setup lang="ts">
import { ref, watchEffect } from "vue";
const element = ref<Element | null>(null);
watchEffect(() => {
console.log(element.value);
});
</script>
In the usage of the component, inject the slot props and use the Function Refs syntax
<MyComponent v-slot="{ setElement }">
<div :ref="(el) => setElement(el)">...</div>
<div>...</div>
</MyComponent>
You could access a slot's root HTML element directly with the slot's vnode property and in your component script, you can using this.$refs.root.
Here is an example:
<template v-slot:[slotName]="{ vnode }">
<div ref="root">
<!-- your slot content goes here -->
</div>
</template>
mounted() {
const root = this.$refs.root as HTMLElement;
console.log(root);
}
EDIT
The official documentation for v-slot can be found here: https://vuejs.org/api/built-in-directives.html#v-slot
Instead, the official documentation for $refs can be found here: https://v3.vuejs.org/guide/composition-api-template-refs.html

How to change the language of whole application from the dropdown in Navbar component in vue

I created a language selection dropdown in my Navbar component. So here is my navbar component:
<div>
<h6>{{ translate("welcomeMsg")}} </h6>
<select name="lang" v-model="lang">
<option value="en">English</option>
<option value="de">Deutsch</option>
</select>
</div>
<script>
export default {
mixins: [en, de],
data() {
return {
lang: "en",
};
},
methods: {
translate(prop) {
return this[this.lang][prop];
}
}
}
</script>
So the parent of this component is an Index.vue which is main component in my application.
<div id="app">
<Topnav/>
<Navbar/>
<router-view></router-view>
<Footer/>
</div>
Currently, I am able to change the language in my Navbar component. So according to the selected value in the dropdown in Navbar component, welcomeMsg is changing. What I am trying to do is I want to put this pieve of code to TopBar "{{ translate("welcomeMsg")}} ", and according to the value of the dropdown in Navbar component, I want to change this value.
Can you help me with this or can you give me an idea how to do it?
If I understand you correctly, you want to use translate method inside Topnav component.
This method is however defined in Navbar, so it's not accessible in Topnav.
To use it elsewhere you could create a mixin with this method to import it to any component. I don't recommend this solution though as mixins are making the code messy.
Another solution is to create a component with translate method defined inside. Let this component do just that: translate a message passed by prop and render it inside some div:
<div>
{{ translatedMessage }}
</div>
<script>
mixins: [en, de],
props: {
message: {
type: String,
default: ''
},
language: {
type: String,
default: 'en'
}
},
computed: {
translatedMessage() {
return this[this.language][this.message];
}
}
</script>
You can reuse this component anywhere in the application. You would still need to pass a language prop somehow, possibly the solution would be to use vuex store to do this, since language is probably global for entire application.
For easier and more robust solutions I would use vue-i18n, which #Abregre has already suggested in his comment: https://stackoverflow.com/a/70694821/9463070
If you want a quick solution for a full-scale application and you don't have a problem with dependencies, you could try to use vue-i18n.
It's a plugin for vue that does exactly this for multi-locale websites/apps in Vue.
https://www.npmjs.com/package/vue-i18n
EDIT
Then in order to use it globally in your app, you should use vuex.
Keep the language selection state there and then wherever you want to use it, you make a computed function with the state.language getter.
The translate function should be a global registered filter
https://v2.vuejs.org/v2/guide/filters.html

How to change data in default layout from nested components in Nuxt.js

I have layout default.vue:
<template>
<div>
<Header></Header>
<div class="body-wrapper">
<Nuxt :numberThird="numberThird" />
</div>
<Footer></Footer>
</div>
</template>
<script>
export default {
data() {
return {
numberThird: 3
};
},
};
</script>
Here I am trying to pass prop numberThird.
I want to be able to change this value in the future through components that are deeply nested.
But there is a problem: my pages don't accept this prop (numberThird), they treat it as $parent.$attr.
The question is: can I somehow change this value through deeply nested child components?
As stated here: https://stackoverflow.com/a/67817642/8816585
Props and listeners are not the most friendly on either <nuxt> nor <nuxt-link>.
Kinda confirmed here by Alexander too: https://github.com/nuxt/nuxt.js/issues/8669#issuecomment-764006062
You better off using Vuex in this case, especially if you are aiming to something deep. Props drilling is not recommended most of the time.

Vue.js render from child into parent when needed just like router-view

I'm trying to dynamically show a footer/header based on a child view in ionic
I am using the Ionic 4 framework in combination with Vue.js. Tried slots and such having the feeling i'm on the right track but not fully there yet.
I've got a Base.vue (component) which holds
<template>
<ion-app>
<ion-page class="ion-page" main>
<page-header />
<router-view />
<page-footer />
</ion-page>
</ion-app>
</template>
<script>
import PageHeader from '#/components/PageHeader'
import PageFooter from '#/components/PageFooter'
import { mapState } from 'vuex'
export default {
name: 'master',
components: {
PageHeader,
PageFooter,
},
}
</script>
As a child view i've got the following; i know that it's not the right approach to include it inside the <ion-content> but don't know how to set this up in the correct way:
<template>
<ion-content fullscreen>
<page-header>
<ion-toolbar>
<ion-title>
Test
</ion-title>
</ion-toolbar>
</page-header>
<ion-content>
<p>Schedule page</p>
</ion-content>
</ion-content>
</template>
<script>
import PageHeader from '#/components/PageHeader'
export default {
name: 'schedule',
components: {
PageHeader,
},
}
</script>
The header component (which should be dynamic):
<template>
<ion-header>
<slot name="header" />
</ion-header>
</template>
<script>
export default {
name: 'page-header',
}
</script>
What i'm trying to do is making a Base.vue with a dynamic header (PageHeader.vue) so based on a given child view i could change or extend the header if needed.
So I think you're saying you want to change the content of the page header depending on the child.
Components cannot directly affect the templates of other components in the tree. Slots give you some control over this, but it is limited to allowing a component to inject templates into sections of a child component, not the other way around.
Your options are:
Add logic to your parent component which detects what child component is shown and then change the page header accordingly. The page header won't be controlled directly by the child component, though.
Use named views with vue-router.
Use something like portal-vue, but don't go crazy with this kind of power...

Adding props on runtime to Vue component

I try to create a highly dynamic wizard as a component in Vue. It contains out of three components: the Wizard component itself, a Step component and a single form "MyForm" component. The form can be in edit mode or in read only mode depending on the current step of the wizard.
After some trial and error I finally managed to create such a component and it works. One problem that I struggled to solve was to pass the information if the form is in edit mode or not from the Step component to the child form component.
MyForm.vue
<template>
<form>
<div v-if="inEditMode"><i>Form is in edit mode</i></div>
<div v-else><i>Form is in read only mode</i></div>
</form>
</template>
<script>
import Vue from "vue";
export default Vue.extend({
props: ["inEditMode"]
// mixins: [wizardStepMixin],
});
</script>
Wizard.vue
<Step>
<MyForm/>
</Step>
Step.vue
<slot :isInEditMode="true"/>
Passing/setting a prop to a slot like I did above did not work (prop did not change).
My solution to set the prop isInEdit on the MyForm is to call a function prepareSlot in the Step component before mounting and updating the Step.
prepareSlot() {
this.$slots.default.forEach(element => {
if (!element.data) return
element.componentOptions.propsData = {
...element.componentOptions.propsData,
inEditMode: this.stepNr === this.currentStep
}
})
}
You can find the complete project on https://codesandbox.io/embed/mzr10wzk0j.
Is there a better way to archive that? Is it safe to do it that way?