<template>
<RecyclablesPopup ref="LVP" class="inline-block m-5px"></RecyclablesPopup>
</template>
<script setup>
import RecyclablesPopup from "../components/popups/RecyclablesPopup";
import { ref } from 'vue';
const LVP = ref(null);
// ... after mounted I have an event with a legacy component and onclick handler:
eventClick: function(calEvent)
{
console.log(LVP.value);
LVP.value.click();
}
</script>
At the end I get Uncaught TypeError: LVP.value.click is not a function after I clicked.
console.log returns me the proxy object as expected Proxy { <target>: Proxy, <handler>: {…} }
Why can't I call click()?
the click function should be exposed by the child component in order be accessed by the parent component :
RecyclablesPopup component
<script setup>
function click(){
//.......
}
defineExpose({
click
})
</script>
for more details please check https://vuejs.org/guide/essentials/template-refs.html#ref-on-component
If you are using script setup you can't access functions, variables, etc., defined inside the referenced component. To change that you have to use the defineExpose compiler macro inside RecyclablesPopup component - check more in documentation
//inside RecyclablesPopup
<script setup>
import { ref } from 'vue'
const click = () => {
//do something
}
defineExpose({
click,
})
</script>
You need to execute the function on the onMounted lifecycle to guarantee that the component is mounted to the DOM, as the value before the component is mounted will be undefined.
onMounted(() => {
eventClick()
})
For more resources
https://vuejs.org/api/composition-api-lifecycle.html#onmounted
Related
<script setup lang="ts">
function callSomething() {
something(); //not working
}
onMounted(() => {
function something() {
console.log("Hello, World");
}
});
</script>
<template>
<div>
<button #click="callSomething">Click</button>
</div>
</template>
In Vuejs I want to call a function from <script setup lang="ts"> which is defined in onMounted lifecycle hook. Though, I can call function/method from onMounted that defined in <script setup lang="ts">
Error in console:
Uncaught TypeError: something is not a function
As per it's name onMounted(), This life cycle hook always execute when component mounted for the first time. For a button click, you can call a method directly outside of this onMounted().
<button #click="callSomething">Click</button>
function callSomething() {
// Logic can come directly here.
}
The something function is defined only in the scope of the onMounted callback, try to define it outside it to be available for the hook and the other function :
<script setup lang="ts">
function callSomething() {
something(); //not working
}
function something() {
console.log("Hello, World");
}
onMounted(() => {
});
</script>
I'd like to test specific methods in my Vue Single File Components using Jest. I'm using Vue 3 with the Composition API, and I would like to use the <script setup> approach but it appears to prevent this.
This works:
Component:
<script>
export default {
setup() {
const testMethod = function(input) {
return input + 1;
};
return { testMethod };
},
};
</script>
Test:
test('should be 2', () => {
expect(TestComponent.setup().testMethod(1)).toBe(2); // success
});
This doesn't work:
Component:
<script setup>
const testMethod = function(input) {
return input + 1;
};
</script>
Test:
test('should be 2', () => {
expect(TestComponent.setup().testMethod(1)).toBe(2); // TypeError: Cannot destructure property 'expose' of 'undefined' as it is undefined.
expect(TestComponent.testMethod(1)).toBe(2); // TypeError: _testComponent.default.testMethod is not a function
});
Is there another way to accomplish this, or is accessing the methods within the component not possible with the <script setup> approach?
Edit: Specifically looking for solutions that don't require mounting the gadget with something like vue-test-utils.
The bindings declared in <script setup> are hidden by default, but you can expose any of them to the component instance with defineExpose():
// MyComponent.vue
<script setup>
const testMethod = function(input) {
return input + 1;
};
defineExpose({
testMethod
})
</script>
Then in your test, access the exposed bindings via wrapper.vm (the component instance from the #vue/test-utils wrapper):
// MyComponent.spec.js
import MyComponent from '#/components/MyComponent.vue'
test('should be 2', () => {
const wrapper = shallowMount(MyComponent)
expect(wrapper.vm.testMethod(1)).toBe(2)
})
demo
if you use the wrapper of the mount method from vue test #vue/test-utils. Then just call the methods using (wrapper.vm as any) it works. Only the typescript compiler is unable to recognize and raise the error as property does not exist but when you try using something like below it works.
Eg: (wrapper.vm as any).someMethod()
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.
What i want to achieve is to build a loading vue directives,
function with v-loading directive will render spinner and block event till function promise resolve or reject.
What i had tried so far:
use addEventListener, but it can only listen dom's native event, not vue event
hijack vue $emit function, but get a warning said that don't override vue native function named $, even if this solution work, i think this is a bad solution.
in directives argument, binding.instance[binding.value.name] refer to onEvent function in component, i tried override it but it doesn't work. When onEvent trigger again, it run old onEvent before override.
third party event emitter(eg, mitt). this method works well, but custom-component have to write extra code to emit event.
As example code below,
user of v-loading have to remember to write 2 emit (mitt and vue's emit).
It is not that straight forward, and it has extra dependency.
// mitt solution
// custom-component template
<custom-component v-loading:event="onEvent">
// custom-component script
setup(props, {emit}) {
function emitEvent() {
emit("event");
// bad: have to remember to write this extra line, and it is third party dependency
mittEmitter.emit("event");
}
}
So, any other solution to listen vue's event(not dom's native event) from vue's $emit?
LoadingDirective.ts
import { Directive } from "vue";
const Loading: Directive = {
mounted(el, binding) {
const eventName = binding.arg;
const onEvent = binding.value;
// I want to listen vue's event(eventName) here
// do something extra
onEvent(); // original onEvent() function to run in App.vue
// do something extra
}
};
export default Loading;
App.vue
<template>
<!-- when onEvent triggered, a spinner will be render in custom-component -->
<custom-component v-loading:event="onEvent" />
</template>
CustomComponent.vue
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
setup(props, {emit}) {
function emitEvent() {
// use only vue's emit
emit("event")
}
return {
onEvent
};
}
});
</script>
The Vue 3 documentation recommends using an external library such as mitt or tiny-emitter.
JSFiddle Example
<template>
<div id="app">
<custom-component v-loading="eventHandler" />
</div>
</template>
const emitter = mitt();
const customComponent = { template: '<h1>Example</h1>' };
const app = Vue.createApp({
components: { customComponent },
setup() {
setTimeout(() => {
emitter.emit('loadingEvent', { colour: 'Green' });
}, 1000);
const eventHandler = e => console.log('Handled!', e);
return { eventHandler };
},
});
app.directive('loading', {
mounted(el, binding) {
const func = binding.value;
emitter.on('loadingEvent', data => {
// your logic here...
func(data);
});
},
});
app.mount('#app');
I am totally new to Vuejs and my question is:
Is there anyway for v-on to listen on click event, then execute a function which is defined in a module?
For example:
<button v-on:click="executeClick()"></button>
Will execute executeClick() in below module, which will be imported to Vue instance through require:
module.exports = {
executeClick: function () {
// do something
}
}
I am trying to keep vue instance's methods not to be crowded with a bunch of functions.
No, within the model you need features that are in this your component
soluction:
click.js
module.exports = {
executeClick: function () {
}
}
component.vue
<template>
<tag #click="$options.click.executeClick">
</template>
<script>
import click from 'click.js'
export default {
click: click
}
</script>