ng-zorro-antd
ng-zorro-antd copied to clipboard
NzModalService提供input双向绑定
What problem does this feature solve?
16.0.0之前有NzComponentParams,实现了input的绑定,16.1.0后移除了, 导致组件必须绑定一个Nz_Modal_Data,但很多业务组件是复用的,有时候是弹窗 有时候就直接展示,加入Nz_Modal_Data,破坏了可读性 需要大面积维护业务组件
What does the proposed API look like?
和以前的nzComponentParams类似或者angular 14以后的setInput
Translation of this issue:
NZMODALVICE provides input two -way binding
What PROBLEM DOES This Feature Solve?
16.0.0之前有NzComponentParams,实现了input的绑定,16.1.0后移除了, 导致组织必须绑定一个Nz_Modal_Data,但很多业务组件是复用的,有时候是弹窗 有时候就直接展示,加入Nz_Modal_Data,破坏了可读性 需要大面积重载
What does the proposed api look like?
Similar to the previous nzcomponentparams or Setinput after Angular 14
Any progress?
请参考这个例子的最佳实践:https://github.com/NG-ZORRO/ng-zorro-antd/issues/7827#issuecomment-1640833742
NzComponentParams已在15.1.0标记为已弃用,16.0.0和16.1.0区别不大,可在16.0.0将modal改造好了再升级16.1.0
未来有任何规划吗?以后的版本会提供setInput吗?这个例子明显不如原先方便,如果只是为了遵循某些原则,反而write more do same,我不觉得这是个好的更新,我们所希望的是write less do more, 而且我相信很多人并不会想在原先的组件中加入这段 不是必要的 inject 和这个封装组件,对于业务开发者 而言,很鸡肋
Any progress?
Any update in the future?
一些参考: ComponentInputBinding:https://angular.io/api/router/withComponentInputBinding ngComponentOutletInputs:https://angular.io/api/common/NgComponentOutlet
请参考这个例子的最佳实践:#7827 (comment)
NzComponentParams已在15.1.0标记为已弃用,16.0.0和16.1.0区别不大,可在16.0.0将modal改造好了再升级16.1.0
同一个组件可以直接用 drawerService 直接创建,并在 nzContentParams 设置 input 参数。 而现在想要创建一个模态框,要么使用模板的方法,或者是按照给出的范例,新增一个包裹用的组件。
这个例子用一个额外的组件包裹后,也只是处理了input部分的内容,如果原先的组件还有output,那么包裹组件也需要针对每个事件重写一遍样板代码,把他们重新抛出,有些过于繁琐了。
请参考这个例子的最佳实践:#7827 (comment) NzComponentParams已在15.1.0标记为已弃用,16.0.0和16.1.0区别不大,可在16.0.0将modal改造好了再升级16.1.0
同一个组件可以直接用 drawerService 直接创建,并在 nzContentParams 设置 input 参数。 而现在想要创建一个模态框,要么使用模板的方法,或者是按照给出的范例,新增一个包裹用的组件。
这个例子用一个额外的组件包裹后,也只是处理了input部分的内容,如果原先的组件还有output,那么包裹组件也需要针对每个事件重写一遍样板代码,把他们重新抛出,有些过于繁琐了。
支持,一个编辑页面可以通过modal打开,也可通过drawer打开。现在modal组件里移除了NzComponentParams,但drawer组件里还是用原来的nzContentParams方式,这很难办
Any progress?
一些参考: ComponentInputBinding:https://angular.io/api/router/withComponentInputBinding ngComponentOutletInputs:https://angular.io/api/common/NgComponentOutlet
首先确认的一点是,即使有了withComponentInputBinding
、NgComponentOutlet
和setInput
,对于modal类型的组件,也是不适用的。
因为这两个方法本质是对Component
进行数据赋值操作,但是在需要open的modal时,最佳实践是使用官方CDK的Overlay为基础开发的各种modal,dialog,而非是new ComponentI()
以及在template中使用,所以需要将modal所需的数据,在父组件中传递给需要modal的组件。
上述两个API是在Component
创建时,将值传递给Component
的Key上,在创建后不建议也不要去直接修改Component
的值,因为Component
不能感知到值的变化,从而会导致一些莫名其妙的问题,以及导致增加开发人员的心智负担,当然你可以使用各种方法去解决检测值的问题,但这不是这里需要解决的问题。类似的场景是官方的CdkTable,如果dataSource
传递的是一个Array
而非Observable
或者Promise
,你是需要再改变Array
后,手动调用renderRows
ComponentRef
中的setInput
可以解决父组件对已创建的子组件进行变更检测的问题,但是对于Overlay
的场景不适用,因为在打开了Overlay
之后,基本上不建议对其组件进行任何操作(这里指的是Overlay
创建的组件,而非OverlayRef
,动态调整Overlay
窗口大小、位置是OverlayRef
的范畴),多个组件需要同一个数据源的话,建议是使用Service
,provide
到“父组件”即可,无需provide
到root
现在回到主题,再来说说为什么不将modal data赋值到Component
中
1.很重要的一点就是类型问题
因为现在ng使用的是装饰器将Component
的key标记为可Input的key,但是ts没办法区分哪个值“被装饰”了,所以modal在data提供的keyof Component
,实际上是不安全的,因为你可能会无意间设置了其他非Input
的属性。
在未来的信号组件中,也许这个问题可以消失,因为信号的类型是InputSignal<T>
,可以过滤出来可以Input
的值
但我觉得NzComponentParams
基本不会复原,原因是即使信号组件推出了,那么原本的组件也是处于一个并行时期的,从大局上看,为了照顾到所有类型的组件以及API DX与官方保持一致的角度上看,放弃这种方式是没问题的。
我听说以后可能会有Fcuntion Component
,到那时,这个问题将不是问题 : ),也许也变成类React了也说不定
2.无法在
constructor
使用
因为Input
的值是需要等到ngOnInit
后,才会被赋值,这会导致几个问题:
需要强制id!: number
值的类型,如果没传值,或者没在正确的生命周期里使用,会使在使用过程中不容易发现这里实际上是可能为undefined
即id?: number
。如果标记了undefined
,这样使用会在后续业务编写过程中,不断需要去写判断这个值是否为undefined
,如果麻烦了,你还会强制断言,导致ts的优势在写业务过程中全无。举个例子:
class Modal{
id: number = Number()
}
@Component({
...
template: `<p *ngFor="let item of dataSource | async">{{ item.id }}</p>`
})
export class MyComponent {
@Input() id?: number
dataSource: Observable<Modal[]>
constructor(){
this.dataSource = fromFetch('...', {
body: {
// 这样不行因为现在还是``undefined``
// 所以你需要将``fromFetch``写到``ngOnInit``
// 但如果这样,那么``dataSource``的类型就变成了``dataSource?: Observable<Modal[]>``
// 在严格模版类型中是会报错的,``dataSource``是``Observable<Modal[]> | undefined``所以模版要加多一层``ngIf``
// `<div *ngIf="dataSource"><p *ngFor="let item of dataSource | async">{{ item.id }}</p></div>`
id: this.id,
}
})
}
onSave(): void{
const modal = new Modal()
modal.id = this.id // 这样会报错
// 你只能这样
modal.id = this.id as unknown as number
// 或者这样
modal.id = this.id as any
// 又或者写多一个if
if (typeof this.id !== 'number'){
return
}
// 参数变多,后果可想而知,我可是经历过的。
// 如果你全用any,那么上游数据改变了,你又如何知道那些地方用了呢?重构复杂度层层攀升,心智负担越来越重
}
}
这样的体验是从上贯穿到下的,很难受很不符合逻辑
关于这个PR:refactor/nz-contentParams-to-injection,将需要Overlay打开组件的逻辑统一化,这样没问题。
如果官方有了什么新的最佳实践,可以继续回复,我持续保持关注。
暂时想到这么多,后面再想到啥我再补充.
感谢你的回应。我也想分享一些建议,首先,我希望你理解为什么我们希望进行还原:
- 在Angular CDK中,一些组件相对来说较为原生,但缺乏足够多的组件支持,因此在应对各种复杂业务场景时,可能需要寻找第三方解决方案或自行开发组件。Ant Design (antd)则提供了更多成熟和稳定的组件,这些组件通常可以满足绝大多数需求,减少了手动开发的工作量。
- Angular官方关于最佳实践的宣传可能要求较高的开发经验,而实际情况中,不是所有小型团队都能严格遵循这些最佳实践。很多人更希望能够快速上手,迅速进行开发和部署。
- 我个人认为,使用nzComponentParams进行参数传递相较于Angular CDK的方式更简洁且逻辑清晰。这种方式让代码变得更精简但功能更丰富。
- 使用nzComponentParams使组件不必关心参数的传递方式,无论是通过输入属性还是弹窗展示,开发者只需关注组件内部的业务逻辑。
此外,关于您提到的几点观点:
- 我认为新版本中的信号组件用起来效果有限,适用场景有限,理解难度较大,组件内使用体验不佳。
- 大部分情况下,我这种小团队更多地使用ngOnInit而不是构造函数进行初始化。
- 对于大部分非弹窗组件,输入属性通常在ngOnInit中进行初始化,这也是许多小型团队的实践。
- 我赞同在输入属性声明中使用id!: number而不是id?: number,并且在实际项目中也采用了这种做法,避免了您提到的参数书写判断的情况。
- 对于可能为undefined的情况,我认为大部分小型团队都有测试人员或开发者自己编写简单的单元测试,因此解决undefined初始化问题的成本相对较低。
最后,我们提到新项目已经采用了推荐的写法,而老项目则采用了 (NzModalService as any).prototype.attachModalContent 的方法中注入Object.assign(<{}>modalRef.componentInstance, config.nzData);,不推荐这样子搞,主要是为了省事,二十行和几百行还是区别很大的,而且对现有复用的组件业务是否有二次影响,这个也是不确定的,等于还得投入新的测试人力进去,你可能从开发和官方的推荐角度去考虑问题,而我是从自身小团队的角度出发,我也希望antd变得更好,可以更快速的帮助我们小团队去开发交付,当然我们交付的也会不停地去更新,除非真的团队挂了或者业务需要不在维护了。
@nankingcigar
感谢你的回复。
我想先回答你的提出的一些观点
Angular官方关于最佳实践的宣传可能要求较高的开发经验,而实际情况中,不是所有小型团队都能严格遵循这些最佳实践。很多人更希望能够快速上手,迅速进行开发和部署。
这点实际上是不存在的,因为难以上手的是Angular,而非相关组件库。
官方CDK是一个Angular使用的最佳实践,它与Angular同步更新,所以我建议ng-zorro能够与CDK的DX保持高度一致,其次再是拓展,这样可以保证Angular的用户体验从官方到第三方都能保持高度一致
使用nzComponentParams使组件不必关心参数的传递方式,无论是通过输入属性还是弹窗展示,开发者只需关注组件内部的业务逻辑。
如果真的需要nzComponentParams
方便和其他地方复用,其实nz-modal
支持以组件的方式进行打开的
ts <nz-modal [(nzVisible)]="isVisible" nzTitle="This's my modal" (nzOnCancel)="handleCancel()" (nzOnOk)="handleOk()"> <ng-container *nzModalContent> <my-component [id]="modal.id" [name]="modal.name" /> </ng-container> </nz-modal>
我认为新版本中的信号组件用起来效果有限,适用场景有限,理解难度较大,组件内使用体验不佳
信号是Angular未来的响应式解决方案,所以信号是未来nz-zorro考虑的一个因素
大部分情况下,我这种小团队更多地使用ngOnInit而不是构造函数进行初始化 对于大部分非弹窗组件,输入属性通常在ngOnInit中进行初始化,这也是许多小型团队的实践
目前Angular作为使用Class为组件的框架,constructor
是不可忽视的一个点,constructor
用于在Class初始化前,对Class内部的参数进行设置
我赞同在输入属性声明中使用id!: number而不是id?: number,并且在实际项目中也采用了这种做法,避免了你提到的参数书写判断的情况。 对于可能为undefined的情况,我认为大部分小型团队都有测试人员或开发者自己编写简单的单元测试,因此解决undefined初始化问题的成本相对较低。
这样的做法实际上是“掩耳盗铃”,并不是报错不存在这种可能性,并且这会令重构造成一定程度的困难也失去了使用ts的优势。
在modal,drawer等需要服务打开的组件中,可以传递静态的data
用来初始化modal所打开的组件,在该组件内的constructor
进行数据初始化,这样可以避免这些问题,以及可以充分利用依赖注入的特性
但是这并不是你“一定”要断言的理由。我经历过这种强制断言的项目,所以我知道之间的痛苦。因为Angular目前使用装饰器,还没有input
的Function,所以在这方面不能得到很好的支持,所以你可以期待一下11月份更新的Angular V17。在V17里,Angular团队实现了纯信号组件,以及input
Function。
很开心你说到新项目已经采用了新的实践方式,同时我也不太建议你升级旧项目的nz-zorro版本,因为你需要考量一下升级成本,16.0.0
版本支持两种数据传递的方式,可以日后将所有代码修改过后再升级。
我表明一下我的立场,我目前也是站在开发者的角度思考API如何设计,对于如何避免实践项目中踩过的坑也是需要考量,所以我认为框架应该避免设计一些可能会导致开发者错用、滥用的一些API,nzComponentParams
是其中一个。
我说个私心的,如果你是为了快速交付的话,Angular可能并不是很适合你现在所处的项目。Angular的项目对于工程化设计得比较讲究,如果不能潜心研究的话,对于快速交付来说,那是致命的。但是如果你或者你的团队对Angular熟能生巧的话,那肯定没有任何问题。
最后还是谢谢的你反馈。
-
无论是 withComponentInputBinding、NgComponentOutlet,还是 Overlay,本质上都是动态创建组件,并向其传递参数,setInput API 完全可以适用于它们。
-
没有任何实践指南不建议我们修改 overlay 组件的参数,并且 material/dialog 为了做到这一点,选择将 componentRef 暴露给开发者,让开发者可以自己使用 setInput,参考:https://github.com/angular/components/issues/27776
-
个人并不认为 material/dialog 使用 DI 来传递数据仍然是最佳实践,因为如今我们有了 setInput API,它比使用 DI 更方便,更符合人体工学和组件设计。
-
对于类型安全问题,我认为不太需要在这里过度强调,因为官方已经做出了类型不那么安全的功能(withComponentInputBinding、NgComponentOutlet),但这些功能却极大的提高了 DX。虽然 setInput 类型不安全,但它具有一定的运行时安全,如果设置了不存在的 input,会抛出异常。
-
尽管使用 DI 的方式增加了一种传递类型的办法,但这仍然是非类型安全的,因为 DATA 类型并不强制与 overlay 组件绑定,这也是一种“掩耳盗铃”,用官方的话来说,这本质上是无用的类型断言,虽然官方提出了一个可能的解决办法,但从未实施它,参考:https://github.com/angular/components/issues/23985
-
对于无法在 constructor 中使用的问题,我想这并不是我们现在要考虑的,因为这就是 Angular 组件目前的状态,没有人会因为使用 Input 要手动进行类型断言而不使用它。长远来看,我们即将拥有
id = input()
SignalInput API,这不再是问题。
@HyperLife1119
是的,这些问题没有一个最优解,也是各种骚操作,唯一能做的只能是做好类型编写。
withComponentInputBinding
事实上我做过反对,但没卵用hhhhhh,当时还没有信号这个东西 https://github.com/angular/angular/pull/49633#issuecomment-1498209921
ComponentRef
的setInput
是一个ok的做法,它的效果和Input
是一样的,但这不是问题,问题是在open中,ts无法提供可Input的值,我也知道很繁琐每次都要写open<C, D, R>(C)
,可目前来说,保证完善类型的情况下,最佳的写法就是这样了要是有办法可以直接open(C)
我就能知道需要Input啥,Output啥,那肯定最好不过了。
以我为例,我经历过“掩耳盗铃”的ts项目,也有严格完善类型的项目,后者对于开发人员日后维护、重构都起到了良好的作用,前者的话仁者见仁吧,但难看确实难看。
关于constructor
,我想说,在class组件中还是很有必要考虑的,这毕竟是class本身的设计,除非将来使用了Function Component
,但你想Function Component
中的Function
,不就是constructor
:),目前我全部使用了inject
,基本上很少使用constructor
了,但不能因为这样,而放弃了constructor
的使用,因为在一些时候,它是很有用的。
目前我是很期待InputSignal
的,我现在的项目全是以Signal实现了,就等着11月转为纯信号。
事已至此,我想我们真的需要寻求 NG-ZORRO 团队成员的帮助,由他们综合考虑为我们选择一个合适的方案 :) @hsuanxyz @OriginRing @Nicoss54
值得注意的是,在 Signal 方面,Angular v17 只是其转为稳定 API,但尚未实现 InputSignal 和 细粒度变更检查 :)
那要等明年了,哎,要是Function Component的话,那事情解决就容易多了
- 无论是 withComponentInputBinding、NgComponentOutlet,还是 Overlay,本质上都是动态创建组件,并向其传递参数,setInput API 完全可以适用于它们。
- 没有任何实践指南不建议我们修改 overlay 组件的参数,并且 material/dialog 为了做到这一点,选择将 componentRef 暴露给开发者,让开发者可以自己使用 setInput,参考:feat(Dialog): Allow passing in component @Inputs when opening a dialog angular/components#27776
- 个人并不认为 material/dialog 使用 DI 来传递数据仍然是最佳实践,因为如今我们有了 setInput API,它比使用 DI 更方便,更符合人体工学和组件设计。
- 对于类型安全问题,我认为不太需要在这里过度强调,因为官方已经做出了类型不那么安全的功能(withComponentInputBinding、NgComponentOutlet),但这些功能却极大的提高了 DX。虽然 setInput 类型不安全,但它具有一定的运行时安全,如果设置了不存在的 input,会抛出异常。
- 尽管使用 DI 的方式增加了一种传递类型的办法,但这仍然是非类型安全的,因为 DATA 类型并不强制与 overlay 组件绑定,这也是一种“掩耳盗铃”,用官方的话来说,这本质上是无用的类型断言,虽然官方提出了一个可能的解决办法,但从未实施它,参考:feat(dialog): Dialogs can define the type for their dialog data angular/components#23985
- 对于无法在 constructor 中使用的问题,我想这并不是我们现在要考虑的,因为这就是 Angular 组件目前的状态,没有人会因为使用 Input 要手动进行类型断言而不使用它。长远来看,我们即将拥有
id = input()
SignalInput API,这不再是问题。
赞同
- 无论是 withComponentInputBinding、NgComponentOutlet,还是 Overlay,本质上都是动态创建组件,并向其传递参数,setInput API 完全可以适用于它们。
- 没有任何实践指南不建议我们修改 overlay 组件的参数,并且 material/dialog 为了做到这一点,选择将 componentRef 暴露给开发者,让开发者可以自己使用 setInput,参考:feat(Dialog): Allow passing in component @Inputs when opening a dialog angular/components#27776
- 个人并不认为 material/dialog 使用 DI 来传递数据仍然是最佳实践,因为如今我们有了 setInput API,它比使用 DI 更方便,更符合人体工学和组件设计。
- 对于类型安全问题,我认为不太需要在这里过度强调,因为官方已经做出了类型不那么安全的功能(withComponentInputBinding、NgComponentOutlet),但这些功能却极大的提高了 DX。虽然 setInput 类型不安全,但它具有一定的运行时安全,如果设置了不存在的 input,会抛出异常。
- 尽管使用 DI 的方式增加了一种传递类型的办法,但这仍然是非类型安全的,因为 DATA 类型并不强制与 overlay 组件绑定,这也是一种“掩耳盗铃”,用官方的话来说,这本质上是无用的类型断言,虽然官方提出了一个可能的解决办法,但从未实施它,参考:feat(dialog): Dialogs can define the type for their dialog data angular/components#23985
- 对于无法在 constructor 中使用的问题,我想这并不是我们现在要考虑的,因为这就是 Angular 组件目前的状态,没有人会因为使用 Input 要手动进行类型断言而不使用它。长远来看,我们即将拥有
id = input()
SignalInput API,这不再是问题。
赞同
https://twitter.com/Jean__Meche/status/1718244500189954301
有开发者也是在constructor使用Input的值造成问题,这问题根本是class导致的,所以我希望设计组件的时候应该考虑constructor的使用场景
当然这个问题在以后的function component将不复存在
To facilitate communication, I added an English translation:
Whether it is withComponentInputBinding, NgComponentOutlet, or Overlay, they essentially dynamically create components and pass parameters to them, and the setInput API can be applied to them.
There is no practice guide that does not recommend that we modify the parameters of the overlay component, and in order to do this, material/dialog chooses to expose componentRef to developers so that developers can use setInput themselves. Reference: https://github.com/angular/components/issues/27776
Personally, I don't think it is still the best practice for material/dialog to use DI to pass data, because now we have the setInput API, which is more convenient and more ergonomic and component-friendly than using DI.
Regarding type safety issues, I don’t think it needs to be overemphasized here, because the official has already made less type-safe functions (withComponentInputBinding, NgComponentOutlet), but these functions have greatly improved DX. Although setInput is type-unsafe, it has certain runtime safety. If a non-existent input is set, an exception will be thrown.
Although using DI adds a way to pass types, it is still non-type safe, because the DATA type is not forced to be bound to the overlay component. This is also a kind of "covering the ears and stealing the bell". In official terms, this is essentially The above is a useless type assertion. Although the official proposed a possible solution, it was never implemented. Reference: https://github.com/angular/components/issues/23985
Regarding the problem of not being able to be used in the constructor, I think this is not what we have to consider now, because this is the current state of Angular components, no one will not use Input because it requires manual type assertion. In the long run, we will soon have the id = input()
SignalInput API and this will no longer be an issue.
+1 Components with signal inputs does not work with the current modal/popup