Every “control” in Angular has EventEmitters called statusChanges and valueChanges . The former is whether a control is “valid” so will emit an event every time a control changes from invalid to valid or vice versa. valueChanges is when the actual control’s “value” changes. In most cases this is going to be a form element so it will be the value of the textbox, checkbox, input etc. In a custom component’s case, if you implement ControlValueAccessor, then it’s going to be the “value” of your control as bound by the ngModel.
In anycase, I came across an interesting scenario in which I needed to “subscribe” to changes when a control was “touched”. To my surprise, there is currently no event that is emitted when a control is touched – or when it’s status of pristine/dirty is changed. It’s actually pretty bonkers because typically when showing error messages on a form in Angular, you will make liberal use of the “IsDirty” or “IsTouched” properties. So being able to “listen” for them seems pretty important. Ugh. Time to Monkey Patch.
What Is Monkey Patching?
Monkey Patching is a technique to change or modify the default behaviour of code at runtime when you don’t have access to the original code. In our case, we want to be “notified” when “markAsTouched” is called. To do so, we need to modify the actual method of “markAsTouched” to emit an event/run a custom piece of code *as well* as do it’s regular method.
If you are interested in reading more about Monkey Patching, there is a great article (In Javascript no less) on the subject here : audero.it/blog/2016/12/05/monkey-patching-javascript/
Monkey Patching MaskAsTouched/Pristine/Dirty
To make this work, the first thing you need is a reference to the *control* that you are trying to listen for the touched event on. This could be done using ViewChild if you are looking for a child control on the page. To get the “self” control (e.g. in a custom component), a common pattern is to modify your constructor to inject in the “Injector” class which you can use to get the NGControl instance of yourself. You need to do this in the ngAfterViewInit method.
constructor(private injector: Injector) { }
ngAfterViewInit()
{
let ngControl : NgControl = this.injector.get(NgControl, null);
}
OK so we have the reference to the control. Time to patch things up!
let self = this;
let originalMethod = this.control.markAsTouched;
this.control.markAsTouched = function () {
originalMethod.apply(this, arguments);
//Extra code needed here.
}
Let’s walk through this code.
- First we get a reference to ourselves. This isn’t necessarily required, but it’s highly likely we may want to refer to local variables/properties, and inside our function we lose scope of “this”.
- We store the original “markAsTouched” method from our control reference. This line will obviously differ slightly on how you are referencing the control you want to patch.
- We then set the markAsTouched method to a new function, that then itself runs the original method (e.g. Run Angular’s standard “markAsTouched”).
- We can then run our extra code as required. This may be to raise an event on an EventEmitter, set another variable, run a method, whatever!
In my case, when my custom component was touched, I wanted to then manually “touch” another child component. So my full code looked like :
let self = this;
let originalMethod = this.control.markAsTouched;
this.control.markAsTouched = function () {
originalMethod.apply(this, arguments);
self.bankDetailsForm.form.markAllAsTouched();
}
We can use this method to do the same for markAsPristine, markAsDirty, or actually any other method that Angular for unknown reasons doesn’t give us an event for.
Beware Infinite Loops
I would suggest when you do your first monkey patch, be liberal with your console.log statements. I found pretty quickly when trying to touch another control from within my monkey patched markAsTouched method, that I created an infinite chain of touching. It’s super easy to do so you’ll need to be extra careful.
Comments
Thank you very much for this!!! After 7 hours trying to find a solution to the absense of onTouch$ in FormControls, this solves my issue, now I can trigger a side-effect within this monkey patch 😀