Инициализируйте BehaviorSubject значением из Observable

Вопрос или проблема

Я пытался создать счетчик элементов для записей в списке желаемого в своей области заголовка и пытался применить решения, предложенные здесь (для элементов корзины)
Изменить количество товаров в корзине в заголовке в Angular

К сожалению, это не совсем устраивает меня, поскольку решения игнорируют тот факт, что могут быть начальные данные, предоставляемые как Observable. В моем случае я храню объекты списка желаемого с помощью StorageMap (@ngx-pwa/local-storage), и эти данные возвращаются только как Observable, и я не совсем уверен, как связать эти начальные данные с BehaviorSubject в сервисе, который должен отслеживать изменения. Вот почему я придумал идею использовать два отдельных observable, которые я комбинирую…

КУПРОНТ: 
export class WishlistLinkComponent{

  merged$: Observable<number> = combineLatest([this.wishListService.getWishList(),this.wishListService.getCartCount()]).pipe(
    map(([wishlist, count]) => {
      console.log('список желаемого: ',wishlist);
      console.log('количество: ',count);
      return count ?? (wishlist.entries?.length ?? 0);
    })
  )

  constructor(protected wishListService: WishListService) {}

}


СЕРВИС:
export class EmvAnonymousWishListService {
  constructor(
    private storage: StorageMap,
    private productService: ProductService
  ) { }

  private cartSubj:BehaviorSubject<number | undefined> = new BehaviorSubject<number| undefined>(undefined);


  getCartCount(): Observable<number> {
    return this.cartSubj.asObservable();
  }
  
    getWishList(): Observable<Cart> {
    return this.storage.get('wishlist')
      .pipe(
        switchMap((productCodes: string[]) => this.productService.getProducts(productCodes)),
    );
  }
  
    addEntry(productCode: string): void {
    this.this.storage.get('wishlist').subscribe(productCodes => {
        productCodes.push(productCode);
        this.cartSubj.next(productCodes.length)
        //логика хранения...
    });
  }
  
      removeEntry(productCode: string): void {
    this.this.storage.get('wishlist').subscribe(productCodes => {
        const filteredProductCodes = productCodes.filter((code: string) => {
          return code !== productCode;
        });
        this.cartSubj.next(productCodes.length)
        //логика хранения...
    });
  }
  
}

Я знаю, что это некрасиво, но я не знаю, как справиться со сценарием подобного рода, когда только один observable покрывает все.

вы можете упростить это так:

Используйте ReplaySubject: вместо BehaviorSubject вы можете использовать ReplaySubject(1), чтобы убедиться, что начальное состояние из StorageMap захватывается и обновления правильно передаются.

Убедитесь, что начальные данные хранятся: вы хотите инициализировать cartSubj с текущим состоянием списка желаемого, чтобы он мог начинаться с правильного количества до внесения любых изменений.

  // Сервис
  export class EmvAnonymousWishListService {
    constructor(
      private storage: StorageMap,
      private productService: ProductService
    ) {
      this.initializeCartCount();
    }

    private cartSubj: ReplaySubject<number> = new ReplaySubject<number>(1);

    // Инициализация количества корзины на основе сохраненного списка желаемого
    private initializeCartCount() {
      this.storage.get<string[]>('wishlist').pipe(
        map(productCodes => productCodes?.length ?? 0)
      ).subscribe(count => {
        this.cartSubj.next(count);
      });
    }

    getCartCount(): Observable<number> {
      return this.cartSubj.asObservable();
    }

    getWishList(): Observable<Cart> {
      return this.storage.get('wishlist').pipe(
        switchMap((productCodes: string[]) => this.productService.getProducts(productCodes))
      );
    }

    addEntry(productCode: string): void {
      this.storage.get<string[]>('wishlist').subscribe(productCodes => {
        const updatedCodes = productCodes ? [...productCodes, productCode] : [productCode];
        this.storage.set('wishlist', updatedCodes).subscribe(() => {
          this.cartSubj.next(updatedCodes.length);
        });
      });
    }

    removeEntry(productCode: string): void {
      this.storage.get<string[]>('wishlist').subscribe(productCodes => {
        const updatedCodes = productCodes?.filter(code => code !== productCode) || [];
        this.storage.set('wishlist', updatedCodes).subscribe(() => {
          this.cartSubj.next(updatedCodes.length);
        });
      });
    }
  }




  // Компонент
  export class WishlistLinkComponent {
    merged$: Observable<number>;

    constructor(protected wishListService: EmvAnonymousWishListService) {
      this.merged$ = combineLatest([
        this.wishListService.getWishList(),
        this.wishListService.getCartCount()
      ]).pipe(
        map(([wishlist, count]) => {
          console.log('список желаемого: ', wishlist);
          console.log('количество: ', count);
          return count ?? (wishlist.entries?.length ?? 0);
        })
      );
    }
  }

Ключевые улучшения:
ReplaySubject(1): Это заменяет BehaviorSubject для хранения последнего переданного значения, даже если ни один подписчик не был присутствует во время передачи.

Начальное количество: initializeCartCount гарантирует, что количество списка желаемого правильно инициализируется из хранилища.

Обновления после добавления/удаления: после добавления или удаления записей обновленное количество передается в cartSubj, и подписчики получат обновленное количество.

Вы можете попробовать использовать tap, чтобы обновить начальное значение поведения subject, и это позаботится о том, чтобы начальное значение было установлено.

getWishList(): Observable<Cart> {
return this.storage.get('wishlist')
  .pipe(
    switchMap((productCodes: string[]) => this.productService.getProducts(productCodes)),
    tap((wishlist: any) => {
      this.cartSubj.next(wishlist.entries?.length);
    }
);

Компонент может прочитать эти данные и обновить их.

Я не уверен, в чем ваша проблема, но у меня сложилось впечатление, что все дело в подсчете, почему бы вам не воспользоваться оператором count из rxjs?
например:
of(1, 2, 3).pipe(count()).subscribe(console.log);
это выведет “3”
Если это вам не помогает, не могли бы вы подробнее рассказать, что именно вы ищете?

Ответ или решение

Чтобы инициализировать BehaviorSubject со значением, полученным из Observable, вам потребуется выполнить несколько шагов. В этом ответе мы рассмотрим, как это можно сделать с использованием Angular и RxJS, что особенно актуально для вашей задачи — отслеживания количества элементов в списке желаемого.

1. Предпосылки

Необходимо понимать, что BehaviorSubject является видом Subject, который требует начального значения и эмитирует это значение сразу при подписке. В вашей ситуации стартовое значение должно быть получено из Observable, поэтому логика инициализации должна учитывать асинхронное получение данных.

2. Использование ReplaySubject

Один из вариантов — заменить BehaviorSubject на ReplaySubject с буфером 1. Это позволит сохранить последнее эмитированное значение и обеспечить его доступность для всех новых подписчиков, даже если они подписываются позже.

Вот как это можно реализовать:

// Сервис
export class EmvAnonymousWishListService {
  private cartSubj: ReplaySubject<number> = new ReplaySubject<number>(1);

  constructor(private storage: StorageMap, private productService: ProductService) {
    this.initializeCartCount();
  }

  private initializeCartCount() {
    this.storage.get<string[]>('wishlist').pipe(
      map(productCodes => productCodes?.length ?? 0)
    ).subscribe(count => {
      this.cartSubj.next(count);
    });
  }

  getCartCount(): Observable<number> {
    return this.cartSubj.asObservable();
  }

  getWishList(): Observable<Cart> {
    return this.storage.get<string[]>('wishlist').pipe(
      switchMap(productCodes => this.productService.getProducts(productCodes)),
    );
  }

  addEntry(productCode: string): void {
    this.storage.get<string[]>('wishlist').subscribe(productCodes => {
      const updatedCodes = productCodes ? [...productCodes, productCode] : [productCode];
      this.storage.set('wishlist', updatedCodes).subscribe(() => {
        this.cartSubj.next(updatedCodes.length);
      });
    });
  }

  removeEntry(productCode: string): void {
    this.storage.get<string[]>('wishlist').subscribe(productCodes => {
      const updatedCodes = productCodes?.filter(code => code !== productCode) || [];
      this.storage.set('wishlist', updatedCodes).subscribe(() => {
        this.cartSubj.next(updatedCodes.length);
      });
    });
  }
}

3. Обновление компонента

Теперь компонент, который будет использоваться для отображения количества элементов в списке желаемого, может выглядеть следующим образом:

// Компонент
export class WishlistLinkComponent {
  merged$: Observable<number>;

  constructor(protected wishListService: EmvAnonymousWishListService) {
    this.merged$ = combineLatest([
      this.wishListService.getWishList(),
      this.wishListService.getCartCount()
    ]).pipe(
      map(([wishlist, count]) => {
        console.log('wishlist: ', wishlist);
        console.log('count: ', count);
        return count ?? (wishlist.entries?.length ?? 0);
      })
    );
  }
}

4. Преимущества

Используя ReplaySubject, вы гарантируете, что любое новое значение будет доступно для всех подписчиков, даже если они подписались позже. Это упростит управление состоянием вашего компонента и вписание логики обработки асинхронных запросов.

Заключение

Эта реализация позволит вам корректно инициализировать BehaviorSubject, используя значения из Observable, и успешно управлять изменениями в вашем списке желаемого. Если у вас возникли дополнительные вопросы, не стесняйтесь их задать.

Оцените материал
Добавить комментарий

Капча загружается...