Let's say I have a checkout form that contains a child address component as follows
<form [formGroup]="checkoutForm">
<input formControlName="fullName">
/** more inputs **/
<address-form></address-form>
<button type="submit">Submit</button>
</form>
At the moment I have the checkoutForm built like:
this.checkoutForm = this.formBuilder.group({
fullName: ['', Validators.required]
});
The addressForm template is like:
<form [formGroup]="addressForm">
<input formControlName="street">
<input formControlName="town"
</form>
And built like:
this.addressForm = this.formBuilder.group({
street: ['', [Validators.required]],
town: ['', Validators.required]
});
Now the issue I have is I dont know how to
1 - Validate the parent form only when the child form is valid.
2 - Validate the child form when the parent form is submitted.
The only way I could think about it is to have an #Output() addressResponse that will emit on this.addressForm.valueChanges with the validity and data. Something like:
this.addressForm.valueChanges.subscribe(data => {
let form = this.addressForm.valid ?
{ valid: true, value: data }:
{ valid: false, value: data };
this.addressResponse.emit(form);
});
And the parent form component can use this emitted data.
And also have an #Input() parentFormSubmitted that I can use to display the errors in the template of AddressForm
<input formControlName="town"
<div *ngIf="town.hasError('required') && (parentFormSubmitted || town.dirty">Town is a required field</div>
While this would work, I am not sure it is the optimal solution. I was wondering if there a more Reactive Form way of doing things. (Maybe include the AddressForm group in the definition of the CheckoutForm group?...)
You are completely correct with your comment:
I was wondering if there a more Reactive Form way of doing things. (Maybe include the AddressForm group in the definition of the CheckoutForm group?...)
You can build your form in your parent, and pass the nested group address to your child as an #Input. As objects in JS are mutable, your don't need an EventEmitter to pass any changes to your parent from child, changes will be caught "automatically" and you can therefore do all validation from the parent.
So in parent build your form like:
this.checkoutForm = this.formBuilder.group({
fullName: ['', [...]],
address: this.formBuilder.group({
street: ['', [...]],
town: ['', [...]]
})
})
then in your child tag pass the address to child as #Input:
<address-form [address]="checkoutForm.controls.address"></address-form>
In your child you mark the #Input:
#Input() address: FormGroup;
and the template in child would look like:
<div [formGroup]="address">
<input formControlName="street"><br>
<!-- Validation messages -->
<input formControlName="town">
<!-- Validation messages -->
</div>
and as mentioned, you can now handle all validation from the parent, as the parent is aware of what values the child has :)
Related
so I have a component that is rendering a form and it also is pre-filling the fields with data received from ajax request.
My issue is that I want to not only be able to edit fields but also add new fields to submit at the same time, so because of this I am trying to initialize my pre-filled data and new data into the same Object to be submitted with my ajax request. With my current set up the form-data is not consistently filling in the fields before the form is rendered.
This is the form template
<form #submit.prevent="editThisWorkflow" class="d-flex-column justify-content-center" >
<div>
<input type="text" v-model="workflowData.workflow">
</div>
<div >
<div v-for="object in workflowData.statuses" :key="object.id">
<input type="text" v-model="object.status">
</div>
<div v-for="(status, index) in workflowData.newStatuses" :key="index">
<input type="text" placeholder="Add Status" v-model="status.value">
<button type="button" #click="deleteField(index)">X</button>
</div>
<button type="button" #click="addField">
New Status Field
</button>
</div>
<div>
<div>
<button type="submit">Save</button>
<router-link :to="{ path: '/administrator/workflows'}" >Cancel</router-link>
</div>
</div>
</form>
This is the script
data() {
return {
workflowData: {
id: this.$store.state.workflow.id,
workflow: this.$store.state.workflow.workflow,
statuses: this.$store.state.workflow.statuses,
newStatuses: []
},
workflowLoaded: false
}
},
computed: {
...mapGetters(['workflow']),
},
methods: {
...mapActions(['editWorkflow']),
editThisWorkflow() {
this.editWorkflow({
id: this.workflowData.id,
workflow: this.workflowData.workflow,
statuses: this.workflowData.statuses,
newStatuses: this.workflowData.newStatuses
})
},
addField() {
this.workflowData.newStatuses.push({ value: ''});
},
deleteField(index) {
this.workflowData.newStatuses.splice(index, 1);
}
And this is the store method to submit the data
editWorkflow(context, workflowData) {
axios.patch('/workflowstatuses/' + workflowData.id, {
workflow: workflowData.workflow,
statuses: workflowData.statuses,
newStatuses: workflowData.newStatuses
})
.then(response => {
context.commit('editWorkflow', response.data)
})
.catch(error => {
console.log(error.response.data)
})
},
My problem comes in here
data() {
return {
workflowData: {
id: this.$store.state.workflow.id,
workflow: this.$store.state.workflow.workflow,
statuses: this.$store.state.workflow.statuses,
newStatuses: []
},
workflowLoaded: false
}
},
Is there a better way to set this part??
workflowData: {
id: this.$store.state.workflow.id,
workflow: this.$store.state.workflow.workflow,
statuses: this.$store.state.workflow.statuses,
newStatuses: []
},
If you only need to assign store values to your form once then you can use mounted function.
mounted: function() {
this.id = this.$store.state.workflow.id
this.workflow = this.$store.state.workflow.workflow
this.statuses = this.$store.state.workflow.statuses
},
data() {
return {
workflowData: {
id: '',
workflow: '',
statuses: '',
newStatuses: []
},
workflowLoaded: false
}
},
the data property does not accept this, I usually use arrow function in this question because it prohibits me from using this, and prohibits my team from also using this within the data.
Declare all necessary items within the datato maintain reactivity, and assign the value within the mounted of the page.
mounted() {
this.workflowData.id = this.$store.state.workflow.id
this.workflowData.workflow = this.$store.state.workflow.workflow
this.workflowData.statuses = this.$store.state.workflow.statuses
},
data: () => ({
workflowData: {
id: '',
workflow: '',
statuses: '',
newStatuses: []
},
workflowLoaded: false
}
},
})
The way how I resolved this problem turned out to be simpler than most of the solutions presented here. I found it hard to reach data from this.$store.state due to Vuejs life cycle. And assigning values to v-mode tourned out to be impossible because "v-model will ignore the initial value, checked or selected attributes found on any form elements. It will always treat the Vue instance data as the source of truth."
Solution
To pre-fill the field with data received from ajax request e.g. input field of type email I did as follow.
1st. I saved the output of my ajax request in application's storage (Cookies) -it can be Local Storage or Session, depended what is appropriate to you.
2nd. I populated my Vuex's store (single source of truth) with the data from my application storage. I do it every time when I reload a page.
3rd. Instead of binding a data to v-model in Vuejs life cycle, or using value attribute of html input (<input type="email" value="email#example.com">). I Pre-filled input by populating placeholder attribute of html with data coming from Vuex store like this:
<input v-model="form.input.email" type="email" name="email" v-bind:placeholder="store.state.user.data.email">
I'm using FormBuilder for make JSON data,
I have made this form using FormBuilder, FormGroup, Validators, FormControl
this is my forms code
<form [formGroup]="SearchForm" (ngSubmit)="search(SearchForm.value)" name="SearchForms">
<input type="text" formControlName="checkin" value="12:00" placeholder="12:00" readonly> In
<input type="text" formControlName="checkout" value="10:00" placeholder="10:00" readonly> Out
<input type="text" formControlName="guest"> Guest
<input type="text" formControlName="room"> Room
<select formControlName="position">
<option value="sky">sky</option>
<option value="earth">Earth</option>
</select>
<input type="hidden" formControlName="date" name="date" value="{{selected_date}}">
<input type="hidden" formControlName="location" name="location" value="{{location}}">
<button class="btn shadow" type="submit"> Search </button>
</form>
and this my component.ts file
export class HomeComponent implements OnInit {
http: any;
SearchForm : FormGroup;
post:any;
location: string ='';
date:string = '';
checkin:string='';
checkout:string='';
guest:number;
room:number;
position:string='';
constructor(private fb: FormBuilder)
{
this.SearchForm = fb.group({
'location' : [null],
'date' : [null],
'checkin' : [null],
'checkout' : [null],
'guest' : [null],
'room' : [null],
'position' : [null],
});
}
search(post)
{
this.location = post.location;
this.date = post.date;
this.checkin = post.checkin;
this.checkout = post.checkout;
this.guest = post.guest;
this.room = post.room;
this.position = post.position;
this.http.post('http://localhost:4200/searchrequest',JSON.stringify(this.SearchForm))
}
ngOnInit()
{
this.SearchForm = new FormGroup(
{
location: new FormControl(),
date: new FormControl(),
checkin: new FormControl(),
checkout: new FormControl(),
guest: new FormControl(),
room: new FormControl(),
position:new FormControl()
});}
I have no idea what I should fo next for post data from SearchForm I have made.
Any help form my project? if someone has a link for example you can share it here.
Thanks before
search(post)
{
this.location = post.location;
this.date = post.date;
this.checkin = post.checkin;
this.checkout = post.checkout;
this.guest = post.guest;
this.room = post.room;
this.position = post.position;
this.http.post('http://localhost:4200/searchrequest',JSON.stringify(this.SearchForm))
}
should be
search(post)
{
this.http.post('http://localhost:4200/searchrequest',JSON.stringify(post))
}
you are currently assigning all the form values to the component Instance members when you can just as easily send the entire form value.
and you also initialize the searchForm twice, once with the FormBuilder service and the other time with a new FormGroup instance, which are doing exactly the same thing. you can remove the entire ngOnInit() function since you do it already in the constructor.
angular 5 FormBuilder doc's: https://angular.io/api/forms/FormBuilder
adding #marcidius comment as its a valuable part of the solution.
To fix that error you need to add a private http member to your
constructor: constructor(private fb: FormBuilder, private http: Http)
Also, in your example you don't even need to be passing the SearchForm
to that function since it is a class member already. Reactive Forms`
values are updated via their bindings so any update to the form
elements updates the model automatically. Just call this.searchForm in
the JSON.stringify(...) but don't worry about passing it into the
function.
I needed to get my this.contactForm.value in order to get the object of values . Than i did the stringify
I have a child component which deals with the array of input controls. I want to have a formcontrol over the child component.
I am passing the array of json object, what would be the correct way to bind parent form to the child component's FormArray having 2 form control with Validator required on first.
This is the initial code
<h1>Child</h1>
<div formArrayName="names">
<div *ngFor="let c of names.control">
<input formControlName="firstName">
<input formControlName="lastName">
</div>
</div>
Intention is to bind parent form with the array of input control in the child component. Also form will become invalid if one of the input control in child component doesn't have required field.
http://plnkr.co/edit/HznCJfSEiSV28ERqNiWr?p=preview
I love solve old post :)
The key is that your custom Form Component has inside a FormArray, then use "writeValue" to create the formArray, see stackblitz
#Component({
selector: "my-child",
template: `
<h1>Child</h1>
<div *ngFor="let group of formArray.controls" [formGroup]="group">
<input formControlName="firstName" (blur)="_onTouched()" />
<input formControlName="lastName" (blur)="_onTouched()"/>
</div>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: Child,
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: Child,
multi: true
}
]
})
export class Child implements ControlValueAccessor {
formArray: FormArray;
_onChange;
_onTouched;
writeValue(value: any) {
this.formArray = new FormArray(
value.map(x => {
return new FormGroup({
firstName: new FormControl(x.firstName, Validators.required),
lastName: new FormControl(x.firstName, Validators.required)
});
})
);
this.formArray.valueChanges.subscribe(res => {
this._onChange(res);
});
}
registerOnChange(fn: (value: any) => void) {
this._onChange = fn;
}
registerOnTouched(fn: (value: any) => void) {
this._onTouched = fn;
}
validate({ value }: FormControl) {
return !this.formArray || this.formArray.valid ?
null : { error: "Some fields are not fullfilled" };
}
}
You have to use formArrayName directive and *ngFor like this:
<form [formGroup]="form" (ngSubmit)="sayHello()">
<input formControlName="name"><br>
<input formControlName="email"><br>
<div formArrayName="username">
<div *ngFor="let user of username.controls; let i=index">
<my-child formControlName="i"></my-child>
</div>
</div>
<button type="submit">Register</button>
</form>
And with FormBuilder you have to use FormArray as well.
form = new FormGroup({
name: new FormControl('My Name'),
username: new FormArray([
new FormControl("value"),// ControlValueAccesor is applied only to one control, not two. So you cannot use javascript object like you are using below this line.
{firstName:"Anna", lastName:"Smith"},
{firstName:"Peter", lastName:"Jones"}
])
});
For more details, see this doc.
Case 2: passing FormGroup:
form = new FormGroup({
name: new FormControl('My Name'),
username: new FormArray([
new FormGroup({
firstName: new FormControl('Anna'),
lastName: new FormControl('Smith')
}),
new FormGroup({
firstName: new FormControl('Peper'),
lastName: new FormControl('Jones')
}),
])
})
If you are tring to pass the FormGroup as a ngModel parameters, you can't!
Adding to a FormArray via input field
To add values, via a form input, to an existing array I can use (click)="addAddress()" in the html where addAddress is defined in the component.ts to update values in an array in the form AppComponent:
ngOnInit() {
this.myForm = this._fb.group({
name: ['', [Validators.required, Validators.minLength(5)]],
addresses: this._fb.array([
this.initAddress(),
])
});
}
initAddress() {
return this._fb.group({
street: ['', Validators.required],
postcode: ['']
});
}
addAddress() {
const control = <FormArray>this.myForm.controls['addresses'];
control.push(this.initAddress());
}
And back in the html ngFor is used to add a set of input fields each time the 'add' button is clicked":
<div formArrayName="addresses">
<div *ngFor="let address of myForm.controls.addresses.controls; let i=index" >
<span>Address {{i + 1}}</span>
<div [formGroupName]="i">
<input type="text" formControlName="street">
<input type="text" formControlName="postcode">
</div>
</div>
</div>
Like the full working example here: https://embed.plnkr.co/sUjE1ULYhfDHLNBw2sRv/1
Adding to form FormGroup via input field
I would like to understand how to add a completely new FormGroup via form input.
So taking the case of the example above...
Rather than adding to an array of addresses:
{
"name": "",
"addresses": [
{
"street": "Baker Street",
"postcode": "w2"
},
{
"street": "Bond Street",
"postcode": "w1"
}
]
}
Each time an address is added a new FormGroup is created where the user adds the FormGroupName for each via form input. For example:
{
"name":"",
"home":{
"street":"Baker Street",
"postcode":"w2"
},
"work":{
"street":"Bond Street",
"postcode":"w1"
}
}
Using the addControl method this can be achieved like so:
app.component.html excerpt:
<div class="margin-20">
<a (click)="addGroup(newName.value)" style="cursor: default">Add Group +</a>
</div>
app.component.ts excerpt:
ngOnInit() {
this.myForm = this._fb.group({
});
}
addGroup(newName:string) {
let data = this._fb.group({
foo: ['what a load of foo'],
bar: ['see you at the bar']
})
this.myForm.addControl(newName, data);
document.getElementById('newName').value='';
}
Working example in this plunk:
For an example where a corresponding input field is published for each formControl that is added in the new named group I created this plunk
I am stuck here and would very much appreciate help. I have a form in a razor view with a input field for current city which looks like this:
#Html.LabelFor(x => x.UserModel.CurrentCity)
#Html.TextBoxFor(x => x.UserModel.CurrentCity, new { #data_bind = "value: UserModel.CurrentCity ", #class = "city", #data_val = "true", #data_val_required="City is required" })
#Html.ValidationMessageFor(x => x.UserModel.CurrentCity)
I want autocomplete for this field and am using jquery token input plugin for this like:
$(".city").tokenInput('#Url.Action("AutocompleteCity", "Settings")',{ minChars: 2, tokenLimit: 1, hintText: "Type in a city" });
$(".city").tokenInput("add", {name: viewModel.UserModel.CurrentCity()});
Everything works fine except the clientside unobtrusive validation. The form gets posted even if CurrentCity is empty.
I also tried to change the MVC helpers to plain html:
<input data-val="true" data-val-required="City is required" type="text" class="city" data-bind = "value: UserModel.CurrentCity, attr: { name: 'UserModel.CurrentCity', id: 'UserModel.CurrentCity'}" />
<span class="field-validation-valid" data-valmsg-for="UserModel.CurrentCity" data-valmsg-replace="true"></span>
This approach prevents the form from being submitted but the validation-error class is not injected into the span and the error message does not show up.
Any suggestions?
The original input element you created is hidden. You will likely need to enable validation of hidden elements: jquery.validate v. 1.9 ignores some hidden inputs or https://stackoverflow.com/a/13295938/173225.