CRM App - Part 11 Reactive Forms - Edit Company
Overview
Time: 5min
In this lesson, we will build on the work we did in the previous lesson to allow editing companies.
Add navigation to the 'Edit' button
Time: 1 min
- Add the routerLink to the edit button on the company table.
src/app/company/company-table/company-table.component.html
<button class="btn btn-default" [routerLink]="['/company/edit', company.id]">Edit</button>
Implement the getCompany logic
Time: 5 min
- If we determine from the route params that we are editting an existing component
- load the component from the database.
src/app/company/company-edit/company-edit.component.ts
ngOnInit() {
this.companyId = this.activatedRoute.snapshot.params['id'];
this.isNewCompany = !this.companyId;
this.buildForm();
if (!this.isNewCompany) {
this.getCompany();
}
}
- Update the company service to be able to query a single company.
src/app/company/company.service.ts
getCompany(companyId: number): Observable<Company> {
return this.httpClient.get<Company>(`${this.API_BASE}/company/${companyId}`)
.pipe(catchError(e => this.errorHandler<Company>(e)));
}
Implement the saveCompany logic
Time: 5 min
- Update the component to call updateCompany instead of addCompany if the company already exists.
src/app/company/company-edit/company-edit.component.ts
saveCompany(): void {
if (this.isNewCompany) {
this.companyService
.addCompany(this.companyForm.value)
.subscribe(() => this.router.navigateByUrl('/company/list'));
} else {
const newCompany = { ...this.companyForm.value, id: this.companyId };
this.companyService
.updateCompany(newCompany)
.subscribe(() => this.router.navigateByUrl('/company/list'));
}
}
- Update the company service to contain a method to update a company.
src/app/company/company.service.ts
updateCompany(company: Company): Observable<Company> {
return this.httpClient.put<Company>(
`${this.API_BASE}/company/${company.id}`, company,
{ headers: new HttpHeaders().set('content-type', 'application/json') }
).pipe(catchError(e => this.errorHandler<Company>(e)));
}
EXTRA : Dynamic Reactive Forms
Time: 10min
Reactive Forms make dynamic validation easy to implement. In this section, we will see a very simple example of dynamic validation for the Phone number.
Add a checkbox to the Edit form
src\app\company\company-edit\company-edit.component.html
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
value=""
id="checkPhone"
formControlName="checkPhone"
/>
<label class="form-check-label" for="checkPhone"> Add phone number </label>
</div>
<div class="form-group">
<label for="phone">Phone</label>
<input type="text" class="form-control" name="phone" formControlName="phone" />
</div>
<div *ngIf="companyForm.get('phone')!.hasError('required')" class="alert alert-danger">
Phone number is required
</div>
src\app\companycompany-edit\company-edit.component.ts
buildForm(){
this.companyForm = this.formBuilder.group({
name: ['', Validators.required],
email: [''],
phone: [''],
checkPhone: []
});
}
- Add checkbox control to the Form
- Add the checkPhone property to the form object
Add the logic for validation
Reactive Forms controls exposes useful Observables, such as "valueChanges", which emits new values everytime it changes.
src\app\companycompany-edit\company-edit.component.ts
...
this.companyForm.get('checkPhone')!.valueChanges
.subscribe(value => {
if(value){
this.companyForm.get('phone')!.setValidators(Validators.required)
}else{
this.companyForm.get('phone')!.clearValidators();
}
this.companyForm.get('phone')!.updateValueAndValidity();
});
...
- Subscribe to the valueChanges observable
- if 'value' is true (checkbox ticked), set 'Required' validator on 'phone' control
- if 'value' is false, reset validators and update validity
EXTRA 2 : Custom Form Controls
So far we have shown how to bind form controls to <input>
elements - but what if we want more control over the UI for a form control? We can create our own components and turn them into form controls by implementing the ControlValueAccessor
interface.
First create a new component:
ng g component controls/addressForm --skip-tests
Edit the component code to implement 'NgValueAccessor'
src/app/controls/address-form/address-form.component.ts
@Component({
selector: 'fbc-address-form',
templateUrl: './address.component.html',
styleUrls: ['./address.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: AddressFormComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: AddressFormComponent,
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator
{
@Input()
legend: string = '';
form: FormGroup = this.fb.group({
addressLine1: [null, [Validators.required]],
addressLine2: [null, [Validators.required]],
city: [null, [Validators.required]],
postCode: [null, [Validators.required]],
});
onTouched: Function = () => {};
onChangeSubs: Subscription[] = [];
constructor(private fb: FormBuilder) {}
ngOnDestroy() {
for (let sub of this.onChangeSubs) {
sub.unsubscribe();
}
}
registerOnChange(onChange: any) {
const sub = this.form.valueChanges.subscribe(onChange);
this.onChangeSubs.push(sub);
}
registerOnTouched(onTouched: Function) {
this.onTouched = onTouched;
}
setDisabledState(disabled: boolean) {
if (disabled) {
this.form.disable();
} else {
this.form.enable();
}
}
writeValue(value: any) {
if (value) {
this.form.setValue(value, { emitEvent: false });
}
}
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
errors = this.addControlErrors(errors, 'addressLine1');
errors = this.addControlErrors(errors, 'addressLine2');
errors = this.addControlErrors(errors, 'postCode');
errors = this.addControlErrors(errors, 'city');
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
Edit the template to diplay the label and <input>
elements
src/app/controls/address-form/address-form.component.html
<fieldset [formGroup]="form">
<legend>{{ legend }}</legend>
<div class="form-group">
<label for="addressLine1">Address Line 1</label>
<input
class="form-control"
formControlName="addressLine1"
(blur)="onTouched()"
/>
</div>
<div class="form-group">
<label for="addressLine2">Address Line 2</label>
<input
class="form-control"
formControlName="addressLine2"
(blur)="onTouched()"
/>
</div>
<div class="form-group">
<label for="city">City</label>
<input class="form-control" formControlName="city" (blur)="onTouched()" />
</div>
<div class="form-group">
<label for="postCode">Post Code</label>
<input
class="form-control"
formControlName="postCode"
(blur)="onTouched()"
/>
</div>
</fieldset>
Add an address
form control to the form group
src/app/company/company-edit/company-edit.component.ts
buildForm() {
this.companyForm = this.fb.group({
name: ['', Validators.required],
phone: [''],
checkPhone: [],
email: [''],
address: [''],
});
Finally, change the edit form to use our new control.
src/app/company/company-edit/company-edit.component.html
<div class="form-group">
<label for="email">Email</label>
<input
type="text"
class="form-control"
name="email"
formControlName="email"
/>
</div>
<fbc-address-form legend="Address" formControlName="address"></fbc-address-form>
<div class="form-group">
<button (click)="saveCompany()" [disabled]="!companyForm.valid" class="btn btn-default">Submit</button>
</div>
With this approach we move the 'boiler plate' markup for displaying the form layout inside the control - which simplifies the amout of code required to write the form itself. This technique is recommended for apps where there are lots of forms or there there are big forms.
When using ControlValueAccessor
our component can be bound to reative forms via formControlName
or [formControl]
or Template-Driven Forms via [(ngModel)]