Fragmented Thought

AngularJS - Hold To Activate Button Directive

By

Published:

Lance Gliser

Heads up! This content is more than six months old. Take some time to verify everything still works as expected.

I needed a hold button that gave visual feedback. This fills with a color and duration you define then calls whatever method you give it. Arguments are allowed as part of the success callback. I'll continue to tweak on it as I use it more.

Inspiration for this code came from lazyentropy. I just wanted something a little more self-contained than his methods allowed for.

Gist available on GitHub.

directives.js

app.directive("holdButton", function ($parse, $interval) { //The framerate of the progress bar, progression will be evaluated every 5ms. var tickDelay = 10; var minFill = 5; return { restrict: "A", scope: { holdButtonSuccess: "&", }, link: function postLink(scope, element, attrs) { var started, completed, stop; var engage = function ($event) { var holdDelay = attrs.holdButtonDelay ? $parse(attrs.holdButtonDelay)(scope) || 400 : 400; var counter = 0; var nbTick = holdDelay / tickDelay; element.removeClass("hold-success", "hold-failure"); element.addClass("holding"); completed = false; var fillColor = !!attrs.holdButtonFillColor ? attrs.holdButtonFillColor : "#A1A1A1"; element.css( "background-image", "linear-gradient(to right, " + fillColor + " " + minFill + "%, transparent " + minFill + "%)" ); // Call the onTick function `nbTick` times every `tickDelay` ms. // stop is the stopper function stop = $interval(onTick, tickDelay, nbTick); started = true; function onTick() { counter++; var percentage = Math.max( Math.round((counter / nbTick) * 100), minFill ); element.css( "background-image", "linear-gradient(to right, " + fillColor + " " + percentage + "%, transparent " + percentage + "%)" ); // If we reach `nbTick` then we're done if (counter === nbTick) { if (!!scope.holdButtonSuccess) { scope.holdButtonSuccess(); } completed = true; element.addClass("hold-success"); element.css("background-image", null); } } }; var disengage = function ($event) { // Prevent standard events for all interactions $event.stopPropagation(); $interval.cancel(stop); element.removeClass("holding"); element.css("background-image", null); if (!started || completed) { return; } element.addClass("hold-failure"); // TODO add a failure callback? }; element.on("mousedown", function ($event) { engage($event); }); element.on("mouseup", function ($event) { disengage($event); }); element.on("mouseleave", function ($event) { disengage($event); }); // Touch events element.on("touchstart", function ($event) { engage($event); }); element.on("touchend", function ($event) { disengage($event); }); }, }; });

Usage

<button hold-button hold-button-success="holdAction()" hold-button-delay="" hold-button-fill-color="" > Hold to Activate </button>
button.holding { border-style: dashed; } button.hold-success { background-color: #43ac6a; border-color: #368a55; color: #fff; }

Protractor / Jasmine Test

It seems this produces errors in Firefox. I haven't yet fixed them:

UnknownError: Cannot press more then one button or an already pressed button.'
UnknownError: Cannot press more then one button or an already pressed button.' when calling method: [wdIMouse::down]
describe("hold buttons", function () { // You'll need to write the page definition yourself var holdButton = page.getHoldButton(); it("should give feedback on interaction", function () { browser.actions().mouseDown(holdButton).perform(); browser.sleep(50); expect(holdButton.getAttribute("class")).toMatch("holding"); browser.actions().mouseUp(holdButton).perform(); }); it("should not complete in under .5 seconds", function () { browser.actions().mouseDown(holdButton).perform(); browser.sleep(50); browser.actions().mouseUp(holdButton).perform(); expect(holdButton.getAttribute("class")).toMatch("hold-failure"); }); it("should complete eventually", function () { browser.actions().mouseDown(holdButton).perform(); browser.sleep(2000); browser.actions().mouseUp(holdButton).perform(); expect(holdButton.getAttribute("class")).toMatch("hold-success"); }); });