<!DOCTYPE html>
<html>
<head>
<title>Angular input dropdown demo</title>
<link rel="stylesheet" href="style.css" />
<style>
body {
font-family: Arial, sans-serif;
padding: 10px 0 0 40px;
}
h1,
h2 {
font-weight: normal;
}
h2 {
font-size: 22px;
margin: 50px 0 15px 0;
}
button[type='submit'] {
font-size: 15px;
padding: 10px;
}
/* Custom input dropdown styles */
.input-dropdown {
margin: 0 30px 0 0;
width: 350px;
/* set the width of the input and dropdown */
}
.input-dropdown input[type='text'] {
font-size: 15px;
padding: 5px;
}
.input-dropdown ul > li {
transition: background .15s;
}
</style>
</head>
<body ng-app="inputDropdownDemo">
<div class="content" ng-controller="InputDropdownController as dropdownCtrl">
<h1>Angular input dropdown demo</h1>
<p>Code on <a href="https://github.com/hannaholl/angular-input-dropdown">Github</a>.</p>
<h2>Select a country from a list of strings</h2>
<form name="demoFormStrings" ng-submit="dropdownCtrl.submitFormStrings()" novalidate="">
<input-dropdown input-placeholder="Country string" input-name="country-strings" input-required="true" selected-item="dropdownCtrl.countryString" default-dropdown-items="dropdownCtrl.defaultDropdownStrings" filter-list-method="dropdownCtrl.filterStringList(userInput)"
item-selected-method="dropdownCtrl.itemStringSelected(item)"></input-dropdown>
<button type="submit" ng-disabled="demoFormStrings.$invalid">Submit</button>
</form>
<p>{{dropdownCtrl.stringMessage}}</p>
<h2>Select a country from a list of objects</h2>
<form name="demoFormObjects" ng-submit="dropdownCtrl.submitFormObjects()" novalidate="">
<input-dropdown input-placeholder="Country object" input-name="country-object" input-required="true" selected-item="dropdownCtrl.countryObject" default-dropdown-items="dropdownCtrl.defaultDropdownObjects" filter-list-method="dropdownCtrl.filterObjectList(userInput)"
item-selected-method="dropdownCtrl.itemObjectSelected(item)"></input-dropdown>
<button type="submit" ng-disabled="demoFormObjects.$invalid">Submit</button>
</form>
<p>{{dropdownCtrl.objectMessage}}</p>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
<script src="inputDropdown.js"></script>
<script>
// Add 'inputDropdown' as a dependency when creating angular app
var demoApp = angular.module('inputDropdownDemo', ['inputDropdown']);
demoApp.controller('InputDropdownController', [
'$scope',
'$q',
function($scope, $q) {
var self = this;
self.stringMessage = '';
self.objectMessage = '';
self.countryString = null; // Holds the selected in demoFormStrings, set with attribute 'selected-item'
self.countryObject = null; // Holds the selected in demoFormObjects, set with attribute 'selected-item'
// Pass strings to the dropdown for simple usage
self.defaultDropdownStrings = [
'China',
'Sweden',
'United Kingdom',
'United States'
];
// Use objects in the dropdown list if more data than just a string is needed.
// Every object needs to have a property 'readableName', this is what will be displayed in the dropdown.
self.defaultDropdownObjects = [{
readableName: 'China',
countryCode: 'CH',
id: 0,
toString: function() {
return '{readableName: ' + this.readableName + ', countryCode: ' + this.countryCode + ', id: ' + this.id + '}';
}
}, {
readableName: 'Sweden',
countryCode: 'SE',
id: 1,
toString: function() {
return '{readableName: ' + this.readableName + ', countryCode: ' + this.countryCode + ', id: ' + this.id + '}';
}
}, {
readableName: 'United Kingdom',
countryCode: 'UK',
id: 2,
toString: function() {
return '{readableName: ' + this.readableName + ', countryCode: ' + this.countryCode + ', id: ' + this.id + '}';
}
}, {
readableName: 'United States',
countryCode: 'US',
id: 3,
toString: function() {
return '{readableName: ' + this.readableName + ', countryCode: ' + this.countryCode + ', id: ' + this.id + '}';
}
}];
// Filter method is passed with attribute 'filter-list-method="method(userInput)"'.
// Called on the onchange event from the input field. Should return a promise resolving with an array of items to show in the dropdown.
// If no filter method is passed to the the directive, the default dropdown will show constantly.
self.filterStringList = function(userInput) {
var filter = $q.defer();
var normalisedInput = userInput.toLowerCase();
var filteredArray = self.defaultDropdownStrings.filter(function(country) {
return country.toLowerCase().indexOf(normalisedInput) === 0;
});
filter.resolve(filteredArray);
return filter.promise;
};
self.filterObjectList = function(userInput) {
var filter = $q.defer();
var normalisedInput = userInput.toLowerCase();
var filteredArray = self.defaultDropdownObjects.filter(function(country) {
var matchCountryName = country.readableName.toLowerCase().indexOf(normalisedInput) === 0;
var matchCountryCode = country.countryCode.toLowerCase().indexOf(normalisedInput) === 0;
return matchCountryName || matchCountryCode;
});
filter.resolve(filteredArray);
return filter.promise;
};
// Called when user selected an item from dropdown. Passed with attribute 'item-selected-method="method(item)"'.
self.itemStringSelected = function(item) {
console.log('Handle item string selected in controller:', item);
self.stringMessage = 'String item selected: ' + item;
};
self.itemObjectSelected = function(item) {
console.log('Handle item object selected in controller:', item);
self.objectMessage = 'Object item selected: ' + item;
};
self.submitFormStrings = function() {
if ($scope.demoFormStrings.$valid) {
console.log('Submit form STRINGS with country:', self.countryString);
self.stringMessage = 'Submit form STRINGS with country: ' + self.countryString;
}
};
self.submitFormObjects = function() {
if ($scope.demoFormObjects.$valid) {
console.log('Submit form OBJECTS with country:', self.countryObject);
self.objectMessage = 'Submit form OBJECT with country: ' + self.countryObject;
}
};
}
]);
</script>
</body>
</html>
.input-dropdown {
display: inline-block;
position: relative;
}
.input-dropdown input[type='text'] {
box-sizing: border-box;
width: 100%;
}
.input-dropdown ul {
background: #fff;
border: 1px solid #000;
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
width: 100%;
z-index: 1000;
}
.input-dropdown ul > li {
cursor: pointer;
padding: 10px;
}
.input-dropdown ul > li.active {
background: #608AEB;
}
angular.module('inputDropdown', []).directive('inputDropdown', [function() {
var templateString =
'<div class="input-dropdown">' +
'<input type="text"' +
'name="{{inputName}}"' +
'placeholder="{{inputPlaceholder}}"' +
'ng-model="inputValue"' +
'ng-required="inputRequired"' +
'ng-change="inputChange()"' +
'ng-focus="inputFocus()"' +
'ng-blur="inputBlur($event)"' +
'input-dropdown-validator>' +
'<ul ng-show="dropdownVisible">' +
'<li ng-repeat="item in dropdownItems"' +
'ng-click="selectItem(item)"' +
'ng-mouseenter="setActive($index)"' +
'ng-mousedown="dropdownPressed()"' +
'ng-class="{\'active\': activeItemIndex === $index}"' +
'>' +
'<span ng-if="item.readableName">{{item.readableName}}</span>' +
'<span ng-if="!item.readableName">{{item}}</span>' +
'</li>' +
'</ul>' +
'</div>';
return {
restrict: 'E',
scope: {
defaultDropdownItems: '=',
selectedItem: '=',
inputRequired: '=',
inputName: '@',
inputPlaceholder: '@',
filterListMethod: '&',
itemSelectedMethod: '&'
},
template: templateString,
controller: function($scope) {
this.getSelectedItem = function() {
return $scope.selectedItem;
};
this.isRequired = function() {
return $scope.inputRequired;
};
},
link: function(scope, element) {
var pressedDropdown = false;
var inputScope = element.find('input').isolateScope();
scope.activeItemIndex = 0;
scope.inputValue = '';
scope.dropdownVisible = false;
scope.dropdownItems = scope.defaultDropdownItems || [];
scope.$watch('dropdownItems', function(newValue, oldValue) {
if (!angular.equals(newValue, oldValue)) {
// If new dropdownItems were retrieved, reset active item
scope.setActive(0);
}
});
scope.$watch('selectedItem', function(newValue, oldValue) {
inputScope.updateInputValidity();
if (!angular.equals(newValue, oldValue)) {
if (newValue) {
// Update value in input field to match readableName of selected item
if (typeof newValue === 'string') {
scope.inputValue = newValue;
}
else {
scope.inputValue = newValue.readableName;
}
}
else {
// Uncomment to clear input field when editing it after making a selection
// scope.inputValue = '';
}
}
});
scope.setActive = function(itemIndex) {
scope.activeItemIndex = itemIndex;
};
scope.inputChange = function() {
scope.selectedItem = null;
showDropdown();
if (!scope.inputValue) {
scope.dropdownItems = scope.defaultDropdownItems || [];
return;
}
if (scope.filterListMethod) {
var promise = scope.filterListMethod({userInput: scope.inputValue});
if (promise) {
promise.then(function(dropdownItems) {
scope.dropdownItems = dropdownItems;
});
}
}
};
scope.inputFocus = function() {
scope.setActive(0);
showDropdown();
};
scope.inputBlur = function(event) {
if (pressedDropdown) {
// Blur event is triggered before click event, which means a click on a dropdown item wont be triggered if we hide the dropdown list here.
pressedDropdown = false;
return;
}
hideDropdown();
};
scope.dropdownPressed = function() {
pressedDropdown = true;
}
scope.selectItem = function(item) {
scope.selectedItem = item;
hideDropdown();
scope.dropdownItems = [item];
if (scope.itemSelectedMethod) {
scope.itemSelectedMethod({item: item});
}
};
var showDropdown = function () {
scope.dropdownVisible = true;
};
var hideDropdown = function() {
scope.dropdownVisible = false;
}
var selectPreviousItem = function() {
var prevIndex = scope.activeItemIndex - 1;
if (prevIndex >= 0) {
scope.setActive(prevIndex);
}
};
var selectNextItem = function() {
var nextIndex = scope.activeItemIndex + 1;
if (nextIndex < scope.dropdownItems.length) {
scope.setActive(nextIndex);
}
};
var selectActiveItem = function() {
if (scope.activeItemIndex >= 0 && scope.activeItemIndex < scope.dropdownItems.length) {
scope.selectItem(scope.dropdownItems[scope.activeItemIndex]);
}
};
element.bind("keydown keypress", function (event) {
switch (event.which) {
case 38: //up
scope.$apply(selectPreviousItem);
break;
case 40: //down
scope.$apply(selectNextItem);
break;
case 13: // return
if (scope.dropdownVisible && scope.dropdownItems && scope.dropdownItems.length > 0) {
// only preventDefault when there is a list so that we can submit form with return key after a selection is made
event.preventDefault();
scope.$apply(selectActiveItem);
}
break;
}
});
}
}
}]);
angular.module('inputDropdown').directive('inputDropdownValidator', function() {
return {
require: ['^inputDropdown', 'ngModel'],
restrict: 'A',
scope: {},
link: function(scope, element, attrs, ctrls) {
var inputDropdownCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
var validatorName = 'itemSelectedValid';
scope.updateInputValidity = function() {
var selection = inputDropdownCtrl.getSelectedItem();
if (selection || !inputDropdownCtrl.isRequired()) {
ngModelCtrl.$setValidity(validatorName, true);
}
else {
ngModelCtrl.$setValidity(validatorName, false);
}
};
}
};
});