Binding ngModel to complex data - html

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();
}
}
ValueAccessorBase:
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());
}
}
Usage:
<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"
Update
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
Update 2
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.
In my example i am drawing new marker.
DefaultValueAccessor just updates value property https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L76
Datepicker in angular2 material is setting inner value as you do https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L202
When 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

Related

Making a date field reactive in Angular

I'm trying to make a datepicker component from ngx-bootstrap a custom date-field, so that I can globalize some functionality and configs. But I can't seem to be able to catch the value of the Date object in the date input field.
My date-field.ts (I'm re-using some setup from a text-field. So bear with me if you see some remnants of the text field component. But I'm sure that my main problem is that my component doesn't know it's a date field)
import { Component, OnInit, forwardRef, Input } from '#angular/core';
import { FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'date-field',
templateUrl: './date-field.component.html',
styleUrls: ['./date-field.component.scss'],
providers:[
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => DateFieldComponent),
}
]
})
export class DateFieldComponent implements OnInit, ControlValueAccessor {
public dateField = new FormControl("")
private onChange: (name: string) => void;
private onTouched: () => void
#Input() name: string;
#Input() label: string;
#Input() required: boolean;
datepickerConfig = {
dateInputFormat: 'ddd, MMMM Do YYYY',
isAnimated: true,
adaptivePosition: true,
returnFocusToInput: true,
containerClass: 'theme-dark-blue'
}
constructor() { }
ngOnInit() { }
writeValue(obj: any): void {
const date = Date
this.dateField.setValue(new Date());
console.log(date);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
if (isDisabled) {
this.dateField.disable();
} else {
this.dateField.enable();
}
}
doInput() {
this.onChange(this.dateField.value)
}
doBlur() {
this.onTouched();
}
}
The template HTML:
<label
*ngIf="label"
for="{{name}}"
class="col-auto col-form-label {{required ? 'required' : '' }}">
{{label}}
</label>
<div class="col-expand relative">
<input
type="text"
class="form-control date-field"
#dp="bsDatepicker"
[formControl]="dateField"
(input)="doInput()"
(blur)="doBlur()"
ngModel
bsDatepicker
[bsConfig]="datepickerConfig"
required="{{required}}">
</div>
Using it in parent forms like this:
<date-field
name="dateChartered"
label="Date local union chartered"
formControlName="dateChartered"
required="true">
</date-field>
<p><strong>Date chartered is:</strong> {{dateChartered}}</p>
Assuming in your parent component you're correctly initializing FormGroup in controller and using it correctly in template, you have two main errors in your component.
First, as Belle Zaid says, you should remove ngModel from you custom datepicker's <input>.
Second, you are binding doInput() to (input), but it will fire only if you type in your input field, same for (change). You should bind to (bsValueChange) that's an output event exposed by BsDatepicker and it's safer, unless you plan to update value on user's input.
The resulting template will look like this:
<label *ngIf="label"
for="my-custom-datepicker"
class="col-auto col-form-label {{required ? 'required' : '' }}">
{{label}}
</label>
<div class="col-expand relative">
<input id="my-custom-datepicker"
type="text"
class="form-control date-field"
required="{{required}}"
bsDatepicker
[formControl]="dateField"
[bsConfig]="datepickerConfig"
(blur)="doBlur()"
(bsValueChange)="doInput()">
</div>
Once done the two changes you will notice an error in your console:
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was
checked. Previous value for 'ng-pristine': 'true'. Current value: 'false'.. Find more
at https://angular.io/errors/NG0100
This is due to the fact that calling this.dateField.setValue(obj) in writeValue will also trigger (bsValueChange) and, thus, doInput(). To overcome this issue, you can edit your code like the following:
private componentInit = false;
// ...
doInput() {
if (!this.componentInit) {
this.componentInit = true;
return;
}
this.onChange(this.dateField.value);
}

Angular async pipe with input

i'm getting a list from json which i want to display and being able to filter using an input box. so far it's working but the list only displays once i input something into the searchbox not before that. i would like the list being displayed at the start.I tried to set the search box value at start with empty string but it's not working.
looked up the web didn't find the way to achieve. Any help would be appreciated.
my component html:
<h4><label for="search-box">Hero Search</label></h4>
<input #searchBox id="search-box" (input)=search(searchBox.value) />
<ul class="heroes">
<li *ngFor="let hero of heroes$ | async">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero"
(click)="delete(hero)">Delete</button>
</li>
</ul>
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
component ts:
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes$: Observable<Hero[]>=null;
heroes: Hero[];
selectedHero: Hero;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
service ts:
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return full array.
return this.getHeroes();
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
You can declare your searchTerms as an BehaviorSubject instead of an simple Subject. The main difference is that as an subcriber of that BehaviorSubject, you will get the last value emitted from that observable no matter when your subscription will happen. Be aware that BehaviorSubject need an initial value that will be emitted when you initialized it.
private searchTerms: BehaviorSubject<string> = new BehaviorSubject<string>('');
Because in your case it's connected to the input event, you need to automatically trigger a first value to start your pipeline.
You can do this by using the startWith operator
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
startWith(''), // <-- start your pipe with an empty string
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);

Angular: ExpressionChangedAfterItHasBeenCheckedError when trying to disable button

I use mat-dialog to edit details of my profile page. I'm getting an ExpressionChangedAfterItHasBeenCheckedError when I click the 'Edit age' button and the dialog window pops up.
I decided to extract the styling of all edit dialogs into a single edit.component:
edit.component.html
<div class="navigation-control">
<mat-icon (click)="onCancelButtonClicked()"
class="close-button">close</mat-icon>
</div>
<div class="content-main">
<ng-content select=".content-main"></ng-content>
</div>
<div class="content-bot">
<button mat-raised-button
(click)="onCancelButtonClicked()">Cancel</button>
<button mat-raised-button
(click)="onActionButtonClicked()"
[lnDisableButton]="actionButtonDisabled">{{actionButtonValue}}</button>
</div>
edit.component.ts
#Component({ selector: 'ln-edit', ... })
export class EditComponent {
#Input() actionButtonValue: string;
#Input() actionButtonDisabled: boolean;
#Output() cancelButtonClicked = new EventEmitter<void>();
#Output() actionButtonClicked = new EventEmitter<void>();
onCancelButtonClicked() {
this.cancelButtonClicked.emit();
}
onActionButtonClicked() {
this.actionButtonClicked.emit();
}
}
To avoid the ExpressionChangedAfterItHasBeenCheckedError when trying to disable buttons and controls, I used this snippet. But that didn't solve this issue.
disable-button.directive.ts
#Directive({ selector: '[lnDisableButton]' })
export class DisableButtonDirective {
#Input('lnDisableButton') isDisabled = false;
#HostBinding('attr.disabled')
get disabled() { return this.isDisabled; }
}
The following is the contents of a mat-dialog window. This gets instantiated when I click the 'Edit age' button. When I remove the [actionButtonDisabled]="actionButtonDisabled", the error goes away, but obivously I need that line to make the functionality disable the button.
age-edit.component.html
<ln-edit [actionButtonValue]="actionButtonValue"
[actionButtonDisabled]="actionButtonDisabled"
(cancelButtonClicked)="onCancelButtonClicked()"
(actionButtonClicked)="onActionButtonClicked()">
<form [formGroup]="ageForm"
class="content-main">
<ln-datepicker formControlName="birthday"
[appearance]="'standard'"
[label]="'Birthday'"
class="form-field">
</ln-datepicker>
</form>
</ln-edit>
I handle the disabling/enabling the button in the 'ts' part of the mat-dialog popup.
age-edit.component.ts
#Component({ selector: 'ln-age-edit', ... })
export class AgeEditComponent implements OnInit, OnDestroy {
ageForm: FormGroup;
private initialFormValue: any;
actionButtonDisabled = true;
private unsubscribe = new Subject<void>();
constructor(
private editPhotoDialogRef: MatDialogRef<AgeEditComponent>,
private fb: FormBuilder,
#Inject(MAT_DIALOG_DATA) public dialogData: Date) { }
ngOnInit() {
this.initializeAgeForm();
this.loadDataToAgeForm(this.dialogData);
this.trackFormDistinct();
}
private initializeAgeForm(): void {
this.ageForm = this.fb.group({
birthday: null,
});
}
loadDataToAgeForm(birthday: Date | null): void {
if (!birthday) { return; }
this.ageForm.setValue({ birthday });
this.initialFormValue = this.ageForm.value;
}
get birthdayAC() { return this.ageForm.get('birthday') as AbstractControl; }
get actionButtonValue(): string {
return this.birthdayAC.value ? 'Update age' : 'Add age';
}
onCancelButtonClicked(): void {
this.editPhotoDialogRef.close();
}
onActionButtonClicked(): void {
this.editPhotoDialogRef.close({ ... });
}
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
ngOnDestroy() { ... }
}
I suspect this has something to do with content projection, but I'm not sure.
(...or perhaps with my custom 'ln-datepicker'?)
Any ideas?
Thanks.
From what I can tell, the problem resides in trackFormDistinct() method:
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
Looks like because of this.ageForm.valueChanges, will have different values in the 2 change detection cycles. I think this.ageForm.valueChanges emits due to <ln-datepicker>.
In a tree of form controls, if one node calls setValue, all its ancestors will have to be updated. I've written more about how Angular Forms work in this article.
I'm thinking of 2 alternatives:
skip the first emission of ageForm since it indicates the initialization of the form control tree, so this is irrelevant to the logic inside subscribe's callback.
this.ageForm.valueChanges.pipe(
skip(1),
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(/* .... */)
initialize actionButtonDisabled with false, since the error complains that it switched from true to false
actionButtonDisabled = false;

Auto refer to the components in the template?

I'm developing a web application using Angular 6. Is it possible to refer to a component (in this case, my custom-component) used in the template, like in this case:
<custom-component #select
name="name1"
title="Select first option"
[(ngModel)]="select.value"
>
</custom-component>
As you can see, the [(ngModel)] propery has value select.value. This value is a property of the CustomComponent (that I always need to be connect to the ngModel). To refer to it, I used #select, but
I would like to know if there are other ways or keywords that allow me to use the value property without using the #select decorator in every use of the custom component in the template.
You can use ngModel along with ControlValueAccessor on a custom component.
Inside the custom-componen class extend the ControlValueAccessor
export class CustomComponent implements , ControlValueAccessor {
onChange = (val: string) => { };
onTouched = () => { };
writeValue(val: string): void {
// value passed from parent throug ngModel will come under this funtion
}
registerOnChange(fn: (val: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
ngOnInit() {
}
// If you want to emit value to parent use the onChange function
myEmitFunction(){
this.onChange("value u want to emit")
}
}

Bind angular 2 model to polymer dropdown

I decided to take some time this weekend to look at Angular 2 and Polymer. I'm really interested in angular 2 and would really like to start building something with it. One downside with starting with Angular 2 now is that there is no good component library yet. However, since Angular 2 claims that it should work so good together with Web Components I thought of giving Polymer a try. I have succeeded to bind data to simple components like an input field. What I'm stuck at for the moment is how to bind a model to the selected object of a paper-dropdown-menu. Since I'm very new into both I don't really know how to do it but this is what I have tried so far. Has anyone accomplished to bind an angular 2 model to a polymer dropdown?
<paper-dropdown-menu >
<paper-menu class="dropdown-content" valueattr="id" [(ng-model)]="model">
<paper-item *ng-for="#m of options" id="{{m.id}}" (click)="onClick()">{{m.name}}</paper-item>
</paper-menu>
</paper-dropdown-menu>
EDIT: I have now created a ValueAccessor which seems to work acceptable with one exception. I try to get the dropdown to have a pre-selected value by setting the selected attribute in the writeValue method. At first this seemed to work but after I made this change I can no longer change the selected value. It works if I hardcode the value in the template so it seems to have something to do with angular together with polymer. I tried to follow the stacktrace and compare the difference between the two are. When I hardcode the value a setter method for selected is executed which trigger an item-select event. When I follow the same trace when I set the property in the valueAccessor there the setter method is no longer executed. Seems to be a problem with the interaction between angular 2 and polymer.
import {Directive, ControlValueAccessor, ElementRef, Renderer, NG_VALUE_ACCESSOR, Provider, forwardRef} from "angular2/angular2"
import {isBlank, CONST_EXPR} from 'angular2/src/facade/lang';
import {setProperty} from "angular2/src/common/forms/directives/shared"
const PAPER_DROPDOWN_VALUE_ACCESSOR = CONST_EXPR(new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => PaperDrowpdownMenuAccessor), multi: true}));
#Directive({
selector: 'paper-menu[ng-model]',
bindings: [PAPER_DROPDOWN_VALUE_ACCESSOR]
})
export class PaperDrowpdownMenuAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {
var self = this;
this._elementRef.nativeElement.addEventListener('iron-select', function(e, v, s){
console.log(e.target.selected);
self.onChange(e.target.selected);
});
}
writeValue(value: any): void {
if(value){
if(this._elementRef.nativeElement.select) {
this._elementRef.nativeElement.select(value);
}
else {
//this._elementRef.nativeElement.attributes["selected"]
setProperty(this._renderer, this._elementRef, 'selected', value);
}
}
}
registerOnChange(fn: () => any): void {
this.onChange = fn;
}
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
}
I finally solved this by my own by implementing a custom Value Accessor, mainly by looking at how the default value accesssor is implmented. https://github.com/angular/angular/blob/2.0.0-alpha.46/modules/angular2/src/common/forms/directives/default_value_accessor.ts
I struggled a bit with this since paper-menu wants the pre-selected value to be set as an attribute in the rendered html. In my first attempt I used angulars internal setProperty to set the selected value. However, this sets the DOM property and not the HTML attribute and resulted in that polymer didn't create a get,set property of selected which prevented the menu to trigger iron-select event which the dropdown menu listens for. Lesson learned, remember the difference between HTML and DOM.
import {Directive, ControlValueAccessor, ElementRef, Renderer, NG_VALUE_ACCESSOR, Provider, forwardRef} from "angular2/angular2"
import {CONST_EXPR} from 'angular2/src/facade/lang';
const PAPER_MENU_VALUE_ACCESSOR = CONST_EXPR(new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => PaperMenuAccessor), multi: true}));
#Directive({
selector: 'paper-menu[ng-model]',
bindings: [PAPER_MENU_VALUE_ACCESSOR]
})
export class PaperMenuAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {
this._elementRef.nativeElement.addEventListener('iron-select', (e) => {
this.onChange(e.target.selected);
});
}
writeValue(value: any): void {
if(this._elementRef.nativeElement.select) {
this._elementRef.nativeElement.select(value);
}
else {
this._elementRef.nativeElement.setAttribute("selected", value);
}
}
registerOnChange(fn: () => any): void { this.onChange = fn; }
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
}