Find nearest parent Vue component of template ref (Vue 3) - vue.js

When a Vue template ref is mounted, I want to get the nearest parent Vue component. This should be generic and work for any template ref so I've put it in a composition function (but that's just an implementation detail).
I had this working but my implementation used elem.__vueParentComponent while iteratively searching an element's ancestors. While reading the Vue source code I saw __vueParentComponent was only enabled for dev mode or if dev tools is enabled in production. Thus, I don't want to rely on that flag being enabled.
I thought this might be possible using vnodes but this isn't easily google-able. Here's an example of what I'm trying to do:
function useNearestParentInstance(templateRef) {
function getNearestParentInstance(el) {
// code here
}
onMounted(() => {
const el = templateRef.value;
const instance = getNearestParentInstance(el);
// do something with instance
});
}
<template>
<div>
<SomeComponent>
<div>
<div ref="myElem"></div>
</div>
</SomeComponent>
</div>
</template>
<script>
export default {
setup() {
const myElem = ref();
// nearest would be SomeComponent's instance in this case
useNearestParentInstance(myElem);
...
}
}
</script>

If you want the nearest vue parent you can simply use
ref().$parent // Not sure if syntax is same in vue3
ref().$parent will get the first vuecomponent that is the parent of the ref that you placed.

Related

How to access refs from app instance in Vue3

In Vue2 it was possible to access refs from the vue instance like so:
vm.$refs.someRef
How can I achieve this in Vue3? Access the refs from outside the app instance i.e from js code.
If you are using options API, it's the same. If you want to use composition API, you pass a ref to the template. It gets a little confusing because there are two different refs one is the attribute in the template (ref="myref") and the other is the function const myref = ref(null)
When used in the template, the ref value gets updated and can be then accessed via myref.value
from https://gitlab.com/mt55/maintegrity-fim-web-interface-server/-/jobs/3293032688/artifacts/download
<script setup>
import { ref, onMounted } from 'vue'
// declare a ref to hold the element reference
// the name must match template ref value
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>
If the ref is needed from outside of the app, it can be accessed through the instance with:
const app=createApp(App)
app._instance?.refs
however that only works if the ref is in the App component. For every other component, while the ref is available somewhere in the app object, traversing through the structure is much more complicated.

Vue3 Reactivity in script setup for translation

I am adding some DOM elements in the script setup side but I want the messages to change when I change the language. I am using vue-i18n plugin. It's easy to do it in the template section because I can basically use the useI18n().t method but how can I do this in the script setup section. Using the useI18n().t method doesn't ensure reactivity.
Example Code:
$(".time")[0].innerHTML = `
<div>0<span>${useI18n().t("details.hour")}</span></div>
<div>0<span>${useI18n().t("details.minute")}</span></div>
<div>0<span>${useI18n().t("details.second")}</span></div>
`
Manipulating DOM directly inside the script leads to inconsistence in your app, you should drive your component by different reactive data to achieve your goal.
In your current situation try to define a computed property based on the translation then render it inside the template based on its different properties :
<script setup>
const {t} =useI18n()
const time = computed(()=>{
return {
hour:t(""details.hour"),
minute:t(""details.minute"),
second:t(""details.second"),
}
})
</script>
<template>
<div class="time">
<div>0<span>{{time.hour}}</span></div>
<div>0<span>{{time.minute}}</span></div>
<div>0<span>{{time.second}}</span></div>
</div>
</template>

How to make provide/inject reactive in Vue 3 to avoid props drilling?

I have a root component that has a lot of descendants. In order to avoid props drilling, I want to use provide/inject.
In the root component in the setup function, I use provide.
In the child component in the setup function, I get the value via inject.
Then the child component might emit an event, that forces the root component to reload data that it provides to the child components.
However, the data in the child component is not changed.
Previous answers that I found usually were related to Vue 2, and I'm struggling with Vue 3 composition API.
I tried to use watch/watchEffect, and "re-provide" the data, but it didn't work (and I'm not sure if it's a good solution).
Sample code: https://codesandbox.io/s/mystifying-diffie-e3eqyq
I don't want to be that guy, but read the docs!
Anyway:
App.vue
setup() {
let randomNumber = ref(Math.random());
function updateRandomNumber() {
randomNumber.value = Math.random()
}
// This should be an AJAX call to re-configurate all the children
// components. All of them needs some kind of config.
// How to "re-provide" the data after a child component asked this?
provide("randomNumber", {
randomNumber,
updateRandomNumber
});
},
ChildComponent.vue
<template>
<div>Child component</div>
<button #click="updateRandomNumber">Ask root component for re-init</button>
<div>Injected data: {{ randomNumber }}</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
// How to "re-inject" the data from parent?
const {randomNumber, updateRandomNumber} = inject("randomNumber");
return {
randomNumber,
updateRandomNumber
};
},
};
</script>

How to force Vue to update modified HTML

I use a custom directive to render LaTeX-code with KaTeX' renderMathInElement function. This, obviously, changes the component's innerHTML. I would like to re-run KaTeX once the content changes, but: The content never does!
A simple reproduction of the problem does not need KaTeX or directives and still shows, that reactivity works, but stops to work for the parts of a component with changed innerHTML:
<template>
<div>
{{content}}
<span ref="elem">{{content}}</span>
</div>
</template>
<script lang="ts">
import { Component, Ref, Vue } from "vue-property-decorator";
#Component({})
export default class Test extends Vue {
content = "Hello World!";
#Ref()
elem!: HTMLSpanElement;
mounted(): void {
// Without the following statement, Vue correctly re-renders the whole component after a second with the new content
// With this line, the update does not happen for the span element.
this.elem.innerHTML = "<b>Hello World!</b>";
setTimeout(() => {
this.content = "Greetings!";
}, 1000);
}
}
</script>
I suppose this is intended behavior - but that doesn't solve my problem. Is there some way to force Vue to replace all the component's DOM as soon as a re-render takes place?
You can use a key on your span, but if you don't want to tie it in with content, you can instead set it to a number, and increment it every time you want to make a change. Like so (I am not using TS here):
Set a key on your span:
<span :key="content_key">{{ content }}</span>
Then you can watch content and update the key accordingly:
watch: {
content() {
this.content_key ++;
}
}
In this way you can avoid setting the key to content directly.
Does this work for you?

Why vue need to forceUpdate components when they include static slot

Why vue needs to forceUpdate child component that has a static slot when it update self
It will trigger too much update calculate when a component has lots of child components that has a static slot
// my-button.vue
<template>
<div>
<slot></slot>
</div>
</template>
// my-com.vue
<template>
<div>
<span>{{ foo }}</span>
<template v-for="(item, index) in arr">
<my-button>test</my-button>
</template>
</div>
</template>
<script>
export default {
data() {
return {
foo: 1,
arr: (new Array(10000)).fill(1)
}
}
}
</scirpt>
If run this.foo = 2 will lead update queue include 10000 watcher. When I read source code I found the following code
function updateChildComponent (
...
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
...
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
...
}
I have found this issue on GitHub.
Unfortunately, any child components with static slot content still
need to be forced updated. This means the common use case of
<parent><child></child></parent> doesn't benefit from this change,
unless the default slot is explicitly forced into a scoped slot by
using <parent v-slot:default><child></child></parent>. (We cannot
directly force all slots into scoped slots as that would break
existing render function code that expects the slot to be present on
this.$slots instead of this.$scopedSlots)
Seems like it's fixed in 2.6.
In 2.6, we have introduced an optimization that further ensures parent
scope dependency mutations only affect the parent and would no longer
force the child component to update if it uses only scoped slots.
To solve your problem just update your Vue version to 2.6. Since it's just a minor update nothing will break down. What about the reason to call forceUpdate - only Evan You knows that :)
Hello i solved this problem with this:
export function proxySlots(scopedSlots: any): any {
return Object.keys(scopedSlots).reduce<any>(
(acc, key) => {
const fn = scopedSlots[key];
fn.proxy = true;
return { ...acc, [key]: fn };
},
{ $stable: true },
);
}
const ctx = {
// ...
scopedSlots: proxySlots({ someSlot: () => <span>Hello</span>})
// or from provided slots : this.$scopedSlots or context.slots if using composition api
}
It's a bit hackish but no more forced update.