RxJS - 使用成对确认和恢复输入字段

RxJS - Using pairwise to confirm and revert input field

所以我对 observables 有点陌生,我正在为一个场景而苦苦挣扎,我认为它可能是 SO 问题的一个很好的候选者。我们开始...

场景是这样的:我有一个下拉框;当它改变时,我想

  1. 根据字段的先前值和新值检查条件
  2. 如果条件通过,则请求用户确认,并且...
  3. 如果用户没有确认,则恢复该字段的值。

这是带有注释的代码:

  ngOnInit(): void {
    
    // I am waiting for my view-model to load, then initializing my FormGroup using that view model data.
    // NOTE: My view model is for "Contact" (this code is for contact list)
    this.addSubcription(this.vm$.subscribe((vm) => this.initFormGroup(vm)));
    
    const field:string = 'customerEmployerId'; // the field's name
    // I create the observable that should listen to changes in the field, and return them in pairs
    const employerValueChanges$ = this.formInit$.pipe(
      switchMap(form=> form.get(field).valueChanges.pipe(
        startWith(form.get(field).value)
      )),
      pairwise()
    );

    // I combine the changes observable with my other observables to access data from each
    let employerCheckSub = combineLatest([
      employerValueChanges$, // the value-changes obs
      this.vm$, // the view-model data
      this.customers$ // a list of customers from a CustomerService
    ]).subscribe(
      ([
        [oldid,newid], // values from value-changes obs
        contact, // the contact info / data
        customers // the list of customers
      ])=> {
        
        // check the previously and newly selected employer values
        // request confirmation if contact was listed as the primary contact for the previously selected employer
         if(oldid > 0 && newid !== oldid){
           const employer = customers.find(c=> c.customerId === oldid && c.contactId === contact.contactId);
           if(employer === null) return;
           if(!confirm('Warning: changing this contact\'s employer will also remove them '+
           'as the primary contact for that customer. Are you should you want to continue?')){ 
             // user clicked cancel, so revert back to the previous value without emitting event
             this.contactEditForm.get(field).setValue(oldid, {emitEvent:false});
           }
         }
      });

    this.addSubcription(employerCheckSub);
  }

问题是,当我在不发出事件的情况下恢复值时,成对可观察对象在下一次值更改时发出不正确的“先前”值。我希望有一两个 RxJS 运算符是我所缺少的,并且可以在这里完美地工作。有没有人有解决这个问题的技巧可以分享?


更新工作代码:

首先,特别感谢。他对 scan 运算符的使用绝对是正确的选择。我只需要一个小修复,即设置 crt(或下面代码中的 current)值以及累加器中的 prev 值。瞧!这是我的最终工作版本:

/**
   * Requests confirmation when attempting to change a contact's employer if that contact is also
   * designated as the employer's primary contact.
   */
  private addEmployerChangeConfirmation() {
    // NOTE: In this scenario, "customers" are synonymous with "employers"; i.e., our customers are employers of these contacts.
    const field: string = 'customerEmployerId'; // the field's name
    const valueChanges$ = this.formInit$.pipe(
      switchMap((form) => form.get(field).valueChanges)
    );

    let employerCheckSub = combineLatest([
      // the value-changes obs
      valueChanges$,
      // the id needed from the view model
      this.vm$.pipe(
        filter((vm) => vm !== null),
        map((vm) => vm.contactId)
      ),
      // the customer/employer list
      this.customers$,
    ])
      .pipe(
        // once the user approves, I don't bother re-confirming if they change back in same session
        // NOTE: I use a "$$" naming convention to indicate internal subjects that lack a corresponding public-facing observable.
        takeUntil(this.employerChangeApproved$$),
        scan(
          (acc, [current, contactId, customers], i) => ({
            prevOfPrev: acc.prev,
            ///////////////////////////////////////////////////////////////////////////////////////////////////
            // NOTE: This was an interesting issue. Apparently the seed value is resolved immediately.
            // So, there is no way I found to seed a value from another obs.
            // Instead, I just check if this is the first run, and if so I use the resolved data for prev value.
            // I know the data is resolved because an upstream obs provides it.
            ///////////////////////////////////////////////////////////////////////////////////////////////////
            prev: i === 0 ? this.contactData.customerEmployerId : acc.current, // <-- setting seed manually on first emission
            current,
            contactId,
            customers,
          }),
          {
            prevOfPrev: null,
            prev: null,
            current: this.contactData?.customerEmployerId,
            contactId: this.contactData?.contactId,
            customers: [],
          }
        ),
        // only continue if condition passes
        filter((data) =>
          this.checkIfChangeWillRemoveAsPrimaryContact(
            data.prev,
            data.current,
            data.contactId,
            data.customers
          )
        ),
        // we only want to revert if user clicks cancel on confirmation box.
        // NOTE: If they approve change, this also triggers the "employerChangeApproved$$" subject.
        filter((data) => !this.confirmRemoveAsPrimaryContact())
      )
      // and now we actually subscribe to perform the action
      .subscribe((data) => {
        data.current = data.prev;
        data.prev = data.prevOfPrev;
        this.contactEditForm
          .get(field)
          .setValue(data.current, { emitEvent: false });
      }); 

    this.addSubcription(employerCheckSub);
  }

这是我的方法:

form.valuesChanges.pipe(
  scan(
    (acc, item) => ({
      // Needed in case we need to revert
      prevOfPrev: acc[prev],

      prev: acc[crt],
      crt: item,
    }), 
    { prevOfPrev: null, prev: null, crt: null }
  ),

  // 'check a condition based on the previous and new values of the field'
  filter(v => condition(v.prev, v.crt)),

  // 'request from the user a confirmation if the condition passes'
  switchMap(
    v => confirmationFromUser().pipe(
      // 'then revert the value of the field if the user did not confirm'
      tap(confirmed => !confirmed && (v[prev] = v[prevOfPrev])),
    )
  ),

  // Go further only if the user confirmed
  filter(v => !!v),
)