I use Observables to carry values from parent to child components.
Here is my top level app component:
import { Component } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { Observer } from 'rxjs/Observer';
import { ViewModel } from './ViewModel.data';
import { ViewComponent } from './ViewComponent.component';
#Component({
selector: ...,
directives: [
ROUTER_DIRECTIVES,
ViewComponent
],
precompile: [],
styles: [],
template: `
<div>
<ul class="nav nav-tabs">
<li *ngFor="let v of views"><a (click)="setView(v)">{{ v.name }}</a></li>
</ul>
<viewcomponent
*ngFor="let v of views"
[viewName]="v.name"
[activeViewObservable]="activeViewObservable"></viewcomponent>
</div>
`
})
export class AppComponent{
views:ViewModel[];
activeViewObservable:Observable<ViewModel>;
viewObserver:Observer<ViewModel>;
activeView:ViewModel;
constructor() {
this.views = [{name: 'one'}, {name: 'two'}, {name: 'three'}, {name: 'four'}];
this.activeViewObservable = new Observable<ViewModel>(observer => this.viewObserver = observer);
}
public setView(view:ViewModel):void {
this.viewObserver.next(view); // load values here
}
}
I use a component called viewcomponent here:
#Component({
selector: 'viewcomponent',
directives: [
ROUTER_DIRECTIVES
],
template: `
<div class="tab-pane" [ngClass]="{ 'active': isActive() }">
...
</div>
`
})
export class ViewComponent {
// these values are always the last view. why???
#Input() viewName:string;
#Input() activeViewObservable:Observable<TabViewModel>;
private activeView:ViewModel;
constructor() {}
ngOnInit() {
this.activeViewObservable.subscribe( //listen to values loaded with the viewObserver
activeView => {this.activeView = activeView;},
error => console.error(error));
}
public isActive():boolean {
let bool:boolean = false;
if (this.activeView) {
console.log(this.viewName); // <---- this value is always the last view, 'four'
bool = this.activeView.name == this.viewName;
}
console.log(bool);
return bool;
}
}
The data model im using is here:
export interface ViewModel {
name: string;
}
I'm trying to load an observer with values in the AppComponent and then subscribe to them in the child. However, the value emitted by the observable is always the last element.
I want to call the setView method in the parent and then apply a class to that specific child view.
Possible solution: use Subject if you want to call .next() on an Observer outside of the instantiation of the Observable. Subject behaves as both. You can subscribe to them separately from where you pass events to it. Plunker Example
Why your code does not work?
Your code that uses rxjs could write like this:
let viewObserver;
const myObservable = new Observable(observer => {
viewObserver = observer;
});
myObservable.subscribe(
activeView => {
console.log(1, activeView);
},
error => console.error(error));
myObservable.subscribe(
activeView => {
console.log(2, activeView);
},
error => console.error(error));
viewObserver.next({ name: 'one' });
https://jsfiddle.net/t5z9jyf0/
What is expected output?
2, { name: 'one' }
Why?
Let's open rxjs documentation
http://reactivex.io/rxjs/manual/overview.html#subscribing-to-observables
Key point there is:
When calling observable.subscribe with an Observer, the function
subscribe in Observable.create(function subscribe(observer) {...}) is
run for that given Observer. Each call to observable.subscribe
triggers its own independent setup for that given Observer.
let viewObserver;
var myObservable = new Observable<ViewModel>(function subscribe(observer) {
console.log(observer, observer.destination._next);
viewObserver = observer;
});
myObservable.subscribe( // this triggers subscribe function above
activeView => {
console.log(1, activeView);
},
error => console.error(error));
myObservable.subscribe( // this also triggers subscribe function above
activeView => {
console.log(2, activeView);
},
error => console.error(error));
viewObserver.next({ name: 'one' }); // notify subscriptions
https://jsfiddle.net/t5z9jyf0/1/
So that code does not work because after
myObservable.subscribe(
activeView => {
console.log(2, activeView);
},
error => console.error(error));
method is executed, viewObserver will be overwritten and it will be Subscriber object from activeView => { console.log(2, activeView); }, so viewObserver.next will give us
console.log(2, { name: 'one' });
That's why only last subscription is executed
Related
I just try to show the value of a property in the template. But at the moment nothing is shown.
So this is the component:
export class ServerStatusComponent implements OnInit {
snovieCollection: SnovietatusDto = {};
constructor(private snovierStatus: snovieStatusService) {}
ngOnInit(): void {
this.sensorStatus
.getSensorStatuses()
.pipe(
map((data) => {
console.log(data.cameraSensors);
})
)
.subscribe((status) => {
});
}
}
And this is the template:
<p>Camera sensoren</p>
<tr *ngFor="let camera of snovieStatusCollection.key|keyvalue">
test
<h3> {{camera | json}}</h3>
</tr>
So I just want to show in the template the value of key. And the console.log returns this:
0: {key: "T", latestTimestamp: "2021-03-12T10:09:00Z"}
So I don't get any errors. But also nothing is shown.
Two things:
You aren't returning anything from the map. So undefined would be emitted to the subscription. Use tap for side-effects instead.
You aren't assigning the response to this.sensorStatusCollection in the subscription.
export class ServerStatusComponent implements OnInit {
sensorStatusCollection: SensorStatusDto = {};
constructor(private sensorStatus: SensorStatusService) {}
ngOnInit(): void {
this.sensorStatus
.getSensorStatuses()
.pipe(
tap((data) => { // <-- `tap` here
console.log(data.cameraSensors);
})
)
.subscribe((status) => {
this.sensorStatusCollection = status; // <-- assign here
});
}
}
Update: Type
As pointed out by #TotallyNewb in the comments, the type of this.sensorStatusCollection needs to be an array of type SensorStatusDto
export class ServerStatusComponent implements OnInit {
sensorStatusCollection: SensorStatusDto[] = [];
...
}
I have a parent component (B) that is getting data from it's parent input (A)
(C) have is (B) child component.
Inside (B) I'm having a selector that gets data from the store.
export class BComponent implements OnChanges {
#Input() branchId;
ngOnChanges() {
this.selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection })
);
this.selectedDataByBranch$.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.lineChart.data.datasets = this.trainsDatasets ? this.trainsDatasets : [];
this.lineChart.update();
});
directionChanged(event) {
this.selectedDirection = event;
this.selectedDataByBranch$ = this.store.pipe(
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection })
);
}
}
directionChanged is the Output event that I get from (C)
The issue this that selectedDataByBranch subscription is not getting the new data update triggered inside selectedDataByBranch$
I have also tried this way
directionChanged(event) {
this.selectedDirection = event;
select(selectBranchDirections, { branchId: this.branchId, dir: this.selectedDirection });
}
What i could suggest is. Turn your parameters into a Subject then merge with the store selection, in your directionChanged(event) method provide value to subject.
So your final code will be something like this:
export class BComponent implements OnChanges {
#Input() branchId;
criterias$= new Subject<{branchId:number,dir:number}>;
ngOnChanges() {
this.selectedDataByBranch$ = this.criterias$.pipe(mergeMap(criteria=> this.store.pipe(
select(selectBranchDirections, { branchId: criteria.branchId, dir: this.searchDirection})
)));
this.selectedDataByBranch$.subscribe(selectedDataByBranch => {
this.trainsDatasets = this.getDatasets(selectedDataByBranch);
this.lineChart.data.datasets = this.trainsDatasets ? this.trainsDatasets : [];
this.lineChart.update();
});
this.criterias$.next({branchId:this.branchId,dir:this.sortDirection}); // init first call
}
directionChanged(event) {
this.selectedDirection = event;
this.criterias$.next({ branchId: criteria.branchId, dir: this.searchDirection}});
);
}
}
This stackblitz tries to materialize what i say.
I have a page with three components:
1. Products list component which gets some products as input and display them.
2. Filters component which displays some filters list i.e. (size, colour,...) and also display the added filters.
3. Main component which is the root component
Let say a user adds 1 filter which fires a http request to get new filtered products and while the request is pending he removes the added filter which fires another http request to fetch all the products
How to cancel the first request so we don't display the filtered products?
Here is my code:
class FiltersService {
private _filters: any[];
get filters() {
return this._filters;
}
addFilter(filter) {
this._filters.push(filter);
}
removeFilter(filter) {
// Remove filter logic ...
}
}
class DataService_ {
constructor(private http: HttpClient) {
}
getProducts(filters) {
return this.http.post<any[]>('api/get-products', filters)
}
}
#Component({
selector: 'app-main',
template: `
<div>
<app-filters [filtersChanged]="onFiltersChange()"></app-filters>
<app-products-list [products]="products"> </app-products-list>
</div>
`
})
class MainComponent {
products: any[];
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
let filters = this.filtersService.filters;
this.dataService.getProducts(filters)
.subscribe(products => this.products = products)
}
onFiltersChange() {
this.setProducts();
}
}
#Component({
selector: 'app-filters',
template: `
<div>
Filters :
<ul>
<li *ngFor="let filter of filters" (click)="addFilter(filter)"> {{ filter.name }}</li>
</ul>
<hr>
Added Filters:
<ul>
<li *ngFor="let filter of filtersService.filters"> {{ filter.name }} <button (click)="removeFilter(filter)"> Remove</button></li>
</ul>
</div>
`
})
class FiltersComponent {
filters = [{ name: 'L', tag: 'size' }, { name: 'M', tag: 'size' }, { name: 'White', tag: 'colour' }, { name: 'Black', tag: 'colour' }]
#Output() filtersChanged = new EventEmitter()
constructor(public filtersService: FiltersService) {
}
addFilter(filter) {
const isAdded = this.filtersService.filters.find(x => x.name === filter.name);
if (isAdded) return;
this.filtersService.addFilter(filter);
this.filtersChanged.emit()
}
removeFilter(filter) {
this.filtersService.remove(filter);
this.filtersChanged.emit()
}
}
#Component({
selector: 'app-products-list',
template: `
<div>
<h1>Products</h1>
<ul *ngIf="products.length">
<li *ngFor="let product of products">
{{product.name }}
</li>
</ul>
</div>
`
})
class ProductsListComponent {
#Input() products
constructor() {
}
}
Long story short:
Easiest way to handle such situations is by using the switchMap operator. What this does is cancel the internal subscription as soon as a new event comes along.
One implementation would be:
class MainComponent {
products: any[];
private _filters$ = new Subject();
constructor(private dataService: DataService_, private filtersService: FiltersService) {
}
ngOnInit() {
this.setProducts()
}
setProducts() {
this._filters$
.switchMap((filters)=> this.dataService.getProducts(filters)) // or .let(switchMap...) if you are using rxjs >5.5
.subscribe(products => this.products = products);
}
onFiltersChange() {
this._filters$.next(this.filtersService.filters);
}
}
Long story:
What happens here is:
When you change filter the onFilterChange is triggered. You then emit the latest filters (inside this.filtersService.filters) through the _filters$ Subject (a subject is almost identical to an EventEmitter).
Back in time during component initialization the ngOnInit method has called setProducts, which has subscribed to the _filters$ subject for future events (none has happened at this point). When an event arrives on _filters$ then we trigger the getProducts method of dataservice, passing it the filters that where contained in the event. We will be waiting on this line until the http call has completed. As soon as it completes the result of the http call will be assigned to the products of the component.
If while we are waiting for the http response to get back, onFiltersChange is fired again, then a new event will arive at the switchMap and it will cancel the previous http request so that it can handle the new event.
This is a very powerful approach as changing a single operator, you can easily change the behavior of your app. For instance, changing switchMap to concatMap will make the request wait for the previous one to complete (will happen serially). Changing it to flatMap will have the same behaviour as the original code you posted (http requests will happen as soon as filters change, without affecting previous ones, order of responses will not predictable) and so on.
Note : to cancel the request just use unsubscribe.
For exmple
const course$ = this.service$.getCourses(`/api/courses`).subscribe(courses => { console.log(courses) }
setTimeout(() => course$.unsubscribe(),1000) // cancel the request
I need to create new list item(value from api)on button press but don't know how to do it. Any help please?
here is the code:
<ul>
<li *ngFor="let joke of jokes">{{joke.value}}</li>
</ul>
<button (click)="loadMore">more jokes</button>
`,
providers: [RandomService]
})
export class PocetnaComponent {
jokes: Joke[];
constructor(private jokesService: RandomService){
this.jokesService.getRandomJokes().subscribe(jokes => {this.jokes =
[jokes]});
}
loadMore(){
this.jokes.push();
}
}
interface Joke{
id: number;
value: string;
}
here is the service:
#Injectable()
export class RandomService {
constructor(private http: Http){
console.log('working');
}
getRandomJokes(){
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
}
}
Just push an empty object
this.jokes.push({});
or if its going to be hooked up to a modal
Create a class and push that
Class IJoke {
id: number;
value: string;
constructor(){
}
}
this.jokes.push(new IJoke());
Or if you want to push from an API
#Injectable()
export class RandomService {
constructor(private http: Http){
console.log('working');
}
getRandomJokes(){
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
}
getNextJoke(){
return this.http.get('https://api.chucknorris.io/jokes/next')
.map(res => res.json());
}
}
Directive
loadMore(){
this.jokesService.getNextJoke().subscribe(joke => {
this.jokes.push(joke);
});
}
I'm not sure if you load some random jokes and you want to load one more, or if you want to keep loading random jokes. If the later, you will want to take out the next function, and instead init your jokes array and keep pushing/applying to it. like so
jokes: Joke[] = new Array();
constructor(private jokesService: RandomService){
this.jokesService.getRandomJokes().subscribe(jokes => {
this.jokes.push(jokes)
});
You have a few problems...
You have this interface:
interface Joke{
id: number;
value: string;
}
what you are receiving is much more properties, so you'd need to pick the properties you want:
getRandomJokes(){
return this.http.get('https://api.chucknorris.io/jokes/random')
.map(res => res.json());
// pick the properties you want/need
.map(joke => <Joke>{id: joke.id, value: joke.value})
}
Then you have problems in the subscribe, you should push the data to your jokes array and not do:
.subscribe(jokes => {this.jokes = [jokes]})
but:
.subscribe(joke => this.jokes.push(joke)}
notice above that I named this (joke => this.jokes.push(joke)) to make it clearer that you are actually just receiving one joke.
Also I would remove the request from the constructor, we have the OnInit hook for this. Also I would apply the request in a separate function, so that it's easy to call when you want to retrieve new jokes and also therefore reuse the function, so something like this:
ngOnInit() {
this.getJoke()
}
getJoke() {
this.jokesService.getRandomJokes()
.subscribe(joke => {
this.jokes.push(joke)
})
}
So then in your template just call getJoke when you want to retrieve a new joke:
<ul>
<li *ngFor="let joke of jokes">{{joke.value}}</li>
</ul>
<button (click)="getJoke()">more jokes</button>
Here's a DEMO
I have a rather simple, working Angular 2 component. The component renders some div sibling elements based off of an array of values that I have sorted. This component implements OnChanges. The sorting functionality happens during the execution of ngOnChanges(). Initially, my #Input() attribute references an array of values that are unsorted. Once the change detection kicks in, the resulting DOM elements are sorted as expected.
I have written a Karma unit test to verify that the sorting logic in the component has taken place and that the expected DOM elements are rendered in sorted order. I programmatically set the component property in the unit test. After calling fixture.detectChanges(), the component renders its DOM. However, what I am ngOnChanges()` function, I see it is never executed. What is the correct way to unit test this behavior?
Here is my component:
#Component({
selector: 'field-value-map-item',
templateUrl: 'field-value-map-item.component.html',
styleUrls: [ 'field-value-map-item.component.less' ]
})
export class FieldValueMapItemComponent implements OnChanges {
#Input() data: FieldMapping
private mappings: { [id: number]: Array<ValueMapping> }
ngOnChanges(changes: any): void {
if (changes.data) { // && !changes.data.isFirstChange()) {
this.handleMappingDataChange(changes.data.currentValue)
}
}
private handleMappingDataChange(mapping: FieldMapping) {
// update mappings data structure
this.mappings = {};
if (mapping.inputFields) {
mapping.inputFields.forEach((field: Field) => {
field.allowedValues = sortBy(field.allowedValues, [ 'valueText' ])
})
}
if (mapping.valueMap) {
// console.log(mapping.valueMap.mappings.map((item) => item.input))
// order mappings by outputValue.valueText so that all target
// values are ordered.
let orderedMappings = sortBy(mapping.valueMap.mappings, [ 'inputValue.valueText' ])
orderedMappings.forEach((mapping) => {
if (!this.mappings[mapping.outputValue.id]) {
this.mappings[mapping.outputValue.id] = []
}
this.mappings[mapping.outputValue.id].push(mapping)
})
}
}
}
The component template:
<div class="field-value-map-item">
<div *ngIf="data && data.valueMap"
class="field-value-map-item__container">
<div class="field-value-map-item__source">
<avs-header-panel
*ngIf="data.valueMap['#type'] === 'static'"
[title]="data.inputFields[0].name">
<ol>
<li class="field-value-mapitem__value"
*ngFor="let value of data.inputFields[0].allowedValues">
{{value.valueText}}
</li>
</ol>
</avs-header-panel>
</div>
<div class="field-value-map-item__target">
<div *ngFor="let value of data.outputField.allowedValues">
<avs-header-panel [title]="value.valueText">
<div *ngIf="mappings && mappings[value.id]">
<div class="field-value-mapitem__mapped-value"
*ngFor="let mapping of mappings[value.id]">
{{mapping.inputValue.valueText}}
</div>
</div>
</avs-header-panel>
</div>
</div>
</div>
</div>
Here is my unit test:
describe('component: field-mapping/FieldValueMapItemComponent', () => {
let component: FieldValueMapItemComponent
let fixture: ComponentFixture<FieldValueMapItemComponent>
let de: DebugElement
let el: HTMLElement
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
FieldValueMapItemComponent
],
imports: [
CommonModule
],
providers: [ ],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
fixture = TestBed.createComponent(FieldValueMapItemComponent)
component = fixture.componentInstance
de = fixture.debugElement.query(By.css('div'))
el = de.nativeElement
})
afterEach(() => {
fixture.destroy()
})
describe('render DOM elements', () => {
beforeEach(() => {
spyOn(component, 'ngOnChanges').and.callThrough()
component.data = CONFIGURATION.fieldMappings[0]
fixture.detectChanges()
})
it('should call ngOnChanges', () => {
expect(component.ngOnChanges).toHaveBeenCalled() // this fails!
})
})
describe('sort output data values', () => {
beforeEach(() => {
component.data = CONFIGURATION.fieldMappings[1]
fixture.detectChanges()
})
it('should sort source field allowed values by their valueText property', () => {
let valueEls = el.querySelectorAll('.field-value-mapitem__value')
let valueText = map(valueEls, 'innerText')
expect(valueText.join(',')).toBe('Active,Foo,Terminated,Zip') // this fails
})
})
})
A solution to this problem is to introduce a simple Component in the unit test. This Component contains the component in question as a child. By setting input attributes on this child component, you will trigger the ngOnChanges() function to be called just as it would in the application.
Updated unit test (notice the simple #Component({...}) class ParentComponent at the top). In the applicable test cases, I instantiated a new test fixture and component (of type ParentComponent) and set the class attributes that are bound to the child components input attribute in order to trigger the ngOnChange().
#Component({
template: `
<div id="parent">
<field-value-map-item [data]="childData"></field-value-map-item>
</div>
`
})
class ParentComponent {
childData = CONFIGURATION.fieldMappings[1]
}
describe('component: field-mapping/FieldValueMapItemComponent', () => {
let component: FieldValueMapItemComponent
let fixture: ComponentFixture<FieldValueMapItemComponent>
let de: DebugElement
let el: HTMLElement
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
FieldValueMapItemComponent,
ParentComponent
],
imports: [
CommonModule
],
providers: [ ],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})
fixture = TestBed.createComponent(FieldValueMapItemComponent)
component = fixture.componentInstance
de = fixture.debugElement.query(By.css('div'))
el = de.nativeElement
})
// ...
describe('sort output data values', () => {
let parentComponent: ParentComponent
let parentFixture: ComponentFixture<ParentComponent>
let parentDe: DebugElement
let parentEl: HTMLElement
beforeEach(() => {
parentFixture = TestBed.createComponent(ParentComponent)
parentComponent = parentFixture.componentInstance
parentDe = parentFixture.debugElement.query(By.css('#parent'))
parentEl = parentDe.nativeElement
parentFixture.detectChanges()
})
it('should sort source field allowed values by their valueText property', () => {
let valueEls = parentEl.querySelectorAll('.field-value-mapitem__value')
let valueText = map(valueEls, 'innerText')
expect(valueText.join(',')).toBe('Active,Foo,Terminated,Zip')
})
it('should sort target field mapped values by `inputValue.valueText`', () => {
parentComponent.childData = CONFIGURATION.fieldMappings[2]
parentFixture.detectChanges()
let valueEls = parentEl.querySelectorAll('.field-value-mapitem__mapped-value')
let valueText = map(valueEls, 'innerText')
expect(valueText.join(',')).toBe('BRC,FreeBurrito,FreeTaco')
})
})
})
The other way is calling ngOnChanges directly.
it('should render `Hello World!`', () => {
greeter.name = 'World';
//directly call ngOnChanges
greeter.ngOnChanges({
name: new SimpleChange(null, greeter.name)
});
fixture.detectChanges();
expect(element.querySelector('h1').innerText).toBe('Hello World!');
});
ref: https://medium.com/#christophkrautz/testing-ngonchanges-in-angular-components-bbb3b4650ee8