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")
}
}
Related
I have 2 component parent (Login Screen) and a child (user-list). Parent component has dropdowlist. The grid loads according to the item chosen in the drop down list I need to fire function of the child component and this is not working for me. I have the following code:
parent component html:
I have the following code:
<div>[items]="UserTypeSelectItems"[(ngModel)]="UserTypeId" id="fieldType"
bindLabel="value" bindKey="key" (change)="changeUserType()" [clearable]="false">
</div>
<app-user-list></app-user-list>
parent component ts:
I have the following code:
export class Login-ScreenComponent implements OnInit {
#ViewChild(UserListComponent)child:UserListComponent;
userTypeSelectItems: Array<SelectItem>;
userTypeId: any;
items: any;
constructor(
private userTypeSettingsService: userTypeSettingsService,
) {
this.userTypeSettingsService.getuserTypes().subscribe((data) => {
this.userTypeSelectItems = data;
if (
this.userTypeSelectItems &&
this.userTypeSelectItems.length > 0
) {
this.userTypeId =
this.userTypeSettingsService.selectedContractTypeId ??
this.userTypeSelectItems[0].key;
this.userTypeSettingsService.setContractTypeId(this.contractTypeId);
this.userTypeSettingsService.fillSelectedFields(this.userTypeId).subscribe(dataFields => {
this.items = dataFields;
this.child.getUser();
});
}
});
}
changeUserType() {
this.child.getUser();
}
child component ts:
I have the following code:
getUser() {
this.loading = true;
this.userService
.getAllUsers(this.userTypeId)
.pipe(finalize(() => (this.loading = false)))
.subscribe(
(data) => {
this.rows = data.map(notif => {
return {
user_status_id: status_id,
});
},
(err) => this.toastr.error(err),
() => (this.loading = false)
);
}
'''''''
if I understand your question correctly, that's what I'd suggest
Parent HTML
<div>[items]="UserTypeSelectItems"[(ngModel)]="UserTypeId" id="fieldType"
bindLabel="value" bindKey="key" (change)="changeUserType()" [clearable]="false">
</div>
<app-user-list [userTypeId]="userTypeId"></app-user-list>
In the parent ts remove all the calls of the this.child.getUser()
In the child component you should have input parameter userTypeId with setter. It will invoke the getUser() function every time when value is changed.
private _userTypeId: number;
#Input()
get userTypeId(): number {
return this._userTypeId;
}
set userTypeId(value: number): void {
this._userTypeId = value;
this.getUser();
}
You also can use the external service which will be injected in the parent and child components or use some Subject in the parent component, create Observable base of it which will be sent as input parameter to the child component. There you subscribe on the observable and then you need to emit the value with subjectvar.next(value) and as result function will be called. I can write down the example if you need.
UPD: example with observables
Parent component ts file:
private userTypeIdSubject$ = new Subject<string>();
private userTypeId$ = this.userTypeIdSubject$.asObservable();
changeUserType(): void {
// some code goes here
this.userTypIdSubject$.next(userTypeId); // this should send the message to the observer (child)
}
Parent HTML:
<div>[items]="UserTypeSelectItems"[(ngModel)]="UserTypeId" id="fieldType"
bindLabel="value" bindKey="key" (change)="changeUserType()" [clearable]="false">
</div>
<app-user-list [userTypeObservable]="userTypeId$"></app-user-list>
Child TS
#Input()
userTypeObservable: Observable<string>;
ngOnInit() {
if(this.userTypeObservable) {
this.userTypeObservable.subscribe(
(userTypeId) => {
this.userTypeId = userTypeId;
this.getUser();
}
}
}
}
Error when component loading dynamic
DynamicBuilderComponent.ngfactory.js:198 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-pristine: true'. Current value: 'ng-pristine: false'.
Problem
after binding json in select2data to select2 component Angular throw exception.
component code
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
Imported changeDetection in component.
template: `
<div [formGroup]="form">
<ng-container>
<ng-select2
[data]="select2data"
[options]="options"
[width]="500"
[formControlName]="field.code"
(keyup)="changed($event.target.value)">
</ng-select2>
</ng-container>
</div>`
})
select2 component class
export class Select2Component implements OnInit {
#Input() field: any = {};
#Input() form: FormGroup;
public exampleData: Array<Select2OptionData>;
public options: Options;
public value: string[];
select2data: any;
public selected: string;
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Problem Area After Binding subscribe data in ng select2 component
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
}
}
},
error => {
console.log('error = ', error);
});
}
}
i tried to print this.select2data in console.log its return me json.
Vendor.js
function expressionChangedAfterItHasBeenCheckedError(context, oldValue, currValue, isFirstCheck) {
var msg = "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: '" + oldValue + "'. Current value: '" + currValue + "'.";
if (isFirstCheck) {
msg +=
" It seems like the view has been created after its parent and its children have been dirty checked." +
" Has it been created in a change detection hook ?";
}
return viewDebugError(msg, context);
}
Great Article
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
Reference
Expression ___ has changed after it was checked
any suggestion is most welcome.
I believe that you put your component select2 inside another component which contains a form which you then pass to select2 for create another <form> tag, is that correct? I mean do you have something like that?
<form [formGroup]="form">
<!-- Some code -->
<select2 [field]="something" [form]="form"></select2>
</form>
If so, then your select2 component SHOULD NOT contain re-declaration of form, it should not contain anything related to forms at all. It should be a form control. Please read a post by Netanel Basal on how to create custom form controls. You will need to create ControlValueAccessor for your select2 and wire it up to Angular forms through a custom provider.
The issue you're facing is that since you include form object twice in the DOM data changes are propagated twice as well and you run into issues. There should be only one reference to a specific instance of FormGroup in your templates.
Solution that worked
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'select2',
export class Select2Component implements OnInit {
constructor(public cl: Services,private cd: ChangeDetectorRef) {
this.options = {
width: '258',
multiple: true,
tags: false
};
}
Binding function
changed(search: any) {
//call service pass search text to service
return this.cl.searchFunc(search).subscribe(
res1 =>
this.select2data = res1.data;
this.cd.markForCheck(); // marks path
this.cd.detectChanges();
}
}
},
error => {
console.log('error = ', error);
});
}
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
I can't figure out how to bind the fields to the component so that the fields update when i change the properties in OnDataUpdate().
The field "OtherValue" has a working two way binding to the input-field and the field for "Name" displayes "test" when the component is displayed. But when i refresh the data, none of the fields are updated to display the updated data.
The first logged value of "this.name" is undefined(???), the second is correct, but the field bound to the same property does not update.
How can the component provide the initial value for the name-field, but when the data update is trigged, the name-property is suddenly undefined?
stuff.component.ts
#Component({
moduleId: __moduleName,
selector: 'stuff',
templateUrl: 'stuff.component.html'
})
export class StuffComponent {
Name: string = "test";
OtherValue: string;
constructor(private dataservice: DataSeriesService) {
dataservice.subscribe(this.OnDataUpdate);
}
OnDataUpdate(data: any) {
console.log(this.Name);
this.Name = data.name;
this.OtherValue = data.otherValue;
console.log(this.Name);
}
stuff.component.html
<table>
<tr>
<th>Name</th>
<td>{{Name}}</td>
</tr>
<tr>
<th>Other</th>
<td>{{OtherValue}}</td>
</tr>
</table>
<input [(ngModel)]="OtherValue" />
The this context is lost if you pass it like that in the subscribe() function. You can fix this in several ways:
by using bind
constructor(private dataservice: DataSeriesService) {
dataservice.subscribe(this.OnDataUpdate.bind(this));
}
by using an anonymous arrow function wrapper
constructor(private dataservice: DataSeriesService) {
dataservice.subscribe((data : any) => {
this.OnDataUpdate(data);
});
}
change the declaration of the function
OnDataUpdate = (data: any) : void => {
console.log(this.Name);
this.Name = data.name;
this.OtherValue = data.otherValue;
console.log(this.Name);
}
Passing method references this way breaks the this reference
dataservice.subscribe(this.OnDataUpdate);
use this instead:
dataservice.subscribe((value) => this.OnDataUpdate(value));
By using ()=> (arrow function) this is retained and keeps referring to the current class instance.
You are losing this context, to keep context you can use bind.
dataservice.subscribe(this.OnDataUpdate.bind(this));
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; }
}