【发布时间】:2020-02-28 14:47:17
【问题描述】:
假设我有一个非常简单的编辑器组件。没什么复杂的,只是一个输入字段,也可以从外部源加载一些数据:
@Component({
selector: 'my-editor',
template: `<input [disabled]="loading" [(ngModel)]="text">`
})
class EditorComponent {
loading: boolean = false;
text: string;
load(item: Observable<string>) {
this.loading = true;
item.subscribe(value => {
this.text = value;
this.loading = false;
});
}
}
除了从外部源加载一些数据时输入被禁用,因为加载后任何数据都会被丢弃,我们不想让用户措手不及。
然后在某些情况下,我们有一些模板可供用户选择。也没有什么复杂的,看起来像这样:
@Component({
selector: 'my-template-manager',
template: `<button [disabled]="disabled">Dummy</button>`
})
class TemplateManagerComponent implements OnInit {
@Input()
disabled: boolean = false;
@Output()
load = new EventEmitter<Observable<string>>();
constructor(private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.load.emit(of('initial value').pipe(delay(1000)));
}
}
也是非常简单的组件,它并没有做太多事情。当它启动时,它将从某个 HTTP 服务加载默认模板(此处由 delay 模拟),否则,如果用户点击按钮,则会加载不同的模板(为简单起见,此处未实现)。
现在我想同时使用这两个组件,这也很简单:
@Component({
template: `
<my-template-manager [disabled]="editor.loading" (load)="editor.load($event)"></my-template-manager>
<my-editor #editor></my-editor>
`
})
class ParentComponent {
}
这只是这些组件的一些粘合剂。所以我有小型轻量级和可测试的组件,它们的 API 很好地定义了它们,可以像这样粘合在一起。但是,现在我遇到了麻烦:当编辑器加载某些内容时,我还希望禁用模板管理器。但愚蠢的我:我刚刚在这两个组件之间创建了双向数据流:在onInit-Method 中,模板管理器发出一个事件,该事件以禁用状态的形式返回到模板管理器,并且角度非常好通知我们,在ExpressionChangedAfterItHasBeenCheckedError 的帮助下,这确实是一种不好的做法。 (要更深入地了解正在发生的事情,请参阅:https://stackoverflow.com/a/44691880)。另外,我不能只离开[disabled]-binding,因为loading-state 可能会因不同的情况而被触发...
有一些变通方法,例如setTimeout 或Promise.resolve().then() 或new EventEmitter(true)。但是,例如Maxim Koretskyi says:
我不建议使用它们,而是重新设计您的应用程序
我倾向于同意。至少异步包装事件只是“让它工作”感觉很尴尬。单独查看这两个组件(编辑器和模板管理器)看起来也不错,不是吗?毕竟,将代码分离到多个组件中的目的还包括它们可以单独开发、审查和测试,并且通过添加这样的包装,我承认事实并非如此。所以我在设计交互的方式上可能犯了一个大错误。
如何根据角度设计原则设计此处所示的交互?
【问题讨论】:
-
Stackblitz demo 原问题
标签: angular angular-components