How to render a vnode (slot of a vue component) and mount it on an element? - vue.js

For example, here we have a dialog component.
...
<dialog>
<div>
{{data}}
</div>
</dialog>
...
As we known, due to the stack context, it's hard to make the dialog always be the topmost element.
So we need to mount the dialog element as a body's child.
I tried this in dialog's mounted hook.
var slot=this.$slots.default
var vm = new Vue({
render: ()=>slot
}
var div=document.createElement('div')
document.body.append(div)
vm.mount(div)
The result is: it mounted successfully.
However, when I changed the data, the mounted vm didn't change.
I wonder how to make it reactive or is there any other way to achieve this.

Now that I can't find any methods to solve this.
I have to read some open source ui framework's source code because I know they do put the dialog in the body's child level.
And I found that they do render the <slot>, but I can't find any code rendered in the corresponding position and I can't find any code they use to 'remove' it.
...
Then, I found a line of code, document.body.append(this.$el). I am stupid. I didn't realize that append will just remove the element from it's original position and append it to the new position util I re-read the whole code once again.
I did think about this way, just append $el to somewhere. But I am afraid that it'll break the component down.
So, the solution is:
just define a normal vue component
ship the $el or child element to anywhere you want using element.append etc. after the component mounted
no need to remove the element manually when component is destroyed if you just ship the $el, vue will take care of it.

Related

Vue.js initialize custom component only once and use it as base for multiple views

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.

VueJS - Get child width using $refs

I'm trying to extract the clientWidth of a child component inside VueJS. I'm calling $refs inside mounted, and assign its value to some data:
this.width = this.$refs.refName.clientWidth
The issue is that the width of this component is defined as 100% the one of its parent. But according to Vue lifecycle, the child DOM will be mounted before its parent. That causes the width in the child to always be 0 at mounted, because its parent isn't rendered yet.
My only solution would be to hardcode values in CSS, but I'd like to avoid that. What is the best way to achieve the desired effect?
Try using $nextTick():
this.$nextTick(
() => { this.width = this.$refs.refName.clientWidth); }
);
This will run after finishing all of the pending Vue work and browser updates. So, it should catch the element at that point.
If that still doesn't work (for various reasons it can be delayed beyond that point at times), you are usually stuck with a window.setTimeout() call with a kludgy delay value in it.
What is a little better in that case but more work is a window.setInterval() hander that loops waiting for the element to finally be drawn and gain a width.
One thing you cannot do is bind to that value and expect it to work. $refs is not reactive nor are any of a DOM's element attributes.

Access dom element with vuejs and vuetify in v-tabs

I have a div tag with an 'id="meet"' into v-tabs.
I need to access the node of this tag after I click a tab. I am using
let node=document.querySelector("#meet").
My problem is that it always returns "null". Here is the codepen: https://codepen.io/luizalves/pen/WNrepxz
What is wrong here?
There is no guarantee that after $nextTick you will see DOM element immediately.
<div id="meet"></div>
<button #click="onTest">test</button>
...
methods: {
onTest () {
let node=document.querySelector("#meet");
console.log('meet here',node)
},
After you clicked test you'll see in a console:
"meet here" "<div id='meet'></div>"
You can try to extract this div with an unique id into a separate component and inside it you can use mounted hook to know that it exists in DOM
Of course, according to vuetify docs, you may add eager prop to v-tab-item component, and that's it, but...possibly you are doing something wrong.
It's not a good idea to always add this prop because you are losing one of the vuetify 2.X advantages - lazy loading. This may lead to big performance problems if there are many elements on the tab.
Maybe it would be better if you will work directly with reactive variables (like variables in v-model directive), not DOM objects.

Default-expand-all doesn't work for q-tree? Vue.Js

I have to code a web application and the most important element is the q-tree. I'm already able to load and show data (passing an array called list), but I want that all nodes are expanded.
The vue.js examples of the official documentation show that you're be able to do this with the 'default-expand-all' attribute but this isn't working for me.
It only shows me the root node with an arrow, where I have to expand the children nodes manually.
<q-tree
:nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
default-expand-all
></q-tree>
Taking a cue from the accepted answer, I realised that the dom has already been created with the tree component on first render.
In my use case, I want to update the Tree when data comes back from the server.
So, I had to force it to re-render with the expanded functionality using:
this.$nextTick(function () {
this.$refs.nodes.expandAll();
})
The nextTick function will update the dom in the next window of execution, by which time the nodes will get expanded by calling the expandAll function.
And NB: For those confused by the astericks on the ref attribute or how to add it to the component, here goes:
<q-tree :nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
ref="nodes"
>
Solved my problem as following:
I have added a ref attribute to the QTree DOM Element which makes it possible to access predefined methods of QTree API.
<q-tree
:nodes="list"
:selected.sync="selected"
#update:selected="onSelectionChangedNode"
node-key="NodeNr"
label-key="NodeTxt"
**ref="nodes"**
>
The function I have been using is expandAll().
updated() {
this.$refs.nodes.expandAll();
}
The most important thing for me was, I had to find out which lifecycle hook was the right one for me. The update() hook was the one I was looking for.
The reason:
Called after a data change causes the virtual DOM to be re-rendered and
patched.
The component’s DOM will have been updated when this hook is called, so you
can perform DOM-dependent operations here.
The default-expand-all is only applied on the first rendering of that Component.
So if your Component renders when the nodes aren't assigned they wont expand if assigned afterwards.
https://v1.quasar-framework.org/vue-components/tree
You have to work with scoped slots and an expanded attribute if you dont have the nodes on first rendering.

Initialize dynamic Component in Code using Vue.js

I am currently developing a web application that is used to display elements for events on a map provided by HERE Maps. I am using Vue.
I have some components, but the relevant component is the component HereMaps.vue which initializes the map using the HERE Maps Api.
The HERE Maps Api provides the possibility to place so called InfoBubbles on the map showing additional information. These InfoBubbles can be provided some HTML-code in order to customize their appearance.
Please refer to the documentation for additional information
Following the documentation the code looks something like this:
let bubble = new H.ui.InfoBubble(marker.getPosition(), {
content: "<div class='someClass'>Some Content</div>"
});
this.ui.addBubble(bubble)
This is happening after mount in the "mounted" method from Vue in the "HereMaps" component.
The Bubbles are added in a "closed" (hidden) form and dynamically "opened" to reveal their content when the corresponding marker icon on the map is clicked. Therefore the HTML-code is present on the DOM after the component is mounted and is not removed at a later stage.
Now instead of supplying custom code within each bubble added to the UI i want to just add a component like this:
let bubble = new H.ui.InfoBubble(marker.getPosition(), {
content: "<myDynamicComponent></myDynamicComponent>"
});
this.ui.addBubble(bubble)
It does not matter to me wether the component is initialized using props or if it is conditionally rendered depending on the state of a global variable. I just want to be able to use the "myDynamicComponent" in order to customize the appearance in a different file. Otherwise the design process gets very messy.
As far as i know this is not possible or at least i was not able to get it work. This is probably due to the fact that the "myDynamicComponent" is not used within the "template" of the "HereMaps" component und thus Vue does not know that it needs to render something here after the directive is added to the DOM in the "mounted" method.
This is what the InfoBubble looks using normal HTML as an argument:
This is what the InfoBubble looks using the component as an argument:
It appears to just be empty. No content of the "myDynamicComponent" is shown.
Does anyone have any idea how i could solve this problem.
Thank You.
Answer is a bit complicated and I bet you wouldn't like it:)
content param can accept String or Node value. So you can make new Vue with rendered your component and pass root element as content param.
BTW, Vue does not work as you think, <myDynamicComponent></myDynamicComponent> bindings, etc exists in HTML only in compile time. After that all custom elements(components) are compiled to render functions. So you can't use your components in that way.
Give us fiddle with your problem, so we can provide working example:)