Increment/Decerement buttons "the Angular way"

5,253 views
Skip to first unread message

Chris Nicola

unread,
Sep 11, 2013, 3:30:41 PM9/11/13
to ang...@googlegroups.com
This seems like something that should be incredibly simple and yet I've found it exceedingly challenging to do. What I was is simple, a directive "incrementors" that adds two buttons +/- to a number (or text) field type and will try to increment or decrement the model value by the value in the "step" attribute. Other requirement is that -/+ should be disabled if the value is less/greater than min/max attribute values

The problem is there is no simple way to approach this. Here is what we've done first. Use a template for this which effectively replaces the existing input placing the ng-model binding as a parent of the input field in the template. We have to use an isolate scope binding like scope: { ngModel: '=' } to bind the other ng-model binding to an inner one. I've found this is suboptimal though as now all the validation is on a new ngModelController and not on the original one so min/max validation states on our buttons now becomes an additional chore as well as getting and setting values through the directive.

The "angular" way in my mind would be to build a new input type using this directive which effectively replaces it and does all the DOM manipulation and event handling for itself, but this seems like a lot of redundant work as we are basically using the same validation and code already in the built-in directives. Yet there seems to be no way to reuse and just extend the directives for `inputTypeText` and `inputTypeNumber` as part of a new directive.

Am I missing something, is there an easier way to approach this problem?

William Szeliga

unread,
Sep 11, 2013, 10:32:42 PM9/11/13
to ang...@googlegroups.com
Something like this:


Hope, this helps.

-Bill


--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular.
For more options, visit https://groups.google.com/groups/opt_out.

Witold Szczerba

unread,
Sep 12, 2013, 4:19:57 AM9/12/13
to ang...@googlegroups.com
Hi,
Sometimes it can be hard to reuse current directives, because there is
no simple way to make a custom directive delegate stuff to another.
The directive's controller is supposed to be shared and adapted in
different scenarios, unfortunately there is no way to ask directive X
to get controller of directive Y when X is at the same node (or child
node).

Fortunately, most of the time one can skip the problem by adjusting a
little bit the concept. In your case, I would write a directive and
apply it on the text input field, so you could get the controller of
the input directive. Example:
<input type=text ng-model=something inc-buttons><button
up>+</button><button down>-</button>

OK, now we have to write a inc-button directive. This directive is
operating on the input element, so you can ask for controller. You
also want to attach actions to two buttons. Since you know the buttons
are next to the input, you can find them easily:

elem.next('button[up]').click(function() {
scope.$apply(function() { ..... });
});

elem.next('button[down]').click(function() {
scope.$apply(function() { ..... });
});

Another option would be to create a container element with directive
with controller like this:
<div inc-buttons>
<input type=text ng-model=something inc-button><button
inc="+">+</button><button inc="-">-</button>
</div>

Now, in you have to write
inc-buttons directive with a directive controller,
inc-button and inc directives which will ask for controller created by
"inc-buttons". This controller would act as a common communication
channel for between inc-button and inc directives.

I think the first approach requires less code but forces you to assume
where are the + and - buttons are.


How do you find those approaches?

Regards,
Witold Szczerba

Chris Nicola

unread,
Sep 12, 2013, 3:07:10 PM9/12/13
to ang...@googlegroups.com
William my component includes an input field as well so the number can be typed which added some complexity. I also wanted to keep the existing field validation behaviour from the built in directives.

Thanks for the suggestions though I've tried a lot of approaches now and spent a lot of time looking at existing directives and how ngModelController and the ngFormController work. Ended up with a couple of different approaches to this. Curious if people think this is "the Angular way" though.

## Solutions

For my "incrementor" directive I ended up using a full template to replace the existing input control which used an isolate scope and a { model: '=ngModel' } type binding as well as '@' bindings for min, max and step. Internally the template contains it's own number field which is bound through. To handle validation and state changes I used the `ngForm` directive on the root div in the template and through the scope and naming I could access and use all the internal state. Where I think I got tripped up was not truly treating this isolated scope directive as isolated. I was trying to "require" the ngModel controller from the original input element, but really it was just about using the '=' binding and then doing everything else in the isolated state of the directive.

I think there are a couple other ways this could have been done overriding the ngModelController.$render() and .$setViewValue() methods is one other way I might have considered, but I think this was simpler.

We also have another directive which takes a number field but formats it as a "pretty printed" currency value. We wanted it to work just like a number other than formatting though which didn't work quite as we expected. This was due both to the way browsers don't like you putting non numeric characters in number inputs as well as AngularJS' `element.val()` function not allowing non numeric characters if the element is a number field. We hacked around this by just using `element.attr('type', 'text') in our directives linking function. So basically it starts as a number field to load Angular's formatters and parsers and then we switch to a text field and and our custom formatting/parsing for the currency format on top. This worked like a charm (it did require us to also override $setViewValue).

## Suggestions/Thoughts

The need to "fake" out Angular on the number input makes me think that Angular could use some more ways to use these directives. Say a redundant attribute like `ng-input-type="number"` so that this extra hack isn't needed. It would also be interesting if directives could "extend" or automatically apply other directives somehow, not just "require" their controllers.

We'll try to post both of these directives soon to get some feedback on the "Angularness" of them.

Tony pee

unread,
Sep 12, 2013, 7:27:07 PM9/12/13
to ang...@googlegroups.com
do you have plunkr examples of your solutions?


--
You received this message because you are subscribed to the Google Groups "AngularJS" group.
To unsubscribe from this group and stop receiving emails from it, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at http://groups.google.com/group/angular.
For more options, visit https://groups.google.com/groups/opt_out.



--
Tony Polinelli

Reply all
Reply to author
Forward
0 new messages