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; }
}
Related
I have Tags Components in my project and I reused that component in other components. In my Tags component ngOnInit, I called backend to get all the existing tags. The problem I have right now is that call is applied to every other components even though the call is not needed at other components other than Edit Components. Since I only need the backend call to show existing tags just for Edit Components, I tried to move that call to Edit Components ngOninit but it didn't show me the existing tags anymore. I would be really appreciated if I can get any help or suggestion on how to fix this.
Tags Component TS
ngOnInit(): void {
this.tagService.getAllTagsByType('user').subscribe((normalTags) => {
this.loading = true;
if (normalTags)
this.allnormalTags = normalTags;
this.allnormalTags.forEach(normalTags => {
this.allTagNames.push(normalTags.tag);
});
this.loading = false;
})
}
If i add this call in Tags Component, it show me all the existing tags in drop down. I tried to move this to Edit component ngOnIt since I only want Eidt Component to use that call but It didn't show me existing tags anymore.
Tags.Service.ts
getAllTagsByType(tagType: any){
return this.http.get<Tag[]>(`${environment.api.chart}/tags/all/${tagType}`).pipe(first());
}
You could try to setup a flag to trigger the backend call using #Input.
tags.component.ts
import { Component, OnInit, Input } from '#angular/core';
export class TagsComponent implements OnInit {
#Input() getAllTags = false;
ngOnInit(): void {
if (this.getAllTags) { // <-- check here
this.tagService.getAllTagsByType('user').subscribe(
(normalTags) => {
this.loading = true;
if (normalTags)
this.allnormalTags = normalTags;
this.allnormalTags.forEach(normalTags => {
this.allTagNames.push(normalTags.tag);
});
this.loading = false;
},
error => {
// handle error
}
);
}
}
}
Now pass the value true to getAllTags when you wish to make the backend call. Since ngOnChanges hook is triggered before ngOnInit, the call will not be made if the property isn't passed in the component selector.
<!-- edit component -->
<mc-tags
[getAllTags]="true"
[workspace]="workspace"
[removable]="true"
[selectable]="true"
[canAdd]="true" ]
[editMode]="true"
(added)="tagAdded($event)"
(removed)="tagRemoved($event)"
> </mc-tags>
<!-- other components -->
<mc-tags [workspace]="workspace"></mc-tags>
Try to use RxJS. You should keep your Tags Data in TagService as a Subject (observable). Btw it is always best practise to store data in service layer.
TagService:
#Injectable({
providedIn: 'root'
})
export class TagService {
tagsSource = new BehaviorSubject<Tag[]>(null);
allnormalTags$ = this.tagsSource.asObservable();
getAllTagsByType(type: string){
http.request.subscribe(resultData => this.tagsSource.next(resultData))
}
}
Then in your component you can check whether data are already loaded and don't call it again.
export class ProductListComponent implements OnInit {
constructor(private tagService: TagService) { }
ngOnInit(): void {
if (isNullOrUndefined(this.tagService.tagSource.getValue())
this.tagService.getAllTagsByType('user')
}
P.S. You don't need to explicitly subscribe service observable in your component. Instead you can directly get your data from service subject/observable with async pipe.
<table *ngIf="tagService.allnormalTags$ | async as allnormalTags">
<tbody>
<tr class="product-list-item" *ngFor="let tag of allnormalTags">
<td data-label="name"> {{tag.name}} </td>
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);
});
}
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")
}
}
For my app, the ItemDetailComponent is where info of an item will be displayed. I have a service that retrieves all items using promise. I use ActivatedRoute to retrieve the item ID from the url path, then run the service, get all items, then find the item with the ID retrieved above, and assign it to selectedItem variable.
Here is item-detail.component.ts:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
console.log('Inside promise', this.selectedItem)
})
console.log('Outside promise', this.selectedItem)
}
}
And here is item-detail.component.html template so I could display my item, just an example:
<div>
<h1>{{title}}</h1>
<div *ngIf="selectedItem">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app returns nothing but the title unfortunately. I then added the two console.log() commands and found out that the one outside of the promise as well as the html template are rendered before the promise is fulfilled, and no selectedItem is available at that time. How could I force the app to execute them only after the promise is resolved in order to have the selectedItem in place for displayed?
EDIT: I added a new line in the html template to examine further:
<div>
<h1>{{title}}</h1>
<div><label>Item ID 1: </label>{{selectedItem.itemId}}</div>
<div *ngIf="selectedItem">
<div><label>Item ID 2: </label>{{selectedItem.itemId}}</div>
</div>
</div>
The app displays "Item ID 1:" label but with no actual id there. The console shows me an error saying that "Cannot read property 'itemId' of undefined", again confirming that the whole template is rendered before promise resolved and is not re-rendered after data is loaded. So weird.
You could create a Resolver for the route that fetches the desired data.
https://angular.io/api/router/Resolve
https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html
Add a boolean variable in to your class like
private dataAvailable:boolean=false;
and in the subscription to the promise,make this true when the data is available
then((selectedItem) => {
this.selectedItem = selectedItem;
this.dataAvailable=true;
console.log('Inside promise', this.selectedItem)
})
and in the template render when the data is available
<div>
<h1>{{title}}</h1>
<div *ngIf="dataAvailable">
<div><label>Item ID: </label>{{selectedItem.itemId}}</div>
</div>
</div>
It should do the trick
Update
ngOnInit() seems to be just a event handler hook - returning anything won't affect anything it seems. Hence my old answer will not work.
There are other workarounds like using *ngIf or putting it in routes etc. but I wish there was something like resolvePromise(): Promise hook that would put a condition on resolution before rendering.
This is instead of developers putting the boilerplate in every component.
Old answer
Most likely that is because you are missing return statement in the second then.
then((selectedItem) => {
this.selectedItem = selectedItem
console.log():
return selectedItem;//
}
Is it possible that the ChangeDetection is set to OnPush somewhere up the component tree?
If that is the case, the template does not automatically rerender, because nothing triggers the ChangeDetection for this component.
Look out for a Component with the setting changeDetection: ChangeDetectionStrategy.OnPush
#Component({
selector: 'example',
template: `...`,
styles: [`...`],
changeDetection: ChangeDetectionStrategy.OnPush
})
Also you already have a valid solution by using a Resolver you could check if this helps:
export class ItemDetailComponent implements OnInit {
private title = 'Item Details'
private selectedItem: object
constructor(
private route: ActivatedRoute,
private itemService: ItemService,
// the reference to the components changeDetector is needed.
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
const selectedItemId = this.route.snapshot.params.itemId
return this.itemService.getAllItems()
.then((items) => {
return _.find(items, item => item.itemId === selectedItemId)
})
.then((selectedItem) => {
this.selectedItem = selectedItem
// this triggers the changedetection and the template should be rerendered;
this.changeDetectorRef.detectChanges();
console.log('Inside promise', this.selectedItem)
});
console.log('Outside promise', this.selectedItem)
}
}
Here is a great article about Angulars ChangeDetection: https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.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