0%

angular forkjoin和响应式表单经验

forkJoin 控制多个请求并行

在angular项目中使用rxjs的observable来控制异步请求很方便很舒服,但是有些时候得考虑一些特殊的问题,比如有两个请求相互依赖的情况,希望在所有请求都响应后再采取行动,如何处理?

以前使用Promise的时候,有个promise.all的方法,可以控制所有请求请求完成后执行操作,相同的,rxjs也提供了forkJoin操作符来控制请求的并行。

forkJoin操作符接受一组observable作为参数,当所有的observable完成时,将每个observable的最新值作为数组发出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 获得商户列表,并且同时获取已选择的商户,需要选中已选择的商户
* 并行发送请求,先置请求完成后,请求数据列表,类似于promise.all
*/
import {forkJoin} from 'rxjs';
const preApi = forkJoin([
this.commonApi.getMerchantNameList(false),
this.tokenApi.getChannelMethod({id: this.data.id}),
]);
preApi.subscribe(([merchantListRes, channelListRes]) => {
// merchantListRes为第一个接口发出的值
// channelListRes为第二个接口发出的值
// 其他处理逻辑
});

响应式表单嵌套检测问题

angular使用响应式表单,字段检测变得非常清晰和可配置话,使用起来很爽,但是最近遇到一个问题,比如我一个表单中有一个多选选项,多选选项上面有一个全选的checkbox,我在选择上面的全选或全不选的时候需要更新下面的多选列表,在点击下面的多选列表的时候也要更新上面的全选checkbox,这种情况怎么办?第一反应是直接来绑定formContrllor:

1
2
3
4
this.validateForm = this.fb.group({
checkList: [[], Validators.required],
allChecked: [false],
});

我们checkList就是我们多选选项,allChecked就是全选按钮,我们要分别跟踪对应的触发事件,所以我们直接来订阅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 this.selectAllSub = this.validateForm.controls['allChecked'].valueChanges.subscribe(value => {
this.indeterminate = false;
if (value) {
this.validateForm.controls['checkList'].patchValue(this.validateForm.controls['checkList'].value.map(item => {
return {
value: item.value,
label: item.label,
checked: true,
};
}), {
emitEvent: false,
});
} else {
this.validateForm.controls['checkList'].patchValue(this.validateForm.controls['checkList'].value.map(item => {
return {
value: item.value,
label: item.label,
checked: false,
};
}));
}
});
this.merchantListSub = this.validateForm.controls['checkList'].valueChanges.subscribe(data => {
if (data.every(item => item.checked === false)) {
this.validateForm.controls['allChecked'].patchValue(false);
this.indeterminate = false;
} else if (data.every(item => item.checked === true)) {
this.validateForm.controls['allChecked'].patchValue(true);
this.indeterminate = false;
} else {
this.indeterminate = true;
}
});

第一个监听的是全选选择框的值,如果全选了,就将checkList全设置为true,如果全不选就将checkList为false。第二个监听的是checkList的值,如果全选中了就将全选选择框选中,如果全不选中就将全选选择框设置为未选中,其中indenterminate是介于全选和不选的中间状态,即选择了部分。

逻辑很清晰,也可以跑起来。但是我们操作后发现,选择一下之后,就说maxsize了,也就是堆栈溢出。。。。

why?

来分析下,我们两个监听,A和B,监听A当A发生变化时改动B,监听B当B发生变动时改动A,好像陷入死循环了啊???

那我们得找一个方法,间听A当A发生变动时改动B,但这时B的监听不需要作出动作。还能有这种方法???

我们这里用了formBuild的formController,而监听发生在赋值的过程之后,那么赋值的地方是不是有什么文章?

formController的赋值操作有两个方法:

  • setValue(value, options) 当值发生变化时,该配置项决定如何传播变更以及发出事件。
  • pathValue()

在 FormControl 这个层次上,该函数的功能和 setValue 完全相同。 但 FormGroup 和 FormArray 上的 patchValue则具有不同的行为。

那么来看setValue的options:

  • onlySelf?: boolean; 如果为true,则每次变更只影响该控件本身,不影响父控件,默认为false
  • emitEvent?: boolean; 当控件值发生变化时,statusChanges和valueChanges这两个Observable都会以最近的状态和值发出事件,如果为false,则不会发出事件。默认为true。
  • emitModelToViewChange?: boolean; 如果为ture,则每次变化都会触发一个onChange事件以更新试图。默认为true
  • emitViewToModelChange?: boolean; 如果为true,则每次变化都会触发一个ngModelChange事件以更新模型。默认为true。

ok,我们需要注意emitEvent这个配置,如果这个为false,那么是不是可以达到我们想要的效果?修改下我们前面的监听函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
this.merchantListSub = this.validateForm.controls['merchantList'].valueChanges.subscribe(data => {
if (data.every(item => item.checked === false)) {
this.validateForm.controls['allChecked'].patchValue(false, {
emitEvent: false,
});
this.indeterminate = false;
} else if (data.every(item => item.checked === true)) {
this.validateForm.controls['allChecked'].patchValue(true, {
emitEvent: false,
});
this.indeterminate = false;
} else {
this.indeterminate = true;
}
});

非常nice,可以实现我们的效果。

但是,在提交表单的时候,发现提交的时候会把修改的值清理掉,竟然丢失了。。。。看看我们的保存方法里面涉及到表单的操作,发现有这块代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 let isValid = true;
for (const i of Object.keys(this.validateForm.controls)) {
if (i === 'allChecked') {
continue;
}
this.validateForm.controls[i].markAsDirty();
if (this.validateForm.controls[i].status === 'INVALID') {
isValid = false;
}
this.validateForm.controls[i].updateValueAndValidity();
}
if (!isValid) {
return resolve();
}

看看这里用到的方法:

  • updateValueAndValidity 重新计算控件的值和校验状态
  • markAsDirty 把控件标记为dirty,当控件通过ui修改过时控件会变成dirty的。

也就是,我们在js中修改的值并没有被承认,也就是没有标记为dirty,我们需要添加这个语句:

1
this.validateForm.controls['allChecked'].markAsDirty();

ok完美😊

最终代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
this.merchantListSub = this.validateForm.controls['merchantList'].valueChanges.subscribe(data => {
if (data.every(item => item.checked === false)) {
this.validateForm.controls['allChecked'].patchValue(false, {
emitEvent: false,
});
this.indeterminate = false;
} else if (data.every(item => item.checked === true)) {
this.validateForm.controls['allChecked'].patchValue(true, {
emitEvent: false,
});
this.indeterminate = false;
} else {
this.indeterminate = true;
}
this.validateForm.controls['allChecked'].markAsDirty();
});
码字辛苦,打赏个咖啡☕️可好?💘