---
title: Buttons with Hold Events in Angular.js
teaser: A nicely decoupled Angular UI component that can easily be reused across applications.
tags: web,javascript,angularjs
author: Sage Griffin
published_on: 2014-08-12
---

Creating an interaction with a simple button in Angular only requires adding the
[ngClick] directive. However, sometimes an on click style interaction isn't
sufficient.  Let's take a look at how we can have a button which performs an
action as long as it's pressed.

[ngClick]: https://docs.angularjs.org/api/ng/directive/ngClick

For the example, we'll use two buttons which can be used to zoom a camera in and
out. We want the camera to continue zooming, until the button is released. The
final effect will work like this:

[![Zooming in Martial Codex](https://images.thoughtbot.com/martial-codex/zoom-demo.gif)](https://www.martialcodex.com)

Our template might look something like this:

```html
<a href while-pressed="zoomOut()">
  <i class="fa fa-minus">_
</a>
<a href while-pressed="zoomIn()">
  <i class="fa fa-plus">_
</a>
```

We're making a subtle assumption with this interface. By adding the parenthesis,
we imply that `whilePressed` will behave similarly to `ngClick`. The given value
is an expression that will get evaluated continuously when the button is
pressed, rather than us handing it a function object for it to call. In
practice, we can use the `'&'` style of arguments in our directive to capture
the expression. You can find more information about the different styles of
scopes [here][directive-scope].

[directive-scope]: https://docs.angularjs.org/api/ng/service/$compile#-scope-

```coffeescript
whilePressed = ->
  restrict: "A"

  scope:
    whilePressed: '&'
```

## Binding the Events

When defining more complex interactions such as this one, Angular's built-in
directives won't give us the control we need. Instead, we'll fall back to manual
event binding on the element. For clarity, I tend prefer to separate the
callback function from the event bindings. Since we're manipulating the DOM, our
code will go into a [link function][link]. Our initial link function will look
like this:

[link]: https://docs.angularjs.org/guide/directive#creating-a-directive-that-manipulates-the-dom

```coffeescript
link: (scope, elem, attrs) ->
  action = scope.whilePressed

  bindWhilePressed = ->
    elem.on("mousedown", beginAction)

  beginAction = (e) ->
    e.preventDefault()
    # Do stuff

  bindWhilePressed()
```

Inside of our action we'll need to do two things:

1. Start running the action
1. Bind to `mouseup` to stop running the action.

For running the action, we'll use Angular's [`$interval`][$interval] service.
`$interval` is a wrapper around JavaScript's [`setInterval`][setInterval], but
gives us a promise interface, better testability, and hooks into Angular's
digest cycle.

[$interval]: https://docs.angularjs.org/api/ngMock/service/$interval
[setInterval]: https://developer.mozilla.org/en-US/docs/Web/API/Window.setInterval

In addition to running the action continuously, we'll also want to run it
immediately to avoid a delay. We'll run the action every 15 milliseconds, as
this will roughly translate to once per browser frame.

```patch
+TICK_LENGTH = 15
+
-whilePressed = ->
+whilePressed = ($interval) ->
   restrict: "A"

   link:
     action = scope.whilePressed

@@ -23,7 +24,7 @@
     beginAction = (e) ->
       e.preventDefault()
+      action()
+      $interval(action, TICK_LENGTH)
+      bindEndAction()
```

In our `beginAction` function, we call `bindEndAction` to set up the events to
stop running the event. We know that we'll at least want to bind to `mouseup` on
our button, but we have to decide how to handle users who move the mouse off of
the button before releasing it. We can handle this by listening for `mouseleave`
on the element, in addition to `mouseup`.

```coffeescript
bindEndAction = ->
  elem.on('mouseup', endAction)
  elem.on('mouseleave', endAction)
```

In our `endAction` function, we'll want to cancel the `$interval` for our
action, and unbind the event listeners for `mouseup` and `mouseleave`.

```coffeescript
unbindEndAction = ->
  elem.off('mouseup', endAction)
  elem.off('mouseleave', endAction)

endAction = ->
  $interval.cancel(intervalPromise)
  unbindEndAction()
```

We'll also need to store the promise that `$interval` returned so that we can
cancel it when the mouse is released.

```patch
 whilePressed = ($parse, $interval) ->
   link: (scope, elem, attrs) ->
     action = scope.whilePressed
+    intervalPromise = null

     bindWhilePressed = ->
       elem.on('mousedown', beginAction)
@@ -23,7 +24,7 @@
     beginAction = (e) ->
       e.preventDefault()
       action()
-      $interval(action, TICK_LENGTH)
+      intervalPromise = $interval(action, TICK_LENGTH)
       bindEndAction()
```

## Cleaning Up

Generally I consider it a smell to have an isolated scope on any directive that
isn't an element. Each DOM element can only have one isolated scope, and
attribute directives are generally meant to be composed. So let's replace our
scope with a manual use of [`$parse`][$parse] instead.

`$parse` takes in an expression, and will return a function that can be called
with a scope and an optional hash of local variables. This means we can't call
`action` directly anymore, and instead need a wrapper function which will pass in
the scope for us.

[$parse]: https://docs.angularjs.org/api/ng/service/$parse

```patch
-whilePressed = ($interval) ->
-  scope:
-    whilePressed: "&"
-
+whilePressed = ($parse, $interval) ->
   link: (scope, elem, attrs) ->
-    action = scope.whilePressed
+    action = $parse(attrs.whilePressed)
     intervalPromise = null

     bindWhilePressed = ->
@@ -26,14 +23,17 @@ whilePressed = ($interval) ->

     beginAction = (e) ->
       e.preventDefault()
-      action()
-      intervalPromise = $interval(action, TICK_LENGTH)
+      tickAction()
+      intervalPromise = $interval(tickAction, TICK_LENGTH)
       bindEndAction()

     endAction = ->
       $interval.cancel(intervalPromise)
       unbindEndAction()

+    tickAction = ->
+      action(scope)
```

And that's it. Our end result is a nicely decoupled Angular UI component that
can easily be reused across applications. The final code looks like this.

```coffeescript
TICK_LENGTH = 15

whilePressed = ($parse, $interval) ->
  restrict: "A"

  link: (scope, elem, attrs) ->
    action = $parse(attrs.whilePressed)
    intervalPromise = null

    bindWhilePressed = ->
      elem.on('mousedown', beginAction)

    bindEndAction = ->
      elem.on('mouseup', endAction)
      elem.on('mouseleave', endAction)

    unbindEndAction = ->
      elem.off('mouseup', endAction)
      elem.off('mouseleave', endAction)

    beginAction = (e) ->
      e.preventDefault()
      tickAction()
      intervalPromise = $interval(tickAction, TICK_LENGTH)
      bindEndAction()

    endAction = ->
      $interval.cancel(intervalPromise)
      unbindEndAction()

    tickAction = ->
      action(scope)

    bindWhilePressed()
```
