var app = angular.module('plunker', []);
app.controller('DemoController', [function () {
var demo = this;
demo.tags = ['foo'];
}]);
app.directive('tags', [function () {
return {
restrict: 'E',
replace: true,
require: ['tags', 'ngModel'],
scope: {},
controller: 'TagsController as vm',
bindToController: {
'placeholder': '@',
'tags': '=ngModel'
},
template: `
<div class="tags-host form-control"
ng-class="{
'focus': vm.hasFocus,
'has-error': vm.ngModel.$invalid
}"
ng-click="vm.handleClick($event)"
>
<div class="tag-item label label-default"
ng-blur="vm.handleBlurTag($event, $index)"
ng-click="vm.handleFocusTag($event, $index)"
ng-keydown="vm.handleTagKeyDown($event, $index)"
ng-repeat="tag in vm.tags"
tabindex="-1"
>
<span class="tag-name">{{ ::tag }}</span>
<button type="button" class="tag-remove btn btn-default btn-xs"
ng-click="vm.handleRemoveTag($event, $index)"
>
×
</button>
</div>
<input class="tag-input"
ng-blur="vm.handleBlur($event)"
ng-focus="vm.handleFocus($event)"
ng-keydown="vm.handleKeyDown($event)"
ng-keyup="vm.handleKeyUp($event)"
ng-model="vm.input"
placeholder="{{ vm.placeholder }}"
>
</div>
`,
link: function ($scope, $element, $attrs, ctls) {
ctls[0].ngModel = ctls[1];
ctls[0].$element = $element;
ctls[0].inputEl = $element.find('input')[0];
}
};
}]);
app.controller('TagsController', [function () {
var vm = this;
vm.hasFocus = false;
vm.input = '';
vm.handleBlur = handleBlur;
vm.handleClick = handleClick;
vm.handleFocus = handleFocus;
vm.handleFocusTag = handleFocusTag;
vm.handleKeyDown = handleKeyDown;
vm.handleKeyUp = handleKeyUp;
vm.handleRemoveTag = handleRemoveTag;
vm.handleSubmitTag = handleSubmitTag;
vm.handleTagKeyDown = handleTagKeyDown;
function handleBlur($event) {
addTag(vm.input);
vm.hasFocus = false;
}
function handleFocus($event) {
vm.hasFocus = true;
}
function handleClick($event) {
$event.preventDefault();
vm.inputEl.focus();
}
function handleBlurTag($event, idx) {
$event.stopPropagation();
}
function handleFocusTag($event, idx) {
$event.stopPropagation();
focusTag(idx);
}
function handleKeyDown($event) {
switch ($event.keyCode) {
case 8:
if (!vm.input) {
handleFocusTag($event, vm.tags.length - 1);
}
break;
case 9: // Tab
if (!vm.input.trim()) break;
case 32: // Space
case 188: // Comma
$event.preventDefault();
addTag(vm.input);
break;
}
}
function handleKeyUp($event) {
vm.ngModel.$setValidity('tags', !vm.input || isValid(vm.input));
}
function handleRemoveTag($event, idx) {
vm.tags.splice(idx, 1);
focusChild(vm.tags.length);
}
function handleSubmitTag($event, tag) {
$event.preventDefault();
$event.stopPropagation();
addTag(tag);
}
function handleTagKeyDown($event, idx) {
switch ($event.keyCode) {
case 8:
case 46:
$event.preventDefault();
$event.stopPropagation();
return handleRemoveTag($event, idx);
case 37: // Left
return focusChild(idx - 1);
case 39: // Right
return focusChild(idx + 1);
}
}
function addTag(str) {
str = str.trim();
if (isValid(str)) {
vm.tags.push(str);
vm.input = '';
}
}
function isValid(str) {
return !!str.match(/^[-_a-zA-Z0-9]+$/)
&& vm.tags.indexOf(str) < 0;
}
function focusChild(idx) {
if (idx === vm.tags.length) {
vm.inputEl.focus();
} else {
focusTag(idx);
}
}
function focusTag(idx) {
var tags = Array.prototype.slice.call(vm.$element.children(), 0, -1);
var tagEl = tags[idx];
if (tagEl) {
tagEl.focus();
}
}
}]);
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<link data-require="bootstrap-css@*" data-semver="3.3.6" rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />
<script>
document.write('<base href="' + document.location + '" />');
</script>
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@1.4.x" src="https://code.angularjs.org/1.5.0/angular.js" data-semver="1.5.0"></script>
<script src="app.js"></script>
</head>
<body class="container" ng-controller="DemoController as demo">
<div class="well">
<h1>Tags input demonstration</h1>
<tags ng-model="demo.tags" placeholder="Enter a tag"></tags>
</div>
</body>
</html>
/* Put your css in here */
.tags-host.form-control {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
width: 100%;
height: auto;
padding: 4px;
& > .tag-item {
margin: 4px;
}
& > .tag-input {
flex-grow: 1;
margin: 4px;
}
&.focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
}
&.ng-invalid {
border-color: #a94442;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
&.focus {
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;
}
}
}
.tag-item {
&:focus {
border-width: 1px;
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);
}
}
.tag-input {
border: none;
&:focus, &:active {
outline: none;
}
}