skip to Main Content

in my angular project based on firebase and firebase i had this form , and when i try to edit a product ,i could access the id of each product but i can’t access and fill the form by other data (title,price,category…)
here is my code :
product-form.html

<div class="row">
    <div class="col-md-6">
  
<form (ngSubmit)="save(f.value)" #f="ngForm">
    <div class="form-group">
        <label for="id">Id</label>
        <input [(ngModel)]="product.id"  name="id" type="number" class="form-control" id="id" required #id="ngModel">
        
        <div class="alert alert-danger" *ngIf="id.touched && id.invalid">The id is required !</div>
    </div>

    <div class="form-group">
        <label for="title">Title</label>
        <input [(ngModel)]="product.title"  name="title" type="text" class="form-control" id="title" required #title="ngModel">
        
        <div class="alert alert-danger" *ngIf="title.touched && title.invalid">Title is required !</div>
    </div>

    <div class="form-group">
        <label for="price">Price</label>
        <div class="input-group">
            <input type="number" class="form-control" id="price"  [(ngModel)]="product.price" #price="ngModel" name="price" required min="0">
            <span  class="input-group-text">$</span>
        </div>
        <div class="alert alert-danger" *ngIf="price.touched && price.invalid">
            <div *ngIf="price.errors?.['required']">"price is required !"</div>
            <div *ngIf="price.errors?.['pattern']">price must be a number</div>
            <div *ngIf="price.errors?.['min']">price must be =>0 ! </div>
            </div>
    </div> 

     <div class="form-group">
        <label for="category">Category</label>
        <select  class="form-control" id="category" [(ngModel)]="product.category" name="category" #category="ngModel" required>
            <option value=""></option>
            <option *ngFor="let category of categories$ " [value]="category.name">{{category.name}}</option>
            </select>
            <div class="alert alert-danger" *ngIf="category.touched && category.invalid">category is required!</div>
        </div>

            <div class="form-group">
                <label for="imageUrl">Image Url</label>
                <div class="input-group">
                <span  class="input-group-text">http://</span>
                <input type="text" class="form-control" id="imageUrl" #imageUrl="ngModel" [(ngModel)]="product.imageUrl" name="imageUrl" required pattern="https?://.+">
            </div> 
        </div>   
            <div class="alert alert-danger" *ngIf="imageUrl.touched && imageUrl.invalid">
            <div  *ngIf="imageUrl.errors?.['required']">image Url is Required!</div>
            <div *ngIf="imageUrl.errors?.['pattern']">image Url is invalid! must  start with http:// or https://...</div>
        </div>
            <br>
<button class="btn btn-primary"  type="submit">Save</button>
    
</form>

</div>

<div class="col-md-6">
    <div class="card" style="width: 20rem;">
        <img [src]="imageUrl.value" class="card-img-top">
        <div class="card-body">
          <h5 class="card-title" style="color: dodgerblue;" >{{title.value}}</h5>
          <p class="card-text">{{price.value | currency:'USD':true}}</p>
        </div>
      </div>
    </div>

</div>

    <div class="toast align-items-center text-white bg-primary border-0" role="alert" aria-live="assertive" aria-atomic="true">
        <div class="d-flex">
          <div class="toast-body">
           Product Added succefully
          </div>
          <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
        </div>
      </div>

product-form.ts

import { Component,Inject, Input} from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { CategoriesService } from '../../services/categories.service';
import { Router, RouterLink, ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { tap, take } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ChangeDetectorRef } from '@angular/core';

import { AngularFireDatabase } from '@angular/fire/compat/database';
import { ProductService } from 'src/app/services/product.service';
import { AngularFireStorage } from '@angular/fire/compat/storage';
import { AngularFirestore } from '@angular/fire/compat/firestore';


@Component({
  selector: 'app-products-form',
  templateUrl: './products-form.component.html',
  styleUrl: './products-form.component.css'
})
export class ProductsFormComponent {
  categories$:any;
  products:any
  product$:any;

  private destroy$ = new Subject<void>();

  product: any = {
    id: '',
    title: '',
    price: '',
    category:'',
    imageUrl:''
  };


  constructor( categories: CategoriesService,private ProductService :ProductService, private router: Router,private db:AngularFireDatabase, private afs:AngularFirestore ,private route:ActivatedRoute,private cdr: ChangeDetectorRef,) 
  {  
    
    this.db.list('/categories').valueChanges().pipe(
      tap(data => {
        this.categories$ = data;
      })
    ).subscribe(
      () => {
        console.log(this.categories$);
      },
    );


    this.afs.collection('/products/').valueChanges().pipe(
      tap(data => {
        this.products = data;
      })
    ).subscribe(
      () => {
        console.log(this.products);
      },
    );

    const id = this.route.snapshot.paramMap.get('id'); 
    if (id) {
      console.log(id);
      
    
      this.ProductService.getProductWithKey(id).pipe(takeUntil(this.destroy$))
      .subscribe(productData => {
        this.product = productData;
        
        
        console.log(this.product)
        
        
        
      });
  } 





    }


    ngOnInit() {
      const id = this.route.snapshot.paramMap.get('id'); 
      if (id) {
        console.log(id);
        
      
        this.ProductService.getProductWithKey(id).pipe(takeUntil(this.destroy$))
        .subscribe(productData => {
          console.log(productData);
          this.product = productData;
        });
      } 

    }


  save(product:any){
    this.ProductService.create(product);
    this.router.navigate(['/admin/products']);
    console.log(product);
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

}



Product.service.ts

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from '@angular/fire/compat/database';
import {AngularFirestore,AngularFirestoreDocument} from '@angular/fire/compat/firestore';
import { Observable } from 'rxjs';
import { tap, take,map} from 'rxjs/operators';
import { Product } from './Product';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  
  product$:any = {};

  constructor(private db:AngularFireDatabase,public afs: AngularFirestore) { 

  }

  create(product:any){
   this.db.list('/products').push(product).then(()=>{console.log(`New product added with ID: ${product.id}`);});
  this.afs.collection('/products').add(product).then(()=>{console.log(`New product added with ID: ${product.id}`);});
  } 

getAll(){
  return this.afs.collection('/products');
}


getProductWithKey(id: string) {
  return this.afs.doc<Product>(`/products/${id}`).snapshotChanges().pipe(
    map(action => {
     
      const data = action.payload.data() as Product;
      const title = action.payload.data()?.title;
      const price = action.payload.data()?.price;
      const category = action.payload.data()?.category;
      const imageUrl = action.payload.data()?.imageUrl;
      console.log(action.payload.data()?.title); 
      const id = action.payload.id;
      return { id,title,price,category,imageUrl};
    })
  );
}
}

list of products got from firestore databse
expected when click id
got only id in the form !

What i got in the console:

3

Answers


  1. The id that you are passing to this method getProductWithKey(id: string) should be the documentId and not the custom id field that you have in the document fields. For example, you can do the following:

      return this.afs.doc<Product>('/products/Fc8xq5nDMbIf0kZJY2AT').snapshotChanges().pipe(
        map(action => {
    

    This should return the first product that you have created in this list. Or another way is to get all the documents in a collection, for example:

     return this.afs.collection<any>('products').snapshotChanges().pipe(
            map(actions => actions.map(a => a.payload.doc.data()))
         );
      }
    
    Login or Signup to reply.
  2. This function says that everything except id might be undefined in some case:

    getProductWithKey(id: string) {
      return this.afs.doc<Product>(`/products/${id}`).snapshotChanges().pipe(
        map(action => {
         
          const data = action.payload.data() as Product;
          const title = action.payload.data()?.title;
          const price = action.payload.data()?.price;
          const category = action.payload.data()?.category;
          const imageUrl = action.payload.data()?.imageUrl;
          console.log(action.payload.data()?.title); 
          const id = action.payload.id;
          return { id,title,price,category,imageUrl};
        })
      );
    }
    

    So I assume that you caught that case. Also, there might be issue of using undefined in forms. So it would be better to assign null instead of undefined like this for example:

    const imageUrl = action.payload.data()?.imageUrl ?? null;
    

    Also I see you have to subscriptions in which assigns products: one in constructor and one in ngOnInit. It is usually bad practice because in asynchronous code you can’t be sure which assignement be first, so I would recommend to change it.

    Login or Signup to reply.
  3. Either You have to pass firestore ID to the below query

    return this.afs.doc<Product>('/products/Fc8xq5nDMbIf0kZJY2AT').snapshotChanges().pipe(
        map(action => {
          const data = action.payload.data() as Product;
          const title = action.payload.data()?.title;
          const price = action.payload.data()?.price;
          const category = action.payload.data()?.category;
          const imageUrl = action.payload.data()?.imageUrl;
          console.log(action.payload.data()?.title); 
          const id = action.payload.id;
          return { id,title,price,category,imageUrl};
        })
    

    This Firestore ID should be taken when getting collection DOCS.

    OR if the only data in your hand is id:9

    Then you should go with "where" query.

    return this.afs.collection('products', ref => ref.where('id', '==', '9').limit(1))
      .doc(id)
      .snapshotChanges()
      .pipe(
        map(action => {
                const data = action.payload.data() as Product;
          const title = action.payload.data()?.title;
          const price = action.payload.data()?.price;
          const category = action.payload.data()?.category;
          const imageUrl = action.payload.data()?.imageUrl;
          console.log(action.payload.data()?.title); 
          const id = action.payload.id;
          return { id,title,price,category,imageUrl};
        })
      );
    

    Here Docs with "id" field with "9" is filtered out and,by using LIMIT,datas filtered out is limited to only 1.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search