I am building a site using Angular 2. I have a detail page where I access it by passing the id in the url (http://localhost:3000/#/projectdetail/1). I try to access a service to get the correct project by the id that I pass through. This project is saved in a project variable, but the variable is actually undefined all the time.
This are the errors I get when I go to the detail page:
This are the code pieces that I use (if you need more code just ask):
The projectdetail.page.html:
<div class="container-fluid">
<h1>{{project.name}}</h1>
<div>{{project.description}}</div>
</div>
The projectdetail.page.ts:
public project: Project;
constructor(private projectsService: ProjectsService, private route: ActivatedRoute) {
for (let i = 0; i < 4; i++) {
this.addSlide();
}
}
ngOnInit() {
this.route.params.map(params => params['id']).subscribe((id) => {
this.projectsService.getProject(id).then(project => this.project = project);
});
}
The projects.service.ts:
getProject(id: number): Promise<Project> {
return this.http.get(this.url).toPromise().then(x => x.json().data.filter(project => project.id === id)[0]).catch(this.handleError);
}
The error you're getting is from the template.
At the time of template render, project is not available yet.
Try to wrap it in an *ngIf to avoid this.
<div class="container-fluid" *ngIf="project">
<h1>{{project.name}}</h1>
<div>{{project.description}}</div>
</div>
Template rendering is happening before you are getting data,
what you need is to create an empty object for project property,
option 1:
public project: Project = new Project (); // Provided its a class.
option 2:
If you may not instantiate Project (is an interface), you may use nullable properties
{{project.name}} -> {{project?.name}}
Hope this helps!!
Related
I am creating a chat app, and performance is slow when a lot of messages are sent at once. The messages show up but the UI becomes unresponsive for a while. Here is a simplified version of the code and how can I fix this?
HTML:
<div class="message-notification" *ngFor="let newMessage of chatMessages"; trackBy: trackByMsgs>
<custom-notification [incomingChat]="newMessage" (dismissedEvent)="onDismiss($event)" (closedEvent)="onNotifcationClose($event)"></custom-notification>
</div>
TS:
newChatMessages: any[];
constructor(private chatService: ChatService) {}
ngOnInit() {
this.chatService.chatMsg.subscribe((msg:any) => {
if (msg) {
this.activatePopup(msg);
}
}
activatePopup(message) {
if (message.msgId !== null && message.title !== null) {
this.setTitle(message);
//If popup is already open then do not display a duplicate popup for the same message
let index = this.isPopupOpen(message);
if (index === -1) {
newChatMessages.push(message);
}else {
newChatMessages[index] = message;
}
}
}
trackByMsgs(index:number, msg:any) {
return msg.msgId && msg.title;
}
isPopUpOpen(message){
let index = -1;
if (this.newChatMessages){
index = this.newChatMessages.findIndex(
msg => msg.id === message.id && msg.title === message.title);
}
return index;
}
The best in your case is to control the angular change detection manually by using OnPush Change Detection Strategy. You should carefully use it, cause when it's on, angular will detect changes only if onChanges lifecycle is been triggered or async pipe has received a new value. It also applies to all the component children.
Then you would need to detect the changes manually by injecting the ChangeDetectorRef in your component and call the method detectChanges on it each time you want to apply your data changes to your dom.
Read this article for better understanding
Another interesting article for improving the performance of your angular app https://medium.com/swlh/angular-performance-optimization-techniques-5b7ca0808f8b
Using trackBy helps angular to memorize the loaded elements in the ngFor and update only the changed once on change detection. But your trackByMsgs returns a boolean which is not what it should return. If you adjust your trackBy to return a unique key like msg.msgId or the index of the item, you might see a difference.
I'm currently stuck at a problem and was hoping someone here could help me. I also certainly hope this is the right place to ask it.
I'm trying to create a custom Invoice record with its corresponding Invoice Line records upon firing an event. I already have some logic in place to gather ID of selected rows in the JS.
I've gone so far as to be able to create the Invoice record (using LDS) and the Invoice Line records (using Apex), but can't seem to pass the Invoice ID for the Invoice Line records. I know I'm able to create the records because it works when I tested this with a hardcoded Invoice ID.
Would it be possible to pass multiple parameters of List and String to an Apex method in LWC?
I would appreciate any help. Thanks in advance!
JS
selectedRowsEvent(event) {
...some codes here...
this.selectedRecords = Array.from(conIds);
}
handleSave() {
**invId;**
...some codes here...
createRecord(recordInput)
.then(invoice => {
**this.invId = invoice.Id;**
**createInvLines({ lstConIds : this.selectedRecords, invoiceId : this.invId})**
}).catch(error => {
...some codes here...
});
}
Controller
#AuraEnabled
public static void createInvLines(list<Id> lstConIds, string invoiceId){
if(lstConIds.size() > 0){
List<OpportunityLine__c> oppLst = new List<OpportunityLine__c>([SELECT Id, Description__c FROM OpportunityLine__c WHERE Id = :lstConIds]);
try {
List<InvoiceLine__c> lstInvLinesToInsert = new List<InvoiceLine__c>();
for(OpportunityLine__c idCon : oppLst) {
lstInvLinesToInsert.add(new InvoiceLine__c(**InvoiceId__c = invoiceId**, Description__c = idCon.Description__c));
}
if(!lstInvLinesToInsert.isEmpty()) {
insert lstInvLinesToInsert;
}
}
catch(Exception ex) {
throw new AuraHandledException(ex.getMessage());
}
}
}
Yes, you can pass complex parameters to methods marked as #AuraEnabled. On client side it'll be a JSON object with right field names, like you already have { lstConIds : this.selectedRecords, invoiceId : this.invId}. On Apex side it can be a function with multiple arguments or just 1 argument (some helper wrapper class, again with right field names). Salesforce will "unpack" that JSON for you and put into right fields before your code is called.
Your preference which would be cleaner. I tend to use wrappers. If you have a reusable service-like function and you want to add some optional parameters later - you'd simply put new field in wrapper class and job done. Might be not as easy to add a new parameter to function used in other apex code, bit messier.
(in your scenario I'd definitely try to create invoice and line items as 1 call so if anything fails - the normal transaction rollback will help you. if one of items fails - you don't want to be left with just invoice header, right?)
Have you seen https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.apex ? it's a wall of text but it mentions interesting example, search for "apexImperativeMethodWithParams" in there.
Look at JS file here: https://github.com/trailheadapps/lwc-recipes/tree/master/force-app/main/default/lwc/apexImperativeMethodWithComplexParams
And see how it calls
ApexTypesController {
#AuraEnabled(cacheable=true)
public static String checkApexTypes(CustomWrapper wrapper) {
...
Where CustomWrapper is
public with sharing class CustomWrapper {
class InnerWrapper {
#AuraEnabled
public Integer someInnerInteger { get; set; }
#AuraEnabled
public String someInnerString { get; set; }
}
#AuraEnabled
public Integer someInteger { get; set; }
#AuraEnabled
public String someString { get; set; }
#AuraEnabled
public List<InnerWrapper> someList { get; set; }
}
The issue is that the inserts are asynchronous and you are firing them synchronously. So, that means you are trying to insert the lines before the parent record has completed.
// CREATE THE INVOICE RECORD
createRecord(recordInput)
.then(invoice => {
**this.invId = invoice.Id;**
// Call the next function here
// CREATE THE INVOICE LINE RECORDS
**createInvLines({ lstConIds : this.selectedRecords, invoiceId : this.invId})**
.then(result => {
...some codes here...
})
.catch(error => {
...some codes here...
});
);
}
I am wanting to use ion-slides with ion-img to produce a gallery since I am thinking there may be a fair no of images. ion-img in ionic 4 uses lazy-loading to fetch the images which I think is what I want.
I am fetching the images from an Azure storage account which needs to have a dynamic (expiring) key added to it when the url is fetched.
I have managed to make this work pretty well as follows, heres the html:
<ion-slides #photoslider [options]="sliderOptions"
(ionSlideDidChange)="checkKeyandFixText()"
(ionSlidesDidLoad)="checkKeyandFixText()">
<ion-slide *ngFor="let i of userService.user.Images">
<ion-img cache="true" width="50%" [src]="i.ImageUrl + validKey"></ion-img>
</ion-slide>
</ion-slides>
which checks the key (and fixes up an index for text use with each load or image change).
The check key function is this:
checkKeyandFixText() {
console.log(this.validKey);
this.userService.getStorageURLKey('').then(() => { // trigger key update
console.log(this.validKey);
});
.... more irrelevant stuff
}
the 'validKey' used in the html is updated by the 'getStorageURLKey function, because that function fires the service that gets the key and updates an observable, which is set up in the ngOnInit - part of which includes:
this.azureKey$ = this.userService.azurekey$.subscribe((res) => {
this.validKey = res.Key;
});
But this is unreliable, depending on the state of the key and all manner of async timing possibilities.
Its been a bit of a road to here, stating with the idea that writing the 'getStorageURLKey' function would work, by providing the url, it checking if the current key is valid or not then if not getting one, and adding it back.
That function by necessity returns a Promise which the ion-img seems unable to cope with. (Here is the function for completeness:
async getStorageURLKey(url: string): Promise<string> {
const nowplus5 = addMinutes(Date.now(), 5); // 5 mins to give some leeway
console.log(nowplus5);
console.log(url);
console.log(this.azurekey);
if (!this.azurekey || isBefore( this.azurekey.Expires, nowplus5 )) {
console.log('Getting new Key');
const keyObj = await this.getAzureKeyServer().toPromise();
await this.saveAzureKeyStore(keyObj);
this.azurekey = keyObj;
console.log(this.azurekey);
return url + keyObj.Key; // Return the url with the new Key or just the key if url is blank
} else {
console.log('Key is valid till ' + this.azurekey.Expires);
console.log(this.azurekey.Key);
const rval = Promise.resolve(url + this.azurekey.Key);
return rval ; // Key is in time so return it with url
}
}
If I just call the function from inside the html:
<ion-img cache="true" width="50%" [src]="userService.getStorageURLKey(i.ImageURL) "></ion-img>
it just loops forever due to angulars retry and event loops. If I pipe the result to async:
<ion-img cache="true" width="50%" [src]="userService.getStorageURLKey(i.ImageURL) | async "></ion-img>
Then it loops over the images the requisite no of times but doesn't do anything else... sometimes, then sometimes it seems to just make no difference and loops forever.
I have also tried adding my own custom pipe instead of the async but the inbound value always comes in as 'undefined'.
Changing the html just to reference "i.ImageURl | urkKey", here's the pipe I tried:
#Pipe({
name: 'urlKey'
})
export class URLKeyPipe implements PipeTransform {
userService: UserService;
transform(value: string): any {
console.log('At Pipe');
console.log(value);
this.userService.getStorageURLKey(value).then((key) => {
console.log(key);
return key;
});
}
}
I have tried a lot of things...:-s
Fundamentally I need to add an an async key to an async url and seem to be looped up in angular. Am I approching this wrongly, or should I just go try using plain old img instead?
Any ideas? Thanks in advance.
Right, so I am iterating over an array of information and the information is showing the way that I want it to, however, I am getting some amaing looking errors in my console: ERROR TypeError: "_v.context.$implicit is undefined"
api service:
private extractData(res: Response) {
let body = res;
return body || {};
}
getWeather(city: string, isoCode: string): Observable<any> {
return this.http.get(`${this.endPoint}${city},${isoCode}${this.constants.apiKey}`)
.pipe(map(this.extractData));
}
component using api service:
theWeather:any = [];
countryList = COUNTRIES;
isLoading: boolean = true;
showWeather: boolean = false;
constructor(private apiCall:ApiService) { }
ngOnInit() {
this.retrieveWeather()
};
retrieveWeather() {
console.log('COUNTRY LIST', this.countryList);
this.theWeather = [];
this.countryList.map((element, i) => {
this.apiCall.getWeather(element.city, element.countryIso)
.subscribe((data: {}) => {
element.weatherInfo = data;
});
this.isLoading = false;
});
this.showWeather = true;
};
and the html file:
<div class="country-container">
<div *ngIf="isLoading">
<mat-card>
<mat-progress-spinner class="spinner-style" color="primary" mode="indeterminate"></mat-progress-spinner>
</mat-card>
</div>
<div *ngIf="showWeather" class="card-container">
<mat-card *ngFor="let c of countryList" class="card">
<mat-card-title>Weather for: {{c.city}}, {{c.countryName}}</mat-card-title>
<mat-card-content>The current weather is: {{c.weatherInfo.weather[0].description}}</mat-card-content>
</mat-card>
</div>
</div>
finally an image of my console:
Thank you for the help!
edit: made the title more specific to the issue.
You get this error when the angular template is looping through an array of items, and one or more of the array elements is undefined.
This can happen if you create your array by placing a value in a position using the following notation:
myArray[1] = myObject;
If you do this, then myArray[0] will not have a value, and when angular loops through it, you'll get the error "_v.context.$implicit is undefined".
It's safer to use:
myArray.push(myObject);
You might also get this if you remove an element from an array leaving an undefined value at an index.
according to me on the 5th element (it says line 10 index 4 (which is element 5)) in the list it cannot fetch the weather conditions... check for bad request parameters and/or bad return data. remove temp the element or check it and see if the error "moves". maybe the best thing is to check for undefined even when there is no http error.
I am building a site what lets you build a time table (for school subjects) I wrote a component that fetches data from database and then calculates corect placement in time table
<div class="timetable col-lg-8">
<div class="rowT">
<div style="width : 16%">Day</div>
<div *ngFor="let head of timeTableHeader"
[style.width]="headerWidth">
{{head}}
</div>
</div>
<div *ngFor="let sublists of allData" class="rowT">
<div *ngFor="let sublist of sublists"
id="{{sublist[0]}}"
class="timetable-cell"
[style]="getMyStyles(sublist[1])"
(click)="showDetails($event,sublist[3])">
{{sublist[0]}}
</div>
</div>
now I wrote a form which allows somebody to edit particular subject(e.g. time changed or class room) it works fine it saves the changes in DB adn now I want to show these changes in my view I thought I just call the function that calculates subject placements in time table but that results in rendering the time table again and leaving the old one there.
#Component({
selector: 'time-table',
templateUrl: './timetable.component.html',
styleUrls: ['./timetable.component.css']
})
export class TimeTableComponent {
allSubjects: Array<Subject>;
headerWidth: string;
timeTableHeader: Array<string> = new Array();
allData: Array<Array<Array<string>>> = new Array<Array<Array<string>>>();
constructor(private http: Http) {
this.fetchAndMake(); //function that fetches data and calls other function
//to make calculations... too long and not sure if relevant
// so I wont post it here
}
fetchAndMake(){
this.allSubjects = new Array<Subject>();
let params : URLSearchParams = new URLSearchParams();
params.set('userName', this.authService.currentUser.userName);
let reqOption = new RequestOptions();
reqOption.params = params;
this.http.get(this.configurations.baseUrl + "/SubjectModel/TimeTable", reqOption).subscribe(result => {
this.makeSubjects(result.json());
});
}
updateSubject(subj){
let subject = subj as SubjectData;
this.http.post(this.configurations.baseUrl + "/SubjectModel/UpdateSubject",helper)
.subscribe();
this.editSubjectView = false;
this.fetchAndMake();
}
}
Thanks in advance for the help.
First you should not be fetching the data directly from the component but rather from a data service. When you use a data service and inject it into components that use it the data service is a singleton. Not sure if that solves your problem but in any case it is a design issue you should look into before you go any further down this road.
Also, you are calling the primary function in the constructor. The only thing you should be doing in the constructor is to inject the data service. You need to implement the OnInit interface and call your function that fetches the data from the data service you injected in the constructor in the ngOnInit() method.
Check out some of the many Angular 4 tutorials and look for examples of creating a data service, making it a provider in app.module.ts. Then in your component
import { Component, OnInit } from '#angular/core';
import { MyDataService } from '../shared/my-data.service';
....
export class TimeTableComponent Implements OnInit {
...
constructor(private _mydata: MyDataService) { }
ngOnInit(){
// call method in data service that fetches data and do whatever you need to
// with the returned result. Now everything will be available to your view
}
There is a chance that
this.fetchAndMake()
gets called before
this.http.post(this.configurations.baseUrl +
"/SubjectModel/UpdateSubject",helper)
.subscribe();
is complete in updateSubject function. Subscribe just initiates the call , if you want to ensure that the new data is updated only after the post is complete, edit the updateSubject() function as follows :-
updateSubject(subj){
let subject = subj as SubjectData;
this.http.post(this.configurations.baseUrl + "/SubjectModel/UpdateSubject",helper)
.subscribe(result =>{
this.allData = new Array<Array<Array<string>>>();
this.fetchAndMake();
});
this.editSubjectView = false;
}