Vue reactive forms components communication - forms

I have multiple form components, each form as a component. Now, I want to use same component for adding data and editing data. So what I am thinking to do is something like when the Post component receives a prop containing data that means it is in a "editing mode" and populate the fields with its data, if not it is in "create mode".
So how should I use v-model in my form fields?
Should I v-model each form field to a computed property (which has a getter and a setter) and the computed property would check if the data prop is empty and if not use its data to populate fields ? And in the computed property set method to update the prop ?
parent component
<post :data.sync="dataObject"></post>
child (Post) component
<template>
<div>
<form>
<input type="text" label="title" v-model="computedTitle" />
<input type="text" label="message" v-model="computedMessage" />
</form>
</div>
<input type="button" #click="submitted"
<template>
<script>
export default {
data(){
return{
post:{
title:null,
message:null
}
}
},
props:["data"],
computed:{
computedTitle:{
get(){
return data ? data.title : ''
},
set(computedTitle){
computedTitle = computedTitle // trying to update computed property value with the field value...
}
},
computedMessage:{...}
}
}
</script>

You can use watch to check data prop, if it's set then set to local post variable.
If created, then data is null, post.title and post.message are set to null
If updated, then data is not null, post.title is set to data.title and post.message to set to data.message
<template>
<div>
<form>
<input type="text" label="title" v-model="post.title" />
<input type="text" label="message" v-model="post.message" />
</form>
</div>
<input type="button" #click="submitted"
<template>
<script>
export default {
data() {
return{
post: {
title: null,
message: null
}
}
},
props:["data"],
watch: {
data: {
handler(newData) {
if (newData) {
this.post = {
title: newData.title,
message: newData.message
}
}
},
immediate: true // this makes watch is called when component created
}
}
}
</script>
Note that you should use immediate: true to make the watch's function called when component is created

Related

How to change the state of a vue when you click the check button

I am creating a Todo Application using Vue.js, Express.js, and MongoDB.
I want to change the state of the fields that appear as v-for through the checkbox.
The code I want is to change the state of the text when the checkbox is checked and to show another element with v-if.
Here is the code I wrote: But it does not work.
If I have the wrong code, please help me.
List Component
<template>
<div class="todos">
<div v-for="todo in todos" v-bind:key="todo._id" class="todo">
<div>
<input type="checkbox" v-model="todo.done" v-on:click="completeTodo(todo)">
<del v-if="todo.done">{{todo.title}}</del>
<strong v-else>{{todo.title}}</strong>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
todos: {}
}
},
created () {
this.$http.get('/api/todos')
.then((response) => {
this.todos= response.data
})
},
methods: {
completeTodo (todo) {
todo.done = !todo.done
}
}
}
</script>
You don't need v-on:click="completeTodo(todo)". v-model is already doing the trick for you. Also, the todos in your code should be defined and instantiated as array not an object.
v-model is used for two way data binding. That means, whatever data you pass from your code will be bound to the checkbox value in this case and whenever a change is made from UI and value of checkbox is altered by user, that will be captured in v-model variable. v-model is a combo of :value prop and #change event in case of checkbox, hence, it is able to update data in both the ways.
Please refer this snippet.
var app = new Vue({
el: '#app',
data: {
todos: [
{
id: 1,
title: 'Study',
done: false
},
{
id: 2,
title: 'Work',
done: false
},
{
id: 3,
title: 'Play',
done: false
}
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="todos">
<div v-for="todo in todos" :key="todo.id">
<div>
<input type="checkbox" v-model="todo.done"/>
<del v-if="todo.done">{{todo.title}}</del>
<strong v-else>{{todo.title}}</strong>
</div>
</div>
</div>
</div>

Get computed property and pass its value while editing another text field vue.js

I want to send over a computed property at the same time as editing a textfield so there is no button to "save" however I cannot work out how to get the property value from another field or from the computed data to pass over.
Here is my code so far, activeNote.id can be viewed in the template no problem but I want to pass its value whenever I type into the textarea
<template>
<div class="editor">
<form id="editForm">
<h2>Edit</h2>
<button #click="closeEdit()">Close</button>
<textarea
v-bind:value="activeNote.text"
#input="editNote"
class="form-control"
></textarea>
<input v-bind:value="activeNote.id" name="id" readonly />
</form>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
methods: {
// not sure this is best practice to dispatch from here
editNote(e) {
this.$store.dispatch('editNote', e)
// activeNote.id doesnt work here unfortunatly
this.$store.dispatch('noteId', activeNote.id)
//console.log(activeNote.id)
}
closeEdit() {
//console.log('emitclose')
this.$emit('closeEdit')
}
},
computed: mapState({
activeNote: state => state.activeNote
})
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
It was simple in the end so fyi the changed code that worked, adding this.
<template>
<div class="editor">
<form id="editForm">
<h2>Edit</h2>
<button #click="closeEdit()">Close</button>
<textarea
v-model="activeNote.text"
#input="editNote"
class="form-control"
></textarea>
<input v-bind:value="activeNote.id" name="id" readonly />
</form>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
methods: {
editNote(e) {
this.$store.dispatch('editNote', e)
this.$store.dispatch('noteId', this.activeNote.id)
},
closeEdit() {
this.$emit('closeEdit')
}
},
computed: mapState({
activeNote: state => state.activeNote
})
}
</script>

Correct way to pass different v-model values for same parameter name in POST request

FIDDLE
I am assigning v-model values from a form to parameters in an api. There is only one parameter in the api that I have to assign to both inputs i.e.: name. The name is a required field.
Using v-model values of each input, if the first radiobtn is selected, it will return null for the value of name. If the second radiobtn is selected, the user must enter text into the textbox.
I need to check whether the first radio button has been clicked OR the user entered text in the textbox and successfully pass the value of the first radiobtn or the textbox to the same api parameter (name).
How can I do this?
HTML:
<label class="radiogrp"><input type="radio" v-model="picked" name="default_user" value="reg" >Mary</label>
<label class="radiogrp"><input type="radio" v-model="picked" name="new_usr" value="non-reg"><input type="text" v-model="new_user" ></label>
JS:
/* API parameters
name: (string) or (null)
*/
new Vue({
el: '#app',
data: {
picked: Boolean,
new_user: ""
},
/* Two v-model form values for the one property in the API. */
submit_name(){
this.$http.post("https://jsonplaceholder.typicode.com/users",{
name: this.picked || this.new_user})
// ....then() etc.
}
});
You can add #input event which can update the new_usr field on input changes.
Also you can watch picked variable to make sure if you toggle between radio buttons, new_usr should be reset.
function callMe(){
var vm = new Vue({
el : '#root',
data : {picked : '',new_usr:''},
methods: {
callApi(){
let dataApi={}
if(this.new_usr.length){
dataApi.name=this.new_usr;
}else{
dataApi.name=this.picked;
}
console.log('You selected=', dataApi.name)
}
},
watch:{
picked: function (val) {
this.new_usr='';
}
}
})
}
callMe();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.11/dist/vue.js"></script>
<div id='root'>
<div>
<label class="radiogrp">
<input type="radio" v-model="picked" name="default_user" value="reg" >Mary</label>
<label class="radiogrp">
<input type="radio" v-model="picked" name="new_usr" value="non-reg">
<input type="text" #input="evt=>new_usr=evt.target.value" :value="new_usr" > </label>
<button #click="callApi">hit me</button>
<span>Picked: {{ new_usr }}</span>
</div>
</div>

How to validate that at least one checkbox should be selected?

I want to do validation for checkboxes here without form tag. At least one checkbox should be selected.
<div *ngFor="let item of officeLIST">
<div *ngIf=" item.officeID == 1">
<input #off type="checkbox" id="off" name="off" value="1" [(ngModel)]="item.checked">
<label>{{item.officename}}</label>
</div>
<div *ngIf="item.officeID== 2">
<input #off type="checkbox" id="off" name="off" value="2" [(ngModel)]="item.checked">
<label>{{item.officename}}</label>
</div>
<div *ngIf="item.officeID== 3">
<input #off type="checkbox" id="off" name="off" value="3" [(ngModel)]="item.checked">
<label>{{item.officename}}</label>
</div>
</div>
for other field I will put required and do the error|touched|valid etc. but since checkbox is not single input, I cannot put required in every checkbox because all checkbox will be compulsory to checked. so how do I do the validation to alert user atleast one should be checked?
The accepted answer abuses stuff to use in a way they are not meant to be. With reactive forms the best, easiest and probably right way is to use a FormGroup that holds your grouped checkboxes and create a validator to check if at least one(or more) checkbox is checked within that group.
To do so just create another FormGroup inside your existing FormGroup and attach a validator to it:
form = new FormGroup({
// ...more form controls...
myCheckboxGroup: new FormGroup({
myCheckbox1: new FormControl(false),
myCheckbox2: new FormControl(false),
myCheckbox3: new FormControl(false),
}, requireCheckboxesToBeCheckedValidator()),
// ...more form controls...
});
And here is the validator. I made it so you can even use it to check if at least X checkboxes are checked, e.g. requireCheckboxesToBeCheckedValidator(2):
import { FormGroup, ValidatorFn } from '#angular/forms';
export function requireCheckboxesToBeCheckedValidator(minRequired = 1): ValidatorFn {
return function validate (formGroup: FormGroup) {
let checked = 0;
Object.keys(formGroup.controls).forEach(key => {
const control = formGroup.controls[key];
if (control.value === true) {
checked ++;
}
});
if (checked < minRequired) {
return {
requireCheckboxesToBeChecked: true,
};
}
return null;
};
}
In your template don't forget to add the directive 'formGroupName' to wrap your checkboxes. But don't worry, the compiler will remind you with an error-message if you forget. You can then check if the checkbox-group is valid the same way you do on FormControl's:
<ng-container [formGroup]="form">
<!-- ...more form controls... -->
<div class="form-group" formGroupName="myCheckboxGroup">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" formControlName="myCheckbox1" id="myCheckbox1">
<label class="custom-control-label" for="myCheckbox1">Check</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" formControlName="myCheckbox2" id="myCheckbox2">
<label class="custom-control-label" for="myCheckbox2">At least</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" formControlName="myCheckbox3" id="myCheckbox3">
<label class="custom-control-label" for="myCheckbox3">One</label>
</div>
<div class="invalid-feedback" *ngIf="form.controls['myCheckboxGroup'].errors && form.controls['myCheckboxGroup'].errors.requireCheckboxesToBeChecked">At least one checkbox is required to check</div>
</div>
<!-- ...more form controls... -->
</ng-container>
*This template is very static. Of course you could create it dynamically by using an additional array that holds the the form-data(key of FormControl, label, required, etc.) and create the template automatically by use of ngFor.
Please don't abuse hidden FormControl's like in the accepted answer. A FormControl is not meant to store data like id, label, help-text etc. and doesnt even have a name/key. All this, and much more, should be stored separate, e.g. by a regular array of objects. A FormControl only holds an input-value and provides all this cool state's and functions.
I created a working example you can play with: https://stackblitz.com/edit/angular-at-least-one-checkbox-checked
consider creating a FormGroup which contains your check-box group and bind the group's checked value to a hidden formcontrol with a required validator.
Assume that you have three check boxes
items = [
{key: 'item1', text: 'value1'}, // checkbox1 (label: value1)
{key: 'item2', text: 'value2'}, // checkbox2 (label: value2)
{key: 'item3', text: 'value3'}, // checkbox3 (label: value3)
];
Step1: define FormArray for your check boxes
let checkboxGroup = new FormArray(this.items.map(item => new FormGroup({
id: new FormControl(item.key), // id of checkbox(only use its value and won't show in html)
text: new FormControl(item.text), // text of checkbox(show its value as checkbox's label)
checkbox: new FormControl(false) // checkbox itself
})));
*easy to show via ngFor
Step2: create a hidden required formControl to keep status of checkbox group
let hiddenControl = new FormControl(this.mapItems(checkboxGroup.value), Validators.required);
// update checkbox group's value to hidden formcontrol
checkboxGroup.valueChanges.subscribe((v) => {
hiddenControl.setValue(this.mapItems(v));
});
we only care about hidden control's required validate status and won't show this hidden control in html.
Step3: create final form group contains below checkbox group and hidden formControl
this.form = new FormGroup({
items: checkboxGroup,
selectedItems: hiddenControl
});
Html Template:
<form [formGroup]="form">
<div [formArrayName]="'items'" [class.invalid]="!form.controls.selectedItems.valid">
<div *ngFor="let control of form.controls.items.controls; let i = index;" [formGroup]="control">
<input type="checkbox" formControlName="checkbox" id="{{ control.controls.id.value }}">
<label attr.for="{{ control.controls.id.value }}">{{ control.controls.text.value }}</label>
</div>
</div>
<div [class.invalid]="!form.controls.selectedItems.valid" *ngIf="!form.controls.selectedItems.valid">
checkbox group is required!
</div>
<hr>
<pre>{{form.controls.selectedItems.value | json}}</pre>
</form>
refer this demo.
I had the same problem and this is the solution I ended up using with Angular 6 FormGroup because I had few checkboxes.
HTML
Note: I'm using Angular Material for styling, change as needed.
<form [formGroup]="form">
<mat-checkbox formControlName="checkbox1">First Checkbox</mat-checkbox>
<mat-checkbox formControlName="checkbox2">Second Checkbox</mat-checkbox>
<mat-checkbox formControlName="checkbox3">Third Checkbox</mat-checkbox>
</form>
TypeScript
form: FormGroup;
constructor(private formBuilder: FormBuilder){}
ngOnInit(){
this.form = this.formBuilder.group({
checkbox1: [''],
checkbox2: [''],
checkbox3: [''],
});
this.form.setErrors({required: true});
this.form.valueChanges.subscribe((newValue) => {
if (newValue.checkbox1 === true || newValue.checkbox2 === true || newValue.checkbox3 === true) {
this.form.setErrors(null);
} else {
this.form.setErrors({required: true});
}
});
}
Basically, subscribe to any changes in the form and then modify the errors as needed according to the new form values.
On validation (i.e for example some click event) iterate over your array and check whether at least one item is true.
let isSelected: any = this.officeLIST.filter((item) => item.checked === true);
if(isSelected != null && isSelected.length > 0) {
//At least one is selected
}else {
alert("select at least one");
}
Add (ngModelChange)="onChange(officeLIST)" to your checkbox and have below code in your .ts file.
onChange(items) {
var found = items.find(function (x) { return x.checked === true; });
if (found)
this.isChecked = true;
else
this.isChecked = false;
}
Use isChecked variable any places you want.
I implemented a similar solution to the current accepted version proposed by Mick(using FormGroup and a custom Validator), but if you're like me and aren't going to need to handle showing an error for quantities checked > 0, you can simplify the Validator a lot:
export function checkboxGroupValidator(): ValidatorFn {
return (formGroup: FormGroup) => {
const checkedKeys = Object.keys(formGroup.controls).filter((key) => formGroup.controls[key].value);
if (checkedKeys.length === 0) { return { requireCheckboxesToBeChecked: true }; }
return null;
};
}
You should be checking the touched and dirty conditions of the form element
<form #myForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
required name="name" [(ngModel)]="myform.name"
#name="ngModel" >
<div *ngIf="name.errors && (name.dirty || name.touched)"
class="alert alert-danger">
<div [hidden]="!name.errors.required">
Name is required
</div>
</div>
</div>
</form>
You can combine the previous and my answer for both scenarios

VueJS with Large Forms

I have a huge form with 20+ fields. i feel so much redundancy on the code i write now. What is the best way ?
<script>
new Vue({
data : {
user : {
first_name : "",
last_name : "",
username : "",
and 20+.........
}
}
});
</script>
<form>
<input name="first_name" v-model="first_name">
<input name="last_name" v-model="last_name">
<input name="username" v-model="username">
and 20+......... input fields
</form>
i feel something like this would be nice. the user object will be created dynamically.. is this possible ?
<script>
new Vue({
data : {
user : Object
}
});
</script>
<form v-model="user">
<input name="first_name">
<input name="last_name">
<input name="username">
and 20+......... input fields
</form>
Thank you in advance
Completely Redone in Vue 2
Your approach is the reverse of the usual Vue approach, in that you want to lay out your data structure in the view and have Vue pick it up, rather than laying it out in the data and having Vue render it. I can see how that would be desirable if you have a customized layout you want to achieve.
Unconventional needs require unconventional solutions, so this approach is unconventional. In particular, it is not generally recommended that a child component modify data in a parent.
That said, the approach is to create a component that will accept the form inputs as a slot and the parent object as a prop. In mounted, it gets all the input fields with name attributes and
Creates the member in the parent object, using $set
Sets a watch on the newly-created member
Adds an input event listener to complete the two-way binding
You would probably want to add more props to the component, to make the form itself more versatile, but this gives you the functionality you're looking for.
Vue.component('autoBindingForm', {
template: '<form><slot></slot></form>',
props: ['createIn'],
mounted() {
const inputs = this.$el.querySelectorAll('input[name]');
for (const i of inputs) {
this.$set(this.createIn, i.name, i.value);
this.$watch(`createIn.${i.name}`, (newVal, oldVal) => {
i.value = newVal;
});
i.addEventListener('input', (e) => {
this.createIn[i.name] = i.value;
});
}
}
});
const vm = new Vue({
el: '#app',
data: {
user: {}
}
});
// Testing that binding works both ways
setTimeout(() => {
vm.user.last_name = 'Set it';
}, 800);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<auto-binding-form :create-in="user">
<input name="first_name" value="hi">
<input name="last_name">
<input name="username">
<div>{{Object.keys(user)}}</div>
<div>{{Object.values(user)}}</div>
</auto-binding-form>
</div>
How about
data: {
fields: { name: {v:''}, surname: {v:''}, ... }
}
and
<input v-for="(val, prop) in fields" :name="prop" v-model="val.v" />
?
https://jsfiddle.net/gurghet/dhdxqwjv/1/