How to get items in asynchronous environment in different components? - javascript

I have main component with this code(without imports):
class AppComponent {
products = null;
productsUpdated = new EventEmitter();
constructor(product_service: ProductService) {
this._product_service = product_service;
this._product_service.getList()
.then((products) => {
this.products = products;
this.productsUpdated.emit(products)
});
}
}
With template:
<left-sidenav [productsReceived]="productsUpdated"></left-sidenav>
And component for sorting products:
class LeftSidenavComponent {
#Input() productsReceived;
#Output() productsSorted = new EventEmitter();
categories = []
constructor(product_list_service: ProductListService) {
this._product_list_service = product_list_service;
}
ngOnInit() {
this.productsReceived.subscribe((products) => {
this.categories = products.map((elem) => {
return elem.category
})
});
}
}
So when all is drawn, categories array in LeftSidenavComponent is empty.
I think that productUpdated event emitter fires earlier than LeftSidenavComponent subscribes to it, but don't know how to handle that.

I would recommend moving the EventEmitter's to the service you have injected like
#Injectable()
export class DrinksService {
drinkSelected = new EventEmitter<any>();
drinkChanged = new EventEmitter<any>();
drinksToggle = new EventEmitter<any>();
}
The above code is an example from one of my projects, but just change the variable names.
This way rather then relying on the HTML template to modify productsReceived, you simply subscribe to the eventEmitters in ngOnInit.
Currently, your code is using databinding to an event emitter productsUpdated but you could simply databind [productsReceived]="productsUpdated" where productsUpdated is an empty list. Once productsUpdated is populated with values, it will be reflected in the DOM. You have to populate productsUpdated by subscribing to an event emitter like...
this.myEmitter
.subscribe(
(data)=>this.productsUpdated = data
);
Does this help? The main thing is to databind to a list, and not an event emitter.

Related

Is there a way to "subscribe" to an array variable changes in Angular?

I'm looking for a way to wait for the user to stop interaction and then make an HTTP request, for this I'm looking into the debounceTime() operator from RxJs, but the target I'm waiting for is an array I defined.
This is the scenario:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
#ViewChild(MatTable, {static: true}) kwTable: MatTable<any>;
#ViewChild(MatPaginator, {static: false}) paginator: MatPaginator;
#Input() proposalId: string;
keywordInput = new FormControl(null, Validators.required);
dataSource: MatTableDataSource<Keyword>;
displayedColumns = ['action', 'keyword', 'searches', 'competition', 'cpc'];
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
fetchSuggestions(seeds?: string[]) {
const ideas = {
seeds: [],
limit: 25
};
this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3)).subscribe(res => {
this.suggestedKeywords = res;
});
}
}
I'm not including the whole component here, but the idea is the following:
I have a list of suggestedKeywords which I render on the page, each of these should call an addKeyword() method to add that keyword to the dataSource, and after that, I call the fetchSuggestions() method to get new keywords to populate the suggestedKeywords list.
The problem comes when I try to select multiple keywords in quick succession, since that would trigger a request for each of those clicks to update the suggestedKeywords list, so I wanted to use the debounceTime() to prevent the request to trigger until the user stops clicking items for a bit; however this requires an Observable to be the element changing as far as I know, but in my case, it is just a simple array.
Is there someway to keep track of the value of the array so it waits for a while after it changes before making the HTTP request, like an Observable?
EDIT: Used the from() operator as suggested in the comments, in order to actually listen to changes do I need to define other methods? I'm thinking something similar to valueChanges() in FormControls.
Going through more documentation I'm leaning towards Subject, BehaviorSubject, etc; but I'm not sure if this would be a correct approach, could anyone provide an example on how to do this?
Wrap your array in Observable.of() RxJS operator and it will behave like observable
What I ended up doing was using a Subject to keep track of the changes, calling it's next() function evrytime a modified the suggestedKeywords array and subscribing to it as an observable.
My component ended up looking like this:
export class KeywordSelectionComponent implements OnInit {
constructor(private proposalService: ProposalService) { }
keywordInput = new FormControl(null, Validators.required);
suggestedKeywords: Keyword[] = [];
selectedKeywords: string[] = [];
isLoadingResults = false;
tag$ = new Subject<string[]>();
ngOnInit() {
this.tag$.asObservable().pipe(
startWith([]),
debounceTime(500),
switchMap(seeds => this.getSuggestionsObservable(seeds))
).subscribe(keywords => {
this.suggestedKeywords = keywords;
});
}
addSuggestedKeyword(keyword: Keyword) {
const suggestedKeyword = keyword;
const existing = this.dataSource.data;
if (!existing.includes(suggestedKeyword)) {
existing.push(suggestedKeyword);
this.dataSource.data = existing;
}
this.tag$.next(this.getCurrentTableKeywords());
}
fetchKeywordSearch(keyword: string) {
this.isLoadingResults = true;
this.keywordInput.disable();
const search = {
type: 'adwords',
keyword
};
const currentData = this.dataSource.data;
this.proposalService.getKeywordSearch(this.proposalId, search).pipe(retry(3)).subscribe(res => {
currentData.push(res);
this.dataSource.data = currentData;
}, error => {},
() => {
this.isLoadingResults = false;
this.keywordInput.enable();
this.tag$.next(this.getCurrentTableKeywords());
});
}
getCurrentTableKeywords(): string[] {}
getSuggestionsObservable(seeds: string[] = []): Observable<Keyword[]> {
const ideas = {
type: 'adwords',
seeds,
limit: 25
};
return this.proposalService.getKeywordIdeas(this.proposalId, ideas).pipe(retry(3));
}
}

Angular 9 call function when two components loaded

I am trying to have two components, <app-map> and <app-markers-list>.
<app-map> loads Google Maps API and displays a map on the page. It emits a mapLoaded event in ngAfterViewInit()
#Output() mapLoaded: EventEmitter<boolean> = new EventEmitter();
<app-markers-list> loads a list of markers via Angular's HttpClient. It emits a markersLoaded event at the end of HttpClient.get().subscribe() Observable.
#Output() markersLoaded: EventEmitter<boolean> = new EventEmitter();
How do I catch these two events at once so I can call another component's function that will populate the map with the markers?
I think there is a simple solution.
#Output()
allLoaded = new EventEmitter();
oneLoaded = false;
ngAfterViewInit() {
this.emitAllLoaded();
}
yourFunctionWhereHttpClientGetLocated() {
this.http.get(..).subscribe(() => {
this.emitAllLoaded();
});
}
emitAllLoaded() {
if (oneLoaded) this.allLoaded.emit();
oneLoaded = true;
}
You might don't need to use Observable or Subject
I am assuming you are trying to capture events from <app-map> and <app-markers-list> in a parent component.
You can capture as we generally do, put binding in parent template as below:
<app-map (mapLoaded)="mapLoaded($event)"></app-map>
<app-markers-list (markersLoaded)="markersLoaded($event)"></app-markers-list>
Now you can manage these events in parent component. You can use subjects or observables and observable operators (combineLatest, of). Please find the code below how you can use it.
import { combineLatest, of } from 'rxjs';
// this will be your parent component.
export class ParentComponent {
mapLoaded = new Subject()
mapLoaded$ = this.mapLoaded.asObservable();
markerListLoaded = new Subject()
markerListLoaded$ = this.markerListLoaded.asObservable();
constructor() {
const combinedValues = combineLatest(mapLoaded$, markerListLoaded$);
combinedValues.subscribe((value) => {
// Here you can write code when you receive notification from both the events.
})
}
mapLoaded(mapLoadedData) {
this.mapLoaded$.next('map loaded successfully');
}
markersLoaded(markersLoadedData) {
this.markerListLoaded$.next('markers loaded successfully');
}
}

How to add data dynamically to mat-table dataSource?

I have data streaming from backend and i see it printing in console now i am trying to push event to dataSource its throwing error dataSource is not defined. Can someone help how to dynamically add data to materialize table ?
stream.component.html
<mat-table #table [dataSource]="dataSource"></mat-table>
stream.component.ts
import {
Component,
OnInit
} from '#angular/core';
import {
StreamService
} from '../stream.service';
import {
MatTableDataSource
} from '#angular/material';
import * as io from 'socket.io-client';
#Component({
selector: 'app-stream',
templateUrl: './stream.component.html',
styleUrls: ['./stream.component.css']
})
export class StreamComponent implements OnInit {
displayedColumns = ['ticketNum', "assetID", "severity", "riskIndex", "riskValue", "ticketOpened", "lastModifiedDate", "eventType"];
dataSource: MatTableDataSource < Element[] > ;
socket = io();
constructor(private streamService: StreamService) {};
ngOnInit() {
this.streamService.getAllStream().subscribe(stream => {
this.dataSource = new MatTableDataSource(stream);
});
this.socket.on('newMessage', function(event) {
console.log('Datasource', event);
this.dataSource.MatTableDataSource.filteredData.push(event);
});
}
}
export interface Element {
ticketNum: number;
ticketOpened: number;
eventType: string;
riskIndex: string;
riskValue: number;
severity: string;
lastModifiedDate: number;
assetID: string;
}
I have found a solution for this problem, basically if you do:
this.dataSource.data.push(newElement); //Doesn't work
But if you replace the complete array then it works fine. So your final code must be :
this.socket.on('newMessage', function(event) {
const data = this.dataSource.data;
data.push(event);
this.dataSource.data = data;
});
You can see the issue here -> https://github.com/angular/material2/issues/8381
The following solution worked for me:
this.socket.on('newMessage', function(event) {
this.dataSource.data.push(event);
this.dataSource.data = this.dataSource.data.slice();
});
Another solution would be calling the _updateChangeSubscription() method for the MatTableDataSource object:
this.socket.on('newMessage', function(event) {
this.dataSource.data.push(event);
this.dataSource._updateChangeSubscription();
});
This method:
/**
Subscribe to changes that should trigger an update to the table's rendered rows. When the changes occur, process the current state of the filter, sort, and pagination along with the provided base data and send it to the table for rendering.
*/
Here is a very simple and easy solution:
displayedColumns = ['ticketNum', 'assetID', 'severity', 'riskIndex', 'riskValue', 'ticketOpened', 'lastModifiedDate', 'eventType'];
dataSource: any[] = [];
constructor() {
}
ngOnInit() {
}
onAdd() { //If you want to add a new row in the dataSource
let model = { 'ticketNum': 1, 'assetID': 2, 'severity': 3, 'riskIndex': 4, 'riskValue': 5, 'ticketOpened': true, 'lastModifiedDate': "2018-12-10", 'eventType': 'Add' }; //get the model from the form
this.dataSource.push(model); //add the new model object to the dataSource
this.dataSource = [...this.dataSource]; //refresh the dataSource
}
Hope this will help :)
from the docs
Since the table optimizes for performance, it will not automatically check for changes to the data array. Instead, when objects are added, removed, or moved on the data array, you can trigger an update to the table's rendered rows by calling its renderRows() method.
So, you can use ViewChild, and refreshRow()
#ViewChild('table', { static: true }) table;
add() {
this.dataSource.data.push(ELEMENT_DATA[this.index++]);
this.table.renderRows();
}
I put together an example in stackblitz
You could also use the ES6 spread operator or concat if you are not using ES6+ to assign the dataSource data to a new array.
In ES6+
this.socket.on('newMessage', function(event) {
this.dataSource.data = [...this.dataSource.data, event];
});
ECMAscript version 3+
this.socket.on('newMessage', function(event) {
this.dataSource.data = this.dataSource.data.concat(event);
});
I was stuck in same problem while creating select row and apply some action over rows data. This solution for your problem
imports..................
import { MatTableDataSource } from '#angular/material';
#component({ ...
export class StreamComponent implements OnInit {
// Initialise MatTableDataSource as empty
dataSource = new MatTableDataSource<Element[]>();
constructor() {}
...
// if you simply push data in dataSource then indexed 0 element remain undefined
// better way to do this as follows
this.dataSource.data = val as any;
// for your problem
ngOnInit() {
// ** important note .... I am giving solution assuming the subscription data is array of objects. Like in your case stream and in second event(parameter as callback)
this.streamService.getAllStream().subscribe(stream => {
// do it this way
this.dataSource.data = stream as any;
// note if you simply put it as 'this.dataSource.data = stream' then TS show you error as '[ts] Type 'string' is not assignable to type '{}[]''
});
this.socket.on('newMessage', (event) => {
console.log('Datasource', event);
// for this value
// this.dataSource.MatTableDataSource.filteredData.push(event); // I couldn't get what you are doing here
// SO try to explain what you are getting in this callback function val as event parameter ?????????????????
// but you can get it this ways
this.dataSource.filteredData = event as any;
});
}
Hope this will help you . If you have any question just ping me.
For me, nothing of these answers didn't work.
I had an observable that I subscribe to get new data.
the only solution that works for me was:
this.dataService.pointsNodesObservable.subscribe((data) => {
this.dataSource = new InsertionTableDataSource(this.paginator, this.sort, this.dataService);
this.dataSource.data = data;
});
render like a charm!
Insert the element at the beginning or any position you want then call the change subscription method of Mat Table dataSource. Here is the code.
this.dataSource.data.unshift(newItem);
this.dataSource._updateChangeSubscription();

Angular service not updating subscribed components

I have an Angular 2/4 service which uses observables to communicate with other components.
Service:
let EVENTS = [
{
event: 'foo',
timestamp: 1512205360
},
{
event: 'bar',
timestamp: 1511208360
}
];
#Injectable()
export class EventsService {
subject = new BehaviorSubject<any>(EVENTS);
getEvents(): Observable<any> {
return this.subject.asObservable();
}
deleteEvent(deletedEvent) {
EVENTS = EVENTS.filter((event) => event.timestamp != deletedEvent.timestamp);
this.subject.next(EVENTS);
}
search(searchTerm) {
const newEvents = EVENTS.filter((obj) => obj.event.includes(searchTerm));
this.subject.next(newEvents);
}
}
My home component is able to subscribe to this service and correctly updates when an event is deleted:
export class HomeComponent {
events;
subscription: Subscription;
constructor(private eventsService: EventsService) {
this.subscription = this.eventsService.getEvents().subscribe(events => this.events = events);
}
deleteEvent = (event) => {
this.eventsService.deleteEvent(event);
}
}
I also have a root component which displays a search form. When the form is submitted it calls the service, which performs the search and calls this.subject.next with the result (see above). However, these results are not reflected in the home component. Where am I going wrong? For full code please see plnkr.co/edit/V5AndArFWy7erX2WIL7N.
If you provide a service multiple times, you will get multiple instances and this doesn't work for communication, because the sender and receiver are not using the same instance.
To get a single instance for your whole application provide the service in AppModule and nowhere else.
Plunker example
Make sure your Component is loaded through or using its selector. I made a separate component and forgot to load it in the application.

Angular: Event Emitter not received

I have a service with an EventEmitter that gets fired if the data changes.
#Output() reservationChangedEvent = new EventEmitter<any>();
public notifyReservationsChanged() {
this.reservationChangedEvent.emit({});
}
That the data changes is triggered by a modal that is started from a controller.
In my controller I subscribe for those events:
ngOnInit() {
...
this.reservationService.reservationChangedEvent.subscribe(() => this.reloadData());
}
My problem is that I can not receive events in my overview component. If I subscribe for events (for checking) in my service or my modal I do receive them.
Any idea why the overview controller can not receive events?
You should change:
#Output() reservationChangedEvent = new EventEmitter<any>();
to:
reservationChangedSubject = new Subject<any>();
reservationChangedEvent = this.reservationChangedSubject.asObservable()
and this:
public notifyReservationsChanged() {
this.reservationChangedEvent.emit({});
}
to:
public notifyReservationsChanged() {
this.reservationChangedSubject.next({});
}
#Output() and EventEmitter are meant to be used only in components, not in services.
For services, you should use Subject instead.
Your service should contain:
private reservationChangedSource = new Subject<any>();
reservationChanged$ = this.reservationChangedSource.asObservable();
notifyReservationsChanged() {
this.reservationChangedEvent.next({});
}
In your component:
reservationChangeSubscription: Subscription;
ngOnInit() {
...
this.reservationChangeSubscription = this.reservationService.reservationChanged$
.subscribe(() => this.reloadData());
}
ngOnDestroy() {
this.reservationChangeSubscription.unsubscribe();
}

Categories

Resources