Get v-slot's value - vue.js

I got an component like this:
<template>
<Popover v-slot="{ open }" :ref="`${name}-parent`">
<div>
<PopoverButton :ref="name">
<div>
<slot name="buttoncontent"></slot>
</div>
<ChevronDownIcon/>
</PopoverButton>
</div>
<transition>
<PopoverPanel>
<slot name="popovercontent"></slot>
</PopoverPanel>
</transition>
</Popover>
</template>
<script>
import {Popover, PopoverButton, PopoverPanel} from '#headlessui/vue'
import {ChevronDownIcon} from '#heroicons/vue/solid'
export default {
name: 'PopoverMenu',
props: {
name: {
type: String,
},
title: {
type: String,
default: ''
},
},
components: {
Popover,
PopoverButton,
PopoverPanel,
ChevronDownIcon,
},
setup () {
return {}
},
watch: {
'$route' () {
// this.$refs[this.name] ... do fancy stuff on route change here
},
},
mounted () {
console.log(this.$refs[`${this.name}-parent`])
}
}
</script>
Now I'd like to change the open state depending on the change of the route. Ergo: If the user clicks a link the popover should close.
The Popover, PopoverButton and PopoverPanel are provided by headlessui and only offer the open slot just within the component. My idea was to access the open property and change it manually.

My idea was to access the open property
Accessing the open property is easy, in a way you already have it in your template. If you want to hold on to it (e.g. keep a reference to open so that you can use it some time later), you can convert your slot content into a component and receive open as the props.
and change it manually.
However, this is prohibited. The moment you try to mutate the open you will get a warning saying it is readonly. In general, properties are always readonly, this is enforced by vue. Scoped slots are just anonymous components very similar to lambda functions, and the open variable is just one of the properties for the slot.
Ideally the library you are using should expose not just an open state but also two more methods (open() and close()) to the slot. Unfortunately, not all libraries are built that thoughtful.
You can try to move the focus to some other elements when your route changes and see if that can close the popover. If not, you can manually implement the vue wrapper for popover. This is something I would do btw, for the simple use cases (i.e. pop some panel), it is trivial to implement using popover.

Related

What's the most idomatic Vue way of handling this higher-order component?

I have a VueJS organization and architecture question. I have a bunch of pages that serve as CRUD pages for individual objects in my system. They share a lot of code . I've abstracted this away in a shared component but I don't love what I did and I'm looking for advice on the idiomatic Vue way to do this.
I'm trying to create a higher order component that accepts two arguments:
An edit component that is the editable view of each object. So you can think of it as a stand in for a text input with a v-model accept that it has tons of different inputs.
A list component which displays a list of all the objects for that type in the system. It controls navigation to edit that object.
Normally this would be simply something where I use slots and invoke this component in the view page for each CRUD object. So basically I'd have something like this:
<!-- this file is src/views/DogsCRUDPage.vue -->
<template>
<general-crud-component
name="dogs"
backendurl="/dogs/"
>
<template slot="list">
<dogs-list-component />
</template>
<template slot="edit">
<dogs-edit-field v-model="... oops .." />
</template>
</general-crud-component>
</template>
<script>
export default {
name: "DogCRUDPage",
components: {
GeneralCrudComponent,
DogListComponent,
DogEditField,
},
}
</script>
This is nice because it matches the general syntax of all of my other VueJS pages and how I pass props and things to shared code. However, the problem is that GeneralCRUDComponent handles all of the mechanisms for checking if an object is edited, and therefor hiding or unhiding the save button, etc. Therefor it has the editable object in its data which will become the v-model for DogsEditField or any other that's passed to it. So it needs to pass this component a prop. So what I've done this:
// This file is src/utils/crud.js
import Vue from "vue"
const crudView = (listComponent, editComponent) => {
return Vue.component('CrudView', {
template: `
<v-row>
<list-component />
<v-form>
<edit-component v-model="obj" />
</v-form>
</v-row>
`,
components: {
ListComponent: listComponent,
EditComponent: editComponent,
},
data() {
return {
obj: {},
}
},
})
}
export default crudView
This file has a ton of shared code not shown that is doing the nuts and bolts of editing, undo, saving, etc.
And then in my src/router/index.js
//import DogCRUDPage from "#/views/libs/DogCRUDPage"
import crudView from "#/utils/crud"
import DogListComponent from "#/components/DogListComponent"
import DogEditField from "#/components/design/DogEditField"
const DogCRUDPage = crudView(DesignBasisList, DesignBasis)
Vue.use(VueRouter);
export default new VueRouter({
routes: [
{
path: "/dog",
name: "dog",
component: DogCRUDPage,
},
})
This is working, but there are issues I don't love about it. For one, I needed to enable runtimecompiler for my project which increases the size of the payload to the browser. I need to import the list and edit components to my router instead of just the page for every single object I have a page for. The syntax for this new shared component is totally different from the template syntax all the other pages use. It puts all of my page creation into the router/index.js file instead of just layed out as files in src/views which I am used to in Vue.
What is the idiomatic way to accomplish this? Am I on the right track here? I'm happy to do this, it's working, if this really is how we do this in Vue. But I would love to explore alternatives if the Vue community does something differently. I guess I'm mostly looking for the idiomatic Vue way to accomplish this. Thanks a bunch.
How about this:
DogsPage.vue
<template>
<CrudView
:editComponent="DogsEdit"
:listComponent="DogsList"
></CrudView>
</template>
<script>
import DogsEdit from '#/components/DogsEdit.vue'
import DogsList from '#/components/DogsList.vue'
import CrudView from '#/components/CrudView.vue'
export default {
components: { CrudView },
data() {
return { DogsEdit, DogsList }
}
}
</script>
CrudView.vue
<template>
<div>
<component :is="listComponent"></component>
<component :is="editComponent" v-model="obj"></component>
</div>
</template>
<script>
export default {
props: {
editComponent: Object,
listComponent: Object
},
data() {
return {
obj: {}
}
}
}
</script>

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

Vue prop watcher triggered unexpectedly if the prop is assigned to an object directly in template

Here is the code:
<template>
<div>
<foo :setting="{ value: 'hello' }" />
<button #click="() => (clicked++)">{{ clicked }}</button>
</div>
</template>
<script>
import Vue from 'vue'
// dummy component with dummy prop
Vue.component('foo', {
props: {
setting: {
type: Object,
default: () => ({}),
},
},
watch: {
setting() {
console.log('watch:setting')
},
},
render(createElement) {
return createElement(
'div',
['Nothing']
)
},
})
export default {
data() {
return {
clicked: 0,
}
},
}
</script>
I also made a codepen: https://codepen.io/clinicion-lin/pen/LYNJwJP
The thing is: every time I click the button, the watcher for setting prop in foo component would trigger, I guess the cause is that the content in :settings was re-evaluated during re-render, but I am not sure.
This behavior itself does not cause any problem but it might cause unwanted updates or even bugs if not paid enough attention (actually I just made one, that's why I come to ask :D). The issue can be easily resolved by using :setting="settingValue", but I am wondering if there are alternative solutions, OR best practices for it. I saw some code are assigning object in template directly too, and it feels natural and convenient.
Thanks for anyone who can give an explanation or hint.
First, the docs: "every time the parent component is updated, all props in the child component will be refreshed with the latest value"
By using v-bind:propA="XX" in the template, you are telling Vue "XX is JavaScript expression and I want to use it as value of propA"
So if you use { value: 'hello' } expression, which is literally "create new object" expression, is it really that surprising new object is created (and in your case watcher executed) every time parent is re-rendered ?
To better understand Vue, it really helps to remember that everything in the template is always compiled into plain JavaScript and use the tool as vue-compiler-online to take a look at the output of Vue compiler.
For example template like this:
<foo :settings="{ value: 'hello' }" />
is compiled into this:
function render() {
with(this) {
return _c('foo', {
attrs: {
"settings": {
value: 'hello'
}
}
})
}
}
Template like this:
<foo :settings="dataMemberOrComputed" />
is compiled into this:
function render() {
with(this) {
return _c('foo', {
attrs: {
"settings": dataMemberOrComputed
}
})
}
}
It's suddenly very clear new object is created every time in the first example, and same reference to an object (same or different ...depends on the logic) is used in second example.
I saw some code are assigning object in template directly too, and it feels natural and convenient.
I'm still a Vue newbie but I browse source code of "big" libraries like Vuetify from time to time to learn something new and I don't think passing "new object" expression into prop is common pattern (how that would be useful?).
What is very common is <foo v-bind="{ propA: propAvalue, propB: propBValue }"> - Passing the Properties of an Object which is a very different thing...

Get imported components programmatically

I would like to be able to access the root components objects in a component. For instance, In the following component, I import and register two child components, now how do I get the components property in the mounted hook?
<template>
<div>
<SomeComponent />
<AnotherComponent />
</div>
</template>
<script>
import SomeComponent from '#/components/home/SomeComponent';
import AnotherComponent from '#/components/home/AnotherComponent';
export default {
components: { SomeComponent, AnotherComponent }, // I need to access this from the mounted hook
mounted() {
console.log('registered components:', Object.keys(componentsObject))
}
}
</script>
I have tried this.components, but that returns undefined, and then I tried just logging this to see what's available, but there's no components property, or anything that resembles what I'm after, so I dunno if there's some other way I could access it?
UPDATE: The reason that I want to do this is that I'm essentially creating a component slideshow, so I want to do Object.keys(components).length to let me programmatically determine the amount of slides, without the need for a amount variable that I have to manually update every time I create another slide.
There is a better approach to this problem. You may insert a property called slidesData in your data() method in your parent component.
data() {
slidesData: [
{ id: 1, title: 'Sample Slide title', description: 'Lorem ipsum', ... },
{ id: 2, title: 'Sample Slide 2 title', description: 'Dolor sit amet', ... },
]
}
and with this sample data, you can create a reusable component called SlideComponent. You can then "v-for" the data and pass it to the SlideComponent and there, you can format the data to your liking.
Going back to your problem, with this approach, you don't need to access the "Slide" component itself, you just have to access the slideData instead. This is a "Vue way" to solve the problem.

Good practice for re-usable form component in Vue

I'm trying to write a re-usable form component for a "user". I'd like to be able to use it in both an "edit" and a "create" flow. I'd pass it a user object, it would validate as a user enters/modifies the user data and it would update the user object. The parent component (e.g EditUser, CreateUser) will do the actual saving.
How should I do this without mutating props? I've avoided using events so far because I'd need to fire one on every user input, passing a copy of the user object back up to the parent (I think).
EDIT: adding some (non-working) code to demonstrate. I can't share my exact code.
Parent component
<template>
<div >
<h1>header</h1>
<MyFormComponent
v-model="user"
>
</MyFormComponent>
<button>Save</button>
</div>
</template>
<script>
export default {
data(){
return {
user: {
name: 'User 1'
}
}
}
}
</script>
Form component
<template>
<form>
<MyFormInputComponent
v-model="user.name"
></MyFormInputComponent>
</form>
</template>
<script>
export default {
props: ['user'],
model: {
prop: 'user'
}
}
</script>
Thanks!
I don't know exactly your context, but this is how I use to do:
First, you don't need both components Parent and Child. You can do all you want inside Form Component.
To deal with the differences between create and edit modes, an option is computed property based on current route (if they are different according to create/edit operations).
Using this property, you decide if data will be fetched from API, if delete button will has shown, the title of the page and so on.
Here is an example:
async created() {
if (this.isEditMode) {
// fetch form data from API according to User ID and copy to a local form
},
},
computed: {
formTitle() {
return (this.isEditMode ? 'Update' : 'Create') + ' User';
},
}