For some time I have been researching if, and how to bind complex model to ngModel. There are articles showing how it can be done for simple data (e.g. string) such as this. But what I want to do is more complex. Let's say that I have a class:
export class MyCoordinates {
longitude: number;
latitude: number;
}
Now I am going to use it in multiple places around the application, so I want to encapsulate it into a component:
<coordinates-form></coordinates-form>
I would also like to pass this model to the component using ngModel to take advantage of things like angular forms but was unsuccessful thus far. Here is an example:
<form #myForm="ngForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" [(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="coordinates">Coordinates</label>
<coordinates-form [(ngModel)]="model.coordinates" name="coordinates"></coordinates-form>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
Is actually possible to do it this way or is my approach simply wrong? For now I have settled on using component with normal input and emitting event on change but I feel like it will get messy pretty fast.
import {
Component,
Optional,
Inject,
Input,
ViewChild,
} from '@angular/core';
import {
NgModel,
NG_VALUE_ACCESSOR,
} from '@angular/forms';
import { ValueAccessorBase } from '../Base/value-accessor';
import { MyCoordinates } from "app/Models/Coordinates";
@Component({
selector: 'coordinates-form',
template: `
<div>
<label>longitude</label>
<input
type="number"
[(ngModel)]="value.longitude"
/>
<label>latitude</label>
<input
type="number"
[(ngModel)]="value.latitude"
/>
</div>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CoordinatesFormComponent,
multi: true,
}],
})
export class CoordinatesFormComponent extends ValueAccessorBase<MyCoordinates> {
@ViewChild(NgModel) model: NgModel;
constructor() {
super();
}
}
import {ControlValueAccessor} from '@angular/forms';
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
private innerValue: T;
private changed = new Array<(value: T) => void>();
private touched = new Array<() => void>();
get value(): T {
return this.innerValue;
}
set value(value: T) {
if (this.innerValue !== value) {
this.innerValue = value;
this.changed.forEach(f => f(value));
}
}
writeValue(value: T) {
this.innerValue = value;
}
registerOnChange(fn: (value: T) => void) {
this.changed.push(fn);
}
registerOnTouched(fn: () => void) {
this.touched.push(fn);
}
touch() {
this.touched.forEach(f => f());
}
}
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
<coordinates-form
required
hexadecimal
name="coordinatesModel"
[(ngModel)]="coordinatesModel">
</coordinates-form>
<button type="Submit">Submit</button>
</form>
The error I am getting Cannot read property 'longitude' of undefined. For simple model, like string or number it works without a problem.
The value property is undefined at first.
To solve this issue you need to change your binding like:
[ngModel]="value?.longitude" (ngModelChange)="value.longitude = $event"
and change it for latitude as well
[ngModel]="value?.latitude" (ngModelChange)="value.latitude = $event"
Just noticed you're running onChange event within settor so you need to change reference:
[ngModel]="value?.longitude" (ngModelChange)="handleInput('longitude', $event)"
[ngModel]="value?.latitude" (ngModelChange)="handleInput('latitude', $event)"
handleInput(prop, value) {
this.value[prop] = value;
this.value = { ...this.value };
}
Updated Plunker
Plunker Example with google map
When you deal with custom form control you need to implement this interface:
export interface ControlValueAccessor {
/**
* Write a new value to the element.
*/
writeValue(obj: any): void;
/**
* Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: any): void;
/**
* Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: any): void;
/**
* This function is called when the control status changes to or from "DISABLED".
* Depending on the value, it will enable or disable the appropriate DOM element.
*
* @param isDisabled
*/
setDisabledState?(isDisabled: boolean): void;
}
Here is a minimal implementation:
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
// view => control
onChange = (value: T) => {};
onTouched = () => {};
writeValue(value: T) {
// control -> view
}
registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
}
It will work for any type of value. Just implement it for your case.
You do not need here array like (see update Plunker)
private changed = new Array<(value: T) => void>();
When component gets new value it will run writeValue where you need to update some value that will be used in your custom template. In your example you are updating value property which is used together with ngModel in template.
DefaultValueAccessor just updates value property https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L76Datepicker in angular2 material is setting inner value as you do https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L202When you need to propagate changes to AbstractControl you have to call onChange method which you registered in registerOnChange.
I wrote this.value = { ...this.value }; because it is just
this.value = Object.assign({}, this.value)
it will call setter where you call onChange method
Another way is calling onChange directly that is usually used
this.onChange(this.value);
Your example https://plnkr.co/edit/Q11HXhWKrndrA8Tjr6KH?p=preview
DefaultValueAccessor https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L88
Material2 https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L173
You can do anything you like inside custom component. It can have any template and any nested components. But you have to implement logic for ControlValueAccessor to do it working with angular form.
If you open some library such angular2 material or primeng you can find a lot of example how to implement such controls
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With