零、需求分析
前置条件:本文基于Angular动态组件
https://angular.cn/guide/dyna...
这是一个有着三层组件的动态表单,由于表单项是动态生成的,与父组件不共用FromControl,因此对于必填项的拦截无法反馈到最外层组件的submit上。
为了方便交流,我们把最外层的模态框页面称为第一层
把Angular动态组件加载器称为第二层
把表单项子组件称为第三层。
一、第一层和第二层之间传值
这里用到的是普通的组件间传值。
先创建一个变量:
// 第一层:TS
// 当前组件按钮是否可以点击
submitButtonActive: boolean;
在第一层的提交按钮上加上[disabled]="!submitButtonActive"
,
// 第一层:HTML
// Submit按钮
<button [disabled]="!submitButtonActive" class="btn btn-primary" (click)="onSubmit()" type="button">确认</button>
这样button就指向了一个变量,以便后面在第二层数据变化时,改变第一层的变量从而控制按钮是否激活。
然后在第二层组件建立一个弹射器
// 第二层:TS
@Output()
isFormGroupValid = new EventEmitter<boolean>();
由于第二层组件有一堆子组件,这些子组件的输入是否合法由第二层决定,第二层只负责向第一层弹射一个计算后的最终值即可。
当第二层弹射新的值时,将会更新第一层提交按钮的激活情况。
二、第三层向第二层传值
接下来就是考虑第二层和第三层传递数据的问题了。
这段内容基于Angular动态组件。
先回顾一下动态组件是怎么生成的:
首先需要一个接口,接口规定了第二层组件加载器和第三层组件间可以传递什么值;
然后动态组件加载器就可以生成组件并向接口规定的变量中传值了。
基于此方式,如果向让子组件向父组件传值,我们还需要两个变量:
一个输入值index,用来标记当前子组件在父组件中是第几个被加载出来的,即位置索引。
一个输出值isValid,用来告诉父组件,当前子组件的信息是否合法。
当输出值isValid变化时,将变化值弹射给父组件(也就是第二层组件加载器),第二层校验所有组件的表单值是否全部合法,校验完成后向第一层弹射。
修改动态组件实现的接口,增加两个变量,
// 接口
// 组件索引,类型:number
@Input()
index: number;
// 当前组件的表单值是否合法,类型:弹射器
@Output()
isValid: EventEmitter<{index: number, isValid: boolean}>;
并且所有成员实现这两个变量:
// 第三层:TS
@Input()
index: number;
@Output()
isValid = new EventEmitter<{index: number, isValid: boolean}>();
在第二层增加赋值和订阅:
// 第二层:TS 加载动态组件时
componentRef.instance.index = i;
componentRef.instance.isValid.subscribe(({index, isValid}) => {
console.log(index);
console.log(isValid);
})
在第三层组件上,增加对formControl的订阅,输出valid属性:
// 第三层:TS , ngOninit方法中
this.formControl.valueChanges.subscribe(value => {
this.formItemValue.value = value;
// 打印表单值是否合法
console.log(this.formControl.valid);
})
此时,一旦把输入框的内容全部删掉,就会打印false属性:
、
接下来我们删掉第三层的console.log,在valid变化后把它弹射出去:
// 第三层:TS , ngOninit方法中
// 弹射内容包括当前索引和表单合法性
this.formControl.valueChanges.subscribe(value => {
this.formItemValue.value = value;
// 弹射表单值合法性
this.isValid.emit({index: this.index, isValid: this.formControl.valid});
})
此时,第三层没有输出语句了,而它的弹射会触发第二层的输出语句,我们来验证下效果:
每次值变化都会向第二层输出当前组件的索引和当前组件表单值是否合法。
接下来就是在第二层创建一个map,用来储存所有的组件信息:
// 第二层:TS
/**
* 组件映射,用来储存每个组件的索引和表单值是否合法
* 当值发生变化时,会统计当前映射的所有boolean,全部合法则弹射合法,否则弹射不合法
*/
componentMap = new Map<number, boolean>();
在加载动态组件时,每一次循环都向map中添加一个值。
有几个组件,就有几个对键值对,前面是索引,后面是合法性:
// 第二层:TS 加载动态组件时
// 向map中添加当前组件的信息
this.componentMap.set(i, false);
修改第二层订阅时的行为,去掉console.log,修改map的值。
并且统计map中所有的值,将统计好的结果弹射到第一层:
// 第二层:TS 加载动态组件时
componentRef.instance.isValid.subscribe(({index, isValid}) => {
// 修改map中本组件的值
this.componentMap.set(index, isValid);
// 统计map中所有的值,并向外弹射
let isValidResult = true;
// 只要有一个是false,最终结果就是false
this.componentMap.forEach((_isValid) => {
if (_isValid === false) {
isValidResult = false;
}
});
// 弹射
this.isValid.emit(isValidResult);
})
这样数据就到了第一层,只发送了一个boolean数据。
三、补全第一层对于第二层传值的处理
在第一层增加对第二层的订阅,也就是在对于第二层组件的引用上增加(isValid)="isValidChange($event)"
,并实现这个方法。
// 第一层组件的HTML
<app-form-item [formItems]="formItems" (isValid)="isValidChange($event)" [type]="'edit'" [formItemValues]="formItemValues"></app-form-item>
// 第一层组件的TS
/**
* 当子组件弹射出表单项是否合法时的行为
* @param isValid
*/
isValidChange(isValid: boolean) {
console.log(isValid);
}
再次测试,其他层已经不再有输出语句,因此只会显示True或False。
全部选择后,将会变成true,其他情况都是false:
还差最后一步,就是让变化的isValid作用到按钮上:
/**
* 当子组件弹射出表单项是否合法时的行为
* @param isValid
*/
isValidChange(isValid: boolean) {
// 在变化时取isValid和当前formControl的交集,从而改变按钮激活状态
this.submitButtonActive = isValid && this.formGroup.valid;
}
终于大功告成!
四、总结
本文基于Angular动态组件,所以需要有对于动态组件的了解作为前置条件。
核心内容就是组件间传值@Input @Output的使用方法,以及创建动态组件时,如何调用子组件属性的问题,在此整理一下。
创建动态组件时,父组件只能获取到子组件的引用componentRef
,而不能像静态组件间传值那样看到子组件的HTML代码,因此不能用类似[formItems]="formItems" (isValid)="isValidChange($event)"
的写法去绑定输入输出。
而是这样用:
对于子组件的@input属性,用这种写法:
// 对应子组件的input
componentRef.instance.index = i;
对于子组件的@output弹射器,用这种订阅的写法:
// 对应子组件的output
componentRef.instance.isValid.subscribe(({index, isValid}) => {
console.log(index);
console.log(isValid);
})
掌握这些核心内容后,动态组件的校验就和普通静态父子组件传值的难度差不多了,只不过有很多数据经过多层传递才能到达,看上去略显麻烦。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。