Angular: composite ControlValueAccessor to implement nested form - forms

Composition of ControlValueAccessor to implement nested form is introduced in an Angular Connect 2017 presentation.
https://docs.google.com/presentation/d/e/2PACX-1vTS20UdnMGqA3ecrv7ww_7CDKQM8VgdH2tbHl94aXgEsYQ2cyjq62ydU3e3ZF_BaQ64kMyQa0INe2oI/pub?slide=id.g293d7d2b9d_1_1532
In this presentation, the speaker showed a way to implement custom form control which have multiple value (not only single string value but has two string field, like street and city). I want to implement it but I'm stuck. Sample app is here, does anybody know what should I correct?
https://stackblitz.com/edit/angular-h2ehwx
parent component
#Component({
selector: 'my-app',
template: `
<h1>Form</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit(form.value)" novalidate>
<label>name</label>
<input formControlName="name">
<app-address-form formControlName="address"></app-address-form>
<button>submit</button>
</form>
`,
})
export class AppComponent {
#Input() name: string;
submitData = '';
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = fb.group({
name: 'foo bar',
address: fb.group({
city: 'baz',
town: 'qux',
})
});
}
onSubmit(v: any) {
console.log(v);
}
}
nested form component
#Component({
selector: 'app-address-form',
template: `
<div [formGroup]="form">
<label>city</label>
<input formControlName="city" (blur)="onTouched()">
<label>town</label>
<input formControlName="town" (blur)="onTouched()">
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AddressFormComponent)
}
]
})
export class AddressFormComponent implements ControlValueAccessor {
form: FormGroup;
onTouched: () => void = () => {};
writeValue(v: any) {
this.form.setValue(v, { emitEvent: false });
}
registerOnChange(fn: (v: any) => void) {
this.form.valueChanges.subscribe(fn);
}
setDisabledState(disabled: boolean) {
disabled ? this.form.disable() : this.form.enable();
}
registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
}
and error message I got
ERROR TypeError: Cannot read property 'setValue' of undefined
at AddressFormComponent.writeValue (address-form.component.ts:32)
at setUpControl (shared.js:47)
at FormGroupDirective.addControl (form_group_directive.js:125)
at FormControlName._setUpControl (form_control_name.js:201)
at FormControlName.ngOnChanges (form_control_name.js:114)
at checkAndUpdateDirectiveInline (provider.js:249)
at checkAndUpdateNodeInline (view.js:472)
at checkAndUpdateNode (view.js:415)
at debugCheckAndUpdateNode (services.js:504)
at debugCheckDirectivesFn (services.js:445)
I think FormGroup instance should be injected to nested form component somehow...

Couple issues, on your AppComponent change your FormBuilder to:
this.form = fb.group({
name: 'foo bar',
address: fb.control({ //Not using FormGroup
city: 'baz',
town: 'qux',
})
});
On your AddressFormComponent you need to initialize your FormGroup like so:
form: FormGroup = new FormGroup({
city: new FormControl,
town: new FormControl
});
Here's the fork of your sample: https://stackblitz.com/edit/angular-np38bi

We (at work) encountered that issue and tried different things for months: How to properly deal with nested forms.
Indeed, ControlValueAccessor seems to be the way to go but we found it very verbose and it was quite long to build nested forms. As we're using that pattern a lot within our app, we've ended up spending some time to investigate and try to come up with a better solution. We called it ngx-sub-form and it's a repo available on NPM (+ source code on Github).
Basically, to create a sub form all you have to do is extends a class we provide and also pass your FormControls. That's it.
We've updated our codebase to use it and we're definitely happy about it so you may want to give a try and see how it goes for you :)
Everything is explained in the README on github.
PS: We also have a full demo running here https://cloudnc.github.io/ngx-sub-form

Related

Angular - syncfusion ejs autocomplete selecting incorrect value

Using a Syncfusion EJS Autocomplete element in a search box.
The issue being reported is that the user is not able to select the value searched
I know the issue, is because the data passed to the AutoComplete has some duplicate values, but they are distinct based on a second value.
The code below hopefully show the issue
<div class="control-section" style="margin:130px auto;width:300px">
<ejs-autocomplete
id="sample-list"
#sample
[dataSource]="countriesData"
[autofill]="isBool"
[fields]="fields"
(select)='selectIssuer($event)'
filterType="Contains"
>
<ng-template #itemTemplate let-data>
<!--set the value to itemTemplate property-->
<div class='item'>
<div>{{data.Name}} -- {{data.Structure != 'SPV' ? 'BT' : data.Structure}}</div>
</div>
</ng-template>
</ejs-autocomplete>
</div>
/**
* AutoComplete Highlight Sample
*/
import { Component, ViewChild } from '#angular/core';
import { AutoCompleteComponent } from '#syncfusion/ej2-angular-dropdowns';
#Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css'],
})
export class AppComponent {
public countriesData = [
{ Name: 'Client 1' , Id: 'A3D49279-18DC-40FB-B843-B6207518B379', Structure: 'BT'},
{ Name: 'Client 1' , Id: '77ED2BD8-2309-4792-9264-01DEAFC3227E', Structure: 'SPV'},
{ Name: 'Client 2' , Id: 'BA017D4F-DD5C-4F2D-852C-DD17AF209436', Structure: 'BT'},
{ Name: 'Client 3' , Id: '78FCDCB9-06EA-4D9B-A352-171B1594AE24', Structure: 'SPV'},
{ Name: 'Client 4' , Id: '48C3168A-FA2A-4EF7-B184-61F18C47AB6D', Structure: 'BT'},
{ Name: 'Client 4' , Id: 'E734CA83-91FF-4475-B35E-BE232ACBF137', Structure: 'SPV'}
];
public fields: Object = { value: 'Name' };
public isBool: Boolean = true;
}
selectIssuer(_issuer: any) {
this.getSearchIssuer.emit({ issuer: <CombinedIssuer>_issuer.itemData, clear: false });
}
AS is visible, some of the Client Names are the same, but what makes them distinct is the combination with the Structure.
The issue is that when a user selects say Client 4 that has an SPV Structure, it still loads the Client 4 with the BT structure.
Is it possible for the EJS Autocomplete to take in to consideration the combination of fields to make sure the correct item is selected or is is possible for the EJS Autocomplte to use the Item Id as well
Can it be possible to pass in the Id value to the Fields property ?
I was able to figure this out, so sharing my findings:
The additional code is shown below with ...
<ejs-autocomplete id='combinedIssuerSearch' #searchCombinedIssuers
[dataSource]='ixDispalyCombinedIssuerList'
[fields]='issuerFields'
ShowBorder='False'
(select)='selectIssuer($event)'
[placeholder]='defaultText'
[filterType]='issuerFilterType'
*(filtering)='onFiltering($event)'*
[showClearButton]="false"
class="auto-complete-search">
<ng-template #itemTemplate let-data>
<!--set the value to itemTemplate property-->
<div class='item'>
<div class='issuer-name'> {{data.Name}}</div>
<div class="ls_spv">{{data.Structure != 'SPV' ? 'BT' : data.Structure}}</div>
</div>
</ng-template>
</ejs-autocomplete>
in the ts file I added code to handle the OnFiltering event
onFiltering(args) {
args.preventDefaultAction = true;
var predicate = new Predicate('Name', 'contains', args.text, true);
predicate = predicate.or('Structure', 'contains', args.text, true);
var query = new Query();
query = args.text != '' ? query.where(predicate) : query;
args.updateData(this.ixDispalyCombinedIssuerList, query);
}

ControlValueAccessor with FormArray in Angular 2

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!

angular 2 on from submit error self.context.onSubmit is not a function

I am using 2.0.0-rc.6 in my angular 2 application.
on form submit I am getting this error - self.context.onSubmit is not a function
also it is appending form values in browser.
http://localhost:3000/register
on submit the page reloading and url become like this.
http://localhost:3000/register?firstName=vcvvc&lastName=vcv&userName=cvv&password=vcv&password=vcv
the codes are
form
<form class="ui form" (ngSubmit)="onSubmit()" #registrationForm="ngForm">
----
----
<button type="submit" class="ui button"> Register</button>
</form>
the service
import { Component } from '#angular/core';
import { User } from '../models/user';
import { RegisterService } from '../services/register.service';
#Component({
selector: 'side-panel',
templateUrl: 'app/components/register.component.html'
})
export class RegisterComponent {
newuser: User = new User();
theText: string;
constructor(private _registerService: RegisterService){
}
onsubmit(){
console.log('form submit clicked..');
this._registerService.sendUser(this.newuser).subscribe(
date =>{
this.newuser = new User();
},
error => console.log(error)
);
}
}
This error occurs when the name of the methods called in an event not matched with the template declaration and inside the class
In your template you have specified onSubmit() as camel case
<form class="ui form" (ngSubmit)="**onSubmit()**" #registrationForm="ngForm">
but inside the class, its not a camelCase "onsubmit()"
onsubmit(){
console.log('form submit clicked..');
this._registerService.sendUser(this.newuser).subscribe(

Angular2 custom validator not called

I have written a custom validation directive like that:
const DURATION_VALIDATOR = new Provider(
NG_VALIDATORS,
{useExisting: forwardRef(() => DurationDirective), multi: true}
);
#Directive({
selector: '[ngModel][duration], [formControl][duration]',
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DurationDirective),
multi: true }]
})
export class DurationDirective implements Validator{
constructor(public model:NgModel){
console.error('init')
}
validate(c:FormControl) {
console.error('validate')
return {'err...':'err...'};
}
}
My Html looks like this:
<input
type="text"
[(ngModel)]="preparation.duration"
duration
required
>
My problem is that while the validator is initialized, i.e. 'init' is logged to console, the validate function is never called, i.e. 'validate' is never logged to the console, when typing into the input field. Since the validator is initialized, I assume that I "wired" up everything correctly. So what is missing?
My best bet is that you haven't bootstraped Angular with regards to forms:
import { App } from './app';
import { disableDeprecatedForms, provideForms } from '#angular/forms';
bootstrap(App, [
// these are crucial
disableDeprecatedForms(),
provideForms()
]);
You can see this plunk - it does output "validate" to the console.
I forked and improved #batesiiic 's plunk: https://plnkr.co/edit/Vokcid?p=preview
validate(c:FormControl) {
console.error('called validate()')
return parseInt(c.value) < 10 ? null : {
duration: {message: 'Please enter a number < 10'}
};
}
The validate() method must return null if the input is valid, otherwise, it returns an object { key: value, ... } where key is the type of error and value can be anything you can use in your template to generate an error message.
The template also contains a div to display the error message if the input is not valid.
Instead of writing the custom validator function as class/component/Directive method,
Writing Custom validator outside the component/Directive should work.
validate(c:FormControl) {
console.error('validate')
return {'err...':'err...'};
}
#Directive({
selector: '[ngModel][duration], [formControl][duration]',
providers: [{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DurationDirective),
multi: true }]
})
export class DurationDirective implements Validator{
constructor(public model:NgModel){
console.error('init')
}
}

Form-Providers ngForm

I've got the following problem. I want to work with NG2 Forms. According to the angular 2 documentation, using the ngForm directive on the form and the ngControl directive on the input, the form should always have access to the validity of the input.
This works if the inputs are in the same component as the form, but as soon as I move the inputs into a child directive, they don't get the ngForm-Provider anymore.
This works:
import { Component, Input } from 'angular2/core';
import { FORM_DIRECTIVES } from 'angular/common';
#Component({
directives: [FORM_DIRECTIVES],
template: `
<form #heroForm="ngForm">
<input type="text"
[(ngModel)]="input.test"
ngControl="name">
</form>
`
})
export class FormTest1 {
public input = {
test: ""
}
}
However, this doesn't:
import { Component, Input } from 'angular2/core';
import { FORM_DIRECTIVES } from 'angular/common';
#Component({
directives: [FORM_DIRECTIVES],
template: `
<input *ngIf="data"
[(ngModel)]="data.test"
ngControl="name">
`
})
export class FormInput {
#Input() data;
}
#Component({
directives: [FormInput, FORM_DIRECTIVES],
template: `
<form #heroForm="ngForm">
<form-input
[data]="input">
</form-input>
</form>
`
})
export class FormTest1 {
public input = {
test: ""
}
}
As this throws:
EXCEPTION: No provider for t! (t -> t) in [null]
As soon as I remove the ngControl-attribute from the input, the error disappears, but the form in the parent doesn't receive any information about the input anymore. How do I go about passing the ngForm down to the child component?
Thanks in advance.
Here's a little example:
form-test.component.js
#Component({
selector: 'form-test',
directives: [FormInput, FORM_DIRECTIVES],
template: `
<form [ngFormModel]="heroForm">
<br>Is Form Valid? - {{heroForm.valid}}<br>
<br>Data: - {{input | json}}<br>
<input type="text" [(ngModel)]="input.test1" required [ngFormControl]="heroForm.controls['test1']">
<form-input [hForm]="heroForm" [data]="input">
</form-input>
<button type="submit">Submit</button>
</form>
`
})
export class FormTest1 {
public heroForm:ControlGroup;
constructor(private _builder:FormBuilder){
this.heroForm = _builder.group({
test1: ["", Validators.required],
test2: ["", Validators.required]
});
}
public input = {
test1: "",
test2: ""
}
}
form-input-test.ts
#Component({
selector: 'form-input',
directives: [FORM_DIRECTIVES,NgForm],
template: `
<label>sdsd</label>
<input type="text" [(ngModel)]="data.test2" [ngFormControl]="hForm.controls['test2']" required>
`
})
export class FormInput {
#Input() hForm:ControlGroup;
#Input() data;
}
I did two things mainly:
1- you only have access to the form in the parent object not in the child, I added another input so you can pass it along.
2-There's two ways to create a ControlGroup, one is implicitly like the one you did with ngForm and ngControl, and the other one is explicitely like I did with ngFormModel and ngFormControl, the second one gives you more control over the form so you can you things like this.
I recommend you to read this link