Proper way to propagate computed value on Angular2 - javascript

I want to use computed value by #Input properties
But propagating initial value is not work well.
https://plnkr.co/edit/1MMpOYOKIouwnNc3uIuy
I create App (root component with template driven form) and NumComponent (child component that just saving typed value) component.
When i pass attribute to NumComponent like [useThree]="true" then i want set default value '3' to NumComponent
But i can't find way without using setTimeout
Is there way to propagate initial value without setTimeout?
Edited at 5/5
App component
#Component({
selector: 'my-app',
template: `
<div>
<form novalidate #form="ngForm">
<app-num name="num" ngModel [useThree]="true"></app-num>
</form>
<pre>{{form.value | json}}</pre>
</div>
`
})
export class App {}
NumComponent
export const NumValueAccessor = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumComponent),
multi: true
};
#Component({
selector: 'app-num',
template: `<input [(ngModel)]="num" type="text" (ngModelChange)="updateValue()" />`,
providers: [NumValueAccessor]
})
export class NumComponent implements ControlValueAccessor {
num = 0;
// I want set literal number 3 to `num` property
// when `useThree` is true.
#Input() useThree = false;
onChange = (_: any) => {};
updateValue(num = this.num) {
this.onChange(String(num));
}
writeValue(value: string): void {
if (this.useThree) {
/**********
* ISSUE
**********/
// this code is not work. after code ran, `NumComponent` has
// value 3 but AppComponent's internal FormComponent value
// is '' (empty string)
// this.num = 3;
// this.updateValue(3);
// ran code with `setTimeout` solve this problem. but
// I don't want using setTimeout for this feature.
// setTimeout(() => {
// this.num = 3;
// this.updateValue(3);
// }, 0);
// Is there any way to propagate computed initial value?
this.num = 3;
this.updateValue(3);
/**********
* ISSUE
**********/
this.useThree = false;
return;
}
this.num = Number(value);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean): void {}
}
It seems that parent component don't realize propagated value at initialization lifecycle time.

I am not sure I have understood the problem fully, but one possible solution to what you are looking for could be
private _useThree = false;
#Input() set useThree(value: boolean) {
this._useThree = value;
if (this._useThree) {
this.num = 3;
}
}
This way, any time you want to set the value of the input useThree property from the parent component, you actually execute the code of the setter method defined above.

Related

Why does the child component mutation my variable in angular

I'm trying to find a way to share a variable between my components in Angular. My variable that I pass to my child components are interchangeable between their child components. I used service for this, but still could not solve the problem. I am sharing a demo by simplifying my operations using BehaviorSubject.
demo : https://stackblitz.com/edit/angular-ivy-6usdww?file=src%2Fapp%2Fhello.component.ts
My problem is:
i am sending variables from my app.component.ts component to hello and hello-a components. i am mutating variable in hello component. However, just before that I point the first version to a service. however, the changed status is forwarded.
hello.component
export class HelloComponent {
#Input() colDef: any[];
col: any[];
constructor(private columnDefService: ColumnDefService) {}
ngOnInit() {
let colDefFirst = Object.assign([], this.colDef);
this.columnDefService.setFirstColDef(colDefFirst);
this.setColumnDef(this.colDef);
}
setColumnDef(colDef: any[]): void {
let rC: any = Object.assign([], colDef);
rC[0].hide = true;
this.columnDefService.setColDef([].concat(rC));
this.columnDefService.colDef$.subscribe((colDef: any) => {
this.col = colDef;
});
}
}
hello-a.component
export class HelloAComponent {
#Input() colDef: any[];
firstColDef: any[] = [];
constructor(private ColumnDefService: ColumnDefService) {}
ngOnInit() {
this.ColumnDefService.firstColDef$.subscribe((firstColDef: any) => {
console.log('firstColDef', firstColDef);
this.firstColDef = firstColDef;
});
}
}
coldef.service
#Injectable({
providedIn: 'root'
})
export class ColumnDefService {
private colDefSource = new BehaviorSubject([]);
private firstColDefSource = new BehaviorSubject([]);
colDef$ = this.colDefSource.asObservable();
firstColDef$ = this.firstColDefSource.asObservable();
setColDef(colDef: any) {
this.colDefSource.next(colDef);
}
setFirstColDef(colDef: any) {
this.firstColDefSource.next(colDef);
}
}
this is what i want to do:
storing my variable in a component for future use. i want to continuously change my variable on demand in my other component.
thanks for your advice

Angular 4: How to watch an object for changes?

ETA: I know that there are various ways to watch my form for changes. That is not what I am trying to do. As the title says, I am asking how to watch for changes to an object. The app shown below is for illustration purposes only. Please answer the question that I have asked. Thanks!
I have this simple app:
import { Component, OnInit } from '#angular/core';
export class Customer {
firstName: string;
favoriteColor: string;
}
#Component({
selector: 'my-app',
template: `
<div *ngIf="customer">
<input type="text" [(ngModel)]="customer.firstName">
<input type="text" [(ngModel)]="customer.favoriteColor">
</div>
`
})
export class AppComponent implements OnInit {
private customer: Customer;
ngOnInit(): void {
this.customer = new Customer();
// TODO: how can I register a callback that will run whenever
// any property of this.customer has been changed?
}
}
Note the TODO. I need to register a callback that will run whenever any property of this.customer has been changed.
I cannot use ngChange on the inputs. I need to subscribe directly to changes on the model. The reasons pertain to my use case, and aren't worth going into here. Just trust me that this isn't an option.
Is this possible? I've done a lot of Googling, but I've come up dry.
Angular usually uses injected into constructor KeyValueDiffers class.
For your case it could look like:
import { KeyValueChanges, KeyValueDiffer, KeyValueDiffers } from '#angular/core';
export class Customer {
firstName: string;
favoriteColor: string;
}
#Component({
selector: 'my-app',
templateUrl: `./app.component.html`
})
export class AppComponent {
private customerDiffer: KeyValueDiffer<string, any>;
private customer: Customer;
constructor(private differs: KeyValueDiffers) {}
ngOnInit(): void {
this.customer = new Customer();
this.customerDiffer = this.differs.find(this.customer).create();
}
customerChanged(changes: KeyValueChanges<string, any>) {
console.log('changes');
/* If you want to see details then use
changes.forEachRemovedItem((record) => ...);
changes.forEachAddedItem((record) => ...);
changes.forEachChangedItem((record) => ...);
*/
}
ngDoCheck(): void {
const changes = this.customerDiffer.diff(this.customer);
if (changes) {
this.customerChanged(changes);
}
}
}
Stackblitz Example
One more option is using setter on properties that you want to check.
See also
http://blog.mgechev.com/2017/11/14/angular-iterablediffer-keyvaluediffer-custom-differ-track-by-fn-performance/
I need to subscribe directly to changes on the model.
Then you need to listen to model changes with ngModelChange
Template:
<input type="text" (ngModelChange)="doSomething($event)" [ngModel]="customer.firstName">
Class:
doSomething(event) {
console.log(event); // logs model value
}
DEMO
You can't watch changes in an object. Its not angular 1 there are no watchers here. Another solution will be via observables.
use form
<form #f="ngForm">
<input type="text" name="firstName" [(ngModel)]="customer.firstName">
<input type="text" name="favoriteColor" [(ngModel)]="customer.favoriteColor">
</form>
in code
#ViewChild('f') f;
ngAfterViewInit() {
this.f.form.valueChanges.subscribe((change) => {
console.log(change)
})
}
You could use custom setters to trigger your callback:
class Customer {
private _firstName: string
get firstName(): string {
return this._firstName
}
set firstName(firstName: string) {
this.valueChanged(this._firstName, firstName)
this._firstName = firstName
}
private _lastName: string
get lastName(): string {
return this._lastName
}
set lastName(lastName: string) {
this.valueChanged(this._lastName, lastName)
this._lastName = lastName
}
valueChanged: (oldVal, newVal) => void
constructor (valueChanged?: (oldVal, newVal) => void) {
// return an empty function if no callback was provided in case you don't need
// one or want to assign it later
this.valueChanged = valueChanged || (() => {})
}
}
Then just assign the callback when you create the object:
this.customer = new Customer((oldVal, newVal) => console.log(oldVal, newVal))
// or
this.customer = new Customer()
this.customer.valueChanged = (oldVal, newVal) => console.log(oldVal, newVal)
visit https://github.com/cartant/rxjs-observe. it bases on rxjs and proxy.
import { observe } from "rxjs-observe";
const instance = { name: "Alice" };
const { observables, proxy } = observe(instance);
observables.name.subscribe(value => console.log(name));
proxy.name = "Bob";
We are tasked with converting an Angular 1.x app to Angular 9. It's an application with ESRI maps, so we have some neat tools that the ESRI framework brought to the table. ESRI has watchUtils that do a whole lot more than just watch for changes.
But I missed Angular 1's simple $watch. Besides, we create Entities and Models in our application, and we may need to observe these from time to time.
I created an abstract class called MappedPropertyClass. It uses a Map<string, any> to map class properties, which allows me to easily implement toJSON and other utility functions.
The other Map this class has is _propertyChangeMap: Map<string, EventEmitter<{newvalue,oldvalue}>;
We also have a function called... $watch, which takes a string and a callback function.
This class can be extended by Entities as well as components or services
I'm happy to share, the caveat is your properties must look like this:
public get foo(): string {
return this._get("foo");
}
public set foo(value:string) {
this._set("foo", value);
}
--------------------------------------------------------------------
import { EventEmitter } from '#angular/core';
export abstract class MappedPropertyClass {
private _properties: Map<string, any>;
private _propertyChangeMap: Map<string, EventEmitter<{ newvalue, oldvalue }>>;
protected _set(propertyName: string, propertyValue: any) {
let oldValue = this._get(propertyName);
this._properties.set(propertyName, propertyValue);
this.getPropertyChangeEmitter(propertyName).emit({ newvalue:
propertyValue, oldvalue: oldValue });
}
protected _get(propertyName: string): any {
if (!this._properties.has(propertyName)) {
this._properties.set(propertyName, undefined);
}
return this._properties.get(propertyName);
}
protected get properties(): Map<string, any> {
var props = new Map<string, any>();
for (let key of this._properties.keys()) {
props.set(key, this._properties.get(key));
}
return props;
}
protected constructor() {
this._properties = new Map<string, any>();
this._propertyChangeMap = new Map<string, EventEmitter<{ newvalue: any,
oldvalue: any }>>();
}
private getPropertyChangeEmitter(propertyName: string): EventEmitter<{
newvalue, oldvalue }> {
if (!this._propertyChangeMap.has(propertyName)) {
this._propertyChangeMap.set(propertyName, new EventEmitter<{ newvalue,
oldvalue }>());
}
return this._propertyChangeMap.get(propertyName);
}
public $watch(propertyName: string, callback: (newvalue, oldvalue) => void):
any {
return this.getPropertyChangeEmitter(propertyName).subscribe((results) =>
{
callback(results.newvalue, results.oldvalue);
});
}
}

Angular, Get Index Of Dynamically Created Component Inside ViewContainerRef

I am trying to get the index of a dynamically created component inside ViewContainerRef
I need to get the index so I can destroy the component if I wanted too.
Code Below
#ViewChild('dynamicInsert', { read: ViewContainerRef }) dynamicInsert: ViewContainerRef
componentFactory
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
private viewContainerRef: ViewContainerRef,
) { }
ngAfterViewInit() {
this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(AssetOptionComponent);
}
addAssetOption() {
const dynamicComponent = <AssetOptionComponent>this.dynamicInsert.createComponent(this.componentFactory).instance
// how to get index of this dynamically generated component ^^^^
}
Trying to use
this.dynamicInsert.remove(index: number) to destroy component
but I first need the index of the dynamically created component
this.dynamicInsert.indexOf(viewRef: viewRef)
To get the index you can use indexOf method and hostView property:
const index = this.dynamicInsert.indexOf(dynamicComponent.hostView)
Also note that if you don't specify the index view container will destroy the last component:
remove(index?: number): void {
const viewData = detachEmbeddedView(this._data, index);
if (viewData) {
Services.destroyView(viewData);
}
}
export function detachEmbeddedView(elementData: ElementData, viewIndex?: number): ViewData|null {
const embeddedViews = elementData.viewContainer !._embeddedViews;
if (viewIndex == null || viewIndex >= embeddedViews.length) {
viewIndex = embeddedViews.length - 1;
}
So if you have only one component you don't need to pass index.
To remove all components you can use clear method.
If you are looking to destroy the created component you may consider a shortcut by just subscribing to it's observable destroy:
addAssetOption() {
const dynamicComponent: ComponentRef<any> = this.dynamicInsert.createComponent(this.componentFactory);
dynamicComponent.instance.destroy.subscribe(() => dynamicComponent.destroy())
}
and then upon removing event, in AssetOptionComponent, call it:
export class AssetOptionComponent {
destroy = new Destroyable();
delete(){
this.destroy.delete();
}
}
export class Destroyable extends Subject<any>{
delete() {
this.next();
}
}
Working demo

Angular: In which lifecycle hook is input data available to the Component

I have a component which receives an array of image objects as Input data.
export class ImageGalleryComponent {
#Input() images: Image[];
selectedImage: Image;
}
I would like when the component loads the selectedImage value be set to the first object of the images array. I have tried to do this in the OnInit lifecycle hook like this:
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnInit() {
this.selectedImage = this.images[0];
}
}
this gives me an error Cannot read property '0' of undefined which means the images value isn't set on this stage. I have also tried the OnChanges hook but I'm stuck because i can't get information on how to observe changes of an array. How can I achieve the expected result?
The parent component looks like this:
#Component({
selector: 'profile-detail',
templateUrl: '...',
styleUrls: [...],
directives: [ImageGalleryComponent]
})
export class ProfileDetailComponent implements OnInit {
profile: Profile;
errorMessage: string;
images: Image[];
constructor(private profileService: ProfileService, private routeParams: RouteParams){}
ngOnInit() {
this.getProfile();
}
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
}, error => this.errorMessage = <any>error
);
}
}
The parent component's template has this
...
<image-gallery [images]="images"></image-gallery>
...
Input properties are populated before ngOnInit() is called. However, this assumes the parent property that feeds the input property is already populated when the child component is created.
In your scenario, this is not the case – the images data is being populated asynchronously from a service (hence an http request). Therefore, the input property will not be populated when ngOnInit() is called.
To solve your problem, when the data is returned from the server, assign a new array to the parent property. Implement ngOnChanges() in the child. ngOnChanges() will be called when Angular change detection propagates the new array value down to the child.
You can also add a setter for your images which will be called whenever the value changes and you can set your default selected image in the setter itself:
export class ImageGalleryComponent {
private _images: Image[];
#Input()
set images(value: Image[]) {
if (value) { //null check
this._images = value;
this.selectedImage = value[0]; //setting default selected image
}
}
get images(): Image[] {
return this._images;
}
selectedImage: Image;
}
You can resolve it by simply changing few things.
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnChanges() {
if(this.images) {
this.selectedImage = this.images[0];
}
}
}
And as another one solution, you can simply *ngIf all template content until you get what you need from network:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
...
And switch flag value in your fetching method:
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
this.imagesLoaded = true; /* <--- HERE*/
}, error => this.errorMessage = <any>error
);
}
In this way you will renderout child component only when parent will have all what child needs in static content. It's even more useful when you have some loaders/spinners that represent data fetching state:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
<loader-spinner-whatever *ngIf="!imagesLoaded" [images]="images"></loader-spinner-whatever>
...
But short answer to your questions:
When inputs are available?
In OnInit hook
Why are not available to your child component?
They are, but at this particular point in time they were not loaded
What can I do with this?
Patiently wait to render child component utul you get data in asynchronous manner OR learn child component to deal with undefined input state

how can I listen to changes in code in angular 2?

I'm using angular 2. I have a component with an input.
I want to be able to write some code when the input value changes.
The binding is working, and if the data is changed (from outside the component) I can see that there is change in the dom.
#Component({
selector: 'test'
})
#View({
template: `
<div>data.somevalue={{data.somevalue}}</div>`
})
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
dataChagedListener(param) {
// listen to changes of _data object and do something...
}
}
You could use the lifecycle hook ngOnChanges:
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
ngOnChanges([propName: string]: SimpleChange) {
// listen to changes of _data object and do something...
}
}
This hook is triggered when:
if any bindings have changed
See these links for more details:
https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
As mentioned in the comments of Thierry Templier's answer, ngOnChanges lifecycle hook can only detect changes to primitives. I found that by using ngDoCheck instead, you are able to check the state of the object manually to determine if the object's members have changed:
A full Plunker can be found here. But here's the important part:
import { Component, Input } from '#angular/core';
#Component({
selector: 'listener',
template: `
<div style="background-color:#f2f2f2">
<h3>Listener</h3>
<p>{{primitive}}</p>
<p>{{objectOne.foo}}</p>
<p>{{objectTwo.foo.bar}}</p>
<ul>
<li *ngFor="let item of log">{{item}}</li>
</ul>
</div>
`
})
export class ListenerComponent {
#Input() protected primitive;
#Input() protected objectOne;
#Input() protected objectTwo;
protected currentPrimitive;
protected currentObjectOne;
protected currentObjectTwo;
protected log = ['Started'];
ngOnInit() {
this.getCurrentObjectState();
}
getCurrentObjectState() {
this.currentPrimitive = this.primitive;
this.currentObjectOne = _.clone(this.objectOne);
this.currentObjectTwoJSON = JSON.stringify(this.objectTwo);
}
ngOnChanges() {
this.log.push('OnChages Fired.')
}
ngDoCheck() {
this.log.push('DoCheck Fired.');
if (!_.isEqual(this.currentPrimitive, this.primitive)){
this.log.push('A change in Primitive\'s state has occurred:');
this.log.push('Primitive\'s new value:' + this.primitive);
}
if(!_.isEqual(this.currentObjectOne, this.objectOne)){
this.log.push('A change in objectOne\'s state has occurred:');
this.log.push('objectOne.foo\'s new value:' + this.objectOne.foo);
}
if(this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)){
this.log.push('A change in objectTwo\'s state has occurred:');
this.log.push('objectTwo.foo.bar\'s new value:' + this.objectTwo.foo.bar);
}
if(!_.isEqual(this.currentPrimitive, this.primitive) || !_.isEqual(this.currentObjectOne, this.objectOne) || this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)) {
this.getCurrentObjectState();
}
}
It should be noted that the Angular documentation provides this caution about using ngDoCheck:
While the ngDoCheck hook can detect when the hero's name has changed,
it has a frightful cost. This hook is called with enormous frequency —
after every change detection cycle no matter where the change
occurred. It's called over twenty times in this example before the
user can do anything.
Most of these initial checks are triggered by Angular's first
rendering of unrelated data elsewhere on the page. Mere mousing into
another input box triggers a call. Relatively few calls reveal actual
changes to pertinent data. Clearly our implementation must be very
lightweight or the user experience will suffer.

Categories

Resources