I have a messaging component that makes it possible to write messages with another user in my system. The problem is when I open the component the site gets super slow and it takes up to 15 seconds before the messages are loaded. What could I do different in the code to make it work as it should?
allMessages() {
return this.messages.concat(this.uploadingMessages);
}
<ng-container #messageList *ngFor="let m of allMessages(); let i = index; trackBy: trackByMessageId">
<div *ngIf="case !== null" [class.unread]="case.hasReadUntilTimestamp < m.timestamp && !m.sender.isMe && !m.setRead">
<div class="newMessage">
<div class="newMessage__line"></div>
<div class="newMessage__line"></div>
</div>
<div class="row messageAndHeader line" *ngIf="allMessages()[i - 1]?.sender.id !== m.sender.id; else elseBlock">
<div class="row messageInfo">
<div class="profileImg">
<img *ngIf="m.sender.profileImageUrl !== null" [src]="m.sender.profileImageUrl" />
*ngIf="m.sender.profileImageUrl === null">
</div>
In angular DOM will be refreshed for all events, so here allMessages() will be called repeatedly, SO do this operation in NgOnInit() assign to one variable like below
ngOnInit(){
this.allMesage = this.messages.concat(this.uploadingMessages);
}
and in compoent.html modify the code as below
<ng-container #messageList *ngFor="let m of allMessage; let i = index; trackBy: trackByMessageId">
if the messages are getting populated after making API call then add async pipe like below
<ng-container #messageList *ngFor="let m of allMessage | async; let i = index; trackBy: trackByMessageId">
And remove calling method In ngif
<div class="row messageAndHeader line" *ngIf="allMessage[i - 1]?.sender.id !== m.sender.id; else elseBlock">
If it is api call modify the code in ngOnInit() like below
ngOnInit(){
apiCall.subscribe(data=>{
this.allMesage = data.messages.concat(data.uploadingMessages);
},
error=>{
console.log(error)
})
Related
Please help a little bit.
I have a list of 7 events displayed already with Angularjs. I'd like when I click on the <h2> (the event name) of some event, to open an ovelay that displays the same data from the database but only for this event which is clicked.
I'm sure that 'filter' will do the work but it seems I'm doing something wrong.
Here is my code. The ng-app and ng-controller are in the <main> tag.
Angularjs version: 1.7.9
My Html:
<main ng-app="eventsApp" ng-controller="eventsCtrl">
<!-- Overlay that holds and displays a single event -->
<div>
<div ng-repeat="x in singlePageEvent | filter:hasName(x.eventName)">
<div>
<img ng-src="{{x.eventImgSrc}}" alt="{{x.eventImgName}}"/>
<h2 class="event-name">{{x.eventName}}</h2>
<p>{{x.eventTime}}</p>
<p>{{x.eventPlace}}</p>
</div>
</div>
</div>
<!-- A list with all the events -->
<div ng-repeat="x in events">
<div>
<img ng-src="{{x.eventImgSrc}}" alt="{{x.eventImgName}}"/>
<h2 ng-click="singleEventOpen(x)" class="event-name">{{x.eventName}}</h2>
<p>{{x.eventTime}}</p>
<p>{{x.eventPlace}}</p>
</div>
</div>
</main>
My script:
let eventsApp = angular.module('eventsApp', []);
this filter below is not working at all. It continues to show all the events.
eventsApp.filter('hasName', function() {
return function(events, evName) {
var filtered = [];
angular.forEach(events, function(ev) {
if (ev.eventName && ev.eventName.indexOf(evName) >-1) {
filtered.push(ev);
}
});
return filtered;
}
});
eventsApp.controller('eventsCtrl', function($scope, $http) {
let x = window.matchMedia("(max-width: 450px)");
let singleEventOverlay = angular.element(document.querySelector('div.single-event.overlay'));
let singleEvent = singleEventOverlay;
function responsiveEventImages(x) { //this displays the list with events
if (x.matches) {
$http.get('./includes/events_res.inc.php').then(function(response) {
$scope.events = response.data.events_data;
});
} else {
$http.get('./includes/events.inc.php').then(function(response) {
$scope.events = response.data.events_data;
});
}
}
...and then by invoking singleEventOpen() the overlay appears, but it displays all the data, not just the clicked event
$scope.singleEventOpen = function(singleEvent) {
let clickedEvent = singleEvent.eventName; //I got the value of each h2 click thanx to #georgeawg but now what?
console.log("Fetching info for ", singleEvent.eventName);
$http.get('./includes/single_event.inc.php').then(function(response) {
$scope.singlePageEvent = response.data.events_data;
});
singleEventOverlay.removeClass('single-event-close').addClass('single-event-open');
}
});
The php file with the database extraction is working fine so I won't display it here.
What should I do to make the overlay display only the event which <h2> is clicked?
Here is a pic of the list with events
Here is a pic of the overlay
Thanx in advance.
EDITED
I got the value of each h2 click thanx to #georgeawg but now what?
UPDATE
Hey, thanx a lot #georgeawg . After many attempts I finally did this:
$scope.singleEventOpen = function(singleEvent) {
$http.get('./includes/single_event.inc.php').then(function(response) {
let allEvents = response.data.events_data;
for (var i = 0; i < allEvents.length; i++) {
singleEvent = allEvents[i];
}
});
console.log('Fetching data for', singleEvent);
$scope.ex = singleEvent;
});
And it works well.
Change the ng-click to pass an argument to the singleEventOpen function:
<div ng-repeat="x in events">
<div>
<img ng-src="{{x.eventImgSrc}}" alt="{{x.eventImgName}}"/>
<h2 ng-click="singleEventOpen(x)" class="event-name">{{x.eventName}}</h2>
<p>{{x.eventTime}}</p>
<p>{{x.eventPlace}}</p>
</div>
</div>
Then use that argument:
$scope.singleEventOpen = function(singleEvent) {
console.log("Fetching info for ", singleEvent.eventName);
//...
//Fetch and filter the data
$scope.ex = "single item data";
}
Adding an argument is the key to knowing which <h2> element was clicked.
Update
Don't use ng-repeat in the overlay, just display the single item:
<!-- Overlay that holds and displays a single event -->
̶<̶d̶i̶v̶ ̶n̶g̶-̶r̶e̶p̶e̶a̶t̶=̶"̶x̶ ̶i̶n̶ ̶s̶i̶n̶g̶l̶e̶P̶a̶g̶e̶E̶v̶e̶n̶t̶ ̶|̶ ̶f̶i̶l̶t̶e̶r̶:̶h̶a̶s̶N̶a̶m̶e̶(̶x̶.̶e̶v̶e̶n̶t̶N̶a̶m̶e̶)̶"̶>̶
<div ng-if="ex"">
<div>
<img ng-src="{{ex.eventImgSrc}}" alt="{{ex.eventImgName}}"/>
<h2 class="event-name">{{ex.eventName}}</h2>
<p>{{ex.eventTime}}</p>
<p>{{ex.eventPlace}}</p>
</div>
</div>
I have component which can contain 1 of 10 different child components, based on some condition.
<div *ngIf="type === 1">
<component_1></component_1>
</div>
<div *ngIf="type === 2">
<component_2></component_2>
</div>
<div *ngIf="type === 3">
<component_3></component_3>
</div>
...
In that component I need method which will return proper child component reference in any moment (on button click for example).
getComponentRef() {
switch (this.type) {
case 1: {
// return component_1 ref;
break;
}
case 2: {
// return component_2 ref;
break;
}
case 3: {
// return component_3 ref;
break;
}
...
}
}
Any idea ?
Do this:-
parent.component.html
<div *ngIf="type === 1">
<component_1 (emittedEvent)="setActiveChildTab($event)"></component_1>
</div>
<div *ngIf="type === 2">
<component_2 (emittedEvent)="setActiveChildTab($event)"></component_2>
</div>
<div *ngIf="type === 3">
<component_3 (emittedEvent)="setActiveChildTab($event)"></component_3>
</div>
...
parent.component.ts create a variable to store the reference of child e.g. activeTab: string
setActiveChildTab(activeTab: string) {
this.activeTab = activeTab;
}
In every child.component.ts create a variable to emit the active tab e.g.
#Output() emittedEvent: EventEmitter<any> = new EventEmitter().
ngOnInit() {
this.emittedEvent.emit('Child1');
}
Let me know if it works or otherwise.
1) You have already type value in ngIf. You can use it.
2) You need to create one EventEmitter in common Service file.
With that you can emit the flag from all the component's ngOnInit() and subscribe in component.
3) You can manage via cookie also.
4) Manage via parent-child data transfer also.
I have seen similar questions here, but these haven't been helpful so far.
I have a component that has an array state:
eventData: []
Some logic watches for events and pushes the objects to the state array:
eventData.unshift(result.args);
this.setState({ eventData });;
unshift() here is used to push the new elements to the top of the array.
What I want to achieve is rendering the content of the state array. I have written a conditional that checks for a certain state, and based on that state decides what to output.
let allRecords;
if (this.state.allRecords) {
for (let i = 0; i < this.state.eventData.length; i++) {
(i => {
allRecords = (
<div className="event-result-table-container">
<div className="result-cell">
{this.state.eventData[i].paramOne}
</div>
<div className="result-cell">
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}}
</div>
<div className="result-cell">
{this.state.eventData[i].paramThree.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFour.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{this.state.eventData[i].paramSix.c[0]}
</div>
</div>
);
}).call(this, i);
}
} else if (!this.state.allRecords) {
for (let i = 0; i < this.state.eventData.length; i++) {
if (this.state.account === this.state.eventData[i].paramOne) {
(i => {
allRecords = (
<div className="event-result-table-container">
<div className="result-cell">
{this.state.eventData[i].paramOne}
</div>
<div className="result-cell">
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}}
</div>
<div className="result-cell">
{this.state.eventData[i].paramThree.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFour.c[0]}
</div>
<div className="result-cell">
{this.state.eventData[i].paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{this.state.eventData[i].paramSix.c[0]}
</div>
</div>
);
}).call(this, i);
}
}
}
Problems that I have with this piece of code:
The code always renders the very last value of eventData state object.
I would like to limit the rendered elements to always show not more than 20 objects (the last 20 records of the state array).
paramTwo is a bool, and according to its value I expect to see either Win or Loose, but the field is empty (I get the bool value via the console.log, so I know the value is there)
Is this even the most effective way of achieving the needed? I was also thinking of mapping through the elements, but decided to stick with a for loop instead.
I would appreciate your help with this.
A few things :
First, as the comments above already pointed out, changing state without using setState goes against the way React works, the simplest solution to fix this would be to do the following :
this.setState(prevState => ({
eventData: [...prevState.eventData, result.args]
}));
The problem with your code here. Is that the arrow function was never called :
{() => {
if (this.state.eventData[i].paramTwo) {
<span>Win</span>;
} else {
<span>Loose</span>;
}
}
}
This function can be reduced to the following (after applying the deconstructing seen in the below code) :
<span>{paramTwo ? 'Win' : 'Lose'}</span>
Next up, removing repetitions in your function by mapping it. By setting conditions at the right place and using ternaries, you can reduce your code to the following and directly include it the the JSX part of your render function :
render(){
return(
<div> //Could also be a fragment or anything
{this.state.allRecords || this.state.account === this.state.eventData[i].paramOne &&
this.state.eventData.map(({ paramOne, paramTwo, paramThree, paramFour, paramFive, paramSix }, i) =>
<div className="event-result-table-container" key={i}> //Don't forget the key like I just did before editing
<div className="result-cell">
{paramOne}
</div>
<div className="result-cell">
<span>{paramTwo ? 'Win' : 'Lose'}</span>
</div>
<div className="result-cell">
{paramThree.c[0]}
</div>
<div className="result-cell">
{paramFour.c[0]}
</div>
<div className="result-cell">
{paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{paramSix.c[0]}
</div>
</div>
)
}
</div>
)
}
Finally, to only get the 20 first elements of your array, use slice :
this.state.eventData.slice(0, 20).map(/* CODE ABOVE */)
EDIT :
Sorry, I made a mistake when understanding the condition you used in your rendering, here is the fixed version of the beginning of the code :
{this.state.allRecords &&
this.state.eventData.filter(data => this.state.account === data.paramOne).slice(0, 20).map(/* CODE ABOVE */)
Here, we are using filter to only use your array elements respecting a given condition.
EDIT 2 :
I just made another mistake, hopefully the last one. This should ahve the correct logic :
this.state.eventData.filter(data => this.state.allRecords || this.state.account === data.paramOne).slice(0, 20).map(/* CODE ABOVE */)
If this.state.allRecords is defined, it takes everything, and if not, it checks your condition.
I cleaned up and refactored your code a bit. I wrote a common function for the repetitive logic and passing the looped object to the common function to render it.
Use Map instead of forloops. You really need to check this this.state.account === this.state.eventObj.paramOne statement. This could be the reason why you see only one item on screen.
Please share some dummy data and the logic behind unshift part(never do it directly on state object), we'll fix it.
getRecord = (eventObj) => (
<React.Fragment>
<div className="result-cell">
{eventObj.paramOne}
</div>
<div className="result-cell">
{eventObj.paramTwo ? <span>Win</span> : <span>Loose</span>}
</div>
<div className="result-cell">
{eventObj.paramThree.c[0]}
</div>
<div className="result-cell">
{eventObj.paramFour.c[0]}
</div>
<div className="result-cell">
{eventObj.paramFive.c[0] / 10000}
</div>
<div className="result-cell-last">
{eventObj.paramSix.c[0]}
</div>
</React.Fragment>
)
render() {
let allRecords;
if (this.state.allRecords) {
allRecords = <div>{this.state.eventData.map(eventObj => this.getRecord(eventObj)}</div>;
} else if (!this.state.allRecords) {
allRecords = <div>{this.state.eventData.map(eventObj => {
if (this.state.account === this.state.eventObj.paramOne) {
return this.getRecord(eventObj);
}
return null;
})}</div>;
}
return (<div className="event-result-table-container">{allRecords}</div>);
}
In a previous question here, I am trying to show a date only when it has changed between 2 messages.
The difference is that I am now connected to a realtime database (Firebase), so I subscribe to the data source and pass it through the async pipe before going through index and *ngIf:
<div *ngFor='let message of (chat | async) ; let i = index'>
<button *ngIf="i == 0 || (message.timestamp | date: 'ddMMMMyyyy') != ((chat | async)[i-1].timestamp | date: 'ddMMMMyyyy')">
{{ message.timestamp | date:'ddMMM' }}
</button>
...
<!--More html elements below (omitted)-->
...
</div>
This works well when I first load the view, however, if I push a new entry into chat, I get the following error:
TypeError: Cannot read property '0' of null
Maybe I am not too sure how the async pipe works, but when I try returning {{ (chat | async).length }}, it works as intended. Any suggestions on a workaround/proper practice?
In case someone else finds this since it's the first result with the key words, I was able to get the index of a *ngFor with the async pipe like this (assuming messages is an Observable of an iterable):
<div *ngFor="let message of (messages | async); let i = index;">
Still not sure how the AsyncPipe can be manipulated here, but I did mange to find a workaround that involves not using the pipe. I will await a better answer hopefully before marking this closed.
I subscribe to the data source in my class, and manipulate the array with a for loop before displaying it.
The (template) code in my question has now become:
<div *ngFor='let message of messages; let i = index'>
<button *ngIf="message.isNewDay">
{{ message.timestamp | date:'ddMMM' }}
</button>
...
<!--More html elements below (omitted)-->
...
</div>
And in the controller:
private chatid: string //chat id passed from previous screen
private chat: FirebaseListObservable<any>;
private messages: any = [];
constructor(private firebase: FirebaseService,
private _datepipe: DatePipe) {
}
ionViewLoaded() {
this.chat = this.firebase.database.list('/chat-messages/' + this.chatid);
this.chat.subscribe((data) => {
this.messages = data;
for (let i: number = 0; i < this.messages.length; i++) {
if (i === 0)
this.messages[i].isNewDay = true;
else {
let date1 = this._datepipe.transform(this.messages[i].timestamp, 'ddMMMMyyyy');
let date2 = this._datepipe.transform(this.messages[i-1].timestamp, 'ddMMMMyyyy');
if (date1 !== date2)
this.messages[i].isNewDay = true;
else
this.messages[i].isNewDay = false;
}
}
});
}
Note that I am currently using DatePipe in the class code (as well as in the template), so it is necessary to use providers: [DatePipe] along with pipes: [DatePipe].
If you want to use *ngFor, AsyncPipe, and index together - you should use this construction:
<ul>
<li *ngFor="let person of obsPersons | async as persons; index as i">
{{i + 1}} out of {{persons.length}}: {{person.personId}}
</li>
</ul>
Tested and works on Angular 7.
As mentioned above by #Primal Zed let company of (filteredCompaniesAliases | async); let i = index" should do the trick, syntax sample with plain html table:
<table>
<tr *ngFor="let company of (filteredCompaniesAliases | async); let i = index"">
<td>
{{selectedDiscountableCharge.name}}
</td>
<td>
${{selectedDiscountableCharge.amount}}
</td>
<td>
${{selectedDiscountableCharge.amount - ( (selectedDiscountableChargePercentanges / 100) * selectedDiscountableCharge.amount ) }}
</td>
</tr>
</table>
in your component:
companyAliasFilterCtrl: FormControl;
filteredCompaniesAliases: Observable<any[]>;
...
this.filteredCompaniesAliases = this.companyAliasFilterCtrl.valueChanges
.pipe(
startWith(''),
map(company => company ? this.filterOperatorsAliases(company) : this.companies.slice())
);
I use a function for showing (or not) the date. So to show the date (in this example) only when it changes:
<ion-list no-lines>
<ion-item-sliding *ngFor="let trip of tripsExt$ | async; let i = index">
<ion-item>
<ion-row (click)="change(trip)">
<ion-col col-6>
{{showOnChange(trip.date)}}
</ion-col>
<ion-col col-6>
{{trip.end_address_name}}
</ion-col>
</ion-row>
</ion-item>
<ion-item-options side="left">
<button ion-button color="primary" (click)="delete(trip)">
Delete
</button>
</ion-item-options>
</ion-item-sliding>
</ion-list>
With showOnChange:
showOnChange(currentDate) {
if (currentDate != this.previousDate) {
this.previousDate = currentDate
return currentDate
}
return ''
}
Im trying to build a learning app where at the moment it needs to get the first 7 posts from a subreddit and then if that is not already in the database add it to the database. However it runs both the if and else 7 times each for some reason and I cannot figure out why. Here is the helper method:
Template.posts.helpers({
posts : function () {
Meteor.call('getPosts', "tifu", function(e, results){
var result = JSON.parse(results.content).data.children;
Session.set('postsResults', result);
});
for(var i=0; i<7; i++){
var result = Session.get('postsResults')[i].data;
if(Posts.find({r_id: result.id}).count() == 0){
console.log("if");
} else {
console.log("else");
};
};
return Posts.find();
}
});
and the html side:
<template name="posts">
<div class="col-md-12 posts-div">
{{#each posts }}
<div class="col-md-8">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">{{ title }}</h3>
</div>
<div class="panel-body">{{{ body }}}</div>
<div class="panel-footer">
<div class="col-md-2">{{ score }}</div>
<div class="col-md-2 col-md-offset-3">{{ subreddit }}</div>
<div class="col-md-2 col-md-offset-3">{{ createdBy }}</div>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-primary">
</div>
</div>
{{/each}}
</div>
<hr>
</template>
I have replaced the insert code with simple console logs and these are the results I get:
if
if
if
if
if
if
if
else
else
else
else
else
else
else
When I run the Posts.find({r_id: result.id}).count() == 0 in the console I get false same with Posts.findOne({r_id: result.id}) == null but for some reason in javascript file it still runs the true portion and I then end up with like 50 copies of the same post which is what I am trying to avoid.
It is not optimal to use Meteor.call in a helper. The helper's function will re-run every time there is a reactive change. This is why it runs so many times.
Use the Template.onCreated callback instead:
Template.posts.onCreated(function() {
Meteor.call('getPosts', "tifu", function(e, results){
var result = JSON.parse(results.content).data.children;
Session.set('postsResults', result);
});
});
And your helper:
Template.posts.helpers({
posts : function () {
var r = Session.get('postsResults')
for(var i=0; i<7; i++){
if(!r) continue;
var result = r[i].data;
if(Posts.find({r_id: result.id}).count() == 0){
console.log("if");
} else {
console.log("else");
};
};
return Posts.find();
}
});
Second thing to keep in mind is Session.get('postsResults') will be null while the result of Meteor.call is returned, for a few hundred milliseconds. If you do Session.get('postsResults')[i].data you will get an exception.
This is why i added a conditional check to continue the loop if r is null. This way you wait for the result and the posts function will re-run and recalculate the results with the new data.