Let's say I have a very simple editor component. Nothing to complicated, just an input field that can also load some data from an external source:
@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;
});
}
}
Besides the input is disabled when it is loading some data from an external source, because after loading any data would be discarded and we don't want to catch the user off-guard.
Then in some contexts we have some templates that the user can choose from. Nothing to complicated either, looks like this:
@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)));
}
}
Also quite simple component, it isn't doing to much. When it kicks into action it will load a default template from some HTTP-Service (here simulated by a delay
) and otherwise if the user hits a button a different template is loaded (not implemented here for simplicity).
Now I want to use those two components together and also that is quite simple:
@Component({
template: `
<my-template-manager [disabled]="editor.loading" (load)="editor.load($event)"></my-template-manager>
<my-editor #editor></my-editor>
`
})
class ParentComponent {
}
That is just some glue for those components. So I have small lightweight and testable components nicely defined by their API, that can be glued together like this. However, now I got myself into trouble: When the editor is loading something, I also want the template manager to be disabled. But stupid me: I just created bi-directional data flow between those two components: In the onInit
-Method the template manager emits an event which comes back to the template manager in the form of the disabled state and angular is so kind to inform us, that this is really bad practice with the help of the ExpressionChangedAfterItHasBeenCheckedError
. (For a deeper understanding of what is happening see: https://stackoverflow.com/a/44691880). Also I can't just leave away the [disabled]
-binding, because the loading
-state might be triggered due to different circumstances...
There are workarounds, such as setTimeout
or Promise.resolve().then()
or new EventEmitter(true)
. But so e.g. Maxim Koretskyi says:
I don’t recommend using them but rather redesign your application
and I tend to agree. At least it feels awkward to just wrap the event asynchronously just "to make it work". Also the two components (editor and template manager) look fine by looking at them individually, don't they? And after all the purpose of separating the code in multiple components also includes that they can be developed, reviewed and tested separately and by adding such a wrapping I would acknowledge that this not the case. So I probably made a big mistake designing the interaction the way I did.
How to design such interactions as shown here according to the angular design principles?