AngularJS: $doCheck hook for textarea character counter

Radek Anuszewski
5 min readApr 1, 2017

--

Textarea with character count is much more user-friendly, and it’s easy to do with Angular component $doCheck hook with some native HTML DOM operations. If you are impatient check out my Plunk here, but read this post if you also want to know not only how, but also why.

We forgot how to write pure JS code, without framework — but sometimes we have to go back to the basics. Photo credits: Unsplash.

Update 2017–04–03:

This post and my Plunker had some weird error — when I tried to fake asynchronous data loading with with $q.when(), it does not calculate length properly (see my SO question). Now, I add a $timeout to force Angulardigest and it works — it looks like an assignment inside $doCheck does not trigger digest itself. So, code it this article has been updated to use $timeout.

Textarea component with character count — the “why”

Ability to modify provided value fast

It’s easy to imagine. Your application user needs to migrate item descriptions from paper notebook to your system. You set database constraint — item description can’t be longer than 150 characters. But in notebook, many of items have description longer that your constraint, for example 10% of them have 200 characters and 30% have between 160 and 180 characters. And even if you provide client-side validation which is faster than backend validation (no HTTP request needed), your users will go crazy — they have to click submit button, they see validation message, then remove some words (it’s important to remove as little as possible, cause description needs to be comprehensive), then submit again, and again, and again… Even if you provide validation on input change, which requires one click less than on for submit, user still doesn’t know if value is valid and has to remove words one by one.

Character counter — the “how”

Not as easy as it seems to be

If you survive JS framework proliferation (also known as Javascript fatigue) and you know how to use native Javascript/DOM APIs (if you don’t, it’s time to go back to the basics), you will make this feature within few minutes: get element with document.querySelector() (really, it’s time to forget getElementBy friends), take its value and display somewhere, with known maximum length of input.
But, in Angular you want to follow Angular way. Without pure DOM operation, without vanilla bindings to onchange-like event which are outside from Angular eco-system and are not visible from controllers (for example, Angular’s digest won’t run for this bindings). So, how to do it and stay clean in the same time?

Not as hard as it seems to be — $doCheck to the rescue!

Bad news is, and I was pretty surprised when I found it, that Angular sets model value to undefined when value is invalid. Why it’s bad? You can’t rely on value which is longer than maxlength and you can not display its length. Don’t believe me? Check my Plunk: Simple Angular textarea component with characters counter, made with $doCheck component hook which summarize this post, provide too long value and you will see that current description value is not displayed — cause it’s undefined.
But the good news is, that even invalid, it’s still displayed. So you can make a DOM operation and check value length:

$ctrl.$onInit = function () {
$ctrl.elementId = $ctrl.name + '_' + new Date().getTime();
};
var domElement = $document[0].querySelector('#' + $ctrl.elementId;
$ctrl.currentLength = domElement.value.length;

$onInit component hook is useful to create unique ID cause every bindings are ready (beginning from Angular 1.6, they are not ready before $onInit/first $onChanges) and name is unlikely to change in future. But still you don’t know where and how to call it.
ng-changeexpression? No, cause value in ng-model is undefined. But there is quite fresh hook: $doCheck. What is important, it runs on every digest cycle and it perfectly meets our requirements:

var domElement = undefined;
$ctrl.$doCheck = function () {
if (!domElement) {
if (!$ctrl.elementId) {
return;
}
domElement = $document[0].querySelector(‘#’ + $ctrl.elementId);
if (!domElement) {
return;
}
}
var currentLength = domElement.value.length;
if (currentLength !== $ctrl.currentLength) {
$timeout(function _forceDigestToDisplayDataOnView () {
$ctrl.currentLength = currentLength;
});
}
};

Implementation may look a little bit weird at first. But, it has a sense. domElement is introduced as variable, to be retrieved from a DOM only once. HTML DOM operations are always heavy, and with combination with $doCheck hook which runs very often it may make application slow. And it’s presence needs to be checked twice — first, to check if it were retrieved from DOM at all. Second, when it’s retrieved, if HTML DOM is not ready ($doCheck could run before HTML for directive is built) and element does not exists, which results with null from querySelector. You also have to check if elementId is built, cause $doCheck can run before $onInit. I also added additional check if value length changes from last fire, to unnecessary assignments. That’s all!

But hey, what about $timeout!?

$timeout makes app slower, $timeout forces digest which may be unneeded… Yes, you are right. But, what if you use $q.when() to load hard-coded data asynchronously? It looks like assignment inside $doCheck does not trigger digest itself, so — it’s a necessary evil to make it work. Of course, if you know a better solution — do not hesitate to reply a comment! I will be pleased if I can learn a new thing about AngularJS.

Conclusion

At first, $doCheck looks useless. But it has its place in universe, like everything in Angular world. If you found any problems with $doCheck solution to problem, or you know better solution — feel free to comment to give me a possibility to learn new things!

--

--

Radek Anuszewski

Software developer, frontend developer in AltConnect.pl, mostly playing with ReactJS, AngularJS and, recently, BackboneJS / MarionetteJS.