Search code examples
angularangularjsasynchronousroutesangular-routing

Object Attributes are undefined after routing in Angular


A newcomer to Angular here. I know similar questions have been asked before and I read too many of them here, answers about AJAX and async programming, but couldn't solve the issue. I want to see the details of the products when I click on 'Details' button, the routing does its job but no values are visible on the page other than static html. So here it is:

This is my details service to get specific details of a product

@Injectable()
export class DetailsService {

  constructor(private http:HttpClient) { }
  path = "http://localhost:3000/products";

  getProductById(productId: any): Observable<Product> {
    let newPath = this.path
    
    if (productId) {
      newPath += "?id=" + productId;
    }
    console.log(newPath);
    return this.http.get<Product>(newPath).pipe(
      tap(data => console.log(JSON.stringify(data) )),
      catchError(this.handleError)
    );
  }
  //handle error func here

This is the details component

export class DetailsComponent implements OnInit {
  title = 'Product Details'
  productId: any;
  product: Product;

  constructor(private detailsService: DetailsService,
    private activatedRoute: ActivatedRoute,
    private alertifyService: AlertifyService,
  ) {}

  ngOnInit() {

    this.activatedRoute.params.subscribe(params => {
      this.productId = params["id"];
      console.log(this.productId);

      this.detailsService.getProductById(this.productId).subscribe(data => {
        this.product = data;
        console.log(this.product);
        console.log('38')
      });
      console.log(this.productId + " " + typeof this.productId);
    });

}

Everything works without any errors or something, but the attributes are not shown on details.component.html which is;

<div *ngIf="product">
    <img class="listing-photo" [src]="product.imageUrl"
      alt="Exterior photo of {{product.name}}"/>
    <section class="listing-description">
      <h2 class="listing-heading">{{product.name}}</h2>
      <p class="listing-location">{{product.description}}, {{product.price}}</p>
    </section>
    <section class="listing-features">
      <h2 class="section-heading">About this housing location</h2>
      <ul>
        <li>Units available: {{product.price}}</li>
        <li>Does this location have wifi: {{product.price}}</li>
        <li>Does this location have laundry: {{product.price}}</li>
      </ul>
    </section>
</div>

The call is made from product.component.html

//snippet with ngFor etc
<div class="card-footer">
        <a (click)="addToCart(product)" class="btn btn-primary text-white">Add to cart</a>
        <a [routerLink]="['/products',product.id]" class="btn btn-secondary ml-3 ">Details</a>
      </div>
//the rest

I tried hundred things if two hundred and I'm lost now. I understand the nature of async programming but can't get it work as intended.

EDIT: Log of console.log(this.product) is as follows:

Array(1)
0: 
{category: 'PC Components', categoryId: 1, description: 'Mechanical gaming keyboard with customizable RGB lighting.', id: '1', imageUrl: 'https://images.unsplash.com/photo-1612198188060-c7…xMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', …}
length: 1
[[Prototype]]: 
Array(0)

Solution

  • Since I have no idea what edit you made to the endpoint I'll keep the same you provided. Regarding my answer, this is the optimized version of your code with comments where needed:

    Details Service

    @Injectable()
    export class DetailsService {
      private _http = inject(HttpClient); // We directly inject the HttpClient
      private _path = "http://localhost:3000/products";
    
      // Since you are checking if the productId is given, I made it optional with the ? symbol.
      public getProductById(productId?: string): Observable<Product> {
        // URL created dynamically using the ternary operator
        return this.http.get<Product>(productId ? `${this._path}?id=${productId}` : this.path);
      }
    

    Details Component

    export class DetailsComponent implements OnInit {
      // Protected because you are not inheriting this component into another.
      protected title = 'Product Details';
      protected product!: Product;
    
      // Private since this variable isn't used in the HTML view.
      private _productId!: string;
      
      constructor(
        private _detailsService: DetailsService,
        private activatedRoute: ActivatedRoute,
      ) {}
    
      public ngOnInit(): void {
        this.activatedRoute.params.subscribe(params => {
          this._productId = params["id"];
    
          // By splitting the service call into a new function, the code will be easier to handle when dealing with large data.
          this._getProductById(this._productId);
        });
    
      }
    
      private _getProductById(productId: string): void {
         this._detailsService.getProductById(productId).subscribe(data => {
            this.product = data;
         });
      }
    

    }

    Details Component html

    The code is actually fine, I'd just put a touch to it in order to not let the view explode if the product has not the data you are trying to display. We achieve this using the ? key:

    <div *ngIf="product">
        <img class="listing-photo" [src]="product?.imageUrl"
          alt="Exterior photo of {{product?.name}}"/>
        <section class="listing-description">
          <h2 class="listing-heading">{{product?.name}}</h2>
          <p class="listing-location">{{product?.description}}, {{product?.price}}</p>
        </section>
        <section class="listing-features">
          <h2 class="section-heading">About this housing location</h2>
          <ul>
            <li>Units available: {{product?.price}}</li>
            <li>Does this location have wifi: {{product?.price}}</li>
            <li>Does this location have laundry: {{product?.price}}</li>
          </ul>
        </section>
    </div>
    

    Product component html

    No need to do anything. I'm not a big fan on using routerLink in the component's template, but that's up to you.

    Code is not tested nor deeply verified for typos / misspell words, but it should give you a strong starting point, and a fresh codebase.

    Few notes:

    • Always type everything, you have no idea how dangerous any can be. If you are not sure on what type to use, you can always cast it to unknown. A great article about it can be found here
    • Keep the scope of your variable clear, using private , public and protected accordingly.
    • if Product is a class or an interface consider changing it to a type. It is much more better. To the why, there are a lot of articles that explain it in detail, but long story short:

    Types can be employed with primitive types like type string, whereas interfaces can't be used with primitive types. Types are generally more flexible and expressive.

    If you need any help, feel free to ask.