I'm currently in the process of upgrading an application from Angular 7 to Angular 16, and while that has presented several problems relating to features being deprecated, this one seems to be failing...differently.
The relevant part of the template (the rest has to do with validation):
<div *ngIf="response && response.result && response.isSuccess">
<div class="p-fluid">
<div class="grid">
<div class="grid-12">
<h5>{{ agency.name }}</h5>
</div>
<div class="grid-12">
<h6>Select Fare</h6>
</div>
<div class="md:col-2 sm:col-12" *ngFor="let fareType of fareTypes">
<button style="width: 100%;"
[attr.disabled]="purchase.paymentToken ? '' : null"
[ngClass]="{ 'btn': true, 'btn-primary': selectedFareType.id === fareType.id, 'btn-outline-primary': selectedFareType.id !== fareType.id }" /////THESE ARE NOT BEING APPLIED, INSTEAD AN EMPTY RECTANGLE APPEARS. ALSO THE LINE WHERE THE ERROR IS THROWN IN THE STACK BELOW
(click)="selectFareType(fareType)">
{{ fareType.name }} /////THIS IS EMPTY WHEN THE PAGE LOADS
</button>
</div>
</div>
<div *ngIf="selectedFareType" class="grid">
<div class="grid-12">
<div class="alert alert-primary">
<span>{{ selectedFareType.description }}</span>
</div>
</div>
</div>
<div class="grid" *ngIf="selectedFareType">
<div class="grid-12">
<h6>Select Pass</h6>
</div>
<div class="md:col-2 sm:col-12" *ngFor="let passTemplate of availablePassTemplates">
<button style="width: 100%;"
[attr.disabled]="purchase.paymentToken ? '' : null"
[ngClass]="{ 'btn': true, 'btn-primary': selectedPassTemplate.id === passTemplate.id, 'btn-outline-primary': selectedPassTemplate.id !== passTemplate.id }"
(click)="selectPassTemplate(passTemplate)">
{{ passTemplate.name }} /////ONCE I CLICK AN EMPTY RECTANGLE ABOVE, THE SAME BEHAVIOR OCCURS WITH THESE
</button>
</div>
</div>
<div *ngIf="selectedPassTemplate && selectedFare" class="grid">
<div class="grid-12">
<div class="alert alert-primary">
<h5>{{ selectedFare.farePrice | currency }}</h5>
<span>{{ selectedPassTemplate.description }}</span>
</div>
</div>
</div>
The relevant part of the module:
import { Component, OnInit, AfterViewChecked } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PublicService } from '../../shared/services/public.service';
import { PassTemplate, Fare } from '../../shared/models/pass-template';
import { Agency, AgencyResponse } from '../../shared/models/agency';
import { environment } from '../../../environments/environment';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ThirdPartyPurchase } from '../../shared/models/purchase';
import { MustMatch } from '../../shared/helpers/must-match.validator';
import { FareType } from '../../shared/models/fare-type';
import { PassUpgrade } from '../../shared/models/pass-upgrade';
import { DialogModule } from 'primeng/dialog';
@Component({
selector: 'purchase',
templateUrl: './purchase.component.html',
styleUrls: ['./purchase.component.css'],
providers: [
PublicService
]
})
export class PurchaseComponent implements OnInit, AfterViewChecked {
logoRootUrl: string = environment.imageUrl;
dataTokenizationKey: string;
response: AgencyResponse;
agency: Agency;
fareTypes: FareType[];
selectedFareType: FareType;
selectedFare: Fare;
passTemplates: PassTemplate[];
availablePassTemplates: PassTemplate[];
selectedPassTemplate: PassTemplate;
purchase: ThirdPartyPurchase = new ThirdPartyPurchase();
formGroup: FormGroup;
displayDialog: boolean = false;
displayDialogAcceptDeclineButtons: boolean = false;
dialogHeader: string;
dialogMessage: string;
dialogButtonText: string = 'OK';
isProcessing: boolean = false;
exitAfterDialogCloses: boolean = false;
upgrade: PassUpgrade;
nodeSrc = 'https://secure.networkmerchants.com/token/Collect.js';
nodeText = `CollectJS.configure({
"paymentSelector": "#payButton",
"buttonText": "Send Pass",
"callback": function (response) {
var input = document.getElementById("payToken");
input.value = response.token;
input.dispatchEvent(new Event('input', { bubbles: true }));
}
});`;
constructor(private route: ActivatedRoute,
private router: Router,
private service: PublicService,
private formBuilder: FormBuilder) { }
ngOnInit() {
this.formGroup = this.formBuilder.group({
recipientPhone: ['', Validators.required],
recipientPhone2: ['', Validators.required],
purchaserName: ['', Validators.required],
purchaserPhone: ['', Validators.required],
purchaserEmail: ['', Validators.email],
paymentToken: [''],
purchaserMessage: [''],
}, { validator: MustMatch('recipientPhone', 'recipientPhone2') });
this.formGroup.get('paymentToken')!.valueChanges.subscribe(val => {
this.purchase.paymentToken = val;
if (this.purchase.paymentToken && !this.isProcessing) {
this.isProcessing = true;
this.dialogHeader = 'Send Pass';
this.dialogMessage = 'Sending Pass ...Please wait';
this.displayDialog = true;
this.completePurchase();
}
});
var agencyId = this.route.snapshot.params.agencyId;
if (!agencyId) {
this.showDialog('Error', 'Agency not found');
return;
}
this.service.getAgency(agencyId).subscribe(response => {
if (!response.isSuccess) {
this.showDialog('Error', response.message);
return;
}
this.response = response;
this.agency = response.result;
this.service.getAgencyDataTokenizationKey(agencyId).subscribe(response => {
if (!response.isSuccess) {
this.showDialog('Error', response.message);
return;
}
this.dataTokenizationKey = response.result;
});
this.service.getPasses(agencyId).subscribe(response => { ///// ERROR OCCURS HERE, APPARENTLY?
if (!response.isSuccess) {
this.showDialog('Error', response.message);
return;
}
this.passTemplates = response.result;
// obtain fare types list
const allFareTypes: any = [];
this.passTemplates.forEach(passTemplate => {
passTemplate.availableFares.forEach(fare => {
if (fare.farePrice > 0)
allFareTypes.push(fare.fareType);
});
});
const distinctFareTypeIds = new Set(allFareTypes.map((x:any) => x.id));
this.fareTypes = [];
distinctFareTypeIds.forEach(x => this.fareTypes.push(allFareTypes.find((a: any) => a.id === x)));
console.log('Passes should be loaded at this point.\n');
this.fareTypes.forEach(x => console.log('Fare type: ' + x.name + '\n'));
///// ALL OF THIS CODE IS RUN BEFORE THE PAGE LOADS, THIS DATA SHOWS UP CORRECTLY IN THE CONSOLE
});
});
}
And the relevant part of the service that provides the data:
import { HttpClient } from '@angular/common/http';
import { Response, TokenResponse, NumberResponse } from '../models/response';
import { AgencyResponse, AgencyListResponse } from '../models/agency';
import { PassTemplateListResponse } from '../models/pass-template';
import { environment } from '../../../environments/environment';
import { Injectable } from '@angular/core';
import { ThirdPartyPurchase, ManualPurchase, EncryptedPurchase, VaultPurchase } from '../models/purchase';
import { PassUpgradeResponse, PassUpgrade } from '../models/pass-upgrade';
import { WalletResponse } from '../models/wallet';
@Injectable({
providedIn: 'root'
})
export class PublicService {
apiUrl: string = `${environment.apiUrl}/public`;
constructor(private http: HttpClient) { }
public getAgencies() {
return this.http.get<AgencyListResponse>(`${this.apiUrl}/agencies`);
}
public getAgency(id: string) {
return this.http.get<AgencyResponse>(`${this.apiUrl}/agencies/${id}`);
}
public getAgencyDataTokenizationKey(id: string) {
return this.http.get<TokenResponse>(`${this.apiUrl}/datatokenizationkey/${id}`);
}
public getPasses(agencyId: string) {
return this.http.get<PassTemplateListResponse>(`${this.apiUrl}/passes/${agencyId}`);
}
public issue(purchase: ManualPurchase) {
return this.http.post<Response>(`${this.apiUrl}/issue`, purchase);
}
public purchase(purchase: ThirdPartyPurchase) {
return this.http.post<Response>(`${this.apiUrl}/thirdpartypurchase`, purchase);
}
public encryptedPurchase(purchase: EncryptedPurchase) {
return this.http.post<Response>(`${this.apiUrl}/creditcardpurchase`, purchase);
}
public vaultPurchase(purchase: VaultPurchase) {
return this.http.post<Response>(`${this.apiUrl}/vaultpurchase`, purchase);
}
public getWallet(phone: string) {
return this.http.get<WalletResponse>(`${this.apiUrl}/getwallet/${phone}`);
}
public getPurchaseCount(passTemplateId: string) {
return this.http.get<NumberResponse>(`${this.apiUrl}/purchasecount/${passTemplateId}`);
}
public getUpgradeInfo(walletId: string, passTemplateId: string) {
return this.http.get<PassUpgradeResponse>(`${this.apiUrl}/getupgradeinfo/${walletId}/${passTemplateId}`);
}
public upgradePass(passUpgrade: PassUpgrade) {
return this.http.post<Response>(`${this.apiUrl}/upgradepass`, passUpgrade);
}
}
Now, the error message I'm getting is not making very much sense:
ERROR TypeError: Cannot read properties of undefined (reading 'id')
at PurchaseComponent_div_0_div_9_Template (purchase.component.html:14:17)
at ReactiveLViewConsumer.runInContext (core.mjs:10425:13)
at executeTemplate (core.mjs:10968:22)
at refreshView (core.mjs:12489:13)
at detectChangesInView (core.mjs:12653:9)
at detectChangesInEmbeddedViews (core.mjs:12597:13)
at refreshView (core.mjs:12513:9)
at detectChangesInView (core.mjs:12653:9)
at detectChangesInEmbeddedViews (core.mjs:12597:13)
at refreshView (core.mjs:12513:9)
I've stepped through the code as it's being run, and at no point are any of those objects having an undefined 'id' property. Why would I be getting an error that indicates I am? I am fairly certain this is why the data isn't visible until I click on the page.
Not sure why this is split up, first time asking a question here, but to reiterate what I've done: Stepped through code to see if data was invalid and causing an error - all data was valid Tried to use async directives (didn't work) Cleared out the caches in case of stale code, renamed some properties (didn't change behavior) Not sure what else to try at this juncture