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.
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 tried to put the minimal example: (not real code, but similar and simpler to demonstrate the problem)
<script>
let parentOverflow = false;
function setParentOverflow() {
if(parentOverflow) return 'hidden'
return 'auto'
}
</script>
<div
style="--parent-overflow:{setParentOverflow()}"
transition:scale={{duration: 2000}}
on:introstart={() => (parentOverflow = true)}
on:introend={() => (parentOverflow = false)}
>
hello world
</div>
<style>
:global(.parent) {
overflow: var(--parent-overflow);
}
</style>
As you see I am using :global() in the style tag,
which means you can style globally also the parent div
everything work fine in all the code (functions, styles, transitions, introstart, introend)
❌ but overflow: var(--parent-overflow) give me the problem
technically in dev tools is shown (so the parent have the style, see image below)
but the the parent can't have the var(), yes, because ✅ if I write overflow: hidden work very good.
but var() not.
is there a way to pass also var() the value reactively to the parent?
another info is that if a child works fine the var()
I can't move the code function to the parent since the animation happens on the child.
but I can pass/export in some way a prop if there is a way for the parent and then write the CSS there.
I am open to everything that can make pass the returned value of the function to the parent and then style the parent reactively from child var.
also if the class .parent is present to multiple is there a way to only change the parent one of the child, and not all .parent?
<parent> <!-- yes only this -->
<child>
<parent>
<parent> <!-- don't change this -->
<parent> <!-- the child style on the first don't need to change this, because they are totally different things -->
<child>
<parent>
basically a concept like this .closest() https://developer.mozilla.org/en-US/docs/Web/API/Element/closest?retiredLocale=it (but without javascript, only css :global())
to mention .closest() can't work correctly in svelte so isn't worth it. I think can be a way with only CSS only (or svelte magic)
sorry for the long details, but I tried to put everything I know.
If you more details tell me I will add them too.
thanks for reading :)
If Child and Parent component are connected via slot
<Parent>
<Child />
</Parent>
you could use setContext / getContext for the communication. Like this only a Parent and it's Children share the information, the other sibling Parent components are not affected. A store is used so that the value is reactive. REPL
Parent.svelte
<script>
import {setContext} from 'svelte'
import {writable} from 'svelte/store'
const parentOverflow = writable('hidden')
setContext('parentOverflow', parentOverflow)
</script>
<div class="parent" style:overflow={$parentOverflow}>
<slot />
</div>
Child.svelte
<script>
import {scale} from 'svelte/transition'
import {getContext} from 'svelte'
let parentOverflow = getContext('parentOverflow')
</script>
<div transition:scale="{{duration: 2000}}"
on:introend="{() => ($parentOverflow = 'auto')}"
>
Lorem ipsum dolor sit amet, ...
</div>
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
}
}
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 am on migrating to Vue from React, and I just liked the v-model thing Vue offers. Now I am in a situation and not able to find out what is the vue-ish way of doing this.
Here is how my component tree looks like:
- FormContainer
-FormView
- CustomInput
- input
I have my states in FormContainer let's call it name and age. I want to avoid writing custom setter methods as I usually do using v-model, how can I go about passing the data down to the input component.
Currently I am doing something like this:
// in my container
<form-view :name="name" :age="age" />
// in my form view I am doing something like
<custom-input v-model="age"/>
<input v-model="name" />
both of them do not work and I get the following error Avoid mutating a prop directly
I should be able to do something like
<form-view #age="age" :age="age" #name="name" :name="name"/>
or something similar. Please let me know if you need more details on this.
I've answered this question twice already, and though my answers had merit, I don't think they were quite on-target for your question, so I've deleted them.
Fundamentally, you want events to "bubble up" from the elements where the events are happening to the Vue that owns the data items. Vue events don't bubble, but native events do, so if you have a native input event being generated at the bottom of a hierarchy, you can catch it with #input.native in any component up the tree from there.
For the example you gave, if the name and age data items live in the top-level Vue, the outermost component can take them as props with the .sync modifier. That means that any update:name and update:age events from the component will result in the data items being updated. So far so good.
Moving inside the form-view component, we have a native input element. Normally, it emits native input events. We want those to bubble up as update:name events, so we handle that like so:
<input :value="name" #input="$emit('update:name', $event.target.value)">
Now here's the fun part: inside the form-view component, we have the custom-input component. Somewhere in its heart (and we don't really care how far down that might be), it generates a native input event. Instead of catching that at each level and bubbling it up, we can let it bubble up naturally and catch it at the root of the custom-input component, and emit the required update:age event:
<custom-input :value="age" #input.native="$emit('update:age', $event.target.value)">
Putting it all together, we have two variables that are passed through components to input elements, two input event handlers, and no computeds.
new Vue({
el: '#app',
data: {
name: 'Jerry',
age: 21
},
components: {
formView: {
props: ['age', 'name'],
components: {
customInput: {
props: ['value']
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<div>Name: {{name}}</div>
<div>Age: {{age}}</div>
<form-view :name.sync="name" :age.sync="age" inline-template>
<div>
<custom-input :value="age" #input.native="$emit('update:age', $event.target.value)" inline-template>
<div>
Age: <input :value="value">
</div>
</custom-input>
<div>Name: <input :value="name" #input="$emit('update:name', $event.target.value)"></div>
</div>
</form-view>
</div>