Skip to content

Instantly share code, notes, and snippets.

@kevinrenskers
Last active January 20, 2017 11:11
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kevinrenskers/d66dbebe43c8533efaf2 to your computer and use it in GitHub Desktop.
Save kevinrenskers/d66dbebe43c8533efaf2 to your computer and use it in GitHub Desktop.
Progress button for AngularJS
/* General styles for all types of buttons */
.progress-button {
position: relative;
display: inline-block;
}
.progress-button .progress-button-content {
position: relative;
display: block;
}
// Success / error icon
.progress-button .progress-button-content::before,
.progress-button .progress-button-content::after {
position: absolute;
right: 20px;
font-family: "Glyphicons Halflings";
opacity: 0;
transition: opacity 0.3s 0.3s;
}
.progress-button .progress-button-content::before {
color: darken(@brand, 20%);
content: "\e013"; /* Checkmark for success */
}
.progress-button .progress-button-content::after {
color: red;
content: "\e014"; /* Cross for error */
}
.progress-button.state-success .progress-button-content::before,
.progress-button.state-error .progress-button-content::after {
opacity: 1;
}
.progress-button .progress-button-progress {
background: @brand;
}
.progress-button .progress-button-progress-inner {
position: absolute;
left: 0;
background: darken(@brand, 20%);
}
.progress-button.horizontal .progress-button-progress-inner {
top: 0;
width: 0;
height: 100%;
transition: width 0.3s, opacity 0.3s;
}
/* Fill horizontal */
/* ====================== */
.progress-button.fill.horizontal {
overflow: hidden;
}
.progress-button.fill.horizontal .progress-button-content {
z-index: 10;
transition: transform 0.3s;
}
.progress-button.fill.horizontal .progress-button-content::before,
.progress-button.fill.horizontal .progress-button-content::after {
top: 100%;
right: auto;
left: 50%;
transition: opacity 0.3s;
transform: translateX(-50%);
}
.progress-button.fill.horizontal.state-success .progress-button-content,
.progress-button.fill.horizontal.state-error .progress-button-content {
transform: translateY(-100%);
}
/* Shrink horizontal */
/* ====================== */
.progress-button.shrink {
overflow: hidden;
transition: transform 0.2s;
}
.progress-button.shrink.horizontal .progress-button-content {
transition: opacity 0.3s, transform 0.3s;
}
.progress-button.shrink.horizontal .progress-button-content::before,
.progress-button.shrink.horizontal .progress-button-content::after {
top: 100%;
right: auto;
left: 50%;
transition: opacity 0.3s;
transform: translateX(-50%);
}
.progress-button.shrink.horizontal.state-loading {
transform: scaleY(0.3);
}
.progress-button.shrink.horizontal.state-loading .progress-button-content {
opacity: 0;
}
.progress-button.shrink.horizontal.state-success .progress-button-content,
.progress-button.shrink.horizontal.state-error .progress-button-content {
transform: translateY(-100%);
}
/* Rotate side down 3d */
/* ====================== */
.progress-button.rotate-side-down .progress {
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: 20px;
transform: rotateX(-90deg);
transform-origin: 50% 0%;
backface-visibility: hidden;
}
.progress-button.rotate-side-down.state-loading .progress-wrap {
transform: rotateX(90deg) translateZ(10px);
}
/* top-line */
/* ====================== */
.progress-button.top-line .progress-inner {
height: 3px;
}
.progress-button.top-line .progress-button-content::before,
.progress-button.top-line .progress-button-content::after {
right: auto;
left: 100%;
margin-left: 25px;
}
<button class="btn btn-primary progress-button fill horizontal">
<span class="progress-button-content" ng-transclude></span>
<span class="progress-button-progress">
<span class="progress-button-progress-inner" style="width:{{ width }}%"></span>
</span>
</button>
angular.module('components.directives').directive('progressButton', function progressButton($parse, $timeout) {
'use strict';
return {
restrict: 'E',
replace: true,
transclude: true,
scope: {
inprogress: '='
},
templateUrl: 'components/directives/progressButton.html',
link: function(scope, element, attrs) {
var _status = 0;
var _incTimeout;
var _completeTimeout;
var _fn;
scope.width = 0;
if (!attrs.type) {
element.type = 'button';
}
function _complete() {
attrs.$set('disabled', false);
element.removeClass('state-loading');
_set(1);
scope.width = 0;
_completeTimeout = $timeout(function() {
element.removeClass('state-success');
element.removeClass('state-error');
}, 1500);
}
function _success(data) {
_complete();
element.addClass('state-success');
return data;
}
function _fail(error) {
_complete();
element.addClass('state-error');
return error;
}
function _set(n) {
scope.width = (n * 100);
_status = n;
$timeout.cancel(_incTimeout);
_incTimeout = $timeout(function() {
_inc();
}, 250);
}
function _inc() {
if (_status >= 1) {
return;
}
var rnd = 0;
if (_status >= 0 && _status < 0.25) {
// Start out between 10 - 15% increments
rnd = (Math.random() * 10 + 5) / 100;
} else if (_status >= 0.25 && _status < 0.65) {
// increment between 0 - 10%
rnd = (Math.random() * 10) / 100;
} else if (_status >= 0.65 && _status < 0.9) {
// increment between 0 - 5%
rnd = (Math.random() * 5) / 100;
} else if (_status >= 0.9 && _status < 0.99) {
// finally, increment it .5 %
rnd = 0.005;
} else {
// after 99%, don't increment:
rnd = 0;
}
var pct = _status + rnd;
_set(pct);
}
function _disable() {
$timeout.cancel(_completeTimeout);
attrs.$set('disabled', true);
element.addClass('state-loading');
_set(0.02);
}
if (attrs.pbClick) {
element.on('click', function(event) {
_fn = $parse(attrs.pbClick);
var promise = _fn(scope.$parent, { $event: event });
if (promise && promise.then) {
_disable();
promise.then(_success, _fail);
}
});
} else if (element[0].form && element[0].form.attributes['pb-submit']) {
element[0].form.onsubmit = function(event) {
_fn = $parse(element[0].form.attributes['pb-submit'].nodeValue);
var promise = _fn(scope.$parent, { $event: event });
if (promise && promise.then) {
_disable();
promise.then(_success, _fail);
}
}
}
scope.$watch('inprogress', function(newValue, oldValue) {
if (newValue === true) {
_disable();
}
if (newValue === 'error' && oldValue === true) {
_fail();
}
if (newValue === false && oldValue === true) {
_success();
}
});
}
};
});
@kevinrenskers
Copy link
Author

The logic to randomly increment the progress bar comes from angular-loading-bar, the css for the button comes from http://tympanus.net/Development/ProgressButtonStyles/.

Usage

As a simple button. The function needs to return a promise.

<progress-button pb-click="doStuff()">Do something</progress-button>

In a form with a onsubmit listener. The function needs to return a promise.

<form pb-submit="submit()">
    <progress-button>Submit</progress-button>
</form>

In a form with a inprogress scope variable.

<form ng-submit="submit()">
    <progress-button inprogress="inprogress">Submit</progress-button>
</form>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment