├── LICENSE ├── README.md ├── TODO.md ├── bower.json ├── demo ├── smarty-config.js ├── smarty.css ├── smarty.html └── smarty.js ├── src ├── smarty-config.js └── smarty.js └── tests └── angular-smarty-tests.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014 Thumbtack, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-smarty 2 | ============== 3 | 4 | Autocomplete UI written with Angular JS. 5 | 6 | Installation 7 | --------------------- 8 | 9 | `bower install angular-smarty` 10 | 11 | Configuration 12 | --------------------- 13 | 14 | Configuration is handled in smarty-config.js which is injected into the main module in smarty.js. 15 | The main configuration variable is the function getSmartySuggestions that returns a promise 16 | containing the suggestions to be used in the autocomplete dropdown list. The default 17 | getSmartySuggestions uses a url endpoint for a backend service that returns suggestions in the 18 | following format: [{Name: suggestion1}, {Name: suggestion:2}, etc.]. 19 | 20 | Usage 21 | --------------------- 22 | 23 | Include smarty-config.js and smarty.js in that order in your html. In your main Angular module, 24 | inject angular-smarty, or if you're not using Angular elsewhere on your page, include 25 | `angular.bootstrap(angular.element("body"), ["angular-smarty"]);` in a script tag in your html and 26 | don't forget that Angular is a dependency. 27 | 28 | ```js 29 | $(function() { 30 | angular.bootstrap(angular.element("body"), ["angular-smarty"]); 31 | }); 32 | ``` 33 | 34 | Model your html after the following: 35 | 36 | ```html 37 | 38 |
39 |

angular-smarty demo

40 |
41 | 44 |
45 | 46 |
47 |
48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | To Do 2 | ====== 3 | 4 | 1. Improve documentation. 5 | 2. Improve demo. 6 | 3. Create a userInteraction service for setSelected, selected, selectionMade, and suggestionPicked. 7 | 4. Create services for clickedSomewhereElse and submitClicked to make code more testable. 8 | 5. Add tests. 9 | 6. Make more contributor-friendly. 10 | 7. Add config variable for number of responses. 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-smarty", 3 | "version": "0.3.0", 4 | "authors": [ 5 | "Katie Thomas " 6 | ], 7 | "description": "Angular add-on for creating a drop-down list of autocomplete suggestions for a form input.", 8 | "main": ["src/smarty.js", "src/smarty-config.js"], 9 | "keywords": [ 10 | "autocomplete", 11 | "drop-down", 12 | "smarty", 13 | "angular" 14 | ], 15 | "license": "BSD-3-Clause", 16 | "homepage": "github.com/thumbtack/angular-smarty", 17 | "ignore": [ 18 | "**/.*" 19 | ], 20 | "dependencies": { 21 | "angular": "1.*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/smarty-config.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var app = angular.module("angular-smarty-config", []); 5 | 6 | app.service("smartyConfig", ["$timeout", "$q", function($timeout, $q) { 7 | var possibleSuggestions = ["bakasana", "adho mukha svanasana", "adho mukha vrkshasana"]; 8 | 9 | function getSmartySuggestions(prefix) { 10 | var deferred = $q.defer(); 11 | $timeout(function() { 12 | var suggestions = findSuggestions(prefix); 13 | if (suggestions.length > 0) { 14 | deferred.resolve(suggestions); 15 | } else { 16 | deferred.reject([]); 17 | } 18 | }, 0); 19 | return deferred.promise; 20 | }; 21 | 22 | function findSuggestions(prefix) { 23 | var prefix = prefix.toLowerCase(); 24 | var suggestions = []; 25 | for (var i = 0; i < possibleSuggestions.length; i++) { 26 | if (possibleSuggestions[i].length < prefix.length) { 27 | continue; 28 | } else if (possibleSuggestions[i].slice(0, prefix.length) == prefix) { 29 | suggestions.push(possibleSuggestions[i]); 30 | } 31 | } 32 | return suggestions; 33 | }; 34 | 35 | return { 36 | getSmartySuggestions: getSmartySuggestions 37 | } 38 | }]); 39 | 40 | app.service("smartySuggestor", ["smartyConfig", function(smartyConfig) { 41 | var getSmartySuggestions = smartyConfig.getSmartySuggestions; 42 | 43 | return { 44 | getSmartySuggestions: getSmartySuggestions 45 | }; 46 | }]); 47 | })(); 48 | -------------------------------------------------------------------------------- /demo/smarty.css: -------------------------------------------------------------------------------- 1 | .container-main { 2 | margin: auto; 3 | width: 300px; 4 | } 5 | 6 | .container-autocomplete { 7 | margin: auto; 8 | width: 200px; 9 | position: relative; 10 | } 11 | 12 | .autocomplete-suggestions-menu { 13 | position: absolute; 14 | z-index: 1; 15 | background-color: white; 16 | border: 1px solid black; 17 | width: 100%; 18 | } 19 | 20 | .container-autocomplete input { 21 | width: 100%; 22 | } 23 | 24 | .autocomplete-suggestions-menu p:hover { 25 | cursor: default; 26 | } 27 | 28 | .autocomplete-suggestions-menu .selected { 29 | color: #CCCCCC; 30 | background: yellow; 31 | } 32 | -------------------------------------------------------------------------------- /demo/smarty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | angular-smarty demo 4 | 5 | 6 | 7 |
8 |

angular-smarty demo

9 |
10 | 13 |
14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /demo/smarty.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var app = angular.module("angular-smarty", ["angular-smarty-config"]); 5 | 6 | app.controller("SmartyController", [ 7 | "$scope", "$document", "smartySuggestor", "$window", "$timeout", 8 | function($scope, $document, smartySuggestor, $window, $timeout) { 9 | /** Once the user has typed something in the input, smarty should 10 | * handle the following situations accordingly: 11 | * - User clicks outside the input or suggestions dropdown: 12 | * - Suggestions dropdown disappears 13 | * - Behaves normally when user clicks back into the input 14 | * - User clicks a suggestion from the dropdown: 15 | * - Suggestions dropdown disappears 16 | * - Input is filled with value of clicked suggestion 17 | * - Focus is on zipcode input 18 | * - User presses up or down arrows, or hovers with mouse 19 | * ($scope.userInteraction = true) 20 | * - The selected suggestion changes accordingly 21 | * - User presses enter or blurs the input 22 | * - If there is a selection made, that selection should fill the input 23 | * - If there is no selection made, whatever the user has currently typed 24 | * should remain in the input 25 | * - Focus is moved to the zipcode input 26 | */ 27 | // $scope.suggestions holds the smart suggestions based on the current prefix. 28 | // If there are suggestions in the array, the suggestions drowdown will show. 29 | // $scope.prefix holds the value of the request input. 30 | // $scope.selected holds the index of $scope.suggestions that is 31 | // currently selected by the user. If $scope.selected is -1, nothing is selected. 32 | $scope.suggestions = []; 33 | $scope.prefix = ""; 34 | $scope.selected = -1; 35 | $scope.selectionMade = false; 36 | $scope.zip = ""; 37 | 38 | $scope.$watch("prefix", function(newValue, oldValue) { 39 | if (newValue != oldValue && $scope.selectionMade == false) { 40 | if ($scope.prefix == "" || angular.isUndefined($scope.prefix)) { 41 | $scope.suggestions = []; 42 | $scope.selected = -1; 43 | } else { 44 | var promise = smartySuggestor.getSmartySuggestions($scope.prefix); 45 | promise.then(function(data) { 46 | $scope.suggestions = data; 47 | }); 48 | } 49 | } 50 | }); 51 | 52 | $scope.clickedSomewhereElse = function() { 53 | $scope.selected = -1; 54 | $scope.suggestions = []; 55 | }; 56 | 57 | $document.bind("click", function() { 58 | $scope.$apply($scope.clickedSomewhereElse()); 59 | }); 60 | 61 | $scope.suggestionPicked = function() { 62 | if ($scope.selected != -1 && $scope.selected < $scope.suggestions.length) { 63 | $scope.prefix = $scope.suggestions[$scope.selected]; 64 | } 65 | $scope.selectionMade = true; 66 | $scope.suggestions = []; 67 | }; 68 | 69 | $scope.setSelected = function(newValue) { 70 | if (newValue > $scope.suggestions.length) { 71 | $scope.selected = 0; 72 | } else if (newValue < 0) { 73 | $scope.selected = $scope.suggestions.length; 74 | } else { 75 | $scope.selected = newValue; 76 | } 77 | }; 78 | 79 | $scope.submitClicked = function() { 80 | if ($scope.requestFormStart.$valid) { 81 | $window.location = "/request?query=" + $scope.prefix + "&zipCode=" + $scope.zip + 82 | "&origin=homepage"; 83 | } 84 | }; 85 | } 86 | ]); 87 | 88 | app.directive("smartyInput", function() { 89 | function link(scope, element) { 90 | element.bind("keydown", function(event) { 91 | switch(event.which) { 92 | case 40: // down arrow 93 | scope.$apply(function() { 94 | scope.select({"x": parseInt(scope.index) + 1}); 95 | }); 96 | break; 97 | case 38: // up arrow 98 | scope.$apply(function() { 99 | scope.select({"x": parseInt(scope.index) - 1}); 100 | }); 101 | break; 102 | case 13: // enter 103 | event.preventDefault(); 104 | if (scope.selectionMade == false) { 105 | if (scope.index == "-1") { 106 | scope.$apply(function() { 107 | scope.listItems = []; 108 | }); 109 | } 110 | scope.$apply(function() { 111 | scope.close(); 112 | }) 113 | } 114 | break; 115 | default: 116 | scope.$apply(function() { 117 | scope.selectionMade = false; 118 | scope.index = -1; 119 | }); 120 | } 121 | }); 122 | 123 | element.bind("blur", function(event) { 124 | if (scope.listItems.length) { 125 | event.preventDefault(); 126 | scope.$apply(function() { 127 | scope.close(); 128 | }) 129 | } 130 | }); 131 | } 132 | return { 133 | restrict: "A", 134 | link: link, 135 | scope: { 136 | prefix: "=ngModel", 137 | select: "&", 138 | index: "=", 139 | selectionMade: "=", 140 | listItems: "=", 141 | close: "&" 142 | } 143 | }; 144 | }) 145 | 146 | app.directive("smartySuggestions", ["$document", function($document) { 147 | // Watches the scope variable prefix, which is bound to an input field. 148 | // Updates suggestions when there is a change in prefix, but only when 149 | // selectionMade equals false. 150 | function link(scope, element, attrs) { 151 | element.bind("click", function(e) { 152 | e.stopPropagation(); 153 | }); 154 | } 155 | return { 156 | restrict: "A", 157 | link: link, 158 | scope: { 159 | suggestions: "=", 160 | selected: "=", 161 | applyClass: "&", 162 | selectSuggestion: "&", 163 | prefix: "@" 164 | }, 165 | template: '

'+ 169 | '{{suggestion}} ' + 170 | '

' + 171 | '

' + 174 | 'Show all for "{{prefix}}" »

' 175 | }; 176 | }]); 177 | 178 | app.directive("focusMe", function() { 179 | return { 180 | restrict: "A", 181 | link: function(scope, element, attrs) { 182 | attrs.$observe("focusWhen", function() { 183 | if (attrs.focusWhen == "true") { 184 | element[0].focus(); 185 | } 186 | }); 187 | } 188 | }; 189 | }); 190 | 191 | app.directive("smartySuggestionsBox", function() { 192 | // Removes the need for duplicating the scode that makes the suggestions list. 193 | return { 194 | restrict: "A", 195 | template: '
' 200 | }; 201 | }); 202 | })(); 203 | -------------------------------------------------------------------------------- /src/smarty-config.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var app = angular.module("angular-smarty-config", []); 5 | 6 | app.service("smartyConfig", ["$http", "$filter", function($http, $filter) { 7 | /* The default getSmartySuggestions function makes a request to requestUrl with the given 8 | * requestParams and expects an array of JSON objects in return, e.g. [{Name: suggestion1}, 9 | * {Name: suggestion2}] 10 | */ 11 | var requestUrl = "", 12 | requestParams = {}; 13 | 14 | /* getSmartySuggestions should return a promise. See the demo for an example of how to 15 | * construct an Angular promise. 16 | */ 17 | function getSmartySuggestions(prefix) { 18 | requestParams["query"] = escape(prefix.toLowerCase()); 19 | var promise = $http.get(requestUrl(), 20 | { 21 | params: requestParams, 22 | cache: true 23 | } 24 | ) 25 | .then(function(response) { 26 | /* response.data is an the array of JSON objects where Name is the key used to identify 27 | * a suggestion 28 | */ 29 | return $filter("limitTo")(response.data, 5).map(function(item) { 30 | return item.Name; 31 | }); 32 | }); 33 | return promise; 34 | } 35 | 36 | return { 37 | getSmartySuggestions: getSmartySuggestions 38 | } 39 | }]); 40 | 41 | app.service("smartySuggestor", ["smartyConfig", function(smartyConfig) { 42 | var getSmartySuggestions = smartyConfig.getSmartySuggestions; 43 | 44 | return { 45 | getSmartySuggestions: getSmartySuggestions 46 | }; 47 | }]); 48 | 49 | })(); 50 | -------------------------------------------------------------------------------- /src/smarty.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var app = angular.module("angular-smarty", ["angular-smarty-config"]); 5 | 6 | app.controller("SmartyController", [ 7 | "$scope", "$document", "smartySuggestor", "$window", "$timeout", 8 | function($scope, $document, smartySuggestor, $window, $timeout) { 9 | /** Once the user has typed something in the input, smarty should 10 | * handle the following situations accordingly: 11 | * - User clicks outside the input or suggestions dropdown: 12 | * - Suggestions dropdown disappears 13 | * - Behaves normally when user clicks back into the input 14 | * - User clicks a suggestion from the dropdown: 15 | * - Suggestions dropdown disappears 16 | * - Input is filled with value of clicked suggestion 17 | * - Focus is on zipcode input 18 | * - User presses up or down arrows, or hovers with mouse 19 | * ($scope.userInteraction = true) 20 | * - The selected suggestion changes accordingly 21 | * - User presses enter or blurs the input 22 | * - If there is a selection made, that selection should fill the input 23 | * - If there is no selection made, whatever the user has currently typed 24 | * should remain in the input 25 | * - Focus is moved to the zipcode input 26 | */ 27 | // $scope.suggestions holds the smart suggestions based on the current prefix. 28 | // If there are suggestions in the array, the suggestions drowdown will show. 29 | // $scope.prefix holds the value of the request input. 30 | // $scope.selected holds the index of $scope.suggestions that is 31 | // currently selected by the user. If $scope.selected is -1, nothing is selected. 32 | $scope.suggestions = []; 33 | $scope.prefix = ""; 34 | $scope.selected = -1; 35 | $scope.selectionMade = false; 36 | $scope.zip = ""; 37 | 38 | $scope.$watch("prefix", function(newValue, oldValue) { 39 | if (newValue != oldValue && $scope.selectionMade == false) { 40 | if ($scope.prefix == "" || angular.isUndefined($scope.prefix)) { 41 | $scope.suggestions = []; 42 | $scope.selected = -1; 43 | } else { 44 | var promise = smartySuggestor.getSmartySuggestions($scope.prefix); 45 | promise.then(function(data) { 46 | $scope.suggestions = data; 47 | }); 48 | } 49 | } 50 | }); 51 | 52 | $scope.clickedSomewhereElse = function() { 53 | $scope.selected = -1; 54 | $scope.suggestions = []; 55 | }; 56 | 57 | $document.bind("click", onDocumentClick); 58 | $scope.$on("$destroy", function() { 59 | $document.unbind("click", onDocumentClick); 60 | }) 61 | function onDocumentClick() { 62 | $scope.$apply($scope.clickedSomewhereElse()); 63 | } 64 | 65 | $scope.suggestionPicked = function() { 66 | if ($scope.selected != -1 && $scope.selected < $scope.suggestions.length) { 67 | $scope.prefix = $scope.suggestions[$scope.selected]; 68 | } 69 | $scope.selectionMade = true; 70 | $scope.suggestions = []; 71 | }; 72 | 73 | $scope.setSelected = function(newValue) { 74 | if (newValue > $scope.suggestions.length) { 75 | $scope.selected = 0; 76 | } else if (newValue < 0) { 77 | $scope.selected = $scope.suggestions.length; 78 | } else { 79 | $scope.selected = newValue; 80 | } 81 | }; 82 | 83 | $scope.submitClicked = function() { 84 | if ($scope.requestFormStart.$valid) { 85 | $window.location = "/request?query=" + $scope.prefix + "&zipCode=" + $scope.zip + 86 | "&origin=homepage"; 87 | } 88 | }; 89 | } 90 | ]); 91 | 92 | app.directive("smartyInput", function() { 93 | function link(scope, element) { 94 | element.bind("keydown", function(event) { 95 | switch(event.which) { 96 | case 40: // down arrow 97 | scope.$apply(function() { 98 | scope.select({"x": parseInt(scope.index) + 1}); 99 | }); 100 | break; 101 | case 38: // up arrow 102 | scope.$apply(function() { 103 | scope.select({"x": parseInt(scope.index) - 1}); 104 | }); 105 | break; 106 | case 13: // enter 107 | event.preventDefault(); 108 | if (scope.selectionMade == false) { 109 | if (scope.index == "-1") { 110 | scope.$apply(function() { 111 | scope.listItems = []; 112 | }); 113 | } 114 | scope.$apply(function() { 115 | scope.close(); 116 | }) 117 | } 118 | break; 119 | default: 120 | scope.$apply(function() { 121 | scope.selectionMade = false; 122 | scope.index = -1; 123 | }); 124 | } 125 | }); 126 | 127 | element.bind("blur", function(event) { 128 | if (scope.listItems.length) { 129 | event.preventDefault(); 130 | scope.$apply(function() { 131 | scope.close(); 132 | }) 133 | } 134 | }); 135 | } 136 | return { 137 | restrict: "A", 138 | link: link, 139 | scope: { 140 | prefix: "=ngModel", 141 | select: "&", 142 | index: "=", 143 | selectionMade: "=", 144 | listItems: "=", 145 | close: "&" 146 | } 147 | }; 148 | }) 149 | 150 | app.directive("smartySuggestions", ["$document", function($document) { 151 | // Watches the scope variable prefix, which is bound to an input field. 152 | // Updates suggestions when there is a change in prefix, but only when 153 | // selectionMade equals false. 154 | function link(scope, element, attrs) { 155 | element.bind("click", function(e) { 156 | e.stopPropagation(); 157 | }); 158 | } 159 | return { 160 | restrict: "A", 161 | link: link, 162 | scope: { 163 | suggestions: "=", 164 | selected: "=", 165 | applyClass: "&", 166 | selectSuggestion: "&", 167 | prefix: "@" 168 | }, 169 | template: '

'+ 173 | '[[suggestion]] ' + 174 | '

' + 175 | '

' + 178 | 'Show all for "[[prefix]]" »

' 179 | }; 180 | }]); 181 | 182 | app.directive("focusMe", function() { 183 | return { 184 | restrict: "A", 185 | link: function(scope, element, attrs) { 186 | attrs.$observe("focusWhen", function() { 187 | if (attrs.focusWhen == "true") { 188 | element[0].focus(); 189 | } 190 | }); 191 | } 192 | }; 193 | }); 194 | 195 | app.directive("smartySuggestionsBox", function() { 196 | // Removes the need for duplicating the scode that makes the suggestions list. 197 | return { 198 | restrict: "A", 199 | template: '
' 204 | }; 205 | }); 206 | })(); 207 | -------------------------------------------------------------------------------- /tests/angular-smarty-tests.js: -------------------------------------------------------------------------------- 1 | describe("smarty", function() { 2 | // To configure angular-smarty-tests.js: 3 | // Variables that need to be edited: 4 | // expectedResults, line 31 5 | // expectedParsedResults, line 70 6 | // requestUrl, line 79 7 | // requestPrefix, line 80 8 | 9 | beforeEach(module("HomepageApp")); 10 | 11 | var scope; 12 | 13 | beforeEach(inject(function($rootScope) { 14 | scope = $rootScope.$new(); 15 | })); 16 | 17 | // --- SMARTY CONTROLLER --- // 18 | describe("smarty controller", function() { 19 | beforeEach(inject(function($rootScope, $controller) { 20 | $controller("SmartyController", { 21 | $scope: scope 22 | }); 23 | })); 24 | 25 | it("should initialize scope correctly", function() { 26 | expect(scope.selected).toEqual(-1); 27 | expect(scope.prefix).toEqual(""); 28 | expect(scope.zip).toEqual(""); 29 | expect(scope.suggestions).toEqual([]); 30 | expect(scope.selectionMade).toBe(false); 31 | }); 32 | }); 33 | 34 | // --- SMARTY SUGGESTOR SERVICE --- // 35 | describe("smarty suggestor", function() { 36 | var $httpBackend; 37 | var expectedResults = [ 38 | { 39 | "ID":2, 40 | "Name":"Handyman", 41 | "Taxonym":"Handyman", 42 | "PluralTaxonym":"Handymen", 43 | "Rank":2.070353290907192 44 | },{ 45 | "ID":835, 46 | "Name":"General Carpentry", 47 | "Taxonym":"Carpenter", 48 | "PluralTaxonym":"Carpenters", 49 | "Rank":1.8778105233780995 50 | },{ 51 | "ID":10, 52 | "Name":"General Contracting", 53 | "Taxonym":"General Contractor", 54 | "PluralTaxonym":"General Contractors", 55 | "Rank":1.7331602733663476 56 | },{ 57 | "ID":35, 58 | "Name":"Interior Design", 59 | "Taxonym":"Interior Designer", 60 | "PluralTaxonym":"Interior Designers", 61 | "Rank":1.7029912403077567 62 | },{ 63 | "ID":317, 64 | "Name":"Metalwork", 65 | "Taxonym":"Metalworker", 66 | "PluralTaxonym":"Metalworkers", 67 | "Rank":1.6196185496387445 68 | } 69 | ]; 70 | var expectedParsedResults = [ 71 | "Handyman", 72 | "General Carpentry", 73 | "General Contracting", 74 | "Interior Design", 75 | "Metalwork" 76 | ]; 77 | 78 | beforeEach(inject(function($injector) { 79 | var requestUrl = ""; 80 | var requestPrefix = ""; 81 | $httpBackend = $injector.get("$httpBackend"); 82 | $httpBackend.when("GET", requestUrl).respond( 83 | expectedResults 84 | ); 85 | })); 86 | 87 | afterEach(function() { 88 | $httpBackend.verifyNoOutstandingExpectation(); 89 | $httpBackend.verifyNoOutstandingRequest(); 90 | }); 91 | 92 | it("returns a promise", inject(function(smartySuggestor) { 93 | $httpBackend.expectGET(requestUrl); 94 | var results; 95 | smartySuggestor.getSmartySuggestions(requestPrefix).then(function(data) { 96 | results = data; 97 | }) 98 | $httpBackend.flush(); 99 | expect(results).toEqual(expectedParsedResults); 100 | })); 101 | }); 102 | 103 | // --- SMARTY FOCUS ME DIRECTIVE --- // 104 | describe("smarty focusMe directive", function() { 105 | var element; 106 | 107 | beforeEach(function() { 108 | var html = ''; 110 | 111 | inject(function($compile, $rootScope) { 112 | scope = $rootScope.$new(); 113 | scope.selectionMade = false; 114 | element = $compile(angular.element(html))(scope); 115 | scope.$digest(); 116 | element.appendTo(document.body); 117 | }); 118 | }); 119 | 120 | afterEach(function() { 121 | element.remove(); 122 | }); 123 | 124 | it("is not in focus before selectionMade is true", function() { 125 | expect(element[0]).not.toEqual(document.activeElement); 126 | }); 127 | 128 | it("is in focus after selectionMade is true", function() { 129 | scope.selectionMade = true; 130 | scope.$digest(); 131 | expect(element[0]).toBe(document.activeElement); 132 | }); 133 | }); 134 | 135 | // --- SMARTY INPUT DIRECTIVE --- // 136 | describe("smarty input directive", function() { 137 | var element; 138 | var e; 139 | 140 | beforeEach(function() { 141 | var html = ''; 144 | 145 | var mockSuggestions = [ 146 | "florist", 147 | "fence builder", 148 | "face painter", 149 | "fence repair", 150 | "flooring" 151 | ]; 152 | 153 | e = $.Event("keydown"); 154 | 155 | inject(function($compile, $rootScope, $controller) { 156 | scope = $rootScope; 157 | $controller("SmartyController", { 158 | $scope: scope 159 | }); 160 | scope.prefix = "f"; 161 | scope.suggestions = mockSuggestions; 162 | element = $compile(angular.element(html))($rootScope); 163 | scope.$digest(); 164 | }); 165 | }); 166 | 167 | it("should adjust index on down arrow presses", function() { 168 | expect(element.isolateScope().index).toEqual(-1); 169 | for (var i = 0; i < 6; i++) { 170 | e.which = 40; 171 | angular.element(element).triggerHandler(e); 172 | expect(element.isolateScope().index).toEqual(i % 6); 173 | }; 174 | }); 175 | 176 | it("should adjust index on up arrow presses", function() { 177 | var expectedIndices = [5, 4, 3, 2, 1, 0]; 178 | expect(element.isolateScope().index).toEqual(-1); 179 | for (var i = 0; i < 6; i++) { 180 | e.which = 38; 181 | angular.element(element).triggerHandler(e); 182 | expect(element.isolateScope().index).toEqual(expectedIndices[i]); 183 | }; 184 | }); 185 | 186 | it("should, on enter, fill with the appropriate value", function() { 187 | e.which = 13; 188 | angular.element(element).triggerHandler(e); 189 | expect(element.isolateScope().prefix).toEqual("f"); 190 | expect(element.isolateScope().selectionMade).toBe(true); 191 | }); 192 | 193 | it("should, on enter, fill with the appropriate value (index != 0)", function() { 194 | e.which = 40; 195 | angular.element(element).triggerHandler(e); 196 | e.which = 13; 197 | angular.element(element).triggerHandler(e); 198 | expect(element.isolateScope().prefix).toEqual(mockSuggestions[0]); 199 | expect(element.isolateScope().selectionMade).toBe(true); 200 | }); 201 | 202 | it("should, on tab, fill with the appropriate value", function() { 203 | e.which = 9; 204 | console.log("before:", element.isolateScope()); 205 | angular.element(element).triggerHandler(e); 206 | console.log("after:", element.isolateScope()); 207 | expect(element.isolateScope().prefix).toEqual("f"); 208 | expect(element.isolateScope().selectionMade).toBe(true); 209 | }); 210 | 211 | it("should, on tab, fill with the appropriate value (index != -1)", function() { 212 | e.which = 40; 213 | angular.element(element).triggerHandler(e); 214 | e.which = 9; 215 | angular.element(element).triggerHandler(e); 216 | expect(element.isolateScope().prefix).toEqual(mockSuggestions[0]); 217 | expect(element.isolateScope().selectionMade).toBe(true); 218 | }); 219 | }); 220 | 221 | // --- SMARTY SUGGESTIONS DIRECTIVE --- // 222 | describe("smarty suggestions directive", function(){ 223 | var element; 224 | 225 | beforeEach(function() { 226 | var html = '
'; 229 | var mockSuggestions = [ 230 | "florist", 231 | "fence builder", 232 | "face painter", 233 | "fence repair", 234 | "flooring" 235 | ]; 236 | 237 | inject(function($compile, $rootScope, $controller) { 238 | $controller("SmartyController", { 239 | $scope: scope 240 | }); 241 | element = $compile(angular.element(html))(scope); 242 | element.scope().suggestions = mockSuggestions; 243 | element.scope().$digest(); 244 | scope = $rootScope.$new(); 245 | }); 246 | }); 247 | 248 | it("should load p elements when scope.suggestions.length > 0", function() { 249 | expect(element.html()).toContain('