Note that im just starting to learn Angular 9:
I'm trying to create a tree using a recursive component. On every recursive call component gets a different object on #input and using ngOnChanges does some math and updates that object.
First tree generation is fine, but when generating second and more times values are multiplying. From what I saw,item corresponding values retain value from previous tree generation that was changed on ngOnChanges()
Here's code:
Recursive Component ts
import { Component, Input, OnChanges } from '#angular/core';
import { IItem, IComponent } from 'src/app/item';
import { notEqual } from 'assert';
import { ItemsService } from 'src/app/items.service';
#Component({
selector:'sf-production2',
templateUrl:'./production.component2.html',
styleUrls:['./production.component2.css']
})
export class ProductionComponent2 implements OnChanges{
#Input() item: IItem
items: IItem[]
#Input() amountNeeded: number;
inputs: IComponent[];
constructor(private itemService: ItemsService){
this.items=itemService.getItems();
}
GetItemByName(name: string){
return this.items.find(x=>x.name === name);
}
ngOnChanges(){
console.log(`${this.item.name}`)
if(this.item.components != null)
{this.item.components.forEach(element => {
console.log(`${element.name}:${element.inputPerMinute}`)
element.inputPerMinute = element.inputPerMinute * (this.amountNeeded/this.GetItemByName(this.item.name).outputPerMin)
});}
}
}
Recursive Component html;
<div *ngFor='let component of item.components'>
<ul>
<img src='{{GetItemByName(component.name).img}}'> {{component.inputPerMinute}}
<sf-production2 *ngIf='GetItemByName(component.name).components' [item]='GetItemByName(component.name)' [amountNeeded]='component.inputPerMinute'></sf-production2>
</ul>
</div>
item.ts
export interface IItem{
name: string;
img: string;
components: IComponent[];
outputPerMin: number;
}
export interface IComponent{
name: string;
inputPerMinute: number;
}
Recursive component gets called for the first time in app.component
<sf-production2 [item]='selectedItem' [amountNeeded]='2'></sf-production2>
Did i miss something crucial? Am i doing it right?
Thanks
I would say that the constructor that gets the data from the database is called only once and after that you are constantly operating on the same objects, not on their copies. By such the values that you are updating persist when you select the same object twice.
Related
I have a component to which I pass an object as its input.
That object comes from an API, so it is not available right away. And the object returned from the API can also be empty.
In the template, I want to hide the value unless it is not an empty object.
So I have this in my component:
import { Component, Input, OnChanges, SimpleChanges } from "#angular/core";
import { BehaviorSubject, Observable, of } from "rxjs";
import { tap } from "rxjs/operators";
import { not, isEmpty } from "ramda";
#Component({
selector: "hello",
template: `
{{ hasName | async }} {{ name | async }}
<h1 *ngIf="(hasName | async)">Hello {{ name | async }}!</h1>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent {
#Input() name: Observable<{} | { first: string }> = of({});
hasName: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
ngOnInit() {
this.name.pipe(
tap(name => console.log(name)),
// the actual logic to calculate the boolean is quite involved, but I
// have simplified it here for brevity.
tap(name => this.hasName.next(not(isEmpty(name))))
);
}
}
The problem is that the console.log in the tap is never printed and the second tap is never run either, so the value for hasName is always false as set at the of the class.
I think what happens is that in OnInit, this.name is still of({}), and not the actual observable passed in through the template from the parent.
I am using of({}) because typing this.name as Observable<{} | { first: string}> | undefined leads to more boilerplate in the class to check it is indeed defined before using it.
How can I make this work in an idiomatic way?
Complete Stackblitz: https://stackblitz.com/edit/angular-ivy-gxqh2d?devtoolsheight=33&file=src/app/hello.component.ts
You can simplify this code a lot.
First off all, the TS definition.
{} | { first: string }
Can be done as
{ first?: string }
While it may look like I'm doing some neat picking here I'm not really. With the first option if you try to access variable.first you'll get an error saying it doesn't exist as the union cannot guarantee this. With the latter it's fine.
Then, the mock. When working with observables, try not to reassign observable. Here's how you can do what you where doing without reassigning:
public name$: Observable<{ first?: string }> = concat(
of({}),
interval(2000).pipe(
map(() => ({
first: `${Math.random()}`
}))
)
);
Finally, the child component. Passing observables as input is a code smell in 99% of the cases. Here all you want is a presentational component (also called dumb component).
This dumb component should not have any logic (as much as possible at least), and here's what you can do:
type Nil = null | undefined
#Component({
selector: "hello",
template: `
<h1 *ngIf="name?.first">Hello {{ name.first }}!</h1>
`
})
export class HelloComponent {
#Input() name: { first?: string } | Nil;
}
Which means that when you call this from the parent, you just do:
<hello [name]="name$ | async"></hello>
Here's an updated stackblitz: https://stackblitz.com/edit/angular-ivy-hnv23a?file=src%2Fapp%2Fapp.component.ts
If you want to react when the input value changes you can write your own setter:
export class HelloComponent {
#Input() set name(name: Observable<{} | { first: string }>) {
this.name$ = name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
);
}
name$: Observable<{} | { first: string }>;
...
}
Then in templates bind async to name$ instead.
Updated demo: https://stackblitz.com/edit/angular-ivy-7u7xkd?file=src%2Fapp%2Fhello.component.ts
I'd suggest to move your logic to app.component.ts instead of passing the whole observable into an input. This could help you to manage the value of that input and prevent assigning a value to it two times (you have assigned an empty object two times in your code - parent & child component).
Simplest thing to make your code work is just to subscribe to the observable you've created inside your ngOnInit:
this.name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
).subscribe();
In case you don't want to manage that (don't want to be worried about non destroyed observables) and you prefer to use the async pipe, you can just reassign this.name inside ngOnInit.
this.name = this.name.pipe(
tap(name => console.log(name)),
tap(name => this.hasName.next(not(isEmpty(name))))
);
I'm expecting React components like so:
class MySetting extends React.Component<{}> {
static name = 'My Setting';
render() {
return (<div>{'setting here'}</div>);
}
}
class Widget extends React.Component<{}> {
static settings = [ MySetting ]
render() {
return (<div>{'widget here'}</div>);
}
}
And I'd like to make a flow definition for them that essentially mean:
A react component that has a static field name
A react component that has a static field settings which is of the above type
How do I do this? I've tried a couple things like:
interface Settings {
name: string;
}
export type SettingsComponent = React.ComponentType<Object> & Settings
interface Widget {
settings: Array<SettingComponent>;
}
export type WidgetComponent = React.ComponentType<Object> & Widget
Or
declare class SettingsComponent<Props, State = void> extends React$Component<Props, State> {
static name: string;
}
declare class WidgetComponent<Props, State = void> extends React$Component<Props, State> {
static settings: Array<Class<SettingsComponent<any, any>>>
}
But they inevitably throw various flow errors that are awkward to decipher. Is there a generally accepted way of doing this?
Looks like with some more research I found a solution (using flow-bin#0.63.1).
The answer is flow's $Subtype<T> utility type:
export type SettingsComponent = {
name: string;
} & $Subtype<React.ComponentType<Object>>
export type VisualizationComponent = {
settings: Array<SettingsComponent>;
} & $Subtype<React.ComponentType<Object>>
Though I'll add that while my previous flow errors are gone and errors do show up when using a component with missing static fields or accessing non-existent fields, my editor (Atom + Nuclide) doesn't show tooltips for the type anymore.
I currently have this code in my app.component.ts
app.component.html
<div [ngClass]="myclass">
...rest of the content here
</div>
This I have the this:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
app.component.ts
export class AppComponent {
myclass: string;
changeClass(myclass) {
this.myclass = myclass;
}
}
Now, all this works fine BUT I now want to put the triggering button on another component.
If I put this on another component:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
How can I get it to change the class?
There are two ways you can do this you can use output with an EventEmit
Or you can set up a service that monitors the changes to a variable and use that as the control point for the change.
Personally, I use services for this instance as its easier to manage the code and its flow.
This answer has all the code in you need to look at.
Changing a value in two different components at the same time Angular 2
Hope that helps
There are at least two options. Subject and Observable or if this another component is a parent you can use #Input.
Subject and Observable method:
angular guide Highly recommended to read whole page.
Some component
export class SomeComponent {
constructor(private ClassService: ClassService) { }
private changeClass(class) {
this.ClassService.changeClass(class);
}
}
Another Component
export class AnotherComponent implements OnInit, OnDestroy {
constructor(private ClassService: ClassService) { }
private class: string = "";
private subscribtion: Subscribtion;
ngOnInit(): void {
this.Subscribtion = this.ClassService.someClass$.subscribe(
(class) => { this.class = class; }
)
}
ngOnDestroy(): void {
this.Subscribtion.unsubscribe();
}
}
Service
#Injectable();
export class ClassService{
constructor() { }
private someClassSource= new Subject<string>();
someClass$= this.someClassSource.asObservable();
changeClass(class) {
this.someClassSource.next(class);
}
}
taken from my answer
#Input method:
angular guide
This is very simple, when you click button changeClass method will change elClass which will be passed to another component by #Input decorator, every change of #Input will cause a detect changes which will detect that value has changed so class will change to myClass.
Parent component
parent.component.html
<another-component [elementClass]="elClass"></another-component>
<button (click)="changeClass('myClass')">change class<button>
parent.component.ts
export class ParentComponnet {
private elClass: string = "";
changeClass(class: string) {
elClass = class;
}
}
Another component (must be child component)
another.component.html
<div [ngClass]="elementClass">
another.component.ts
export class AnotherComponent {
#Input() elementClass: string;
}
There is also Child to Parent interaction via #Output (emitting event) angular guide
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
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.