I'm using Vue 3 and in a v-for loop, I'm creating multiple button elements. The buttons are being made in another component named wb-button. So I call wb-button in every v-for loop.
I add #click event listener to the wb-button that calls a method inside the current component, simple:
<div v-for="(item,key) in items" :key="key">
<span>{{item.name}}</span>
<wb-button #click="deleteItem(item)">
Delete item!
</wb-button>
</div>
This works how I want, the problem starts when I want to pass the wb-button just like a ref to the deleteItem method. The purpose is to make changes to another ref inside the wb-button. So what I basically want to do is this:
export default{
name: 'listComponent',
methods:{
async deleteItem(item,wbButtonRef){
// The next line is what I want to do
wbButtonRef.$refs.btnElement.putInLoadingStatus()
// do the delete item action
}
}
}
I know I can create a ref on each wb-button and pass an ID or something to the method, but I don't think it is the cleanest way to do it.
If there was something to just pass the wb-button element as the method parameter it would be great. Something like this:
<!-- I want to know if there is something I can put in
the second argument of the 'wb-button' #click event -->
<wb-button #click="deleteItem(item, /* what to put here?*/)">
<!-- "this","$this","$event.target" etc ... -->
I have tried $event.target but it returns the root element of wb-button, what I need is the wb-button itself just like a $ref.
Simply put, you can't. And since this logic is relevant only for the button component itself, it's best to keep this logic within it. Adding a prop and render something based on that, like you suggested yourself in the comments, is a good way to go about it.
Considering Other Options
Although I used #paddotk 's answer, 'the props way' to solve my problem, I'm just adding this answer so anyone who reads this question afterward would have a complete answer.
As far as I have found out, there are two more ways of doing this:
1- As #MrFabio_25 mentioned in the comments, I can create a custom event on the child component and $emit with 'this' as a parameter, so I can handle that in the parent:
// wbButton.vue file
<inside-wb-component ref="btnElement" #click="handleClick">
<button>
<slot></slot>
</button>
</inside-wb-component>
//and in the script tag
//...
methods:{
handleClick(){
this.$emit('buttonClick',this)
}
}
//...
And simply in the parent component:
<wb-button #buttonClick="handleButtonClick">
A text here
</wb-button>
// in the script tag
methods:{
handleButtonClick(elem){
elem.$refs.btnElement.putInLoadingStatus()
}
}
2- The second way to do so, without the child component being involved, is to use an array of refs, as explained here:
Vue 3 Documentation - refs inside v-for
<div v-for="(item,key) in items" :key="key">
<wb-button ref="wbButtonRefs" #click="handleButtonClick(item,key)">
A text here
</wb-button>
</div>
// and in scripts tag
//...
methods:{
handleButtonClick(item,index){
const buttonRef=this.$refs.wbButtonRefs[index]
// Now do whatever with buttonRef
buttonRef.$refs.btnElement.putInLoadingStatus()
}
}
//...
<template>
<button #click="test($event)">Test</button>
</template>
methods:{
test(e){
const comp = e.target.__vueParentComponent
const props = comp.props
}
}
Related
I have a hard time figuring out a huge performance issue with a component list via v-for.
Here is my typescript code:
<template>
<template v-for="item in list" :key="item.id">
<TestComponent #mouseenter="hoveredItem = item" #mouseleave="hoveredItem = null" />
</template>
<div v-if="hoveredItem">hovered</div>
</template>
<script lang="ts">
import TestComponent from 'TestComponent.vue';
import { Options, Vue } from 'vue-class-component';
interface IItem {id:number, message:string};
#Options({
props:{},
components:{ TestComponent, }
})
export default class TestView extends Vue {
public list:IItem[] = [];
public hoveredItem:IItem|null = null;
public mounted():void {
for (let i = 0; i < 3; i++) {
this.list.push({ id:i, message:"Message "+(i+1), });
}
}
}
</script>
When I roll over an item (see # mouseeenter), a render() is triggered on all the items of the list which shouldn't be necessary.
I checked with Vue Devtools extension that shows these events for every single item of the list :
render start
render end
patch start
patch end
If i remove the following line, no render/patch is triggered:
<div v-if="hoveredItem">hovered!</div>
If instead of storing the item instance to hoveredItem i just raise a flag to display that div, i don't have the issue.
If instead of instantiating the <TestComponent> I use a simple <div> i don't have the issue.
If I don't use a v-for but manually instantiate items, I don't have the issue.
If I $emit a custom event from the instead of using native #mouseover
The <TestComponent> is just that:
<template>
<div>item</div>
</template>
Here is a codesandbox showing the issue of the first example and the fix via an $emit() from the child component
https://dh5ldo.csb.app
Do you have any hint on why the first example triggers a render on all the list items when it's not something we would expect ?
Thank you for reading me :)
Ok i finally figured it out.
After reading this article:
https://codeburst.io/5-vue-performance-tips-98e184338439
..that links to this github answer:
https://github.com/vuejs/core/issues/3271#issuecomment-782791715
Basically, when using a component on a v-for, Vue needs to know when it has to update it.
To achieve that it looks for any prop used on the DOM and builds up a cache to make further updates faster.
But when using a prop on an event handler, Vue cannot build that cache.
If that prop is updated, Vue will know it is linked to your component but it won't know if it actually should trigger a render or not. It will trigger it just in case.
If you use a prop on every instances of the list, any update of that prop will trigger a render on all the items.
What's unclear to me though is why this does not happen if I simply remove this line from the example of my first post:
<div v-if="hoveredItem">hovered!</div>
The hoverItem is still used on my event handlers so it should still trigger a render.
TLDR; don't use any property/var within a component event handler
(disclaimer: i may not have understood things properly, appologies if I'm wrong on some points)
Your :key="item.id" should be on the <TestComponent>, it does not work on <template>
<template>
<template v-for="item in list" :key="item.id">
<TestComponent #mouseenter="hoveredItem = item" #mouseleave="hoveredItem = null" />
</template>
<div v-if="hoveredItem">hovered</div>
</template>
I also recommend you do get a linter so it will show your such errors
I wanted to create a component that exposes a function in the parent.
While the Svelte docs point to context="module", that script gets called only once, which would break functionality if I have several instances of the same component.
I found several examples on the internet and they all point to a very handy workaround, defining the export in the component and calling it with dot notation:
// Child.svelte
<script>
let element
export function changeColor() {
console.log(element)
element.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16)
}
</script>
<button bind:this={element}>A button</button>
I created a very simple use case, which works when I bind the component to a variable and call it with child.function, but what if I want to call the function from a click on the component itself?
// App.svelte
<script>
import Child from "./Child.svelte"
let child
</script>
<button on:click={() => child.changeColor()}>Click</button> // This works
<Child bind:this={child} on:click={() => child.changeColor()} /> // This doesn't work
I understand the logic behind the first option, however I don't understand why the second option doesn't work!
Should it be the same? I binded the component to a variable, and I am calling it with the same syntax. How can i make it work, without using a second element that calls the function?
<Child bind:this={child} on:click={() => child.changeColor()} />
doesn't work because on:click is not defined, you can easily see this when changing it to on:click={() => console.log('test')}
The reason it doesn't work is because in Svelte events do not 'bubble' out of components, if you want to do that you have to explicitly indicate this in Child.svelte
<button bind:this={element} on:click>A button</button>
I want to build a custom element using Svelte.
Thus in rollup.config.js I set customElement: true, and then I have to use the to refer to my child components.
But I found that in this way, the bind will not work. Here is the code example
HelloWorld.svelte (child)
<script>
import Hello from './components/Hello'
import World from './components/World'
export let value;
</script>
<svelte:options tag={'x-app-helloworld'}/>
<input type="text" bind:value={value} >
<input>
<x-app-hello />
<x-app-world />
App.svslte(parent) part of it.
<x-app-helloworld bind:value={value}/>
Then the parent will show an error: 'value' is not a valid binding on <x-app-helloworld> elements.
How could I solve this bind problem?
Bindings work on regular elements because Svelte knows which event corresponds to each binding — for example, it knows that the value of an <input> changes when the element fires a change or input event.
With custom elements, there's no way to know what event (if any) the parent should be listening for. And there isn't currently a neat way to dispatch events from inside the element. So the best option is to pass in a callback to the custom element, and call it whenever the value changes:
<x-app-helloworld onValueChange="{(x) => value = x}"/>
<script>
export let onValueChange;
export let value;
$: onValueChange(value);
</script>
I'm trying to do a standard implementation of ref so that I can insert children elements into my InfoBox. But whatever I seem to put as a 'ref' element, never makes it to my InfoBox component. The result is always {} undefined from the log calls.
The click handler is to test timing issues, as using created vs mounted seemed to be a common issue.
<InfoBox
v-if="waitingForCode">
<p ref="infoboxcontent">A 7-digit verification code has been sent.</p>
</InfoBox>
and
<template>
<div
class="info-box"
#click="clicked" >
{{ this.$refs.infoboxcontent }}
</div>
</template>
<script>
export default {
name: 'InfoBox',
mounted() {
console.log(this.$refs, this.$refs.infoboxcontent)
},
methods: {
clicked() {
console.log(this.$refs, this.$refs.infoboxcontent)
}
}
}
</script>
<style scoped>
// some style
</style>
I'm starting to think I fundamentally misunderstand the usage of the 'ref' attribute since this seems like a trivial example. Any help would be greatly appreciated.
The ref Vue special attribute is used to refer a DOM node (or a child component) from your current component template.
If you want to pass some content to a custom component, this is the use case for a <slot> Vue built-in component.
I have a component for dialog windows in Vue 2 that is listening to event tcs-show-confirm (it is the same event that shows the dialog, sets the show property to true - in a parent component):
#Prop({ default: false })
show: boolean;
#Prop()
buttons: Array<TcsDialogButton>;
mounted() {
this.$nextTick(function () {
TcsEventBus.$on('tcs-show-confirm', () => {
console.log(this.$refs.tcsdialbuttons);
});
});
}
A html of the component is here (content of the footer slot is not replaced with another componet's html):
...
<slot name="footer">
<div v-if="buttons && buttons.length" ref="tcsdialbuttons" v-for="(button, index) in buttons">
<button tabindex="0" class="tcs-button tcs-dialog__button" #click="$emit('close', button.emitString)">
{{button.text}}
</button>
</div>
<div style="clear: both;"></div>
</slot>
...
Problem is that console.log(this.$refs.tcsdialbuttons) shows an empty array [] with length 0. Why is that? How can I access the buttons and set a focus on the first one?
I also tried to change fat arrow function to normal function:
TcsEventBus.$on('tcs-show-confirm', function() {
console.log(this.$refs.tcsdialbuttons);
});
But now it returns undefined.
I just found out that I cannot reference any of the elements in the component, even if it works in other components in my project. I don't understand..
I strongly suggest considering these 2 good practices first:
https://vuejs.github.io/eslint-plugin-vue/rules/no-use-v-if-with-v-for.html
https://vuejs.github.io/eslint-plugin-vue/rules/require-v-for-key.html
Once that's taken care of, I'd suggest adding an index to your v-for and adding the :ref to the individual buttons so you can find each one of them in a function.
Working with array items via ref is rather well described here: this.$refs[("p" + index)].focus is not a function
You should be able to apply focus to the first button using something like this.$refs[button${index}][0].focus();