How to refactor repetitive attributes in Vue.js - vue.js

Suppose I have a form and many fields in it. I want to subscribe to change for each form field. I will have to add #change="doSome" for every field. If I have many fields it gets somewhat repetitive. How do I refactor it?

You can listen for the change event on the form tag itself instead of listening on the individual inputs.
<form #change="doSomething"> will run the function doSomething() when something inside the form has changed eg: if you type in an input and release focus
In the doSomething function, you want to find out what element changed. We get this info from the event parameter provided from the input event:
methods: {
doSomething(event) {
this.lastEvent = event.target.value;
}
}
You can see this in effect on this Codepen example
If the form element is a child of an element inside a component like so:
<template>
<div>
<form></form>
</div>
</template>
the #changeevent-listener will not work as there is nothing that changes on the root element (div) on the component.
In this case, we need to add the .native modifier, like so: #change.native="doSomething".

Related

Vue seems to be calling all custom directives more often than expected. Why, can this be avoided?

I'm only a few months into vue coming from an angularjs background.
I built my first custom directive and it's acting a little odd to me.
Vue.directive('silly',{
componentUpdated: function (el, binding, vnode) {
console.log("it was called");
}
});
and I place it on my form like this:
<form id="opt-cpmt-form" method="post" class="mri-grid mri-p-none">
<label for="one">name<input id="one" type="text" v-model="local.name" v-silly class="form-control"></label><br/>
<label for="two">phone<input v-isnumeric id="two" type="text" v-model="local.phone" class="form-control "></label><br/>
<label for="two">zip<input id="three" type="text" v-model="local.zip" class="form-control" ></label><br/>
</form>
It kinda works...the part that I didn't understand is that my v-silly directive is called when any of the other fields are updated too. It seems to be related to the model but I only want my directive called when the field changes.
Any ideas??
It's an expected behaviour as the component updates whenever a piece of its data object is updated. To not trigger the logic too many times, you can create an event listener when the directive is bound to its parent and then run the logic when a desired event happens.
Vue.directive('silly', {
bind(el) {
this.updateCallback = function(event) {
// Your logic
};
el.addEventListener('input', this.updateCallback);
},
unbind(el) {
el.removeEventListener('input', this.updateCallback);
}
});
In case you plan to listen to the changes of v-model directive, bear in mind that it uses different events based on what element it's bound to. You can read more about that topic in v-model documentation.
v-model internally uses different properties and emits different events for different input elements:
text and textarea elements use value property and input event;
checkbox and radiobutton inputs use checked property and change event;
select fields use value as a prop and change as an event.
Also, from my experience when it comes to the form validation; I've done it using the directives and regretted it afterwards. I found it best to create reusable functions and create the custom form validation for every form. See custom form validation for more.

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-Bootstrap: how to trigger sorting of one b-table to trigger sorting of the another b-table?

I'm using VueBoostrap <b-table> components, in a combination with sorting routine applied. In my project I have some more complex sorting routine, but for this dummy example, I will use the default one.
When a sorting is applied for a b-table, the table is simply sorted based on the clicked field in the table header. But what I need to achieve is to split the table header from the table content (since I want to place the content in a scrollable div later, while the header to remain static at the top - as user will be scrolling).
The complete code is given on this link (check componets/TableTest.vue), where I have three <b-table> components. First one is just a dummy example, and the next two are identical as the first one, but for one of them the header is hidden and for the other the body is hidden.
What I want to achieve is:
If you take a close look at the docs (https://bootstrap-vue.js.org/docs/components/table) you can see that there are certain events emitted by the <b-table> component.
One of them is sort-changed. So if you listen for that on your header only component and then set a sortBy property that you pass into the body only component you're all set.
//header only
<b-table ... #sort-changed="sortChanged">
// body only
<b-table :sort-by="sortBy" ...>
sortChanged(e) {
this.sortBy = e.sortBy
}
Full example here: https://codesandbox.io/s/y30x78oz81
As I understand it, the OP is asking :
"How do I force a <b-table> component to (re)sort itself without requiring the user to click on that <b-table> component?"
My answer (in their case):
Detect the click event on the "visible header" mentioned
In the function handler for that click event, emit a sort-changed event from the target table
// Assuming an SFC (single file component)
<template>
<div>
<b-button #click="handleClick">Sort the table!</b-button>
<b-table ref="mytable" #sort-changed="handleSortChange"></b-table>
</div>
</template>
<script>
export default {
// ...
methods: {
handleClick(evt) {
/// this is called when you click the button
this.$refs.mytable.$emit('sort-changed')
}
handleSortChange(context) {
// this is called when b-table with ref "mytable" hears the 'sort-changed' event
// that it has emitted
// sorting logic goes here
}
}
//...
}
</script>

How to add a click function to an imported component in Vue

So I have a Vue2 app. I have create a component "u-button"
when i import this and use it in another component, I want to be able to add a click function to it. However at the moment it looks for a function on the u-button component rather than the component it is being used in.
so for example, in the below if i click the first button nothing happens, if i click the second button i get the console log.
<template>
<div>
<u_button #click="clicked">Click me</u_button>
<button #click="clicked">Click me</button>
</div>
</template>
<script>
import u_button from '../components/unify/u_button'
export default {
components: {
u_button
},
methods: {
clicked() {
console.log("Working!");
}
}
}
</script>
However if i add a method on the u-button component, then it calls that. So how can i get my below example to work ? The only thing I can think of is to wrap it in another div and add the click function to that. but I'm wondering if there is a better way?? I dont want to use events to do this either as that gets messy very quickly.
As you can imagine having a reusable button that when clicked always performs the same function is a bit pointless.
It's because of the nature of components for example if we had a (virtual) iframe component which had a button in it and we'd like to detect click event on it we might name the event click and listen for it in the parent component; therefore, Vue introduced a feature called event modifiers for example in Vue, We have .native modifier (you can read more about the Vue modifiers here)
Now, to make your code work, You should add a .native after #click like this:
<u_button #click.native="clicked">Click me</u_button>
By the way, it's better to develop a naming convention for yourself It'd become handy when your projects get larger.

VueJS find element by key

I've just wanted to know if it is possible to find a DOM element by the key attribute of vue?
I'm currently working with a list. I'm displaying it via v-for directive on a div. I'm binding the key with the index of the elements.
v-for="(apk, index) in project.apks" v-bind:key="index"
It would really help me if i could compute something for each of these elements as soon as they are fetch from my server and displayed. It's just parsing a file and looking for keyword, and accordingly choosing a css class for the items.
The problem is I dont know how to call a method for each of these elements as soon as they are added to the DOM. They are a lot of html events but i couldnt find one representing the object beeing inserted to dom :(
The purpose of key is not selecting element. Even if it can be done, don't do it.
The proper way to do it is by using ref.
for example, add ref attribute to your html like this
v-for="(apk, index) in project.apks" v-bind:key="index" :ref="'sample-ref-'+index"
and then in your methods, you can get the DOM using this.$refs['sample-ref-0'],this.$refs['sample-ref-1'] and so on.
Hope this helps.
I found that if you give the 'ref' the same name in a v-for, like this:
<div v-for="data in list" :key="data.id" ref="bar"></div>
Then you will find they just store in an array called 'bar' and you can visit them by:
this.$refs['bar'][index]
something like this could allow you to find a component by key :
this.$children.forEach(child=>{
print("child.$vnode.key")
})
also use mounted , as it gets called when the component is added to the dom:
mounted:function(){
this.$el.querySelector("#ele1")...
}
The problem is I dont know how to call a method for each of these elements as soon as they are added to the DOM. They are a lot of html events but i couldnt find one representing the object beeing inserted to dom :(
You can create a new component with your v-for and just call the created() hook.
Example
/* On your main function */
<div v-for="apk in project.apks">
<apk :data="apk"></apk>
</div>
/* On your 'apk' component */
export default {
props: [ "data" ],
created() {
console.log("Created !");
}
}