Right way to send data to nested custom input component - javascript

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>

Related

Vue3 v-for + component => triggers lots of renders

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

How to access "this" component in Vue 3 #click event?

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
}
}

Svelte bind is not working when customElement: true is set

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>

'this.$refs' is always empty in parent component

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.

Can I 'observe' the dynamic children of a component?

I have an application in which I compose forms using several form components. Now I want to create some kind of a foreign key field which lists the already existing objects and also shows an 'add' button. This button displays another form component in a modal dialog. That 'sub' form is simply displayed using <ng-content>. This all works perfectly
I'll illustrate the situation. This is the template of <form-component>:
<form class="form">
<form-field>
<another-form-component (save)="handleSave()"></another-form-component>
</form-field>
</form>
The template of <form-field>:
<modal-dialog-component [(visible)]="showForm">
<!--
Here the <another-form-component> which was passed
to <form-field> is added:
-->
<ng-content></ng-content>
</modal-dialog-component>
<div class="form-field">
<select> <!-- existing options --> </select>
<button (click)="openForm($event);">Create new</button>
</div>
As you can see <another-form-component> has an #Output() for it's save event. This is an EventEmitter.
My question is: How can I subscribe to this EventEmitter from the <form-field> component? I would like to know when the form is saved so I can close the <modal-dialog>.
Remember: The form is passed using <ng-content>. Can I use #ViewChildren to get the children of <form-field> and use some sort of addEventListener() method? Does something like that even exist?
Hope you can help me!
Greetings,
Johan
You can query ContentChildren from your <form-field> component class and subscribe to events they emit as following:
export class FormFieldComponent implements AfterContentInit {
#ContentChildren(AnotherFormComponent) anotherFormComponents: QueryList<AnotherFormComponent>;
ngAfterContentInit() {
console.log('anotherFormComponents: ', this.anotherFormComponents.toArray());
this.anotherFormComponents.toArray()[0].save.subscribe(valueEmitted => {
console.log('Value emitted from the anotherFormComponents[0]: ', valueEmitted);
});
}
}
QueryList gets updated whenever the AnotherFormComponent is added, removed, or moved. You can 'observe' the changes by subscribing to the QueryList.changes Observable:
ngAfterContentInit() {
this.anotherFormComponents.changes.subscribe(qList => {
console.log('Changed list: ', qList);
});
}
Btw, it's worth knowing: What's the difference between #ViewChild and #ContentChild?

Categories