Skip to content

Instantly share code, notes, and snippets.

@lionelB
Last active December 29, 2015 04:08
Show Gist options
  • Save lionelB/7612233 to your computer and use it in GitHub Desktop.
Save lionelB/7612233 to your computer and use it in GitHub Desktop.
An attempt to do a Numeric Stepper the Angular Way http://plnkr.co/edit/R4FYQKCGyB1GYl2hL4LO
<!doctype html>
<html ng-app="ui">
<head>
<meta charset="UTF-8">
<title>Angular numeric Stepper</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, maximum-scale=1" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/css/bootstrap-theme.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.0.2/css/bootstrap.css" />
<style>
.ui-stepper{
position:relative;
}
.stepper-bt{
position:absolute;
right:0;
height:15px;
border:#ccc 1px solid;
font-size:.8em;
background-image: -webkit-gradient(linear, left 0%, left 100%, from(#fff), to(#e6e6e6)); // Safari 4+, Chrome 2+
background-image: -webkit-linear-gradient(top, #fff, 0%, #e6e6e6, 100%); // Safari 5.1+, Chrome 10+
background-image: -moz-linear-gradient(top, #fff 0%, #e6e6e6 100%); // FF 3.6+
background-image: linear-gradient(to bottom, #fff 0%, #e6e6e6 100%); // Standard, IE10
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff', endColorstr='#e6e6e6', GradientType=0)",argb(#fff),argb(#e6e6e6))); // IE9 and down
display: inline-block;
padding: 0 .5em;
margin-right:-3px;
}
.stepper-bt:hover{
background: #dedede;
background-image: none;
}
.stepper-bt:active{
background: #efefef;
background-image: none;
}
.stepper-bt__up{
top:0;
border-top-right-radius: 3px;
}
.stepper-bt__up .caret{
position:relative;
top:-2px;
}
.stepper-bt__down{
border-bottom-right-radius: 3px;
bottom:0;
}
</style>
</head>
<body>
<div ng-controller="AppController">
<ui-stepper min="0" max="12" value="hours" step-size="1"></ui-stepper>
<span class="help-block text-left">{{hours}} hours</span>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.1/angular.js"></script>
<script src="/js/ui.stepper.js"></script>
<script src="/js/app.js"></script>
</body>
</html>
var app = angular.module('ui', ['ui.stepper']);
app.controller('AppController', ['$scope',
function($scope){
$scope.hours="0";
}
]);
<div class="ui-stepper input-group">
<input type="text" class="form-control input-sm" ng-model="value" />
<a class="stepper-bt stepper-bt__up dropup" ng-click="increment()">
<i class="caret"></i>
<span class="sr-only">Increment</span>
</a>
<a class="stepper-bt stepper-bt__down" ng-click="decrement()">
<span class="caret"></span>
<span class="sr-only">Decrement</span>
</a>
</div>
/* global angular, console */
angular.module('ui.stepper', [])
.constant('stepperConfig',{
stepSize: 10,
min: 0,
max: 100
})
.directive('uiStepper', ['stepperConfig',
function(stepperConfig){
'use strict';
return {
restrict: 'A',
require: 'ngModel',
scope: {
'value': "=ngModel"
},
replace: true,
templateUrl: 'view/stepper.html',
link: function(scope, element, attrs, ngModel ){
// Set default values
attrs.min = parseInt(attrs.min || stepperConfig.min, 10);
attrs.max = parseInt(attrs.max || stepperConfig.max, 10);
attrs.value = parseInt(attrs.value || stepperConfig.value, 10);
attrs.stepSize = parseInt(attrs.stepSize || stepperConfig.stepSize, 10);
attrs.allowCycle = attrs.allowCycle;
console.log(attrs.allowCycle);
var input = element.find('input');
// Allow user to increment / decrement using arrow key
input.bind('keydown', function(evt){
var key = evt.charCode || evt.keyCode || 0;
if (key === 38) {
scope.increment();
scope.$apply();
}
if (key === 40){
scope.decrement();
scope.$apply();
}
});
// Prevent user to enter non numeric char
input.bind('keypress', function(evt){
var key = evt.charCode || evt.keyCode || 0;
if ( !/[0-9]/.test( String.fromCharCode(key) )) {
evt.preventDefault();
return false;
}
});
scope.increment = function(){
var val = parseInt(ngModel.$modelValue, 10)
, updateFn = attrs.allowCycle ? loop : clamp;
input[0].focus();
ngModel.$setViewValue(updateFn( val + attrs.stepSize, attrs.min, attrs.max) );
};
scope.decrement = function(){
var val = parseInt(ngModel.$modelValue, 10)
, updateFn = attrs.allowCycle ? loop : clamp;
ngModel.$setViewValue(updateFn( val - attrs.stepSize, attrs.min, attrs.max) );
};
function loop(val, min, max){
return val < min? max : (val > max? min : val);
}
function clamp(val, min, max){
return val < min? min : (val > max? max : val);
}
}
};
}
]);
@t8g
Copy link

t8g commented Nov 23, 2013

Got error on line 52 when clicking.

var input looks wrong to me as find does not work with class name : http://docs.angularjs.org/api/angular.element

Correction :

  • line 28 : var input = element.find('input');
  • lines 52, 58 : input[0].focus();

And maybe some optimizations :

  • why scoping max, min and stepSize ? You can remove lines 15,16,17 and replace scope.min by attrs.min (and ...)
  • lines 53+54 : use Math.min (idem for line 59+60 with Math.max)
  • Maybe you can use ng-model as a required attribute and make use of the fourth parameter of your link function

@lionelB
Copy link
Author

lionelB commented Nov 23, 2013

Thx a lot... It seems that I made some mistake when isolating my code... Also on plunker, I saw some error when I dropped jquery which I need on my project)... Now I understand!

Not sure about scoping max, min, and stepSize... My goal was I to mabe them updatable/bindable from code or other binding (ex:switching AM/PM, max going from 12 to 24)

About requiring a ng-model, I'm not sure to see the benefit cause it's a bit new to me, but I see some example using that scheme.

Nice to have insightfull comment !

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