I am trying to build a renderless component in vue 3 and want to pass a ref to the default slot.
When I am using the h render function I can just pass a ref:
return h('div', {ref: someRef}); // works
If I try to do the same with the default slot, it does not work:
return slots.default({ ref: someRef}) // does not work
return slots.default({ someRef}) // also does not work
Is there any way to do this without wrapping the default slot into another div or similar?
Checked already the documentation and other resources, but couldn't find any solution.
Direct answer
Yes return a function from your setup hook!
setup(_, slots) {
const someRef = ref()
return () => slots.default({ ref: someRef })
}
vue3 docs link
vue3 docs for renderless component pattern
Contextual answer for those in the comment section questioning the renderless/headless pattern
Yes, sometimes you just want a renderless (or headless as the kids these days say) wrapper for functionality (logic), without limiting the HTML that consumers can use.
But for that functionality to work, the renderless/headless component still needs to identify some of the HTML that consumers render into the default slot, in order to provide DOM-related functionality for example.
I have the same struggle when using this pattern, and have been relying on a "hack": passing specific data attributes via slot scope, that consumers need to bind to the desired elements (not only root) and then using good old document.querySelector to grab them
I has served me well in vue2 and I've been using it in production with no problems, but I was wondering if with the new dynamic :ref attribute of vue3, we can do it differently: probably passing a reactive ref variable and then doing a simple assign, and apparently it works.
<renderless #default="{ someRef }">
<some-consumer-comp :ref="(el) => someRef.value = el" />
</renderless>
Here's a sandbox demo old way for vue 2
Here's a sandbox demo new way for vue 3
Do note that if you want to handle edge cases, like handling conditional rendering of slot content, you probably need to do a bit more work and add some watchers.
This pattern is a bit out of fashion these days, with the introduction of vue composables since you can abstract the logic into a useSomeFunctionality and use it directly on the component you want, but it's sill a valid one IMO.
Cheers!
Related
I created a vue component based on MapBox, which is restricted in initializations before it costs money and that is perfectly fine. But I want to reduce reinitializations of my map component for their and my sake.
That's why I thought if it is possible to define the component once, pass in some properties and then handle the state via vuex.
Right now, I'd have to import my component and add the data like this:
<Map
:sources="geoData.sources"
:layers="geoData.layers"
:mapOptions="mapOptions"
:componentOptions="{ drawingEnabled: toggleMapDrawing, activeLayers: activeMapLayers, activeMarkerGroups: [] }"
#loaded="onMapLoaded" #selectedMarkers="onSelectedObjects"/>
The componentOptions are being watched, so the component changes its state accordingly.
My ideas/approaches so far were the following:
I thought about adding the snippet above to the root vue file, but that won't help since I want to place the map component dynamically and not statically before the rest of the page content.
Passing a rendered vue component into a variable and appending that later would be a bit too hacky, if it is even possible.
Using slots, but from what I've seen in the docs, it's not possible to use a slotted component from a parent component in a child like this.
The best idea that has come to my mind was to define the actual MapBox variable (which I suppose triggers the API for initialization) and then save that globally using the store or something. But since that will immediately append the component to a DOM element that will be specified in the options, so I'd have to store that somehow, too.
The initialization of the map happens in the mounted hook of the component and looks like this:
const baseOptions = {
accessToken: process.env.MAPBOX_TOKEN,
container: 'map',
style: process.env.MAPBOX_STYLE_URL,
minZoom: 10,
maxZoom: 20,
zoom: 13,
bearing: 150,
pitch: 50
}
this.map = new mapboxgl.Map(Object.assign(baseOptions, this.mapOptions))
if (!this.map) { throw new Error('Could not create map. Make sure the token is valid.') }
I might be wrong, maybe there's a better way or maybe this whole idea might be garbage, but hopefully it's not. Also please note that I'm not using the vue-mapbox module, because it's not being maintained anymore.
I'm thankful for any ideas and hints :)
You may use <KeepAlive>, a built-in component available in both Vue2 (docs) and Vue3 (docs).
Basically it ensures that a component tagged with keep-alive will only be mounted once. So in your case, you can place the map wherever you want, and the Map will only be initialized once in its mounted hook.
If you need to utilize the moment that your Map gets "focused" or "activated" so to say, then you can utilize the activated and deactivated hooks.
Why you cannot use KeepAlive.
There is an obvious and logical limitation. As long as the parent is alive and mounted, the component's children that are being kept-alive will stay alive. But if the keep-alive component's parent gets unmounted, then all the children will be unmounted aswell even if they were being kept alive. This is all very obvious but I just felt like pointing it out.
Solution
So, in your use case, you want a component (the <Map> component) to be globally kept-alive after its first initialization. I suggest you cache the map element itself and store it in the store. Then on every <Map> component onBeforeMount (Composition API) or beforeMount (Options API) hook, manually check if the element is cached, if it is then insert the cached map from the store, otherwise initialize the map.
Let's say there is a global component BIcon.vue available everywhere.
And another component, but regular not global, called BIconFake.vue.
We can override BIcon.vue by BIconFake.vue like that:
<template>
<div>
<b-icon icon="plus"><!-- <- Here is it BIconFake component! -->
</div>
</template>
<script>
import BIcon from '~/components/BIconFake'
export default {
components: {
BIcon // <- BIconFake component inside!
}
}
</script>
By this way, Vue.js will display BIconFake component instead of regular BIcon component.
I tried to pass props, events or attributes and it works like expected.
Vue.js is awesome... and big. Really, I don't know everything about it, and I don't want to see side effects or unexpected behavior when doing this override.
So, I want to know if it's safe to do that? Does it make a mess in Vue.js instance? What about memory?
we can override component with pure vue.js. Also, I made this example for Buefy, but we can do that with any UI frameworks like Quasar, Vuetify...
Thinking globaly, Is it good to override components of UI frameworks? What about security, scalability and maintenability?
In fact, I searched a way to build a plugins or addons system to my Nuxt.js app, like wordpress plugins.
Is it a good architecture to start building my app by overriding vue component? Is there another way to build app addons for vue, by using npm or webpack?
If you are going to wrap existing components like that then you should keep in mind the Liskov substitution principle. <b-icon-fake> can likely be used in place of <b-icon> provided that it:
accepts the same props
emits the same events
exposes the same public methods (if it is used with a ref)
behaves in the same way
Most of those points probably do not apply for a simple <b-icon> component.
Also keep in mind the template of your wrapped component now includes an extra <div> around it. This can interfere with styling and things like that.
You can eliminate the additional memory overhead by using a functional component instead, but you will need to write the render function manually to preserve the behavior of the wrapped component. But honestly I wouldn't worry too much about memory usage unless you have determined it to be an issue (after profiling your app).
In terms of whether it is "good" to do this or not, I can't say. There are advantages and disadvantages. In my opinion, wrapping components is fine as long as you are the only consumer of the wrapper component and doing so doesn't affect any existing usage of the wrapped component outside of your code.
I have component nabber/header that has a props, and I want to put the props in that component and then want to use that props on another props, how to put that props to get that data and transfer it to another component ? because I want to use that props to CRUD in database ? is that possible that we use $root to get that props which we put on App.vue ??
my components
header = [ props : 'list' ]
shop = add to cart, ( this which I want to transfer it to props list ) and go CRUD , is that possible ??
I suggest learning a bit more about Vuex to solve this problem.
This will give you a logical place to define database related actions that can also provide reactive data to components that will display it. Even if you're relatively new to Vue, learning Vuex sooner rather than later will payoff.
It may also be possible for you to use v-model to extricate some data from one component... but what you've described seems a bit different. It might be worth looking at how to implement v-model on your own components as you become more familiar with Vue!
Coming from the knockoutJs background. If you don't specific the binding to an element. You can use the model to cover the whole page of elements. For example, i can make a div visible if a click event happened. I'm learning VueJs and from the documentation. I see the vue instance required you to speicif an element with el.
like this:
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
what if my button is not in the same div as the '#app' div. How do i communicate between two vue instance or can I use one vue instance to cover more than one element. what's the vuejs way?
It's very common to bind to the first element inside <body>. Vue won't let you bind to body, because there are all sorts of other things that put their event listeners on it.
If you do that, Vue is managing your whole page, and away you go. The docs cover the case where you have more than one Vue instance on a page, but I haven't come across this outside the docs, and I can't think of a good reason off the top of my head. More commonly, you will be constantly chopping bits out of your root Vue instance and refactoring them into "child" components. This is how you keep file sizes manageable and structure your app.
This is where a lot of folk needlessly complicate things, by over-using props to pass stuff to components. When you start refactoring into components, you will have a much easier time if you keep all your state in a store, outside vue, then have your components talk directly to your store. (put the store in the data element of all components). This pattern (MVVM) is fabulous, because many elements of state will end up having more than one representation on screen, and having a "single source of truth", normalized, with minimal relationships between items in the store, radically reduces the complexity and the amount of code for most common purposes. It lets you structure your app state independently of your DOM.
So, to answer your question, Vue instances (and vue components), don't need to (and shouldn't) talk much to each other. When they do need to (third party components and repeated components), you have props and events, refs and method calls (state outside the store), and the $parent and $root properties (usage frowned on!). You can also create an event bus. This page is a really good summary of the options.
Should your store be Flux/Redux? Vuex is the official implementation of the flux/redux pattern for vue. The common joke goes: when you realize you need it, it's too late. If you do decide to leave Vuex for now, don't just put state in Vue components. Use a plain javascript object in window scope. The right way is easier than the wrong way, and when you do transition to Vuex, your job will be much simpler. Your downstream references might be alright as they are.
Good luck. Enjoy the ride.
You usually put the main Vue instance on the first tag inside the body, then build the rest of your site within it. Everything directly inside that instance (not in a nested component) will have access to the same data.
You can then do this in your HTML:
<body>
<div id="#app">
<p v-if="showMessage">{{message}}</p>
<button v-on:click="showMessage = !showMessage"></button>
</div>
</body>
And set your data to something like this:
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
showMessage: true
}
})
If you want to pass data between components later on you'll have to look up how to emit events, use props, or possibly use Vuex if you got Vue running with the Vue-CLI (which I highly recommend).
If you want to reach tags (such as head tags) outside of the main Vue instance, then there are tools for that. For example you could try: https://github.com/ktquez/vue-head
I haven't tested it thought.
I have a listing/detail use case, where the user can double-click an item in a product list, go to the detail screen to edit and then go back to the listing screen when they're done. I've already done this using the dynamic components technique described here: https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components. But now that I'm planning to use vue-router elsewhere in the application, I'd like to refactor this to use routing instead. With my dynamic components technique, I used keep-alive to ensure that when the user switched back to the list view, the same selection was present as before the edit. But it seems to me that with routing the product list component would be re-rendered, which is not what I want.
Now, it looks like router-view can be wrapped in keep-alive, which would solve one problem but introduce lots of others, as I only want that route kept alive, not all of them (and at present I'm just using a single top level router-view). Vue 2.1 has clearly done something to address this by introducing include and exclude parameters for router-view. But I don't really want to do this either, as it seems very clunky to have to declare up front in my main page all the routes which should or shouldn't use keep-alive. It would be much neater to declare whether I want keep-alive at the point I'm configuring the route (i.e., in the routes array). So what's my best option?
You can specify the route you want to keep alive , like:
<keep-alive include="home">
<router-view/>
</keep-alive>
In this case, only home route will be kept alive
Vue 3
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
Exactly as is, you don't need a Component attribute in the App.vue. Also your this'll work even if your components don't have a name property specified.
I had a similar problem and looked at Vuex but decided it would require too much changes/additions in my code to add to the project.
I found this library https://www.npmjs.com/package/vue-save-state which solved the problem for me, keeping the state of 1 component synchronized with localStorage, and it only took a few minutes and a few lines of code (all documented in the Github page of the package).
One solution without localStorage:
import {Component, Provide, Vue} from "vue-property-decorator";
#Component
export default class Counter extends Vue {
#Provide() count = 0
/**
* HERE
*/
beforeDestroy() {
Object.getPrototypeOf(this).constructor.STATE = this;
}
/**
* AND HERE
*/
beforeMount() {
const state = Object.getPrototypeOf(this).constructor.STATE;
Object.entries(state || {})
.filter(([k, v]) => /^[^$_]+$/.test(k) && typeof v !== "function")
.forEach(([k]) => this[k] = state[k]);
}
}
What seems to me is you are looking for some kind of state management. If you have data which is shared by multiple components and you want to render component in different order, but dont want to load data again for each component.
This works like following:
Vue offers a simple state management, but I will recommend to use Vuex which is a standard for state management among vue community.