Vue js dynamic update set/unset class - vue.js

I have few inputs with submit button. Have some validation logic that adds 'has-error' class to input. How can i unset this class on focus?
Template:
<div class="input-styled badge-icon" :class="{ 'has-error': errors.email}">
<input type="text" #focus="delete errors.email" v-model="email" placeholder="example#gmail.com">
</div>
<button #click="submit">Submit</button>
JS
data() {
return {
errors: {},
email: ''
}
},
methods: {
submit(){
this.errors = {};
if(!this.email){
this.errors.email = 'Something';
}
}
}
I'm trying delete error property, trying #focus='errors.email="" ', but class 'has-error' disappears only when i'm typing something on inputs. #focus event works and i think that i should call some function that will update my DOM?

It is a good practice to move operations on component's data to functions. You can achieve desired validation reset, by creating a resetValidation function and binding it to focus event on input field.
Method itself should reset errors field to falsy values. Example below assumes, there are multiple input fields in the form. Each field should call resetVlidation method with corresponding error field name. If no field is provided, we can reset validation as whole:
resetValidation (field) {
if (field) {
this.errors = {
...this.errors,
[field]: ''
}
} else {
this.errors = {}
}
Please, check the working example below:
codesandbox

Related

Vue search while typing

I have search field and I wish to have the results in real-time,
I have no issue with returning data or showing data but I need a way to send input value to back-end while user is typing it.
Code
HTML
<el-input placeholder="Type something" v-model="search">
<i slot="prefix" class="el-input__icon el-icon-search"></i>
</el-input>
Script
data() {
return {
search: '' // getting data of input field
}
},
I've tried to compute it but it didn't return data
computed : {
searchSubmit: {
get: function () {
console.log(this.search);
}
}
},
Any idea?
For side effects as calling backend you can use Watchers.
watch: {
search(value) {
yourApiCall(value)
}
}

how to validate both input fields when one updates

I am trying to validate both input fields when one changes its value. This is required because otherwise the validation between those two fields would not work properly.
I created an example to reproduce the problem, the html code should be self explanatory
<div id="app">
<v-app id="inspire">
<v-text-field
:value="values[0]"
:rules="firstRules"
#input="setFirstValue"
></v-text-field>
<v-text-field
:value="values[1]"
:rules="secondRules"
#input="setSecondValue"
></v-text-field>
</v-app>
</div>
It is important to note that a v-model is not possible because this component takes in the values as a prop and passes the updated values back to the parent via emitting update events.
The vue instance:
new Vue({
el: '#app',
data () {
return {
values: [5345, 11],
firstRules: [true],
secondRules: [true]
}
},
created: function () {
this.setFirstValue(this.values[0])
this.setSecondValue(this.values[1])
},
computed: {
firstValidation: function () {
return [value => value.length < this.values[1].length || "Must have less characters than second value"]
},
secondValidation: function () {
return [value => value.length > this.values[0].length || "Must have more characters than first value"]
}
},
methods: {
setFirstValue: function (newValue) {
this.values[0] = newValue
this.firstRules = this.validateValue(this.values[0], this.firstValidation)
this.secondRules = this.validateValue(this.values[1], this.secondValidation)
},
setSecondValue: function (newValue) {
this.values[1] = newValue
this.secondRules = this.validateValue(this.values[1], this.secondValidation)
this.firstRules = this.validateValue(this.values[0], this.firstValidation)
},
validateValue: function (value, rules) {
for (const rule of rules) {
const result = rule(value)
if (typeof result === 'string') {
return [result]
}
}
return [true]
}
}
})
On "start" the rules return a valid state but I want to validate both fields when loading the component (created hook?) to update this state immediately.
I have to put the validation rules to the computed properties because they have to access the current values. Otherwise they would validate old values.
Each input event will validate both fields and updates the rules state.
I created an example to play around here
https://codepen.io/anon/pen/OeKVME?editors=1010
Unfortunately two problems come up:
The fields are not validated directly at the beginning
when changing one input field to a valid state the rules will still return an error message
How can I setup a validation for both fields when one field updates?
Seems like you're overthinking things.
By default, a vuetify input's validation logic only triggers when the value bound to that input changes. In order to trigger the validation for the other input, you can wrap both inputs in a v-form component and give it a ref attribute. That way, you'll have access to that component's validate method, which will trigger the validation logic for any inputs inside the form.
The template would look something like this:
<v-form ref="form">
<v-text .../>
<v-text .../>
</v-form>
And to trigger the validation in your script:
mounted() {
this.$refs.form.validate();
}
The above will validate the form when the component is mounted, but you'll also need to trigger the validation for both inputs whenever either input's value changes. For this, you can add a watcher to values. However, you'll need to call the form's validate method after Vue has updated the DOM to reflect the change in values.
To do this, either wrap the call in a this.$nextTick call:
watch: {
values() {
this.$nextTick(() => {
this.$refs.form.validate();
});
}
}
Or use an async function and await this.$nextTick:
watch: {
async values() {
await this.$nextTick();
this.$refs.form.validate();
}
}
So now validation will trigger for both inputs when the component is initialized and whenever either value changes. However, if you prefer to keep the validation call in one spot instead of in both the mounted hook and the values watcher, you can make the watcher immediate and get rid of the call in the mounted hook.
So here's the final example:
watch: {
immediate: true,
async handler() {
await this.$nextTick();
this.$refs.form.validate();
}
}
So now the validation logic is triggering when it would be expected to, but there is still one issue with your validation logic. When your component initializes, the values data property is set to an array of Number type values, which don't have a length property. So if, for example, you changed just the first input to "5" and the second input was still 11, then (11).length is undefined and "5".length < undefined is false.
Anyways, you'll need to change the values you're comparing to strings before comparing their lengths. Something like this:
value => (value + '').length < (this.values[1] + '').length
Finally, because you are able to dynamically call validate on the form, there's an opportunity to reduce much of the complexity of your component.
Here's a simplified version:
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data() {
return {
values: [5345, 11]
}
},
computed: {
rules() {
const valid = (this.values[0] + '').length < (this.values[1] + '').length;
return {
first: [() => valid || "Must have less characters than second value"],
second: [() => valid || "Must have more characters than first value"]
};
}
},
watch: {
values: {
immediate: true,
async handler() {
await this.$nextTick();
this.$refs.form.validate();
}
}
}
})
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<div id="app">
<v-app id="inspire">
<v-form ref="form">
<v-text-field v-model="values[0]" :rules="rules.first"></v-text-field>
<v-text-field v-model="values[1]" :rules="rules.second"></v-text-field>
</v-form>
</v-app>
</div>
I have to do like below to make it work.
watch: {
rangeAmuount: {
async handler() {
await this.$nextTick()
if (this.$refs.form) (this.$refs.form as any).validate()
},
deep: true,
},
}
PS: I'm using typescript on Vue2.

How do I go about creating an always capitalized input in vue.js?

I'm creating a form using vue.js and I need to create inputs in vue that is always capitalized. I know I could use the css property
text-transform: uppercase;
and then transform the data before sending using
data.someData.toUpperCase()
But I wonder if there is a more intelligent way of doing that in vue.js. In react we can create controlled inputs and easily do it. Is there anything like that in Vue.js?
I managed to do it using computed fields, however, I would have to create computed getter and setter for each input in the form. Is there a better way of doing it?
You could create a custom directive.
Vue.directive( 'touppercase', {
update (el) {
el.value = el.value.toUpperCase()
},
})
And then use it where you need. For example:
<input type="text" v-model="modelfield" v-touppercase>
Since you don't have a lot of code to run, you should manually bind events to your textfield and then handle the uppercasing there.
Handling events from a text field can be done by adding an input event handler on them, and then updating the initial state again.
<input :value="text" #input="updateText($event.target.value)"/>
export default {
data() {
return {
text: '',
}
},
methods: {
updateText(newValue) {
this.value = newValue.toUpperCase();
},
}
}
You can also do it inline in a template, but this might make it harder to read depending on your code style preferences
<input :value="text" #input="text = $event.target.value.toUpperCase()"/>
This directive works fine with v-model (last character is in upper case too):
Vue.directive('uppercase', {
update(el) {
const sourceValue = el.value;
const newValue = sourceValue.toUpperCase();
if (sourceValue !== newValue) {
el.value = newValue;
el.dispatchEvent(new Event('input', { bubbles: true }));
}
},
});
Usage:
<input type="text" v-model="myField" v-uppercase />
You can do this:
<input :value="theValue" #input="theValue = theValue.toUpperCase()"/>
as a fix for asologor's answer you should reach input element to change it at vuetify
Vue.directive("uppercase", {
update(el) {
const sourceValue = el.getElementsByTagName("input")[0].value
const newValue = sourceValue.toUpperCase()
if (sourceValue !== newValue) {
el.getElementsByTagName("input")[0].value = newValue
el = el.getElementsByTagName("input")[0]
el.dispatchEvent(new Event("input", { bubbles: true }))
}
}
})
and usage is
<v-text-field
type="text"
outlined
placeholder="placeholder"
v-model="name"
prepend-inner-icon="edit"
v-uppercase
/>
this is defnetely working

How to initialize value based on ajax response?

I need to create a checkbox that will be checked/unchecked depending on the value of a parameter coming from the database.
I'm not able to load that value when I'm rendering the page, so the idea is: render the page, "tell" the checkbox to "ask" the server what is the current value of the parameter and then check/uncheck the checkbox depending on the response. Then, if the user checks/unchecks the checkbox, make a new Ajax request to update the value in the database.
I wrote some code (I'm new in Vuejs, so for sure I'm doing something wrong):
var vm = new Vue({
el: '#root',
computed: {
checked() {
return this.initialize()
},
value() {
return this.checked
}
},
watch: {
checked() {
alert('watcher')
this.update();
}
},
methods: {
initialize(){
// Just pretending an initial value
var randomBoolean = Math.random() >= 0.5;
alert('Ajax request here to initialize it as ' + (randomBoolean ? 'checked' : 'unchecked'));
return randomBoolean;
},
update(){
alert('ajax request here to set it to ' + this.value)
}
}
});
You can check and run the code here: https://jsfiddle.net/hyn9Lcv2/
Basically it works to initialize the checkbox, but then it fails to update. If you check the console, there is this error:
[Vue warn]: Computed property "checked" was assigned to but it has no setter.
First have you thought of using the created() hook from the vue instance instead of watcher?
It's recommended and will execute the code as soon as the component is created.
From the doc:
new Vue({
data: {
a: 1
},
created: function () {
//Ajax call:
//onsuccess(response){
this.a = reponse.data.a
}
}
})
in the created hook you can do your ajax call, (axios is good library for that, worth checking it out: https://github.com/axios/axios ).
Then from your ajax response you can link the desired value to your checkbox by assigning it to a variable in the data object of the instance (in our case 'a')
Then bind it to your checkbox with the v-model like this:
<input
type="checkbox"
v-model="a">
I recommend to check the vue doc for more info on biding: https://v2.vuejs.org/v2/guide/forms.html#Checkbox-1
Hope it helps.
Just add bind click event
<div id="root">
<input id="check" type="checkbox" name="active" v-model="checked" #click="update">
<label for="check">Click me</label>
</div>
You need to fetch the database value when the component is created or mounted.
You then need to bind your checkbox with the initialized data.
Finally you need to watch the data to send an update to the database.
var vm = new Vue({
el: '#root',
data: {
//Your data
checked: null
},
// Function where you are going to fetch your data
mounted: function () {
console.log("Ajax call to initialize");
this.checked = Math.random() >= 0.5;
},
watch: {
// Watcher to save your data in the database
checked: function(newValue, oldValue){
if (oldValue === null) { return; } // to not make an useless update when data has been fetched
console.log("Ajax call to update value " + newValue);
}
}
});
<div id="root">
<input id="check" type="checkbox" name="active" v-model="checked" :disabled="checked === null">
<label for="check">Click me</label>
</div>
To fetch your data you can use for example Axios that works great with Vue.
To know more about life cycle of a component (to know if you should do the fetching at created or mounted) : https://v2.vuejs.org/v2/guide/instance.html

Only null or Array instances can be bound to a multi-select

I'm building a multi-step form in Aurelia where each page shows one question.
I use the same view for every question, with if statements determining what type of form field to show.
When I try to bind my question data to a multiple select element however, Aurelia throws errors and says "Only null or Array instances can be bound to a multi-select.".
What's really strange is that if the first question is a multiple select I don't get the error until I come to a non-multiselect question and then go back to the multiselect question.
I can solve this entire problem by setting activationStrategy: 'replace' for this route, but I really don't want that.
The important code follows:
import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';
#inject(Router)
export class Form {
constructor (router) {
this.router = router;
this.active = 0;
this.field = null;
this.fields = [
{
type: 'text',
value: null
},
{
type: 'select',
value: [],
options: [
'foo',
'bar'
]
},
{
type: 'select',
value: [],
options: [
'foo',
'bar'
]
},
{
type: 'text',
value: null
},
];
}
activate (routeParams) {
this.active = routeParams.fieldIndex || 0;
this.active = parseInt(this.active);
this.field = this.fields[this.active];
}
prev () {
if (typeof this.fields[this.active - 1] !== 'undefined') {
this.router.navigateToRoute('form', {
fieldIndex: this.active - 1
});
return true;
}
else {
return false;
}
}
next () {
if (typeof this.fields[this.active + 1] !== 'undefined') {
this.router.navigateToRoute('form', {
fieldIndex: this.active + 1
});
return true;
}
else {
return false;
}
}
}
And the template:
<template>
<div class="select" if.bind="field.type == 'select'">
<select value.bind="field.value" multiple="multiple">
<option repeat.for="option of field.options" value.bind="option">${option}</option>
</select>
</div>
<div class="text" if.bind="field.type == 'text'">
<input type="text" value.bind="field.value">
</div>
<a click.delegate="prev()">Previous</a> | <a click.delegate="next()">Next</a>
</template>
But you'll probably want to check out the GistRun: https://gist.run/?id=4d7a0842929dc4086153e29e03afbb7a to get a better understanding.
Try setting the first question to a multiselect and you'll notice the error disappears (until you go back to it). You can also try activationStrategy in app.js like mentioned above.
Why is this happening and how can I solve it?
Also note that in my real app I'm actually using compose instead of ifs but have tried with both and both produce the same error. It almost seems as if the select values are bound before the if is evaluated, causing the error to show up because the text field type lacks the options array.
A little late but I wanted to give a suggestion -- for SELECT multi-selects, you should decouple the bound variable from the multi-selector to prevent those errors.
For example, if you in your custom elements that bind to 'selected', they should bind to:
<select multiple value.two-way="selectedDecoupled">
Then when the actual variable 'selected' changes, it only changes in the custom element if the bound value is an array:
selectedChanged( newV, oldV ){
if( typeof newV =='object' )
this.selectedDecoupled = newV;
else
this.selectedDecoupled = [];
$(this.SELECT).val(this.selectedDecoupled ).trigger('change');
}
Example of it in use with a custom select2 element:
https://github.com/codefreeze8/aurelia-select2
Ok so it turns out swapping the order of the HTML, and putting the select after the input solves this issue.
Jeremy Danyow explains it like this:
When Form.field changes, the bindings subscribing to that property's changes evaluate sequentially. Which means there's a period of time when the select AND the input are both on the page. The html input element coaleses null values to empty string which in turn causes field.value to be empty string, which makes the multi-select throw.
Very tricky to track down imo but I'm glad the Aurelia devs are so helpful over on Github.
Working Gist: https://gist.run/?id=3f88b2c31f27f0f435afe14e89b13d56