在 Angular 中使用 RxJS 进行状态管理(撤消操作)

问题描述 投票:0回答:1

我正在学习 Angular 中的状态管理概念,并使用纯 RxJS 构建一个简单的应用程序以用于学习目的。我创建了一个购物车应用程序,可以在其中添加和删除产品。我能够毫无问题地从购物车状态添加和删除商品。但是,我想添加一个功能,当从购物车中删除商品时,可以通过恢复之前的状态来撤消删除操作。

任何人都可以帮助我找到使用 RxJS 和 Angular 实现此功能的最佳方法吗?以下是我当前的代码:

export class CartStateService {
  private readonly initialState: CartState = { items: [] };
  private state$: BehaviorSubject<CartState> = new BehaviorSubject<CartState>(this.initialState);

  constructor() { }

  public get cartItems$(): Observable<CartItem[]> {
    return this.state$.asObservable().pipe(map(state => state.items));
  }

  public addToCart(item: CartItem): void {
    const items = [...this.state.items];
    const index = items.findIndex(i => i.id === item.id);

    if (index > -1) {
      items[index] = { ...items[index], quantity: items[index].quantity + item.quantity };
    } else {
      items.push(item);
    }

    this.setState({ items });
  }

  public removeFromCart(id: number): void {
    const items = this.state.items.filter(item => item.id !== id);
    this.setState({ items });
  }

  private get state(): CartState {
    return this.state$.getValue();
  }

  private setState(newState: Partial<CartState>): void {
    this.state$.next({ ...this.state, ...newState });
  }

  public clearCart(): void {
    this.setState(this.initialState);
  }

  public undoRemove(): void {
    // How do I implement undo functionality here?
  }
}

任何有关如何使用 RxJS 实现“撤消删除”功能而不手动将先前状态存储在数组或变量中的指导,我们将不胜感激。

提前致谢!

angular rxjs state undo behaviorsubject
1个回答
0
投票

我们可以使用 rxjs 运算符

pairwise
。它记录了先前的值和当前的值。然后我们使用
tap
将先前的值存储在服务
previousValue
的属性中。您会注意到我使用
structuredClone
来对状态进行深度克隆。

在 Javascript 中,数组和对象作为引用存储在内存中,如果没有此克隆,对当前状态所做的更新也会设置为存储的先前状态。

public cartHistory$: Observable<any> = this.state$.pipe(
  pairwise(),
  tap((data: any) => {
    this.previousState = structuredClone(data[0]);
  })
);

所以撤消,就是将存储的状态应用到当前状态,然后将存储的状态设置为空。

public undoRemove(): void {
  if (this.previousState) {
    this.state$.next(structuredClone(this.previousState));
    this.previousState = null;
  }
}

您还会注意到,我将历史记录保留为单独的流,我无法使用behaviorSubject将其设为单个流,因此我们需要在组件上订阅它,同时我们还必须确保它已取消订阅。

export class App {
  name = '';
  quantity = 0;
  id = 0;
  cartStateService = inject(CartStateService);
  cartSubscription: Subscription = new Subscription();
  ngOnInit() {
    this.cartSubscription.add(this.cartStateService.cartHistory$.subscribe());
  }

  ngOnDestroy() {
    this.cartSubscription.unsubscribe();
  }

完整代码:

import { Component, inject, Injectable } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import {
  BehaviorSubject,
  Observable,
  map,
  pairwise,
  tap,
  Subscription,
} from 'rxjs';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Injectable({ providedIn: 'root' })
export class CartStateService {
  private readonly initialState: any = { items: [] };
  public previousState = null;
  private state$: BehaviorSubject<any> = new BehaviorSubject<any>(
    this.initialState
  );
  public cartHistory$: Observable<any> = this.state$.pipe(
    pairwise(),
    tap((data: any) => {
      this.previousState = structuredClone(data[0]);
    })
  );

  constructor() {}

  public get cartItems$(): Observable<any[]> {
    return this.state$.asObservable().pipe(map((state: any) => state.items));
  }

  public addToCart(item: any): void {
    const items = [...this.state.items];
    const index = items.findIndex((i) => i.id === item.id);

    if (index > -1) {
      items[index] = {
        ...items[index],
        quantity: items[index].quantity + item.quantity,
      };
    } else {
      items.push(item);
    }

    this.setState({ items });
  }

  public removeFromCart(id: number): void {
    const items = this.state.items.filter((item: any) => item.id !== id);
    this.setState({ items });
  }

  private get state(): any {
    return this.state$.getValue();
  }

  private setState(newState: Partial<any>): void {
    this.state$.next({ ...this.state, ...newState });
  }

  public clearCart(): void {
    this.setState(this.initialState);
  }

  public undoRemove(): void {
    if (this.previousState) {
      this.state$.next(structuredClone(this.previousState));
      this.previousState = null;
    }
  }
}

@Component({
  selector: 'app-root',
  imports: [CommonModule, FormsModule],
  template: `
    {{cartStateService.previousState | json}}
    <hr/>
    @for(item of cartStateService.cartItems$ | async; track $index) {
      <div>{{item.name}} - ({{item.quantity}}) | <button (click)="cartStateService.removeFromCart(item.id)">X</button></div>
    }
    <hr/>
    name: <input [(ngModel)]="name"/><br/>
    quantity: <input [(ngModel)]="quantity" type="number"/><br/>
    <button (click)="addToCart()">add</button>
    <button (click)="cartStateService.clearCart()">clear</button>
    <button (click)="cartStateService.undoRemove()">undo</button>
  `,
})
export class App {
  name = '';
  quantity = 0;
  id = 0;
  cartStateService = inject(CartStateService);
  cartSubscription: Subscription = new Subscription();
  ngOnInit() {
    this.cartSubscription.add(this.cartStateService.cartHistory$.subscribe());
  }

  ngOnDestroy() {
    this.cartSubscription.unsubscribe();
  }

  addToCart() {
    this.id++;
    this.cartStateService.addToCart({
      id: this.id,
      name: this.name,
      quantity: this.quantity,
    });
    this.name = '';
    this.quantity = 0;
  }
}

bootstrapApplication(App);

Stackblitz 演示

© www.soinside.com 2019 - 2024. All rights reserved.