Getting reference to Vue 3 Template Refs in v-for - vue.js

I have a v-for with text input components. The number of text input components is unknown and variable. Each text input component can emit an #update event. When one of these input components emits this #update event I need to be call an exposed method on the input component.
Here's some sample code:
const textInputs = ref([])
function callComponentMethod() {
// Call an exposed method on the TextInput that called this function
}
<TextInput v-for="(section, index) in sections" ref="textInputs" #update="callComponentMethod" />
I thought I might be able to pass the index to the callComponentMethod function and use that to reference the correct textInput ref, but the Vue 3 docs state that "the ref array does not guarantee the same order as the source array", so I don't think that's a viable solution.
How can I reference the correct TextInput within the callComponentMethod function? Is there a way to pass a self reference to it? Like #update="callComponentMethod($self)" or something?
I've researched for answers but I can't find any solution that will work when the # of components within the v-for is variable.
Update:
I considered doing it like this:
<TextInput v-for="(section, index) in sections" ref="textInputs" #update="callComponentMethod(index)" />
function callComponentMethod(index: number) {
// Call an exposed method on the TextInput that called this function
textInputs.value[index].myCustomMethod()
}
But the problem is what the Vue 3 docs state that "the ref array does not guarantee the same order as the source array" so I can't be sure that the indexed Ref I'm accessing is the same one that called the method.

As far as I understand your problem, you wanted to implement multiple inputs at the same time and wanted to get them separately in callComponentMethod based on user input.
There are few ways you use, but I can recommend one way what you can do is pass index as prop in TextInput and pass it back with inputted text when you emit update like this
<TextInput v-for="(section, index) in sections" ref="textInputs" #update="callComponentMethod" :index="index" />
and in copmponent
const data={
index:props.index,
seach:"text"
}
this.$emit("update",data)
and receive them here
function callComponentMethod(data) {
// Call an exposed method on the TextInput that called this function
}

Related

Vue mutate prop correctly

I'm trying to create a simple component whose focus is to display an element in an array, but I'm having issues with Vue's philosophy.
As you may know, if a mutation on a prop is triggered, Vue goes crazy because it doesn't want you to update the value of a prop. You should probably use a store, or emit an event.
The issue is: that since I'm adding functionalities to my codebase (for instance the possibility to start again when I reach the last element of the array), it would be wrong to have an upper component be responsible for this management, as it would be wrong to ask an upper component to change their variable, given that my component is supposed to manage the array, so an emit would be a bad solution.
In the same way, given that I'm making a generic component that can be used multiple times on a page, it would be incorrect to bind it to a store.
EDIT: the reason why the prop needs to be updated is that the component is basically acting as a <select>
Am I missing an obvious way to set this up?
To give an example of my end goal, I'm aiming for a component looking like the one in the picture below, and I think a 2 way bind like in v-model would be more appropriate than having to set an #change just to say to update the value of the passed prop.
If you have a prop the correct way to update the value is with a sync, as in the following example
Parent:
<my-component :title.sync="myTitle"></my-component>
Child:
this.$emit("update:title", this.newValue)
Here is a very good article talking about the sync method.
By the other hand you can alter a Vuex state variable by calling a Vuex mutation when you change the value:
computed: {
title: {
// getter
get() {
return this.$store.state.title
},
// setter
set(newValue) {
this.setTitle(newValue) // Requires mutation import, see the methods section.
// Or without import:
this.$store.commit('setTitle', newValue);
}
}
},
methods: {
...mapMutations("global", ["setTitle"]) // It is important to import the mutation called in the computed section
}
In this StackOverflow question they talk about changing state from computed hook in Vue. I hope it works for you.

Get v-if expression/data as plain text in child component

Hy there,
I try to create a custom Vue component which is shown based on v-if directive. I also want to change the directive data (modalStatus) value from inside the component.
<modal v-if="modalStatus"></modal>
To update the data from the component i use a method similar to this.
closeModal () {
this.$parent.modalStatus = false
}
The problem is that sometimes i don't know the name of the data model (modalStatus) , can be anything.
My question is how can i get the data/expression name as a plain text from the modal component ?
I'm planing to use something like this to update the modalStatus
this.$parent['anyName'] = false
Thanks and stay safe !
Later Edit. I know how to accomplish all of the above using props or v-model. I wonder if is possible using strictly v-if. Thanks!
There are several approaches to get to a method or property in the parent component from the child.
The 'Vue Way' is to emit a message telling the parent to close.
Send the name in as a property
Parent
<child modalName='modalStatus' />
Child
this.$parent[this.modalName]=false
Send in a method
Parent
<child :close='onClose' />
// component method
onClose(){
this.modalStatus=false
}
Child
this.close()
Emit a message
Parent
<child-component #close='modalStatus=false' />
// or call a method
<child-component #close='onClose' />
// component method
onClose(){
this.modalStatus=false
}
Child
this.$emit('close')

Changing value of outer variable used in props after emitted listener from component disables v-model listener effects in Vue.js

Considering following HTML code:
<div id="app">
<comp :is_checked="is_checked" v-on:ch="function(x){is_checked_2=x}"></comp>
<p>{{ is_checked_2 }}</p>
</div>
<script src="app.js"></script>
and app.js:
var tm = `<div>
<input type="checkbox" v-model="is_checked"
v-on:change="$emit('ch',is_checked)"
>{{ is_checked }}
</div>`
Vue.component('comp', {
template: tm,
props: ["is_checked"]
})
new Vue({
el: "#app",
data: function() {
return {
is_checked: null,
is_checked_2: null
};
}
});
If we replace is_checked_2=x by console.log(x) in v-on:ch="function(x){...}, everything works correct and v-model is changing is_checked when input checkbox value changes.
Also if we don't send the value of variable by props and define it locally in component, everything works correct.
It seems that changing the parent Vue's object variable is
regenerating the whole HTML of component where the value of variable
is sent by props and the variable is reset there immediately after
firing the event inside template. It is causing that functions
triggered by events don't change component's variables.
Is it a bug in Vue.js?
To make it more clear, the behavior is following: changing whatever Vue parent value that is not bound and however related to the component (no props, no slots) results at resetting all values in the component that are bound by props. Following only occurs if we reactivelly write to parent/main HTML using modified parent variable, we can achieve that by placing {{ .... }} there. This seems to be the bug.
Example: Vue has variables a and b. We place {{ a }} to the main code. The value of variable b is sent by props to component and matched to variable c. With the time we are changing that value of variable c inside the component. In one moment we decide to change value of a and it results by "forgetting" current state of c and it is reset to initial state by invoked props.
To summarize: the bug itself gets stuck that props should be reactivelly invoked only if corresponding parent variable is changed and not if whatever parent variable changes and at the same time they modify HTML code.
This issue doesn't have nothing to do with event listeners neither events, because it is possible to replicate it without using them.
Conclusion:
{{ is_checked_2 }} or {{ '',is_checked_2 }} or {{ '',console.log(is_checked_2) }} after changing value of is_checked_2 is causing rerendering the whole top Vue component having is_checked as variable and it is resetting child component variable, because it has the same name. The solution is never using the same name for child and parent component variables bound by props. This is issue of Vue/props architecture how it was designed.
I don't think it's a bug.
You're most likely running into a race condition where your change event gets executed before or in place of the internally attached one (hence the seemingly nullified data binding).
Because v-model is essentially just a syntactic sugar for updating data on user input events. Quoting the docs:
v-model internally uses different properties and emits different events for different input elements:
text and textarea elements use value property and input event;
checkboxes and radiobuttons use checked property and change event;
select fields use value as a prop and change as an event.
You might also want to see "Customizing Component v-model".

Prop inheritance from component

Not too sure if i understand "Non-prop attributes" from manual (or vue.js at all): https://v2.vuejs.org/v2/guide/components-props.html
Say i have ChildComponent.vue file:
<template>
<input type="text" class="input" :value="childValue" v-on="listeners">
</template>
<script>
export default {
props: {
childValue: {
type: String,
default: 'blah',
}
},
computed: {
listeners() {
return {
// Pass all component listeners directly to input
...this.$listeners,
// Override input listener to work with v-model
input: event => this.$emit('input', event.target.value)
}
}
}
}
</script>
Then I add it to ParentComponent like this:
<template>
<ChildComponent v-model="parentValue" placeholder="default" #keydown.enter="parentMethod"/>
</template>
<script>
export default {
data () {
return {
parentValue: "",
};
},
methods: {
parentMethod () {
...
}
},
}
</script>
The flow should be (and works like this) - anything written to text field in ChildComponent after pressing enter should be sent all the way up to ParentComponent as parentValue and parentMethod() should be invoked.
If I understand correctly BasicComponent is kind of extension to its template's root component, meaning <input> will not only have props type and class set, but also placeholder (which has "default" value)?
Also, does this mean that the v-model prop to whom parentValue data is assigned will be propagated to <input> element as well, making my :value and v-on bind reduntant?
Another question - how the hell is v-on="listeners" working without specifying an event, does it mean i'm listening to EVERY event?
In the parent component there is a shorthand #keydown.enter which means it's listening for keydown.enter event, yet in listeners() method I'm emitting an input event...
I also have big trouble understanding what is going on in listeners() method at all, so any help in deciphering this will be greatly appreciated. :D
Thanks in advance for help.
Cheers
Let's do this one topic at a time...
Difference between props and non-prop attributes:
Props are the parameters which you define in your props object. With props you can tell the user what types they should use for a given prop, whether they're required or not, default values, assign validation functions, and etc.
Also, props are reactive, so if your template depends on a prop and the prop updates, so will your template.
Attributes you assign to your components, but do not correspond to any props, are passed to the $attrs variable. You can use it to access those values, like $attrs.id to get the id, or $attrs.name to get the name, and so on.
The event flow in your case:
Yes, the things you type on your ChildComponent are passed to ParentComponent. They are passed both via your v-model and via #keydown.enter="parentMethod".
You probably know how events work, but if you don't, here's the gist of it: When you need to pass data from a child component to a parent component, you emit an event in your child and listen to it in your parent.
For example, if you want to emit an event called foo, you would call $emit somewhere in your child, using $emit('foo'). Then, you'd listen to it in the parent by adding #foo="yourHandler" to the child, where yourHandler is a function written to handle the event. Which is what you did with #keydown.enter="parentMethod".
<input> will not only have props type and class set, but also placeholder (which has "default" value)?:
Answer: It depends. What the <input> tag in your template will receive depends on whether or not your root element (<input>) inherits component attributes. That behavior is defined by the inheritsAttrs property of a component, which defaults to true.
What that means is, in your case, since you haven't specified inheritsAttrs it will default to true, and yes, every attribute you pass to <ChildComponent> will be passed to your <input> tag, except for the things you defined manually.
Since you declared your <input> tag like this:
<input type="text" class="input" :value="childValue" v-on="listeners">
Your <input> tag will inherit all attributes from <ChildComponent> except type, value and your listeners (more on that later). The exceptions to that rule are class and style, which are always inherited regardless.
PS: Note that type, class and placeholder are attributes, not props.
Does this mean that the v-model prop to whom parentValue data is assigned will be propagated to element as well, making my :value and v-on bind reduntant?
Answer: No, but it also won't work. Here's why:
When you declare your listeners using this piece of code:
listeners() {
return {
// Pass all component listeners directly to input
...this.$listeners,
// Override input listener to work with v-model
input: event => this.$emit('input', event.target.value)
}
}
You are assigning to your listeners computed property every single event listener placed on your ChildComponent tag, including your keydown event, which is why it works.
The assignment is done in this line:
...this.$listeners,
It uses the spread operator to add all the elements in your $listeners variable (which holds all your component events) to the object you're returning.
The only event which you are not inheriting is input, as defined in this line:
input: event => this.$emit('input', event.target.value)
With that line, you tell your code that the behavior of your input event will be the one you defined, rather than the inherited.
Then, when you assign v-on="listeners" to your input, you're telling it to listen to every single event listed on your listeners variable. That is: You're appending all your inherited events and your custom input event to your input event.
Finally, to explain why it isn't redundant but why it won't work, you must understand how v-model works. It (usually) works by listening on the input event of a component, and using it to update the value prop of the same component. So in this line:
<ChildComponent v-model="parentValue" placeholder="default" #keydown.enter="parentMethod"/>
You are doing two things:
You're assigning the value of parentValue to the value prop of ChildComponent
You're telling your component to update parentValue whenever the input event is called.
That means that assigning a value and listeners to your input tag is not redundant, since you need it for v-model to work properly, but it won't work in the end, since your component doesn't have a value prop. it has a childValue prop instead.
To fix it, you have two options:
Rename childValue to value
Or tell your component to use childValue as model
To do the second approach, just append this piece of code to your ChildComponent:
model: {
prop: 'childValue',
event: 'input'
}
That will tell your component to use that prop and that event to make v-model work.
THE END
A final note: In the future, try narrowing your question down to a single topic. It will be easier to answer and will help people who search for those topics later on.

How can I work with just values in react-select onChange event and value prop?

I try to use react-select in my reactjs app, but I have a problem with the onChange event. onChange is supposed to send two arguments. The first is supposed to be the selected value, but instead of the selected value, the whole option item is passed as the selected value.
For instance
I have an array of option items like options=[{ id: '1', name: 'A'},{ id: '2', name:'B'}]
I set getOptionValue = (i) => i.id; and getOptionLabel = (i)=>i.name;
When select the second item onChange(value) is passed the second option as the value argument ({id:'2',name:'B'}) instead of the value of the second option ('2').
This behavior is inconsistent with most input components out there. I would expect onChange to be passed the value of the item, and for the item itself I would expect another event like onItemSelected or something like that.
Also, when I set value={'2'} (controlled component), the component doesn't show the selected item.
I must say that I use AsyncSelect with loadOptions.
How can I make it work with just simple values, instead of option objects?
If this can't happen I have to abandon react-select for another similar component.
AFAIK currently there's no way to make React-Select work internally with just the value. What I'm doing in my application is implementing a layer to retrieve the object going down, and extract the value going up. Something like this (this is simplified, you may need more validation or handling depending on your application):
const Select extends Component {
handleChange(newSelected) {
// this.props.handleChange is your own function that handle changes
// in the upper scope and receives only the value prop of the object
// (to store in state, call a service, etc)
this.props.handleChange(newSelected.value);
}
render() {
// Assuming you get the simple value as a prop from your store/upper
// scope, so we need to retrieve the option object. You can do this in
// getDerivedStateFromProps or wherever it suits you best
const { options, value } = this.props;
const selectedOption = options.find(option => option.value === value)
<ReactSelect
options={options}
value={selectedOption}
onChange={this.handleChange}
{...props}
/>
}