RxJS - 使用成对确认和恢复输入字段
RxJS - Using pairwise to confirm and revert input field
所以我对 observables 有点陌生,我正在为一个场景而苦苦挣扎,我认为它可能是 SO 问题的一个很好的候选者。我们开始...
场景是这样的:我有一个下拉框;当它改变时,我想
- 根据字段的先前值和新值检查条件
- 如果条件通过,则请求用户确认,并且...
- 如果用户没有确认,则恢复该字段的值。
这是带有注释的代码:
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),
)
所以我对 observables 有点陌生,我正在为一个场景而苦苦挣扎,我认为它可能是 SO 问题的一个很好的候选者。我们开始...
场景是这样的:我有一个下拉框;当它改变时,我想
- 根据字段的先前值和新值检查条件
- 如果条件通过,则请求用户确认,并且...
- 如果用户没有确认,则恢复该字段的值。
这是带有注释的代码:
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),
)