Changing value of outer variable used in props after emitted listener from component disables v-model listener effects in Vue.js - 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".

Related

Is is possible to call watch() on an input ref in Vue 3 to watch the input's value attribute?

I know that we can use the v-model directive on an <input> element and use watch() to trigger a function when the state of the <input>'s value attribute changes.
I'm trying, however, to watch an <input> ref. When I do, the watcher's function is executed when the element is mounted to the DOM, but does not trigger when the <input>'s value attribute changes.
Am I doing something wrong?
<script setup>
import { ref, watch, watchEffect } from 'vue'
const refA = ref(null)
watch(refA, () => {
console.log('refA changed')
console.log(refA)
console.log(refA.value)
console.log(refA.value.value)
}, { deep: true })
</script>
<template>
<input ref="refA" type="text" value="test" /> <br />
</template>
From what I know, template refs can be watched, but they only fire when the .value changes - that's the value of the ref, i.e. the HTML element, not the value of a HTML input element. That fits with what you describe, the watcher firing once at mount and never again. Also, it is not possible to deep-watch a HTML element.
It is fascinating to me how different template refs and data felt in Vue 2, when apparently the mechanism on script-side was always the same, which is now made obvious in Vue 3 composition API. But they still do different things, where one gives access to a HTML element or a Vue component instance, and the other stores a value. I guess the big difference comes from using a ref in the ref attribute, not from the way it is declared.
Anyway, if you want to bind to the value of an input, use v-model (or v-bind:modelValue and #update:modelValue), but if you need access to the node for whatever reason, use ref=. If you want to do both, you need to use both, it is not possible to use just the template ref.
I am not sure what you are trying to achieve. if you don't need to use v-model then you should use v-bind and emit the input event, like the below.
<template>
<input :value="refA" type="text" #input="refA = $event.target.value" /> <br />
</template>
This will trigger watch on every update to your input.
A template ref which you have used as part of your example cannot be used to watch the internal value of the component.See below comments thread regarding the same.

How can I update data object whenever changes happens inside v-for of child

How can I update data object of parent whenever changes happen inside v-for. I have a child component that I use inside parent component.
ParentComponent.vue
<template>
....
....
<child-component
v-for="i in count"
ref="childComponent"
:key="i"
currentPage="i" // currentPage doesn't update.
:page="i"
/>
<q-header>
{{currentPage}} // always displays default value:1
</q-header>
</template>
<script>
data () {
return {
pageCount: 10,
currentPage: 1,
}
},
How can I update currentPage of data object whenever i changes inside v-for. I have tried using watch without much luck. I don't have access to child component and can't modify it.
Much appreciated!
There is some slight confusion with how v-for is working on the child-component here. Writing currentPage="i" as a property (which should actually be v-bind:currentPage in order for the i to be interpreted as JS) will simply declare the attribute on each child-component
How can I update currentPage of data object whenever i changes inside v-for
i doesn't "change" in the traditional context of running a for loop inside of a normal JavaScript application. In Vue, your rendering logic and application logic are separate, and rightly so, because running logic as part of the rendering doesn't really make sense.
For example, let's look at how your app will render the child-component:
<!-- Vue output -->
<child-component ... currentPage="1" />
<child-component ... currentPage="2" />
<child-component ... currentPage="3" />
So let's look at separating the rendering logic from the application logic.
I realise you don't have access to child-component, but based on the context I will assume it is some kind of tabbing functionality (based on you trying to set a value for the "current page" - feel free to be more specific and I can update my answer).
We need to bridge that gap between the rendering logic and the application logic and we can do that by using events:
<child-component
v-for="i in count"
:ref="`childComponent-${i}`" // ref should be unique so add the i as part of it
:key="i"
:page="i"
v-on:click="currentPage = i" // when the user clicks this child component, the current page will be updated
/>
You may have to utilise a different event other than click but I hope this gets you closer to what you are trying to achieve. For the value of currentPage to update there has to be some kind of user input, so just find out which event makes the most sense. Maybe the child-component library you are using has custom events that are more appropriate.
you should look into Custom Events.
https://v2.vuejs.org/v2/guide/components-custom-events.html
Idea is, that whenever there is some update of your desire in child component, you can execute this.$emit(“change”), which will throw an event.
On parent side you can catch this event by #change=“myMethod” as one of the attributes.
methods: {
myMethod() {
console.log("Testing")
}
}
<child-component
v-for="i in count"
ref="childComponent"
:key="i"
currentPage="i"
:page="i"
#change=“myMethod”
/>
Let me know if that helped.

Watching a dynamically rendered field in Laravel Nova Vue component

In Laravel Nova, action modals are rendered in Vue by retrieving a list of fields to display through a dynamic component. I have replaced the action modal with own custom component, but am struggling to achieve the effect I want without also extending the entire set of components for rendering form fields.
I have my CustomResourceIndex.vue, containing a conditionally loaded (via v-if) ActionModal.vue, in which the form fields are rendered like so:
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:resource-name="resourceName"
:field="field"
/>
</div>
where the actual form field component is chosen based on the field.component value.
Those form fields (which I ideally do not want to have to extend and edit) are rendered like so:
<template>
<default-field :field="field" :errors="errors">
<template slot="field">
<input
class="w-full form-control form-input form-input-bordered"
:id="field.attribute"
:dusk="field.attribute"
v-model="value"
v-bind="extraAttributes"
:disabled="isReadonly"
/>
</template>
</default-field>
</template>
I would like to watch the value of specific fields and run methods when they change. Unfortunately due to a lack of ref attribute on the input elements or access to the value that the form element is bound to, I'm not sure how I can accomplish that from within ActionModal.vue.
I am hoping that because I have access to the ids still, there is some potential way for me to emulate this behavior.
Many resources I've found on my own have told me that anything with an ID is accessible via this.$refs but that does not seem to be true. I can only see elements that have an explicitly declared ref attribute in this.$refs, so I am not sure if I've misunderstood something there.
I would recommend looking into VueJS watch property.
You can listen to function calls, value changes etc.
watch: {
'field.component': function(newVal, oldVal) {
console.log('value changed from ' + oldVal + ' to ' + newVal);
},
},
Are those components triggering events? Try looking into the events tab of the Vue DevTools to see if some events are triggered from the default-field component when you update the value.
My guess is that you could write something like:
<div class="action" v-for="field in action.fields" :key="field.attribute">
<component
:is="'form-' + field.component"
:resource-name="resourceName"
:field="field"
#input="doSomething($event)"
/>
</div>
The $event value being the new value of the field.
Hit me on the comments if you have more info on the behavior of the default form fields (Are their complete code accessible somewhere?).

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.

Vue two way prop binding

Below is my current structure (which doesn't work).
Parent component:
<template>
<field-input ref="title" :field.sync="title" />
</template>
<script>
import Field from './input/Field'
export default {
components: {
'field-input': Field
},
data() {
return {
title: {
value: '',
warn: false
}
}
}
}
</script>
Child component:
<template>
<div>
<input type="text" v-model="field.value">
<p v-bind:class="{ 'is-invisible' : !field.warn }">Some text</p>
</div>
</template>
<script>
export default {
props: ['field']
}
</script>
The requirements are:
If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
If the child's <input> is updated (field.value), then the parent's title.value should be updated.
What's the cleanest working solution to achieve this?
Don't bind the child component's <input> to the parent's title.value (like <input type="text" v-model="field.value">). This is a known bad practice, capable of making your app's data flow much harder to understand.
The requirements are:
If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
This is simple, just create a warn prop and pass it from parent to child.
Parent (passing the prop to the child):
<field-input ref="title" :warn="title.warn" />
Child/template (using the prop -- reading, only):
<p v-bind:class="{ 'is-invisible' : !warn }">Some text</p>
Child/JavaScript (declaring the prop and its expected type):
export default {
props: {warn: Boolean}
}
Notice that in the template it is !warn, not !title.warn. Also, you should declare warn as a Boolean prop because if you don't the parent may use a string (e.g. <field-input warn="false" />) which would yield unexpected results (!"false" is actually false, not true).
If the child's <input> is updated (field.value), then the parent's title.value should be updated.
You have a couple of possible options here (like using .sync in a prop), but I'd argue the cleanest solution in this case is to create a value prop and use v-model on the parent.
Parent (binding the prop using v-model):
<field-input ref="title" v-model="title.value" />
Child/template (using the prop as initial value and emitting input events when it changes):
<input type="text" :value="value" #input="$emit('input', $event.target.value)">
Child/JavaScript (declaring the prop and its expected type):
export default {
props: {value: String}
}
Click here for a working DEMO of those two solutions together.
There are several ways of doing it, and some are mentioned in other answers:
Use props on components
Use v-model attribute
Use the sync modifier (for Vue 2.0)
Use v-model arguments (for Vue 3.0)
Use Pinia
Here are some details to the methods that are available:
1.) Use props on components
Props should ideally only be used to pass data down into a component and events should pass data back up. This is the way the system was intended. (Use either v-model or sync modifier as "shorthands")
Props and events are easy to use and are the ideal way to solve most common problems.
Using props for two-way binding is not usually advised but possible, by passing an object or array you can change a property of that object and it will be observed in both child and parent without Vue printing a warning in the console.
Because of how Vue observes changes all properties need to be available on an object or they will not be reactive.
If any properties are added after Vue has finished making them observable 'set' will have to be used.
//Normal usage
Vue.set(aVariable, 'aNewProp', 42);
//This is how to use it in Nuxt
this.$set(this.historyEntry, 'date', new Date());
The object will be reactive for both component and the parent:
I you pass an object/array as a prop, it's two-way syncing automatically - change data in the
child, it is changed in the parent.
If you pass simple values (strings, numbers)
via props, you have to explicitly use the .sync modifier
As quoted from --> https://stackoverflow.com/a/35723888/1087372
2.) Use v-model attribute
The v-model attribute is syntactic sugar that enables easy two-way binding between parent and child. It does the same thing as the sync modifier does only it uses a specific prop and a specific event for the binding
This:
<input v-model="searchText">
is the same as this:
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
Where the prop must be value and the event must be input
3.) Use the sync modifier (for Vue 2.0)
The sync modifier is also syntactic sugar and does the same as v-model, just that the prop and event names are set by whatever is being used.
In the parent it can be used as follows:
<text-document v-bind:title.sync="doc.title"></text-document>
From the child an event can be emitted to notify the parent of any changes:
this.$emit('update:title', newTitle)
4.) Use v-model arguments (for Vue 3.0)
In Vue 3.x the sync modifier was removed.
Instead you can use v-model arguments which solve the same problem
<ChildComponent v-model:title="pageTitle" />
<!-- would be shorthand for: -->
<ChildComponent :title="pageTitle" #update:title="pageTitle = $event" />
5.) Use Pinia (or Vuex)
As of now Pinia is the official recommended state manager/data store
Pinia is a store library for Vue, it allows you to share a state across components/pages.
By using the Pinia store it is easier to see the flow of data mutations and they are explicitly defined. By using the vue developer tools it is easy to debug and rollback changes that were made.
This approach needs a bit more boilerplate, but if used throughout a project it becomes a much cleaner way to define how changes are made and from where.
Take a look at their getting started section
**In case of legacy projects** :
If your project already uses Vuex, you can keep on using it.
Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.