Vue - How to use a dynamic ref with composition API - javascript

I am using the composition API in Vue 3 and want to define a ref to a component.
Usually that would be done by adding ref="name" to the template and then defining a ref with the same name: const name = ref(null).
The problem is I want to use dynamic ref names.
Any idea how to solve this?
In the options API I could do something like this:
:ref="name${x}" and then access it in the code like this: this.$refs[name${x}].
But with the composition API I don't have access to $refs.
Is there a way to do this without using a function inside the template ref?

The :ref prop accepts a function (see documentation), which you can use to assign it to the variable you want:
<script setup>
const dynamicRef = ref(null);
</script>
<template>
<Count :ref="(el) => dynamicRef = el" />
</template>
Vue playground example

I guess the Refs inside v-for is a better match for what you are trying to achieve.
Requires Vue v3.2.25 or above
<script setup>
import { ref, onMounted } from 'vue'
const list = ref([1, 2, 3])
const itemRefs = ref([])
onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
Vue SFC Playground

Related

Vue 3: child to parent and parent to child design pattern

I am new to the Vue 3 ecosystem. I am building a search form using the composition API.
I have a child component that contains a search form input. It emits a doEmitSearch event, and has a payload of the searchterm.
In the parent component I receive the emitted event #doEmitSearch=”doTriggerSearch”
In the parent component I have
<script lang=”ts” setup>
import {doPerformSearch} from "../composables/doPerformSearch"
function doTriggerSearch (value){
return doPerformSearch(value)
}
<script/>
Inside the doPerformSearch.ts I have various functions Search1(value), Search2(value), Search3(value) etc. that do API calls for multiple API searches, and data cleaning etc, and each one returns search results as JSON, which I want to dispatch/pass/display in either in the parent component or other child-components as props.
What syntax in the composition API can I use to display the returned doPerformSearch(value) in the parent component as {{searchResults}}
What syntax can I use to pass and display multiple search results to child components ?
Is that a good design pattern I'm using, or is there better ways to do it ?
Thank you
I would suggest you to start with Vue 3 Docs Components Basics. Be sure to switch the "API Preference" to Composition API.
I did a very basic concept of your task. Check the code below.
You can make your components statefull, like searchInput or stateless, like searchHistory. It's up to you, where you store your data in the app.
Important is to understand the data flow with Vue components. Usually, components become data over binding and respond with events.
const { ref, createApp } = Vue;
var searchInput = {
emits: ['search'],
data() {
return {
searchValue: ''
}
},
template: `
<label>Search:</label>
<input type="text" :value="searchValue"
#input="searchValue = $event.target.value" />
<button type="button" #click="$emit('search', searchValue)">enter</button>`
};
var searchHistory = {
props: {
history: {
type: Array,
required:true,
default:[]
}
},
template: `
<label>Search History:</label>
<ul><li v-for="item in history">{{item}}</li></ul>`
};
const app = createApp({
productionTip: false,
components: {
searchInput,
searchHistory
},
data() {
return {
searchHistory: []
}
},
methods: {
doSearch(value) {
if (value != '' && this.searchHistory.indexOf(value) == -1) this.searchHistory.push(value);
}
}
});
app.mount('#app')
<div id="app">
<search-input #search="doSearch"></search-input><hr>
<search-history :history="searchHistory"></search-history>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
If this can help anyone: here is the answer to 1)
<script lang=”ts” setup>
import {doPerformSearch} from "../composables/doPerformSearch"
let searchResults = ref(0);
async function doTriggerSearch(value){
return searchResults.value = await doPerformSearch(value);
}
<script/>
and in the template I have {{searchResults}}

Vue 3 Access data defined in a child component in a template

I have following three Vue components using the Composite API with the setup option. Where I want to access data in a nested tree.
The following code is a simplification of the code I need to integrate it. Basically the app is following the Atom Design principals (see also https://atomicdesign.bradfrost.com/chapter-2/).
// Wrapper.vue
// High level component composing smaller ones -> Organism
<template>
<div>Wrapper Text</div>
<Comp1 :isEdit="isEdit" :dataArr="data">
<div class="some css">{{item.name}} – {{item.description}}</div>
</Comp1>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {Comp1} from './components';
const isEdit = ref(true);
const data = [
{name: 'Item 1', value: 'item-1', description: 'Item 1 description'},
{name: 'Item 2', value: 'item-2', description: 'Item 2 description'},
…
]
</script>
// Comp1.vue
// Composition consisting of one or more small components -> Molecule
<template>
<div>Foo Bar</div>
<div v-if="!isEdit">{{text}}</div>
<Comp2 v-if="isEdit" :dataArr="data">
<slot></slot>
</Comp2>
</template>
<script lang="ts" setup>
import {default as Comp2} from './Comp2.vue'
withDefaults(defineProps<{isEdit: boolean, dataArr: anx[]}>)(), {isEdit: false, dataArr: () => []})
const text = "Hello World"
</script>
// Comp2.vue
// Simple component rendering data -> Atom
// You could argue here if it is an atom or not, but this is not the point here ;)
<template>
<div>Comp 2 Text</div>
<div v-for="item in dataArr">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
withDefaults(defineProps<{dataArr: any[]}>)(), {dataArr: () => []})
</script>
I want to define a template in the wrapper component where I define how to render data which are available in Comp2 but haven't found a way yet how to do so. I had a look into provide / inject but haven't seen how this could help.
Is there a way how I can access the item in the template which is defined in the wrapper component? If the solution is with provide / inject, please point me in the right direction. :)
You can use scoped slot to do something like this here, a demo I made Link

vuejs how to access a ref inside a child component?

I am new to vuejs and not very experienced in JavaScript. I use Vue3 in Laravel
I have a child component which exposes a ref on an input like this
<input
v-model="raw_input"
ref="raw"
#input="checkLength(limit)"
#keydown="enterNumbers($event)"
type="number"
/>
const raw = ref('');
defineExpose({
raw
})
In the parent
<template lang="">
<PageComponent title="Dashboard">
<SmartVolumeInputVue ref="svi" />
<div class="mt-16 border h-8">
{{ val }}
</div>
</PageComponent>
</template>
<script setup>
import PageComponent from "../components/PageComponent.vue"
import SmartVolumeInputVue from "../components/customInputs/SmartVolumeInput.vue";import { computed, ref } from "vue";
const svi = ref(null)
//let val=svi
//let val=svi.raw
let val=svi.raw.value
</script>
At the bottom of the script there are 3 lines (I only uncomment one at a time)
The first displays this in the template
{ "raw": "[object HTMLInputElement]" }
The second displays nothing
And the third reports an error
Uncaught (in promise) TypeError: svi.raw is undefined
I need some help to get the value of the referenced input in the child component.
Do you need the ref on the element? Or only the value?
If not, just expose the value like this: Live example
// App.vue
<script setup>
import { ref } from 'vue'
import SmartVolumeInputVue from './SmartVolumeInput.vue'
const svi = ref()
</script>
<template>
<SmartVolumeInputVue ref="svi" />
<div>
{{svi?.raw_input}}
</div>
</template>
// SmartVolumeInput.vue
<script setup>
import { ref, defineExpose } from 'vue'
const raw_input = ref(123)
defineExpose({
raw_input
})
</script>
<template>
<input v-model.number="raw_input" type="number" />
</template>

Vue <script setup> Top level await causing template not to render

I'm using the new syntax in Vue 3 and I really like the idea of it, but once I tried to use a top level await I started to run in some problems.
This is my code:
<template>
<div class="inventory">
<a class="btn btn-primary">Test button</a>
<table class="table">
<thead>
<tr>Name</tr>
<tr>Description</tr>
</thead>
<tbody>
<tr v-for="(item, key) in inventory" :key="key">
<td>{{ item.name }}</td>
<td>{{ item.description }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory'
const inventory: GetInventoryResponse = await apiGetInventory()
</script>
As you can see it's not that complicated, the apiGetInventory is just an axios call so I won't bother going into that.
The problem is, that if I have this top level await, my template doesn't render anymore, it's just a blank page in my browser. If I remove the two lines of code, it works fine.
Also the promise seems to revolve just fine, if I place a
console.log(inventory) underneath it I get an array with objects all fine and dandy.
Anyone have a clue what's going wrong here?
Top-level await must be used in combination with Suspense (which is experimental).
You should be able to just do it in onBeforeMount. Not as elegant; but a solid solution. Something like this:
<script setup lang="ts">
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory';
import {ref, onBeforeMount} from 'vue';
const inventory = ref<GetInventoryResponse>()
onBeforeMount( async () => {
inventory.value = await apiGetInventory()
})
</script>
Using onBeforeMount is good, but there are a couple of other options.
#skirtle suggested in Vue Discord chat to do the initialization inside an async lambda or function (possibly as an IIFE):
<script setup lang="ts">
let inventory: GetInventoryResponse
const loadData = async () => inventory = apiGetInventory()
loadData()
</script>
#wenfang-du suggested in How can I use async/await in the Vue 3.0 setup() function using Typescript to use promise chaining:
<script setup lang="ts">
let inventory: GetInventoryResponse
apiGetInventory().then(d: GetInventoryResponse => inventory = d)
</script>
The benefit of doing so is that the code is run before the beforeMount lifecycle hook.
You additionally need to take care of error handling as appropriate in both cases.
if you need for specific template(routes).
You can use router beforeResolve:
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory'
let inventory = false
router.beforeResolve(async to => {
// Skip if loaded or for specific vue file
if (inventory || to.meta?.layout === 404 || to.meta?.layout === 'blank') {
return
}
inventory = await apiGetInventory()
})
The Vue3 documentation says
Top-level await can be used inside script setup. The resulting code will be compiled as async setup()
In addition, the awaited expression will be automatically compiled in a format that preserves the current component instance context after the await.
For example:
<script setup lang="ts">
import { apiGetInventory, GetInventoryResponse } from '#/module/inventory/common/api/getInventory'
const inventory = ref(await apiGetInventory())
</script>
This only works if you use the Suspense compenent in the parent component, for instance:
<Suspense>
<RouterView />
<template #fallback>
Loading...
</template>
</Suspense>

Composition API with Nuxt 2 to get template refs array

I'm trying to get the array of element refs that are not in v-for. I'm using #nuxtjs/composition-api on Nuxt 2.
(Truth: I want to make an array of input elements, so that I can perform validations on them before submit)
This sounds too easy on vue 2 as $refs becomes an array when one or more compnents have the same ref name on html. However, this doesn't sound simple with composition api and trying to perform simple task with that got me stuck from long.
So to handle this scenario, I've created 1 composable function. (Soruce: https://v3-migration.vuejs.org/breaking-changes/array-refs.html#frontmatter-title)
// file: viewRefs.js
import { onBeforeUpdate, onUpdated } from '#nuxtjs/composition-api'
export default () => {
let itemRefs = []
const setItemRef = el => {
console.log('adding item ref')
if (el) {
itemRefs.push(el)
}
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
Here is my vue file:
<template>
<div>
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
<input :ref="input.setItemRef" />
// rest of my cool html
</div>
</template>
<script>
import {
defineComponent,
reactive,
useRouter,
ref
} from '#nuxtjs/composition-api'
import viewRefs from '~/composables/viewRefs'
export default defineComponent({
setup() {
const input = viewRefs()
// awesome vue code here...
return {
input
}
}
})
</script>
Now when I run this file, I don't see any adding item ref logs. And on click of a button, I'm logging input. That has 0 items in the itemRefs array.
What's going wrong?
Nuxt 2 is based on Vue 2, which only accepts strings for the ref attribute. The docs you linked actually refer to new behavior in Vue 3 for ref, where functions are also accepted.
Template refs in Nuxt 2 work the same way as they do in Vue 2 with Composition API: When a ref is inside a v-for, the ref becomes an array:
<template>
<div id="app">
<button #click="logRefs">Log refs</button>
<input v-for="i in 4" :key="i" ref="itemRef" />
</div>
</template>
<script>
import { ref } from '#vue/composition-api'
export default {
setup() {
const itemRef = ref(null)
return {
itemRef,
logRefs() {
console.log(itemRef.value) // => array of inputs
},
}
}
}
</script>
demo
And setup() does not provide access to $refs, as template refs must be explicitly declared as reactive refs in Composition API.

Categories