var GarageApp = angular.module('GarageApp', [], function() {}); /** * focusAfterChange directive causes focus to a focusable element after an expression changes. * * Usage: * <ANY focus-after-change="{expression}">...</ANY> * * focusAfterChange - {expression} - Expression that will be watched for changes. */ GarageApp.directive('focusAfterChange', function() { return function(scope, elem, attrs) { var initChange = true; scope.$watch(attrs.focusAfterChange, function() { // Not initial change if(!initChange) { elem[0].focus(); } initChange = false; }, true); }; }); /** * focusIf directive causes focus to a focusable element after each digest phase as long as an expression is true. * * Usage: * <ANY focus-if="{expression}">...</ANY> * * focusIf - {expression} - Expression whose true value will result in focus to element. */ GarageApp.directive('focusIf', function() { return function(scope, elem, attrs) { var initChange = true; scope.$watch(function() { return { shouldFocus: scope.$eval(attrs.focusIf), nonce: new Date().getTime() + Math.random() }; }, function(data) { // Not initial change and should focus if(!initChange && data.shouldFocus) { elem[0].focus(); } initChange = false; }, true); }; }); /** * @constructor * * @param {String} name * @param {String} type One of Vehicle.TYPE_*. */ var Vehicle = function(name, type) { this.name = name; this.type = type; this.error = null; }; Vehicle.TYPE_CAR = 'car'; Vehicle.TYPE_MOTORCYCLE = 'motorcycle'; Vehicle.displayTypes = {}; Vehicle.displayTypes[Vehicle.TYPE_CAR] = 'Car'; Vehicle.displayTypes[Vehicle.TYPE_MOTORCYCLE] = 'Motorcycle'; /** * @return {String} */ Vehicle.prototype.getDisplayType = function() { return Vehicle.displayTypes[this.type]; }; /** * @return {Vehicle} A clone of this vehicle. */ Vehicle.prototype.clone = function() { return angular.extend(new Vehicle(), this); }; /** * @return {Boolean} TRUE if this vehicle's data is valid, FALSE if not. * If FALSE, this.error is populated with a message. */ Vehicle.prototype.validate = function() { var valid = true; this.error = null; if(this.name === '') { valid = false; this.error = 'Vehicle name required.'; } return valid; }; /** * @constructor */ var GarageCtrl = function($scope) { $scope.vehicles = [ new Vehicle('2011 Nissan Frontier', Vehicle.TYPE_CAR), new Vehicle('2007 Honda CRF450R', Vehicle.TYPE_MOTORCYCLE) ]; // Expose Vehicle class to scope and initialize the new vehicle $scope.Vehicle = Vehicle; $scope.newVehicle = new Vehicle('', Vehicle.TYPE_CAR); /** * @param {Vehicle} Vehicle Vehicle to delete. */ $scope.deleteVehicle = function(vehicle) { for(var i=0; i<$scope.vehicles.length; ++i) { if($scope.vehicles[i] === vehicle) { $scope.vehicles.splice(i, 1); break; } } }; /** * @param {Vehicle|Object} newVehicle Vehicle to add, or generic object castable to a Vehicle. */ $scope.addVehicle = function(newVehicle) { newVehicle instanceof Vehicle || (newVehicle = angular.extend(new Vehicle(), newVehicle)); $scope.addVehicleError = false; if(!newVehicle.validate()) { alert(newVehicle.error); $scope.addVehicleError = true; } // Input OK else { // Clone it onto our set $scope.vehicles.push(newVehicle.clone()); } }; };
<div ng-app="GarageApp" ng-controller="GarageCtrl"> <h1>My Garage</h1> <ul> <li ng-repeat="vehicle in vehicles"> <strong>{{vehicle.getDisplayType()}}</strong> - <span>{{vehicle.name}}</span> - <a class="deleteBtn" href="" ng-click="deleteVehicle(vehicle)">delete</a> </li> </ul> <form ng-submit="addVehicle(newVehicle); newVehicle.name = ''"> <select ng-model="newVehicle.type" ng-options="type as displayType for (type, displayType) in Vehicle.displayTypes"></select> <input type="text" ng-model="newVehicle.name" placeholder="Vehicle name" focus-after-change="newVehicle.type" focus-if="addVehicleError" /> <button type="submit">Add Vehicle</button> </form> </div>