Vue 3: component `:is` in for loop fails - vue.js

I'm trying to loop over a list of component described by strings (I get the name of the component from another , like const componentTreeName = ["CompA", "CompA"].
My code is a simple as:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="compName"></component>
</template>
</template>
Demo here
EDIT
I tried this with not much success.

Use resolveComponent() on the component name to look up the global component by name:
<script setup>
import { resolveComponent, markRaw } from 'vue'
const myGlobalComp = markRaw(resolveComponent('my-global-component'))
</script>
<template>
<component :is="myGlobalComp" />
<template>
demo 1
If you have a mix of locally and globally registered components, you can use a lookup for local components, and fall back to resolveComponent() for globals:
<script setup>
import LocalComponentA from '#/components/LocalComponentA.vue'
import LocalComponentB from '#/components/LocalComponentB.vue'
import { resolveComponent, markRaw } from 'vue'
const localComponents = {
LocalComponentA,
LocalComponentB,
}
const lookupComponent = name => {
const c = localComponents[name] ?? resolveComponent(name)
return markRaw(c)
}
const componentList = [
'GlobalComponentA',
'GlobalComponentB',
'LocalComponentA',
'LocalComponentB',
].map(lookupComponent)
</script>
<template>
<component :is="c" v-for="c in componentList" />
</template>
demo 2
Note: markRaw is used on the component definition because no reactivity is needed on it.

When using script setup, you need to reference the component and not the name or key.
To get it to work, I would use an object where the string can be used as a key to target the component from an object like this:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
const components = {CompA};
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="components[compName]"></component>
</template>
</template>
To use a global component, you could assign components by pulling them from the app context. But this would require the app context to be available and the keys known.
example:
import { app } from '../MyApp.js'
const components = {
CompA: app.component('CompA')
}
I haven't tested this, but this might be worth a try to check with getCurrentInstance
import { ref,getCurrentInstance } from 'vue'
const components = getCurrentInstance().appContext.components;

Related

Vue: How do I call a function inside a Component?

I am trying to execute a function from the Food.vue component from Page.vue.
How can I execute a function from an imported component?
I am using Vue 3 Composition API.
This is what I am trying to do:
Food.vue Component
<script setup>
var food = "blueberry"
function changeFood () {
food = "cherry";
}
</script>
<template>
<div>{{food}}</div>
</template>
Page.vue
<script setup>
import { onMounted } from "vue";
import food from "#/components/Food.vue";
onMounted(async() => {
// I want to execute changeFood() from the imported component. How can I do this?
});
</script>
<template>
<food></food>
</template>
I know this can be done with page props, but that's not what Im trying to do. I am trying to execute a function.
You have to expose the method to the parent using defineExpose;
Food.vue
<script setup>
import { ref } from "vue";
const food = ref("blueberry");
const changeFood = () => {
food.value = "cherry";
};
defineExpose({ changeFood });
</script>
<template>
<div>{{food}}</div>
</template>
Page.vue
<script setup>
import { ref, onMounted } from "vue";
import food from "#/components/Food.vue";
const myFood = ref(null);
onMounted(async() => {
if (myFood.value) {
myFood.value.changeFood();
}
});
</script>
<template>
<food ref="myFood" />
</template>
Demo

Computed value not updating in vue

I am new to Vue and I am working on a project in which I need to display different components based on a boolean value that shows whether a user is logged in or not.
<template>
<div class="container-fluid" v-if="isUserLoggedIn">
<main-navigation/>
<router-view />
<MainFooter />
</div>
<div v-else>
<router-view />
</div>
</template>
<script>
import { defineComponent } from "vue";
import MainNavigation from "./MainNavigation.vue";
import MainFooter from "../../common/MainFooter.vue";
export default defineComponent({
components: {
MainNavigation,
MainFooter,
},
});
</script>
<script setup>
import { useAuthMethod } from "../../../auth/Auth";
import { computed } from "vue";
const auth = await useAuthMethod();
const isUserLoggedIn = computed(() => auth.isUserLoggedIn());
</script>
In the above code the auth.isUserLoggedIn() returns a boolean value indicating if a user is logged in or not. My issue is that the "isUserLoggedIn" const defined by me is not updating automatically or say is not automatically calculating the boolean value and updating the components rendered accordingly.
I am not sure if this is the right way of doing it, so please tell me the correct approach to follow.
I tried removing the "isUserLoggedIn" from computed also but it is of no use, the component does not update automatically.
Looks like auth.isUserLoggedIn() is not reactive, You can make it reactive by assigning its value to a reactive variable and then use that in your computed property. Can you give a try to this :
const loggedIn = ref(null)
const auth = useAuthMethod();
loggedIn.value = auth.isUserLoggedIn();
const isUserLoggedIn = computed(() => loggedIn.value)

Vue3 updating values between components

I have a basic SPA with two child components, a header and a side menu (left drawer).
I wish the user to be able to click a button on the header component to call a function in the side menu component.
I understand I can use props to access a variable between parent & child components however how can I update a value between two sibling components?
Header
<q-btn dense flat round icon="menu" #click="toggleLeftDrawer" />
Left Drawer
import { ref } from 'vue'
export default {
setup () {
const leftDrawerOpen = ref(false)
return {
leftDrawerOpen,
toggleLeftDrawer () {
leftDrawerOpen.value = !leftDrawerOpen.value
}
}
}
}
Use global stores. Create a file
/store.js (you can obviously use any name)
Inside this file store/write the following code:-
import { reactivity } from 'vue'
export const global = reactive({
yourVariable: 'initialValue'
})
You can then import this variable and interact with it from anywhere and the change will be global. See the code below:
In header component:-
<script setup>
import { global } from './store.js'
const clicked = ()=> {
global.yourVariable = 'changed'
}
</script>
<template>
<button #click="clicked">
</button>
</template>
In leftDrawer Component:-
<script setup>
import { global } from './store.js'
</script>
<template>
<div>
{{ global.yourVariable }}
<!--You'll see the change:)-->
</div>
</template>
Then add these two in your main vue:-
<script setup>
import headerComponent from '...'
import leftDrawerComponent from '....'
//....
</script>
<template>
<div>
<header-component />
<left-drawer-component />
</div>
</template>

can't use template ref on component in vue 3 composition api

I want to get the dimensions of a vue.js component from the parent (I'm working with the experimental script setup).
When I use the ref inside a component, it works as expected. I get the dimensions:
// Child.vue
<template>
<div ref="wrapper">
// content ...
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const wrapper = ref(null)
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // works fine!
})
</script>
But I want to get the dimension inside the parent component. Is this possible?
I have tried this:
// Parent.vue
<template>
<Child ref="wrapper" />
</template>
<script setup>
import Child from './Child'
import { ref, onMounted } from 'vue'
const wrapper = ref(null)
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // failed!
})
</script>
the console logs this error message:
Uncaught (in promise) TypeError: x.value.getBoundingClientRect is not a function
In the documentation I can only find the way to use template refs inside the child component
does this approach not work because the refs are "closed by default" as the rfcs description says?
I ran into this issue today. The problem is that, when using the <script setup> pattern, none of the declared variables are returned. When you get a ref to the component, it's just an empty object. The way to get around this is by using defineExpose in the setup block.
// Child.vue
<template>
<div ref="wrapper">
<!-- content ... -->
</div>
</template>
<script setup>
import { defineExpose, ref } from 'vue'
const wrapper = ref(null)
defineExpose({ wrapper })
</script>
The way you set up the template ref in the parent is fine. The fact that you were seeing empty object { } in the console means that it was working.
Like the other answer already said, the child ref can be accessed from the parent like this: wrapper.value.wrapper.getBoundingClientRect().
The rfc has a section talking about how/why this works: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md#exposing-components-public-interface
It's also important to note that, with the <script setup> pattern, your ref in the parent component will not be a ComponentInstance. This means that you can't call $el on it like you might otherwise. It will only contain the values you put in your defineExpose.
I don't this this is necessarily related to the <script setup> tag. Even in the standard script syntax your second example will not work as-is.
The issue is you are putting ref directly on the Child component:
<template>
<Child ref="wrapper" />
</template>
and a ref to a component is NOT the same as a ref to the root element of that component. It does not have a getBoundingClientRect() method.
In fact, Vue 3 no longer requires a component to have a single root element. You can define your Child component as :
<template>
<div ref="wrapper1">// content ...</div>
<div ref="wrapper2">// content ...</div>
</template>
<script >
import { ref } from "vue";
export default {
name: "Child",
setup() {
const wrapper1 = ref(null);
const wrapper2 = ref(null);
return { wrapper1, wrapper2 };
},
};
</script>
What should be the ref in your Parent component now?
Log the wrapper.value to your console from your Parent component. It is actually an object of all the refs in your Child component:
{
wrapper1: {...}, // the 1st HTMLDivElement
wrapper2: {...} // the 2nd HTMLDivElement
}
You can do wrapper.value.wrapper1.getBoundingClientRect(), that will work fine.
You could get access to the root element using $el field like below:
<template>
<Child ref="wrapper" />
</template>
<script setup>
import Child from './Child'
import { ref, onMounted } from 'vue'
const wrapper = ref(null)
onMounted(() => {
const rect = wrapper.value.$el.getBoundingClientRect()
console.log(rect)
})
</script
Right, so here's what you need to do:
// Parent component
<template>
<Child :get-ref="(el) => { wrapper = el }" />
</template>
<script setup>
import Child from './Child.vue';
import { ref, onMounted } from 'vue';
const wrapper = ref();
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // works fine!
});
</script>
and
// Child component
<template>
<div :ref="(el) => { wrapper = el; getRef(el)}">
// content ...
</div>
</template>
<script setup>
import { defineProps, ref, onMounted } from 'vue';
const props = defineProps({
getRef: {
type: Function,
},
});
const wrapper = ref();
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // works fine!
});
</script>
To learn why, we need to check Vue's documentation on ref:
Vue special-attribute 'ref'.
On dynamic binding of (template) ref, it says:
<!-- When bound dynamically, we can define ref as a callback function,
passing the element or component instance explicitly -->
<child-component :ref="(el) => child = el"></child-component>
Since the prop lets you pass data from the parent to a child, we can use the combination of the prop and dynamic ref binding to get the wanted results. First, we pass the dynamic ref callback function into the child as the getRef prop:
<Child :get-ref="(el) => { wrapper = el }" />
Then, the child does the dynamic ref binding on the element, where it assigns the target el to its wrapper ref and calls the getRef prop function in that callback function to let the parent grab the el as well:
<div :ref="(el) => {
wrapper = el; // child registers wrapper ref
getRef(el); // parent registers the wrapper ref
}">
Note that this allows us to have the ref of the wrapper element in both the parent AND the child component. If you wished to have access to the wrapper element only in the parent component, you could skip the child's callback function, and just bind the ref to a prop like this:
// Child component
<template>
<div :ref="getRef">
// content ...
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
getRef: {
type: Function,
},
});
</script>
That would let only the parent have the ref to your template's wrapper.
If you're seeing the wrapper.value as null then make sure the element you're trying to get the ref to isn't hidden under a false v-if. Vue will not instantiate the ref until the element is actually required.
I realize this answer is not for the current question, but it is a top result for "template ref null vue 3 composition api" so I suspect more like me will come here and will appreciate this diagnosis.

How to test this component?

I have a Page level component which implements a component BookingInformation with slots. In the Page component, it's got another component BookingInformationHeader with slots. header and default.
My question is, how should I set up my test so that I can test that the GoogleConversionTrackingImage is visible when #Reservation.State wasBookingJustMade changes to true?
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import { Reservation } from "#/store/vuex-decorators";
import { BookingInformation, BookingInformationHeader } from "#/components";
import GoogleConversionTrackingImage from './components/GoogleConversionTrackingImage.vue';
#Component({
components: {
BookingInformation,
BookingInformationHeader,
GoogleConversionTrackingImage
}
})
export default class ConfirmationPage extends Vue {
renderTrackingImage: boolean = false;
#Reservation.State wasBookingJustMade: boolean;
}
</script>
<template>
<booking-information page-type="confirmation-page" class="confirmation-page">
<template slot="header" slot-scope="bookingInfo">
<booking-information-header>
<template slot="buttons">
// some buttons
</template>
</booking-information-header>
<google-conversion-tracking-image v-if="wasBookingJustMade" />
</template>
</booking-information>
</template>
By using vue test utils https://vue-test-utils.vuejs.org/ and chai https://www.chaijs.com/ in your test file you can do something like:
import mount from "#vue/test-utils";
import expect from "chai";
const wrapper = mount(BookingInformation,<inner components you want to test>);
expect(googleImage.exists()).to.be.true;
wrapper.setData({
wasBookingJustMade: true,
});
const googleImage = wrapper.find("google-conversion-tracking-image");
expect(googleImage.exists()).to.be.false;
You'll probably need to import the page level component as well.
You can give an id to the component you want to find and then search by id.