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))))
);
Related
I would like to know if it is possible to implicitly infer the type from the given example of components below :
#Component({
selector:'child',
template:`<button (click)="triggerChange()">Click Me</button>`
})
export class ChildComponent {
#Output() inputchange = new EventEmitter<string>();
triggerChange(){
this.inputchange.emit('Hello!');
}
}
while reading this output in the parent component
#Component({
template:`<child (inputchange)="childChanged($event)"></child>`
})
export class ParentComponent {
childChanged(event){
console.log(event);
}
}
Currently there is no possible way to implicitly know the type of the output parameter, except if we explicitly define it ( childChanged(event: string){ ... })
So the my question is is there any way to implicitly infer, or know the type of the incoming parameter (or variable) in this case?
Im getting items from a cart service and putting them into my shopping cart component by making them public shoppingCartItems$ Observable<ISticker[]> = of ([]) and then subscribing that public shoppingCartItems: ISticker[] = [];
but when I place them into my html in an ngfor loop it tells me the properties are undefined
import { Component, OnInit } from '#angular/core';
import {CartService} from '../cart.service'
import { of } from 'rxjs';
import {Observable} from 'rxjs'
import { ISticker } from '../classes/isticker';
#Component({
selector: 'app-shopping-cart-component',
templateUrl: './shopping-cart-component.component.html',
styleUrls: ['./shopping-cart-component.component.css']
})
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartItems$: Observable<ISticker[]> = of([]);
public shoppingCartItems: ISticker[] = [];
constructor(private cartService: CartService) {
this.shoppingCartItems$ = this
.cartService
.getItems();
this.shoppingCartItems$.subscribe(_ => this.shoppingCartItems = _);
}
ngOnInit() {
}
In my HTMl I've tried using the async pipe line to unwrap the information
<p>You have {{shoppingCartItems.length}} items in your bag</p>
<ng-container *ngIf=" shoppingCartItems$ | async as shoppingCartStickers"></ng-container>
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
the output to the page shows <p>You have {{shoppingCartItems.length}} items in your bag</p> the amount of items in the cart but doesn't show any of the properties attached to the variable in the loop nor does it show the other paragraph tag.
export class CartService {
private itemsInCartSubject: BehaviorSubject<ISticker[]> = new BehaviorSubject([]);
private itemsInCart: ISticker[] = [];
constructor() {
this.itemsInCartSubject.subscribe(_ => this.itemsInCart = _);
}
public addToCart(item: ISticker){
this.itemsInCartSubject.next([...this.itemsInCart, item]);
}
public getItems(): Observable<ISticker[]> {
return this.itemsInCartSubject.asObservable();
}
This is my cart.services.ts for reference. This is my first time posting to stack overflow so if there is anything that would be helpful to see please let me know as I'm happy to provide it.
Alternatively, you can have your ShoppingCartComponentComponent something like this:
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartStickers: ISticker[] = [];
constructor(
private cartService: CartService
) { }
ngOnInit() {
this.cartService.getItems()
.subscribe(items => {
this. shoppingCartStickers = items;
});
}
}
Now, shoppingCartStickers can be directly used in the component html:
<p>You have {{shoppingCartStickers?.length}} items in your bag</p>
<ng-container *ngIf="shoppingCartStickers?.length">
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
</ng-container>
the output to the page shows You have {{shoppingCartItems.length}} items in your bag the amount of items in the cart
It's because you use shoppingCartItems which calculated in subscribe and will be up to date on every emitted event from shoppingCartItems$.
but doesn't show any of the properties attached to the variable in the loop nor does it show the other paragraph tag
It's because the template variable shoppingCartStickers available only in the block you define it. So you just need to do:
<ng-container *ngIf="(shoppingCartItems$ | async) as shoppingCartStickers">
<app-home *ngFor="let sticker of shoppingCartStickers">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
</ng-container>
I recommended you to use single variable for that. You don't need to do shoppingCartItems$ | async as shoppingCartStickers in template because this data has already stored in the shoppingCartItems property of the component.
Your issue is that you have misplaced the ending tag of <ng-container>.
What you have is essentially:
<ng-container>
<!-- Stickers available ONLY here -->
</ng-container>
<!-- Some code that tries to use stickers, but they are not available in this score --!>
The correct usage of ng-contaner is to actually wrap it AROUND the elements that require the data you're fetching from the service.
In any case, I would not use the ng-container here at all. You use case is:
You have an observable of an array: Observable<ISticker[]>
You want to create an element for each of the ISticker objects.
For this you can combine async pipe together with *ngFor:
<app-home *ngFor="let sticker of shoppingCartItems$ | async">
<p>{{sticker.description}}</p>
<p>stuff</p>
</app-home>
Separately from this, you want to peek at the length of the array to display an extra p tag.
Since you already have the access to the observable, you can easily do
<p>You have {{(shoppingCartItems$|async)?.length}} items in your bag</p>
<!-- OR -->
<p *ngIf="shoppingCartItems$|async as items">
You have {{items.length}} items in your bag
</p>
The problem with this is that now you're accessing shoppingCartItems$ TWICE: one time for the list of items, and one time to get the length. If you're getting this data from an API, this can easily result in TWO requests.
To combat this problem, you can use shareReplay operator, which will allow multiple subscribers to use the same value from a single observable:
export class ShoppingCartComponentComponent implements OnInit {
public shoppingCartItems$: Observable<ISticker[]>;
constructor(private cartService: CartService) {
}
ngOnInit() {
this.shoppingCartItems$ = this.cartService.getItems()
.pipe(shareReplay(1));
}
}
Here's a stackblitz with this example, where you can see how this works.
Now you are free to use shoppingCartItems$ any number of times, and this won't cause any unintended behaviour.
On top of that, now we don't manually subscribe, which means that the async pipe will get rid of the subscription for us when the component is destroyed, preventing a potential memory leak.
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.
I have an abstract class in "File1":
// File1.tsx
import * as React from 'react';
export interface IProps {
prop1: string;
prop2: number;
}
export abstract class Something extends React.Component<IProps> {
public typeName: string;
}
then, in other file (File2) i define infinite classes extending from abstract class Something:
// File2.tsx
import { Something } from './File1';
export class Something1 extends Something {
public typeName: string = 'Type1';
public render() {
return <div>Something 1</div>
}
}
export class Something2 extends Something {
public typeName: string = 'Type2';
public render() {
return <div>Something 2</div>
}
}
Now, here is my problem:
In a 3rd File, i import the classes defined before (Something1 or Something2) and then i passing this class to 2 different components: ReadProperty and RenderComponent. In the first component i need accessing to the property typeName of the class and do some stuff and in the second file i need render that class:
// File3.tsx
import { Something } from './File1';
import { Something1, Something2 } from './File2';
interface IReadProperty {
something: Something;
};
const ReadProperty: React.SFC<IReadProperty> = ({ something }) => {
// here i can access to property typeName. No problem here.
something.typeName // Type1 | Type2 | ... Infinite
...do some stuff
}
interface IRenderComponent {
something: Something;
}
const RenderComponent: React.SFC<IRenderComponent> = ({ something }) => {
// i need do some stuff here before render "something" component
// here i get an error when i try render the component
return <something />;
}
const Main: React.SFC = () => {
return (
<div>
<ReadProperty something={Something1} />
<RenderComponent something={Something1} />
</div>
);
}
but when i try to render the component in RenderComponent, i get the follow error: TS2604: JSX element type 'something' does not have any construct or call signatures.
What is wrong here? i defined type 'something' as abstract Class Something because i can define infinite Classes that extend from Something, so i can't define using: something: Something1 | Something2 | Something50 ...;
here is an example that i trying to do: https://codesandbox.io/s/18yzvkx8jj
The issue is that in your definition of IRenderComponent, you're saying that something is an instance of Something, whereas what you want is a constructor type. One other thing is that React generally complains when you try to instantiate components with lowercase names. Try this:
interface IRenderComponent {
Something: new (props: IProps) => Something1;
};
const RenderComponent: React.SFC<IRenderComponent> = ({ Something }) => {
return <Something prop1="foo" prop2={3} />;
}
For IReadProperty, it looks like you do want an instance of Something (since you want to access typeName, which is an instance property). However, you can't actually pass an instantiated component:
<ReadProperty something={<Something1 prop1="" prop2={3} />;} /> //Error
This is because <Something1 ... /> isn't actually an instance of Something1 - it's an instance of JSX.Element. You could pass an instance of Something1, like this:
<ReadProperty something={new Something1({prop1: "", prop2: 3})} />
But I imagine that's not what you want, since you can't actually use the resulting instance in rendering.
The best way to address the IReadProperty issue depends on what you're actually trying to accomplish. In general, React favors composition over inheritance and reflection, so it might be easier to consider how to achieve your goal by starting with base components and composing them, or using higher-order components.
Make sure that you have jsx:react in tsconfig.json
Make sure that you have consistent version of react react-dom #types/react and #types/react-dom package versions.
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.