Second Experiment With Controlled Inputs (ala ReactJS) In Angular 2 Beta 11
Yesterday, I experimented with trying to create a ReactJS-inspired "controlled input" in Angular 2 Beta 11. The approach mostly worked; but, it was buggy, overly complicated, and typing fast had a noticeable lag to it. After trying a few alternate approaches, I think I finally came up with a controlled input approach that is very responsive and much more stable.
To recap the point of yesterday's post, a "controlled input," to borrow from the ReactJS vernacular, is an input that is completely driven by a one-way data flow. Out of the box, Angular 2 doesn't support one-way data flow for form inputs. So, to make inputs comply with a one-way data flow architecture, we have to create a custom directive that binds to the input and takes over management of the value binding.
My approach yesterday sort of worked. But, it was complicated and buggy; if you typed too fast, it would end up corrupting the text cursor location. In this new approach - outlined in this post - I start out by assuming that the value will be accepted by the calling context. In this way, I'm optimizing for the majority use-case and then taking extra steps to ensure that the minority use-case, in which the input changes are rejected by the calling context, is also upheld.
Another change that I made in this approach is that I'm tracking the text cursor location though its own event binding. This way, I don't have to try and calculate the location based on the input change - I just grab the exact location every time the user goes to interact with the active element.
The final change that I made was that I converted the output "valueChange" EventEmitter to be synchronous instead of asynchronous (the default behavior). This allows the calling context's event handler to be invoked synchronously instead of in the next tick of the application. This provides a more predictable flow of data, allowing the input value binding to be updated at a more useful point in the change detection life-cycle.
I think you'll find that these changes make the code significantly simpler (in addition to being much more stable and performant). In the following code, you'll see that I'm allowing the (input) changes to remain in place by default, only reverting the input rendering if that assumption turns out to be false later on in the change detection life-cycle.
As with the previous approach, I'm still heavily coupled to the Browser DOM (Document Object Model). Since the Renderer abstraction doesn't provide a way to access native element properties - only set them - it means that I can't use the Renderer to get the selectionStart text cursor value. This, in turn, means that I might was well bypass the Renderer abstraction entirely and just use the Browser DOM API.
Unfortunately, this demo can't be illustrated easily with a graphic, so I suggest running the demo yourself or watching the video.
As I stated in my last post, I don't know how much I actually care about "controlled inputs." In theory, it's nice to have a complete one-way data flow architecture. But, pragmatically speaking, when it comes to form input fields, I rarely have a need to override the user's input. Often times, it's just the opposite - I'll take whatever the user provides (using ngModel) and deal with it during the form processing. But, I do think this is a valuable exploration because it makes us think more deeply about one-way data flow, input bindings, and the change detection life-cycle being implemented by Angular 2 Beta 11.
Want to use code from this post? Check out the license.