I am trying to get $refs in Vue 3 using Composition API. This is my template that has two child components and I need to get reference to one child component instance:
<template>
<comp-foo />
<comp-bar ref="table"/>
</template>
In my code I use Template Refs: ref is a special attribute, that allows us to obtain a direct reference to a specific DOM element or child component instance after it's mounted.
If I use Options API then I don't have any problems:
mounted() {
console.log("Mounted - ok");
console.log(this.$refs.table.temp());
}
However, using Composition API I get error:
setup() {
const that: any = getCurrentInstance();
onMounted(() => {
console.log("Mounted - ok");
console.log(that.$refs.table.temp());//ERROR that.$refs is undefined
});
return {};
}
Could anyone say how to do it using Composition API?
You need to create the ref const inside the setup then return it so it can be used in the html.
<template>
<div ref="table"/>
</template>
import { ref, onMounted } from 'vue';
setup() {
const table = ref(null);
onMounted(() => {
console.log(table.value);
});
return { table };
}
On Laravel Inertia:
<script setup>
import { ref, onMounted } from "vue";
// a list for testing
let items = [
{ id: 1, name: "item name 1" },
{ id: 2, name: "item name 2" },
{ id: 3, name: "item name 3" },
];
// this also works with a list of elements
let elements = ref(null);
// testing
onMounted(() => {
let all = elements.value;
let item1 = all[0];
let item2 = all[1];
let item3 = all[2];
console.log([all, item1, item2, item3]);
});
</script>
<template>
<div>
<!-- elements -->
<div v-for="(item, i) in items" ref="elements" :key="item.id">
<!-- element's content -->
<div>ID: {{ item.id }}</div>
<div>Name: {{ item.name }}</div>
</div>
</div>
</template>
<template>
<your-table ref="table"/>
...
</template>
<script>
import { ref, onMounted } from 'vue';
setup() {
const table = ref(null);
onMounted(() => {
table.value.addEventListener('click', () => console.log("Event happened"))
});
return { table };
}
</script>
Inside your other component you can interact with events you already registered on onMounted life cycle hook as with my example i've registered only one evnet
If you want, you can use getCurrentInstance() in the parent component like this code:
<template>
<MyCompo ref="table"></MyCompo>
</template>
<script>
import MyCompo from "#/components/MyCompo.vue"
import { ref, onMounted, getCurrentInstance } from 'vue'
export default {
components : {
MyCompo
},
setup(props, ctx) {
onMounted(() => {
getCurrentInstance().ctx.$refs.table.tempMethod()
});
}
}
</script>
And this is the code of child component (here I called it MyCompo):
<template>
<h1>this is MyCompo component</h1>
</template>
<script>
export default {
setup(props, ctx) {
const tempMethod = () => {
console.log("temporary method");
}
return {
tempMethod
}
},
}
</script>
Related
I have a vue 3 script setup component (composition api), and another file (for printing) that has some logic I want to inject into my main component.
My main file looks like
<template>
<v-app>
<v-main>
<v-table-mobile :headers="headers" :rows="rows" :items-per-page="itemsPerPage" no-data-text="No data to display" pagination-text="{0} - {1} of {2}" :sort-by="sortBy" :group-by="groupBy">
<template #item.a="{ item }">
{{ item.a }}
</template>
</v-table-mobile>
</v-main>
</v-app>
</template>
<script lang="ts">
import * as Print from './utils/print';
export Print;
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import VTableMobile, { TableHeader, TableColumnSort, TableColumnGroup } from './components/MobileTable.vue';
const sortBy = ref({key:'a', order:'asc'} as TableColumnSort);
//const sortBy = ref({} as TableColumnSort);
const groupBy = ref({key:'a', expanded:[1] as any[]} as TableColumnGroup);
//const groupBy = ref({} as TableColumnGroup);
const itemsPerPage = ref(3);
const headers = ref([
{ key: "a", title: "a" },
{ key: "b", title: "b" }
] as TableHeader[]);
const rows = ref([
{ a:1, b:2 },
{ a:3, b:4 },
{ a:1, b:6 },
{ a:7, b:8 },
{ a:9, b:10 },
{ a:11, b:12 },
{ a:13, b:14 },
{ a:15, b:16 },
{ a:17, b:18 },
{ a:19, b:20 }
]);
onMounted(() => {
setTimeout(() => {
rows.value = rows.value.concat([ { a:21, b:22 } ]);
itemsPerPage.value = 4;
}, 3000);
})
</script>
The print file looks like
import { Ref, nextTick } from 'vue';
export default {
mounted() {
alert(1);
},
beforeUnmount() {
alert(2);
},
methods: {
async Print(print_mode:Ref<boolean>) {
print_mode.value = true;
await nextTick();
window.print();
},
DisablePrintMode(print_mode:Ref<boolean>) {
print_mode.value = false;
}
}
}
I want to add the mounted, beforeMount and the 2 methods to be injected into my main component. But this is not working. How can I fix it?
Right now I get an error
Declaration or statement expected.ts(1128) where the export Print is.
Thanks
Vue composables are designed for this.
Composables | VueJS
<!-- print file -->
<script setup>
import { onMounted, onUnmounted, nextTick } from 'vue'
/*
Other logic code
*/
async function Print(print_mode) {
print_mode.value = true;
await nextTick();
window.print();
},
function DisablePrintMode(print_mode) {
print_mode.value = false;
}
onMounted(() => {
// content of this hook
})
onUnmounted(() => {
// content of this hook
})
</script>
<!-- main file -->
<script setup>
import { usePrint } from './print.js'
const printFunc = usePrint()
</script>
I have written a sample code, you can take this reference to your code
//Parent component
<template>
<childComp #onchangeData='changeData' />
</template>
<script>
setup() {
const state = reactive({
data: 'anything
});
function changeData(v){
state.data = v
}
return { changeData}
},
</script>
//Child
<template>
<button #click='change('hello')' />
</template>
<script>
setup() {
function change(v){
this.$emit('onchangeData', v)
}
return{change}
},
</script>
I am struggling to change the parents' reactive state from the child's button click. It's saying this.$emit is not a function. I tried many ways like using #onchangeData='changeData()' instead of #onchangeData='changeData', using arrow functions etc. But nothing works. Here, I wrote an example and minimal code to keep it simple. But I hope my problem is clear.
Look at following snippet, this is not the same in composition as in options API, so you need to use emit passed to setup function:
const { reactive } = Vue
const app = Vue.createApp({
setup() {
const state = reactive({
data: 'anything'
});
function changeData(v){
state.data = v
}
return { changeData, state }
},
})
app.component("ChildComp", {
template: `
<div>
<button #click="change('hello')">click</button>
</div>
`,
setup(props, {emit}) {
function change(v){
emit('onchangeData', v)
}
return { change }
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<child-comp #onchange-data='changeData'></child-comp>
<p>{{ state.data }}</p>
</div>
I have this problem where binding ref in renderless component won't work. I've tried adding class within the instance object and the class binding work but the ref didn't. I also tried logging this.$refs and it just returns an empty object.
App.vue
<template>
<div id="app">
<renderless>
<div slot-scope="{ instance, logger }">
<div v-bind="instance"></div>
<button #click="logger('One')">One</button>
<button #click="logger('Two')">Two</button>
</div>
</renderless>
</div>
</template>
<script>
import Renderless from "./components/Renderless";
export default {
components: {
Renderless
},
};
</script>
components/Renderless.vue
<script>
export default {
render(h) {
return this.$scopedSlots.default({
instance: {
ref: 'container'
},
})
},
mounted() {
this.init()
},
methods: {
init() {
console.log(this.$refs)
},
logger(value) {
console.log(value)
},
},
};
</script>
How can I bind the ref so that the child component know what element to target or any other better solutions/suggestions?
BTW code is also available on codesandbox.
You can use querySelector and querySelectorAll to pick out what you want within the child component. this.$el should provide the based element of the child class once the component has mounted.
init () {
const buttons = this.$el.querySelectorAll('button');
buttons.forEach(button => {
// access each button
});
// fetches an item by element id
const itemById = this.$el.querySelector('#elementid');
// fetches by classes
const itemsByClass = this.$el.querySelector('.className');
}
I need to build list of dynamical components that I can group in group component. Then I need to send all information about builded components and groups.
I can use <component v-for="componentName in myComponents" :is="componentName"></component>, and get information about components using this.$children.map(component => component.getInformation()), but then I can't move some component to group component, because I have only component name not the component instance with data (it just render with default data).
I also can use this:
<template>
<div ref="container"> </div>
</template>
<script>
import someComponent from 'someComponent.vue'
import Vue from 'vue'
export default {
data () {
return {
myComponents: []
}
},
methods: {
addSomeComponent () {
let ComponentClass = Vue.extend(someComponent);
let instance = new ComponentClass({});
myComponents.push(instance);
instance.$mount();
this.$refs.container.appendChild(instance.$el)
},
getInformation () {
return this.myComponents.map(component => component.getInformation());
}
}
}
</script>
But then I can't use reactivity, directives (e.g. directives for drag and drop), and it's not data driven pattern.
Any suggestions?
<template>
<div class="component">
<template v-for="(child, index) in children()">
<component :is="child" :key="child.name"></component>
</template>
</div>
</template>
<script>
import someComponent from 'someComponent.vue'
import Vue from 'vue'
export default {
methods: {
children() {
let ComponentClass = Vue.extend(someComponent);
let instance = new ComponentClass({});
return [
instance
];
},
}
}
</script>
I have a simple application which need to render 2 components dynamically.
Component A - needs to have onClick event.
Component B - needs to have onChange event.
How is it possible to dynamically attach different events to component A/B?
<template>
<component v-bind:is="currentView">
</component>
</template>
<script>
import A from '../components/a.vue'
import B from '../components/b.vue'
export default {
data: function () {
return {
currentView: A
}
},
components: { A, B }
}
</script>
Here is a solution for a little more complicated and realistic use case. In this use case you have to render multiple different components using v-for.
The parent component passes an array of components to create-components. create-components will use v-for on this array, and display all those components with the correct event.
I'm using a custom directive custom-events to achieve this behavior.
parent:
<template>
<div class="parent">
<create-components :components="components"></create-components>
</div>
</template>
<script>
import CreateComponents from '#/components/CreateComponents'
import ComponentA from '#/components/ComponentA'
import ComponentB from '#/components/ComponentB'
export default {
name: 'parent',
data() {
return {
components: [
{
is: ComponentA,
events: {
"change":this.componentA_onChange.bind(this)
}
},
{
is: ComponentB,
events: {
"click":this.componentB_onClick.bind(this)
}
}
]
}
},
methods: {
componentA_onChange() {
alert('componentA_onChange');
},
componentB_onClick() {
alert('componentB_onClick');
}
},
components: { CreateComponents }
};
</script>
create-components:
<template>
<div class="create-components">
<div v-for="(component, componentIndex) in components">
<component v-bind:is="component.is" v-custom-events="component.events"></component>
</div>
</div>
</template>
<script>
export default {
name: 'create-components',
props: {
components: {
type: Array
}
},
directives: {
CustomEvents: {
bind: function (el, binding, vnode) {
let allEvents = binding.value;
if(typeof allEvents !== "undefined"){
let allEventsName = Object.keys(binding.value);
allEventsName.forEach(function(event) {
vnode.componentInstance.$on(event, (eventData) => {
allEvents[event](eventData);
});
});
}
},
unbind: function (el, binding, vnode) {
vnode.componentInstance.$off();
}
}
}
}
</script>
You don't have to dynamically add them.
<component v-bind:is="currentView" #click="onClick" #change="onChange">
If you want to be careful you can bail in the handler of the currentView is not correct.
methods: {
onClick(){
if (this.currentView != A) return
// handle click
},
onChange(){
if (this.currentView != B) return
// handle change
}
}